Site menu Bluetooth HDP in BlueZ

Bluetooth HDP in BlueZ

While working for Signove, I was involved with HDP (Health Device Profile) support in BlueZ, the Linux Bluetooth stack.

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. Take a look at Continua website to get a list of available (or soon-to-be-available) devices.

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

Software requirements

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

All these novelties will eventually make their way to stable versions and to distributions.

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 here 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 piece of code 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. 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.

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 :)

This video shows the script in action:

More about BlueZ HDP API

The script dissected above is a simple HDP sink. Sinks that just accept connections are indeed the simplest case, and probably the majority of applications will do exactly that.

But BlueZ HDP can be a source and initiate connections as well. It can, for example, emulate (*) an oximeter source, doing exactly what Nonin did in video.

HealthDevice interface

When we only accept connections, life is easy because it's guaranteed that remote device is a valid HDP counterpart (if we are sink, it is a source for sure). We received HealthDevice objects for free via ChannelConnected signal.

In the other hand, if we want to initiate connections, we need to find who's around, which devices have been paired etc.

This search is carried out using the normal BlueZ APIs (org.bluez.Manager, Adapter and Device interfaces).

From that point of view, an HDP-capable device has only two distinguishing characteristics:

The HealthDevice interface methods are:

bool Echo()

sends a "ping" to the device, returns a boolean indication success of failure.

This is another standard HDP feature, may be a good thing to probe if device is nearby. It is also a reasonable option if you want to probe whether a device object has the HealthDevice interface.

object CreateChannel(application, configuration)

Initiates a data channel connection.

You need to supply the local HealthApplication path (that you got when you called CreateApplication), because it identifies indirectly the data type and role that you and remote device are going to take for that channel.

For example, if you pass an oximeter source role, data channel will be created only if remote device offers the oximeter sink role. If device is both a source and a sink, or offers two or more data types, don't worry: BlueZ will still match correctly.

The configuration parameter is one of Streaming, Reliable or Any. This maps directly to the L2CAP mode that data channel is going to use. HDP imposes some rules on this; sources must choose between Reliable and Streaming (the first is the most common) and sinks are limited to Any because it is the source which decides. Moreover, the first channel between two given devices must be reliable.

DestroyChannel(object)

Destroys the channel whose path is passed.

Note that you don't need to destroy a channel just because a socket has closed and/or a device went out of range. Calling Acquire() again will (hopefully) reconnect at transport level. HDP channel is a session protocol, and you just destroy it when you are finished with the session.

Now, let's take a look in HealthChannel interface.

HealthChannel interface

We have already used this interface in oximeter sink example. We called Acquire() to get the channel's socket.

It does not matter if you accepted or initiated a data channel; the only way to exchange data is acquiring the socket and using it directly.

There are a couple additional features:

Release()

Tells HDP to shut down the data channel. You achieve similar results by calling shutdown() on socket.

Note that, regardless how the channel is shot down, you are still obligated to close() the socket, because your process holds a reference to the file descriptor. You leak file descriptors if you fail to do that. The safest way to avoid leaks is to close() upon a failed recv(), as I did in last post's example.

Type property

tells whether the channel is Reliable or Streaming.

Device property

tells which device this channel belongs to.

Application property

tells which (local) HealthApplication this channel is related to. This indirectly identifies channel's data type and role.

And that's it. The API is really "small".

One reason why it could be kept "small", is the reuse of BlueZ device objects (an additional interface (HealthDevice) is overloaded on them). So, things like device search etc. don't need HDP-specific APIs. The developer just keeps using the preexisting BlueZ APIs.

API reference

By the way, the full documentation of this API can be found at BlueZ documentation, file health-api.txt. Other documents of interest are adapter-api.txt, device-api.txt and manager-api.txt.

(*) Actually, is not emulation because a BlueZ-powered HDP device is as good as any other. I called that "emulation" because a typical Linux computer does not have pulse and oximetry sensors.

(**) A HDP device which is source and sink at the same time (case of data concentrators) will put both UUIDs in the same SDP record, but BlueZ internal workings recognize just one UUID per record. In this case, the 'UUIDs' property would list either 0x1401 or 0x1402, but not both; until BlueZ is fixed, you need to take this into consideration. A workaround is to fetch and verify the SDP record.