2010/11/05

HDP in BlueZ

In the last few months, while working at Signove, I was involved with HDP (Health Device Profile) support in BlueZ, the Linux Bluetooth stack. Recently, the final parts were accepted, and BlueZ at last offers HDP support, fully featured and very easy to use.

HDP devices are related to health and fitness. Things like oximeters, pedometers, glucose meters, blood pressure measurers, and so on. The profile used to be called MDP (Medical Device Profile, you can find that name in older texts), it was probably changed not to scare people :)

There are very few HDP devices in market -- the specification itself is very recent -- but this number is expected to grow fast. See [2] to get a list of available (or soon-to-be-available) devices.

In this post, I plan to show what you need to play with HDP and a basic review of the API.

Software requirements


* A recent BlueZ version, 4.77 or better.
* A recent kernel. The stock 2.6.36 version will do;
* D-Bus 1.4.0 or better.
* If you are planning to use Python, you need to patch python-dbus with [1]

Let's explain why this supermarket list of new software:

* HDP has been recently integrated into BlueZ.
* HDP uses enhanced retransmission and streaming modes of Bluetooth L2CAP, which are implemented in kernel, and 2.6.36 is the first kernel version where this is enabled by default and reasonably stable.
* HDP API uses D-Bus and uses a new feature, file descriptor passing, that was implemented in D-Bus 1.4.0.
* The current python-dbus binding does not have support to FD passing, not even in GIT; the [1] is a patch that I submitted and may be accepted anytime now.

All these novelties will eventually make their way to stable versions and to distributions. I have put together some packages for 32-bit Ubuntu. You can find them at [3].

A short tour over HDP API


BlueZ HDP application programming interface is exposed via D-Bus. This ensures that it is easy to use, introspectable, independent of any library and available to most programming languages at once.

I will use the script found in [4] as example. Since the script is publicly avaiable, I will deliberately omit some drudgery like imports etc.

In HDP, you don't have clients and servers; you have sources and sinks. And, if you want e.g. to talk with a source like my Nonin oximeter, you need to be a sink yourself. There are no "anonymous" parties in HDP.

So, we begin by creating a sink of appropriate data type (which is 0x1004 for oximeters):


bus = dbus.SystemBus()
signal_handler = SignalHandler()

config = {
"Role": "Sink",
"DataType": dbus.types.UInt16(0x1004),
"Description": "Oximeter sink"
}

manager = dbus.Interface(bus.get_object("org.bluez", "/org/bluez"),
"org.bluez.HealthManager")
app = manager.CreateApplication(config)


This simple action will trigger a lot of actions inside BlueZ: an appropriate SDP record will be published, other devices with compatible data type and role (in case, oximeter sources) are made available via API, and in turn oximeters will be able to find us.

If you want to be both a source and a sink, and/or support more data types, just create more applications with the appropriate configuration. You can create as many as you need.

You probably noted that a SignalHandler() object is created in above code. This object will act upon new data channels. In BlueZ HDP API, applications are notified of new data channels by signals.

Normally, HDP sources initiate connections, and HDP sinks wait for them (though the opposite may happen, too). Since we are a sink, and Nonin oximeter always takes the initiative, we don't need to create connections, we just sit and wait.

SignalHandler class code:


class SignalHandler(object):
def __init__(self):
bus.add_signal_receiver(self.ChannelConnected,
signal_name="ChannelConnected",
bus_name="org.bluez",
path_keyword="device",
interface_keyword="interface",
dbus_interface="org.bluez.HealthDevice")

bus.add_signal_receiver(self.ChannelDeleted,
signal_name="ChannelDeleted",
bus_name="org.bluez",
path_keyword="device",
interface_keyword="interface",
dbus_interface="org.bluez.HealthDevice")

def ChannelConnected(self, channel, interface, device):
channel = bus.get_object("org.bluez", channel)
channel = dbus.Interface(channel, "org.bluez.HealthChannel")
fd = channel.Acquire()
fd = fd.take()
sk = socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM)
os.close(fd) # because socket.fromfd() uses dup()
glib.io_add_watch(sk, watch_bitmap, data_received)

def ChannelDeleted(self, channel, interface, device):
pass


Constructor handles the standard D-Bus drudgery for signals. ChannelConnected is a bit more complicated because of file descriptor receiving. The fd is actually a Bluetooth socket, but encapsulating it as an Unix socket is enough to cheat Python and put socket in good use.

(Remember that D-Bus Python bindings don't support file descriptor passing yet; this code needs dbus-python to be patched with [1] to work. It is a problem you won't have with C language :)

What if there are more than one process in HDP sink role? Since D-Bus signals are received by everyone, all processes will get wind of the new channel, but only the "owner" of the matching application will succeed in getting a file descriptor by calling Acquire(). The others will get an error. So, make sure you handle exceptions in Acquire(), or better yet, call it asynchronously. (I have submitted a patch to BlueZ adding an 'Application' property to HealthChannel; if this is accepted, application path can be matched, not calling Acquire() at all when it is not "our" channel.)

Also, it is important to say that a channel may survive across Bluetooth disconnections. That is, you may receive a ChannelConnected signal with the same path again. This means that device has reconnected and you can resume the conversation with device instead of beginning from scratch. As one would expect, the ChannelDeleted signal indicates when a channel really ceased to exist.

Ok, what do we do with an open data channel?

The application protocol for HDP devices is IEEE 11073. There may be any number of protocols in the future, but this is currently the only one that got an "assigned number" in HDP specification. IEEE 11073 is obviously an IEEE standard and you need IEEE documentation to implement it

In order to make quick tests, we made a couple "dummy" implementations in HDPy (based on protocol dumps and quite device-specific). For the sake of brevity, just import the oximeter specialization here:


from hdp.dummy_ieee10404 import parse_message_str


and use it when we receive some data from socket:


watch_bitmap = glib.IO_IN | glib.IO_ERR | glib.IO_HUP | glib.IO_NVAL

def data_received(sk, evt):
data = None
if evt & glib.IO_IN:
try:
data = sk.recv(1024)
except IOError:
data = ""
if data:
response = parse_message_str(data)
if response:
sk.send(response)

more = (evt == glib.IO_IN and data)

if not more:
try:
sk.shutdown(2)
except IOError:
pass
sk.close()

return more


IEEE protocol specification is very long and complex. Our "dummy" implementation only handles association request and acknowledges data indications from oximeter. It is by no means complete, but it is enough to show your pulse rate :)

Light this candle!


So, if you are wondering whether this script actually works, here's a video:



I did not show the pairing phase because it is exactly like any other Bluetooth device, but it is important to mention that this oximeter is paired with only one device at a time, and it always tries to connect to that device; it does not "scan" the room for HDP sinks.

Conclusion


I think we did a good job in this API; it is really easy to use. HDP is quite complex, in particular the channel reconnection feature is a fertile source of bugs -- very, very easy for an application to mess up if exposed to the inner workings of HDP and MCAP.

A main driver of API architecture was to make almost 100% sure that applications would to the "right thing at first try", not demanding deep knowledge of HDP and MCAP specs from the application developer.

I feel that, among general-purpose operating systems, Linux has by far the best Bluetooth stack, both feature- and stability-wise (I miss it badly when I have to use Bluetooth in Mac), and these recent developments increase the distance even more.

References


[1] https://bugs.freedesktop.org/show_bug.cgi?id=30812
[2] http://www.continuaalliance.org/products/certified-products.html
[3] https://github.com/signove/hdpy/tree/master/deps.
[4] https://github.com/signove/hdpy/blob/master/scripts/hdp_oxi_sink
blog comments powered by Disqus