Using Bluetooth

http://www.drdobbs.com/article/print?articleId=232500828&siteSectionName=mobile

Using Bluetooth

Trying to communicate with a remote device with no other familiar protocol? Bluetooth provides an easy answer with well-documented specs and straightforward programming APIs. 

By Ben DuPont
January 31, 2012
URL:http://www.drdobbs.com/mobile/232500828 

Bluetooth programming can solve several annoying problems outside of its principal domain areas. For example, it works well if you're working on a standalone device and you want to configure the device via a custom Bluetooth profile, or you have a device that speaks a standard protocol that your Linux machine doesn't yet support. In this article, I demonstrate how to code your way out of both problems using Bluetooth.

Linux has a mature and capable Bluetooth stack called BlueZ. Many Linux distributions including Ubuntu 10.10 ship with a Bluetooth-enabled kernel as well as a set of profiles that enable your machine to do things like transfer files to and from a Bluetooth-enabled phone.

There are three main steps to making Bluetooth devices communicate:

  • Scan: Scan for devices that advertise certain Bluetooth services
  • Pair devices: Exchange a key to authenticate the devices and initiate secure communication
  • Connect to a service: After the devices are paired, either end can initiate a connection to a particular service.

We'll use built-in tools to accomplish these tasks, and we'll use the BlueZ API to write a program that will communicate with the device. In particular, we'll implement the Headset profile (HSP), which allows your smart phone to use your computer as a headset. Sample code is provided in C.

Before diving into how to accomplish these tasks, let's cover system setup.

System Setup

The given examples were written on Ubuntu 10.10. You'll need the following packages:

  • gnome-Bluetooth, a gnome applet for administering Bluetooth devices
  • bluez-hcidump, communication debugging tool
  • bluez, Linux Bluetooth stack and associated tools
  • libBluetooth3, the BlueZ library
  • libBluetooth-dev, the development files for linking to the BlueZ library
  • libasound2, ALSA API for using a sound card
  • libasound2-dev, ALSA API for using a sound card

Finally, you'll need a Bluetooth phone that supports the Bluetooth HSP (headset) profile and a Bluetooth adapter. I'm using a TRENDnet TBW-106UB USB Bluetooth adapter. After plugging the dongle into your machine, the Bluetooth-applet should start as indicated by the Bluetooth symbol on the top panel. (Second in from the left; see Figure 1.)

 
Figure 1.

Using the applet, you can scan for devices, initiate and complete the paring process, and connect Bluetooth services between devices. As the topic is programming, I'm not going to go through the process of scanning and pairing devices, but note that you will have to pair your phone with your machine in order to follow along.

BlueZ API

BlueZ exposes a socket API that's similar to network socket programming. If you're familiar with network programming in Linux, learning the BlueZ API will feel familiar. To implement the headset profile (more on what that means in a bit), we'll use two different sockets: RFCOMM and SCO.

RFCOMM sockets are like TCP sockets: They're useful in situations where reliability trumps latency. SCO sockets provide a two-way stream of audio data at a rate of 64 kb/s. SCO packets are not retransmitted.

Bluetooth Profiles

What does it mean to implement a Bluetooth profile? The standard Bluetooth profiles and core specification are published on the bluetooth.org website. The top of the page lists different version of the core specification. Below the core specification documents are the profiles.

We need two files from the website: Headset profile v1.1 (HSP), and the Core Specifcation Version 2.1 + EDR.

The core specification file tells you everything you need to know to create a Bluetooth stack. Weighing in at just over 1400 pages, it would be a hefty read. Fortunately, we'll only be using this document as a debugging aid. I chose version 2.1 + EDR because that's the same version that the dongle supports.

The HSP document is a much more manageable 27 pages. It shows how to implement the Bluetooth headset profile (HSP). With these documents in hand, let's configure the Bluetooth adapter, pair a smart phone, and finally get to coding.

Configure the Bluetooth Adapter

When scanning, Bluetooth looks for nearby devices that provide specific services. Services offered by a device are identified by the device class. The first thing we need to do is set the class of the adapter. We'll use the hciconfig tool to do this. Later, I'll show how to set the class in the code.

Setting the class requires creating a bitmask from a list of assigned constants. The Bluetooth.org site contains a series of assigned numbers documents. The definitions required to set the appropriate class are in the baseband document. I'm not going to go into detail about how the document formatted, but I'll point you to the relevant pieces for the class that we need to build. Using the second document, I'll construct the class value.

From the major service class, we'll select bits 21 and 19 (audio and capturing), and from the major device class, we'll use audio/video (00100). Because we chose the audio/video device class, scroll down to Table 7 in the baseband document where the minor device classes are listed for audio/video major class. From this table, we want hands-free device (000010). Putting the selected values together, the value of the class in hex is 0x280404.

To see the current configuration of your Bluetooth hardware, run hciconfig -a. This command displays various data items about your hardware, most notably, the Bluetooth address and the class. Here's sample output from my machine:

hci0:	Type: BR/EDR  Bus: USB
	BD Address: 00:11:22:33:44:55  ACL MTU: 310:10  SCO MTU: 64:8
	UP RUNNING PSCAN ISCAN 
	RX bytes:7383 acl:0 sco:0 events:175 errors:0
	TX bytes:3583 acl:0 sco:0 commands:175 errors:0
	Features: 0xff 0xff 0x8f 0xfe 0x9b 0xff 0x59 0x83
	Packet type: DM1 DM3 DM5 DH1 DH3 DH5 HV1 HV2 HV3 
	Link policy: RSWITCH HOLD SNIFF PARK 
	Link mode: SLAVE ACCEPT 
	Name: 'CSR - bc4'
	Class: 0x280404
	Service Classes: Capturing, Audio
	Device Class: Audio/Video, Device conforms to the Headset profile
	HCI Version: 2.1 (0x4)  Revision: 0x149c
	LMP Version: 2.1 (0x4)  Subversion: 0x149c
	Manufacturer: Cambridge Silicon Radio (10)

Under the class line, the output contains a text description of the service classes and device class. My machine shows a class of 0x280404, service classes capturing and audio, and device classes audio/video, and device conforms to the headset profile.

Run the following command to set the class: sudo hciconfig hc0 class 0x280404.

After setting the class, run hciconfig -a again and verify that the class, service class, and device class lines match my output. If it does, your phone should recognize the machine as supporting the headset profile. You can scan for your computer from your phone and pair them. Of course, we haven't implemented the headset service yet so your phone has nothing to connect to yet.

Bluetooth Addresses and Conversion Functions

Bluetooth addresses are a 6-byte hex number similar to an Ethernet MAC address. BlueZ provides convenience functions for converting between a 6-byte string in the format 00:11:22:33:44:55 and the bdaddr_t struct. Here are the function prototypes:

  • int ba2str(const bdaddr_t *ba, char *str);
  • int str2ba(const char *str, bdaddr_t *ba);

The function ba2str converts from the internal bdaddr_t to a zero-terminated string (the str parameter should have at least 18 bytes), and str2ba provides the opposite conversion. The first example makes use of the ba2str function.

Implementing the Headset Profile (HSP)

The HSP profile document describes how to implement the HSP profile. The profile overview (on page 204) contains a diagram that shows two distinct components of the HSP: the audio gateway (AG) and the headset (HS); see Figure 2:

 
Figure 2: Diagram copied from page 204 of the HSP profile document.

We're going to implement the headset side, (but implementing the audio gateway side is almost identical — the audio gateway can be implemented by making a few small changes to the code).

Here are the steps:

  • Set the class (as we did earlier, except through a function call)
  • Register with the SDP server
  • Listen for RFCOMM connections
  • Listen for SCO connection
  • Process data from the connections

The following sections provide sample code to accomplish these tasks.

Setting the Class

In a previous example, we used the hciconfig tool to set the device class. In this example, we'll set the class in code with a call to hci_write_class_of_dev as shown in Listing One. Modifying the device attributes is a privileged function so this example must be run as root.

Listing One

#include "btinclude.h"

int main()
{
	int id;
	int fh;
	bdaddr_t btaddr;
	char pszaddr[18];

	unsigned int cls = 0x280404;
	int timeout = 1000;

	printf("this example should be run as root\n");

	// get the device ID
	if ((id = hci_get_route(NULL)) < 0)
		return -1;

	// convert the device ID into a 6 byte bluetooth address
	if (hci_devba(id, &btaddr) < 0)
		return -1;

	// convert the address into a zero terminated string
	if (ba2str(&btaddr, pszaddr) < 0)
		return -1;

	// open a file handle to the HCI
	if ((fh = hci_open_dev(id)) < 0)
		return -1;

	// set the class
	if (hci_write_class_of_dev(fh, cls, timeout) != 0)
	{
		perror("hci_write_class ");
		return -1;
	}

	// close the file handle
	hci_close_dev(fh);

	printf("set device %s to class: 0x%06x\n", pszaddr, cls);

	return 0;
}

Register with the Service Directory Protocol (SDP) Server

The SDP server is an integral part of a Bluetooth system. Services register with the local SDP; remote devices query the SDP to find out how to connect to particular services. In our example, we're going to register the HSP service.

BlueZ provides a tool for communicating with local and remote SDP servers: sdptool. To view the list of services offered by your machine, run this command: dptool browse local. We can use this command to determine whether our program has correctly registered with the SDP server. Here's output from my SDP server:

Browsing FF:FF:FF:00:00:00 ...
Service Name: Headset Audio Gateway
Service RecHandle: 0x10000
Service Class ID List:
  "Headset Audio Gateway" (0x1112)
  "Generic Audio" (0x1203)
Protocol Descriptor List:
  "L2CAP" (0x0100)
  "RFCOMM" (0x0003)
    Channel: 12
Profile Descriptor List:
  "Headset" (0x1108)
    Version: 0x0102

Service Name: Hands-Free Audio Gateway
Service RecHandle: 0x10001
Service Class ID List:
  "Handsfree Audio Gateway" (0x111f)
  "Generic Audio" (0x1203)
Protocol Descriptor List:
  "L2CAP" (0x0100)
  "RFCOMM" (0x0003)
    Channel: 13
Profile Descriptor List:
  "Handsfree" (0x111e)
    Version: 0x0105

Notice that the first two entries reference headset and hands-free; those services need to be disabled so they don't interfere with our program. On my machine, I had to modify /etc/Bluetooth/audio.conf. Under the [General] section, add this line:

	Disable=Headset,Gateway

After modifying the config file, restart Bluetooth by running sudo service Bluetooth restart, and check that the SDP server no longer has references to the headset or hands-free profiles.

The next piece of code, Listing Two, shows how to register the the headset (HS) side of the headset profile. The code is largely borrowed from two sources I found on the Web.

Listing Two

#include "btinclude.h"

// source adapted from:
// http://people.csail.mit.edu/albert/bluez-intro/x604.html and
// http://nohands.sourceforge.net/source.html (libhfp/hfp.cpp: SdpRegister)

uint8_t channel = 3;

int main()
{
	const char *service_name = "HSP service";
	const char *service_dsc = "HSP";
	const char *service_prov = "nebland software, LLC";

	uuid_t hs_uuid, ga_uuid;

	sdp_profile_desc_t desc;

	uuid_t root_uuid, l2cap_uuid, rfcomm_uuid;
	sdp_list_t *l2cap_list = 0,
			   *rfcomm_list = 0,
			   *root_list = 0,
			   *proto_list = 0,
			   *access_proto_list = 0;

	sdp_data_t *channel_d = 0;

	int err = 0;
	sdp_session_t *session = 0;

	sdp_record_t *record = sdp_record_alloc();

	// set the name, provider, and description
	sdp_set_info_attr(record, service_name, service_prov, service_dsc);

	// service class ID (HEADSET)
	sdp_uuid16_create(&hs_uuid, HEADSET_SVCLASS_ID);

	if (!(root_list = sdp_list_append(0, &hs_uuid)))
		return -1;

	sdp_uuid16_create(&ga_uuid, GENERIC_AUDIO_SVCLASS_ID);

	if (!(root_list = sdp_list_append(root_list, &ga_uuid)))
		return -1;

	if (sdp_set_service_classes(record, root_list) < 0)
		return -1;

	sdp_list_free(root_list, 0);
	root_list = 0;

	// make the service record publicly browsable
	sdp_uuid16_create(&root_uuid, PUBLIC_BROWSE_GROUP);

	root_list = sdp_list_append(0, &root_uuid);
	sdp_set_browse_groups( record, root_list );

	// set l2cap information
	sdp_uuid16_create(&l2cap_uuid, L2CAP_UUID);
	l2cap_list = sdp_list_append( 0, &l2cap_uuid );
	proto_list = sdp_list_append( 0, l2cap_list );

	// set rfcomm information
	sdp_uuid16_create(&rfcomm_uuid, RFCOMM_UUID);
	channel_d = sdp_data_alloc(SDP_UINT8, &channel);
	rfcomm_list = sdp_list_append( 0, &rfcomm_uuid );

	sdp_list_append( rfcomm_list, channel_d );
	sdp_list_append( proto_list, rfcomm_list );

	// attach protocol information to service record
	access_proto_list = sdp_list_append( 0, proto_list );
	sdp_set_access_protos( record, access_proto_list );

	sdp_uuid16_create(&desc.uuid, HEADSET_PROFILE_ID);

	// set the version to 1.0
	desc.version = 0x0100;

	if (!(root_list = sdp_list_append(NULL, &desc)))
		return -1;

	if (sdp_set_profile_descs(record, root_list) < 0)
		return -1;

	// connect to the local SDP server and register the service record
	session = sdp_connect( BDADDR_ANY, BDADDR_LOCAL, SDP_RETRY_IF_BUSY );
	err = sdp_record_register(session, record, 0);

	// cleanup
	sdp_data_free( channel_d );
	sdp_list_free( l2cap_list, 0 );
	sdp_list_free( rfcomm_list, 0 );
	sdp_list_free( root_list, 0 );
	sdp_list_free( access_proto_list, 0 );

	while (1)
		sleep(5000);

	return err;
}

The code essentially builds lists of values that are registered with the SDP server. The constants used in the code, like HEADSET_SVCCLASS_ID and GENERIC_AUDIO_SVC_CLASS_ID, are defined specifically from the HSP profile. The values for the headset side of the profile are listed on page 220 of the HSP profile document. Page 221 lists the values for the audio gateway side of the profile.

When a Bluetooth program exits, any services registered with the SDP server are removed. The test program waits in an infinite loop so you can run sdptool and verify that the service is indeed registered. Here's what the service should look like when you run the sdptool command:

Service Name: HSP service
Service Description: HSP
Service Provider: nebland software, LLC
Service RecHandle: 0x10003
Service Class ID List:
  "Headset" (0x1108)
  "Generic Audio" (0x1203)
Protocol Descriptor List:
  "L2CAP" (0x0100)
  "RFCOMM" (0x0003)
    Channel: 3
Profile Descriptor List:
  "Headset" (0x1108)
    Version: 0x0100

The SDP code registers a specific profile with the SDP server. Besides describing the profile that our service provides (HSP in this example), the code also tells the service what RFCOMM channel we will accept connections on. Remote devices that want to use our HSP service will query the SDP server and extract the channel that we registered. When a connection to our service is initiated, the remote device will connect to channel 3.

You might notice that the SDP record is missing the optional parameter that specifies whether remote volume control is supported. It seems that sdp_attr_add or sdp_attr_add_newcould be used to add a value for the constant SDP_ATTR_REMOTE_AUDIO_VOLUME_CONTROL to the SDP record, but it didn't seem to work. At least, when I added the parameter, it did not show up in the listing produced by sdptool.

Listening for RFCOMM Connections

When a device initiates an HSP connection, the first connection created between the devices is through RFCOMM. As long as this connection is active, the headset can assume that the audio gateway will, at any moment, initiate a SCO connection. SCO connections are covered shortly.

The code in Listing Three creates a RFCOMM socket and listens for connections on channel 3.

Listing Three

#include "btinclude.h"

uint8_t channel = 3;

int main()
{

	int sock;		// socket descriptor for local listener
	int client;	// socket descriptor for remote client
	unsigned int len = sizeof(struct sockaddr_rc);

	struct sockaddr_rc remote;		// local rfcomm socket address
	struct sockaddr_rc local;		// remote rfcomm socket address
	char pszremote[18];

	// initialize a bluetooth socket
	sock = socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM);

	local.rc_family = AF_BLUETOOTH;

	// TODO: change this to a local address if you know what
	// address to use
	local.rc_bdaddr = *BDADDR_ANY;
	local.rc_channel = channel;

	// bind the socket to a bluetooth device
	if (bind(sock, (struct sockaddr *)&local, 
sizeof(struct sockaddr_rc)) < 0)
			return -1;

	// set the listening queue length
	if (listen(sock, 1) < 0)
		return -1;

	printf("accepting connections on channel: %d\n", channel);

	// accept incoming connections; this is a blocking call
	client = accept(sock, (struct sockaddr *)&remote, &len);

	ba2str(&remote.rc_bdaddr, pszremote);

	printf("received connection from: %s\n", pszremote);

	return 0;
}

If you're familiar with BSD-style socket programming, this example should look very familiar. If not, here are the basic steps:

  1. Create a socket: line 17
  2. Set the Bluetooth address and channel number: lines 23, and 24. If you want to use a specific Bluetooth device, use the str2ba(...) function to convert the string to abdaddr_t. For exmaple: str2ba("00:11:22:33:44:55:66", &local.rc_bdaddr);
  3. Bind the socket to the address and channel: line 27
  4. Set the number of clients to queue up to wait for a connection handshake (additional clients will be refused): 35
  5. Wait for/accept client connections: line 42

A successful call to accept will return a full duplex socket descriptor. The functions send and recv should be used to send data to, and receive data from the remote device. This connection is used to exchange AT commands.

The AT commands and formats for HSP are defined starting on page 215 of the document. Various actions initiated on the handset, like adjusting the volume, will generate an AT command, telling our program of the event. Our program responds to the event by printing out the command and returning the AT OK command. In a real implementation, the service should adjust the hardware (turn the volume down, for example) as requested by the remote client.

Listening for SCO Connections

Once the RFCOMM connection is established, the audio gateway will connect and release an SCO connection as needed. Recall that the SCO connection carries audio data in both directions.

Creating an SCO connection is nearly identical to creating a RFCOMM connection. The main difference is, SCO connections do not specify a channel: to connect to a remote host, only the host address is specified. Of course, SCO sockets also use different constants and structs. Listing Four provides an example:

Listing Four

#include "btinclude.h"

int main()
{
	int sock;
	int client;
	unsigned int len = sizeof(struct sockaddr_sco);

	struct sockaddr_sco local;
	struct sockaddr_sco remote;

	char pszremote[18];

	sock = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_SCO);

	local.sco_family = AF_BLUETOOTH;

	local.sco_bdaddr = *BDADDR_ANY;

	if (bind(sock, (struct sockaddr*)&local, 
sizeof(struct sockaddr_sco)) < 0)
			return -1;

	if (listen(sock, 1) < 0)
		return -1;

	client = accept(sock, (struct sockaddr*)&remote, &len);

	ba2str(&remote.sco_bdaddr, pszremote);

	printf("sco received connection from: %s\n", pszremote);

	return 0;
}

Following the Signaling Diagrams in the HSP Profile Document

From a BlueZ API standpoint, all of the pieces required to implement the headset profile have been covered. What we haven't discussed is how the headset and audio gateway interact. Interaction between the devices is described in signaling diagrams.

Understanding the signaling diagrams is important in order to know what types of scenarios your program needs to handle. While looking at the diagrams, keep in mind that we're implementing the headset (HS) side. Let's examine the incoming audio connection establishment from page 210 of the HSP profile document. Figure 3 is the table from the document:

 
Figure 3.

The signaling diagram can be viewed as a sequence of mandatory and optional events that happen from the top down. The first event is the establishment of an RFCOMM connection, represented by the arrow labeled Connection establishment. This connection is initiated by the audio gateway. Note that the RFCOMM connection can also be initiated by the headset side as indicated on page 212 of the HSP document.

Next is a RING event, which is generated by an incoming phone call. When the audio gateway receives an incoming phone call, it will send the text RING to the headset. Optionally, the headset will establish a SCO connection and send an in-band ring tone. The audio gateway will continually send RING to the headset until the headset responds with AT+CKPD=200. After receiving this response, the audio gateway responds with OK and then initiates the SCO connection if the connection does not yet exist.

Debugging with hcidump

If you plan to do Bluetooth programming in Linux, hcidump is an excellent debugging tool. I'm going to cover the very basics of how to translate the output from hcidump using the Bluetooth core specification document.

One place where hcidump becomes useful is when your Bluetooth device doesn't seem to be connecting to your program. If you have hcidump running and you don't see any output, the device is not even communicating with your Bluetooth adapter.

The hcidump program contains a number of options for filtering and logging to files or sockets. We won't filter any events and we'll let the output write to the screen. The only option we'll turn on is the hex and ascii output option. Here's the command: sudo hcidump -X.

Here's sample output from my phone connecting to the sample bths program (with modified Bluetooth addresses):

> HCI Event: Connect Request (0x04) plen 10
  0000: 00 11 22 33 44 55 0c 02  5a 01                    u.4Q....Z.
< HCI Command: Accept Connection Request (0x01|0x0009) plen 7
  0000: 00 11 22 33 44 55 00                              u.4Q...
> HCI Event: Command Status (0x0f) plen 4
  0000: 00 01 09 04                                       ....
> HCI Event: Role Change (0x12) plen 8
  0000: 00 00 11 22 33 44 55 00                           .u.4Q...

To interpret the results, turn to Volume 2 (Core System Package), Part E (Host Controller Interface Specification), 7 (HCI Commands and Events) of Core V2.1 + EDR document (this is the core specification file mentioned in the Bluetooth profiles section of this article).

The first line of the sample hcidump output is an event. Events are listed under 7.7. The output tells us the event is a Connect Request (event code 0x04). Connect request is documented in section 7.7.4. Table 1 shows a screenshot of the table.

 
Table 1.

This table tells us the parameters of a connection request are a 6 byte Bluetooth address, a 3 byte class field, and a single byte link type field.

The next line in the sample output is a HCI Command Accept Connection Request which is documented in section 7.1.8.

Putting the Pieces Together

All of the examples are provided in separate files. I've also put all the pieces together in a single sample. The sample includes code that sends audio data from your phone to your computers default sound device. If you run the program, pair and connect your phone, and make a phone call, you should here the audio on your computer speakers. However, the program does not take data from your mic and send it to the phone (through the SCO socket). That's an exercise that I leave to you.

Unpack the samples and run make to compile them. Then follow these steps to run the example:

  1. Disable HSP/HFP: add Disable=Headset,Gateway to /etc/Bluetooth/audio.conf under the [General] section
  2. Restart Bluetooth: sudo service Bluetooth restart
  3. Run bths as root. If the program isn't run as root, it will fail when trying to set the class: sudo ./bths
  4. Pair your phone with your machine.
  5. From your phone, initiate a connection to your machine by selecting your machine in the Bluetooth settings. Upon successful connection from the phone, the program will output received connection from 00:11:22:33:44:55.
  6. Initiate a phone call from your phone. After initiating the call, your phone should create a SCO connection to your machine. The bths program will output sco received connection from 00:11:22:33:44:55, and all audio that you would normally hear on your phone will play through your machine.

The provided example several limitations. One issue is data from the RFCOMM socket is not processed until a SCO connection is initiated. Once both sockets are connected, they are both processed, but the program should process data from the RFCOMM socket even when there's no active SCO connection. This example won't handle incoming calls to your phone for this reason.

Final Observations

This article provides quite a bit of detail on how to interpret and implement Bluetooth profiles. In the introduction, I also promised that you'd learn how to implement a custom profile for configuring a standalone device. Some of the tools to do so are mentioned above, namely RFCOMM sockets, but there are other options as well. Here are a few:

  • Custom protocol over an RFCOMM connection. This would be nearly identical to creating a custom protocol over TCP.
  • Linux supports serial port emulation over Bluetooth. This option would allow a wireless serial link between machines, over which a custom serial protocol could be implemented. It's also useful for existing programs that require a serial interface.
  • Linux supports networking over Bluetooth, often referred to as a Personal Area Network (PAN). This option creates an IP link between devices, allowing protocols such as HTTP to function over Bluetooth. With such a link, one could run a web server on one device and connect to the device with a browser, as mentioned, all over Bluetooth.

If you own an Android phone, you should know that the Bluetooth radio is exposed to the Java API. This allows you to create custom Bluetooth programs on your phone. Now what are the possibilities?


Ben DuPont is a software engineer located in Green Bay, WI.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章