Monday, June 03, 2013

Xamarin Mono.Droid Receiving different bluetooth devices data records

When you are working with several different Bluetooth devices (from different manufacturers) you will have to be able to handle different types of data records. The official Android Bluetooth sample ported for Xamarin/Mono.Droid can be found at BluetoothChatService.cs. This was the code I used for inspiration when I built this app.
I could however not really get the code to work as I wanted and the problem was located in the following method Run().
public override void Run ()
{
 Log.Info (TAG, "BEGIN mConnectedThread");
 byte[] buffer = new byte[1024];
 int bytes;

 // Keep listening to the InputStream while connected
 while (true) {
  try {
   // Read from the InputStream
   bytes = mmInStream.Read (buffer, 0, buffer.Length);

   // Send the obtained bytes to the UI Activity
   _handler.ObtainMessage (BluetoothChat.MESSAGE_READ, bytes, -1, buffer)
    .SendToTarget ();
  } catch (Java.IO.IOException e) {
   Log.Error (TAG, "disconnected", e);
   _service.ConnectionLost ();
   break;
  }
 }
}
The problem is the following call
// Send the obtained bytes to the UI Activity
_handler.ObtainMessage (BluetoothChat.MESSAGE_READ, bytes, -1, buffer).SendToTarget ();
which might not always send back the entire device record to the handler. This is caused by the various devices implementation of how to send the data record back as a byte stream to the mobile device/phone might not finish, before the while loop is starting another iteration, as well as the multithreading and the context switches that occurs in the BluetoothChatService.

What I discovered while testing was that for some devices it was working out of the box with the default code in the Run method above. However on some more complex devices which were sending larger data records back, it never seemed to work.

After spending some time analyzing the problem - being new to the bluetooth scene and Mono.Droid development - I found that data records got chopped into pieces in the while(true) loop in the above Run() method. Assuming that a full data record was 65 bytes, sometimes the code would pass 13 bytes to the SendToTarget() method. The next iteration might yield 31 bytes and the last 21 bytes was yielded on the 3rd pass. 

The number of bytes returned on each iteration was never the same and there was no specific pattern to this. To be able to handle this for different devices sending data in different ways I needed some kind of smart buffering. I came up with the following.
public override void Run ()
{
 Log.Info (TAG, "BEGIN mConnectedThread");
 byte[] buffer = new byte[1024];
 int bytes;
 
 // Keep listening to the InputStream while connected
 while (true) {
  try {
   // Read from the InputStream
   bytes = mmInStream.Read(buffer, 0, buffer.Length);

   var messagePart = new byte[bytes];
   Array.Copy(buffer,messagePart, bytes);

   // Send the obtained bytes to the UI Activity
   _handler.ObtainMessage(MESSAGE_READ, bytes, -1, messagePart)
    .SendToTarget();
  } catch (Java.IO.IOException e) {
   Log.Error (TAG, "disconnected", e);
   _service.ConnectionLost ();
   break;
  }
 }
}
The new code is only a little bit different in that I am no longer sending back the buffer but instead only the partial message (messagePart).

In the handler that handles the read bytes I changed the implementation from what can be found here BluetoothChat.cs.
case MESSAGE_READ:
 byte[] readBuf = (byte[])msg.Obj;
 // construct a string from the valid bytes in the buffer
 var readMessage = new Java.Lang.String (readBuf, 0, msg.Arg1);
 bluetoothChat.conversationArrayAdapter.Add (bluetoothChat.connectedDeviceName + ":  " + readMessage);
 break;
to the following code

case BluetoothService.MESSAGE_READ:
 byte[] readBuf = (byte[])msg.Obj;
 ParseBluetoothDeviceReading(readBuf);
 break;
and the ParseBluetoothDeviceReading() method looking like this
public void ParseBluetoothDeviceReading (byte[] readBuffer)
{
 if (readBuffer == null || readBuffer.Length <= 0)
  return;

 var deviceRecord = _zephyrResponseParser.ParseDeviceResponse(readBuffer);
 _data.LungFunction= deviceRecord.LungFunction;
 lblValue.Text = deviceRecord.LungFunction.ToString("0.00");
}
the data record buffer is implemented in the _zephyrResponseParser which is of type IDeviceResponseParser. The interface IDeviceResponseParser looks like the following

public interface IDeviceResponseParser<TDataRecord>
{
 TDataRecord ParseDeviceResponse(byte[] response);
}
and finally the actual device specific data record parser for the zephyr device.

using System.Collections.Generic;

namespace Droid.Devices.Zephyr
{
 public class ZephyrResponseParser : IDeviceResponseParser<ZephyrRecord>
 {
  public const int DeviceMessageLength = 65;
  
  private List<byte> _messageBuffer = new List<byte>();

  public ZephyrRecord ParseDeviceResponse(byte[] response)
  {
   // Since the data (byte[]) that we are getting is retreived from a BluetoothNetworkStream there are no guarantees that we will get the full message in one go.
   // For that reason we need to buffer it our selves until whe have a full message
   _messageBuffer.AddRange(response);
   if (_messageBuffer.Count < DeviceMessageLength)
   {
    // Not full message return
    return new ZephyrRecord();
   }

   // Full message, pass on to temp so we can reset the local messageBuffer for the next
   // device message
   var temp = _messageBuffer.ToArray();

   // Reset buffer
   _messageBuffer.Clear();
   
   // Now process the full message
   response = temp;
   if (response.Length >= DeviceMessageLength)
   {
    var deviceRecord = ExtractDataFromRawByteStream();
    return new ZephyrRecord { LungFunction = deviceRecord.LungFunction };
   }
   return new ZephyrRecord();
  }
 }

 ...
 
 public class ZephyrRecord
 {
  public float LungFunction { get; set; }
 }
}
This has proven to work so far, without any known problems, for several different devices that are working very differently, has different manufacturers and are measuring different medical parameters.

No comments: