4.4. Service Discovery Protocol

The process of searching for services involves two steps - detecting all nearby devices with a device inquiry, and connecting to each of those devices in turn to search for the desired service. Despite Bluetooth's piconet abilities, the early versions don't support multicasting queries, so this must be done the slow way. Since detecting nearby devices was covered in Section 4.1, only the second step is described here.

Searching a specific device for a service also involves two steps. The first part, shown in Example 4-7, requires connecting to the device and sending the search request. The second part, shown in in Example 4-8, involves parsing and interpreting the search results.

Example 4-7. Step one of searching a device for a service with UUID 0xABCD


#include <bluetooth/bluetooth.h>
#include <bluetooth/sdp.h>
#include <bluetooth/sdp_lib.h>

int main(int argc, char **argv)
{
    uint8_t svc_uuid_int[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
        0, 0, 0xab, 0xcd };
    uuid_t svc_uuid;
    int err;
    bdaddr_t target;
    sdp_list_t *response_list = NULL, *search_list, *attrid_list;
    sdp_session_t *session = 0;

    str2ba( "01:23:45:67:89:AB", &target );

    // connect to the SDP server running on the remote machine
    session = sdp_connect( BDADDR_ANY, &target, SDP_RETRY_IF_BUSY );

    // specify the UUID of the application we're searching for
    sdp_uuid128_create( &svc_uuid, &svc_uuid_int );
    search_list = sdp_list_append( NULL, &svc_uuid );

    // specify that we want a list of all the matching applications' attributes
    uint32_t range = 0x0000ffff;
    attrid_list = sdp_list_append( NULL, &range );

    // get a list of service records that have UUID 0xabcd
    err = sdp_service_search_attr_req( session, search_list, \
            SDP_ATTR_REQ_RANGE, attrid_list, &response_list);
    .
    .

The uuid_t data type is used to represent the 128-bit UUID that identifies the desired service. To obtain a valid uuid_t, create an array of 16 8-bit integers and use the sdp_uuid128_create function, which is similar to the str2ba function for converting strings to bdaddr_t types. sdp_connect synchronously connects to the SDP server running on the target device. sdp_service_search_attr_req searches the connected device for the desired service and requests a list of attributes specified by attrid_list. It's easiest to use the magic number 0x0000ffff to request a list of all the attributes describing the service, although it is possible, for example, to request only the name of a matching service and not its protocol information.

Continuing our example, we now get to the tricky part - parsing and interpreting the results of a search. Unfortunately, there isn't an easy way to do this. Taking the result of our search above, Example 4-8 shows how to extract the RFCOMM channel of a matching result.

Example 4-8. parsing and interpreting an SDP search result


    sdp_list_t *r = response_list;

    // go through each of the service records
    for (; r; r = r->next ) {
        sdp_record_t *rec = (sdp_record_t*) r->data;
        sdp_list_t *proto_list;
        
        // get a list of the protocol sequences
        if( sdp_get_access_protos( rec, &proto_list ) == 0 ) {
        sdp_list_t *p = proto_list;

        // go through each protocol sequence
        for( ; p ; p = p->next ) {
            sdp_list_t *pds = (sdp_list_t*)p->data;

            // go through each protocol list of the protocol sequence
            for( ; pds ; pds = pds->next ) {

                // check the protocol attributes
                sdp_data_t *d = (sdp_data_t*)pds->data;
                int proto = 0;
                for( ; d; d = d->next ) {
                    switch( d->dtd ) { 
                        case SDP_UUID16:
                        case SDP_UUID32:
                        case SDP_UUID128:
                            proto = sdp_uuid_to_proto( &d->val.uuid );
                            break;
                        case SDP_UINT8:
                            if( proto == RFCOMM_UUID ) {
                                printf("rfcomm channel: %d\n",d->val.int8);
                            }
                            break;
                    }
                }
            }
            sdp_list_free( (sdp_list_t*)p->data, 0 );
        }
        sdp_list_free( proto_list, 0 );

        }

        printf("found service record 0x%x\n", rec->handle);
        sdp_record_free( rec );
    }

    sdp_close(session);
}

Getting the protocol information requires digging deep into the search results. Since it's possible for multiple application services to match a single search request, a list of service records is used to describe each matching service. For each service that's running, it's (theoretically, but not usually done in practice) possible to have different ways of connecting to the service. So each service record has a list of protocol sequences that each describe a different way to connect. Furthermore, since protocols can be built on top of other protocols (e.g. RFCOMM uses L2CAP as a transport), each protocol sequence has a list of protocols that the application uses, only one of which actually matters. Finally, each protocol entry will have a list of attributes, like the protocol type and the port number it's running on. Thus, obtaining the port number for an application that uses RFCOMM requires finding the port number protocol attribute in the RFCOMM protocol entry.

In this example, several new data structures have been introduced that we haven't seen before.


typedef struct _sdp_list_t {
    struct _sdp_list_t *next;
    void *data;
} sdp_list_t;

typedef void(*sdp_free_func_t)(void *)

sdp_list_t *sdp_list_append(sdp_list_t *list, void *d);
sdp_list_t *sdp_list_free(sdp_list_t *list, sdp_list_func_t f);

Since C does not have a built in linked-list data structure, and SDP search criteria and search results are essentially nothing but lists of data, the BlueZ developers wrote their own linked list data structure and called it sdp_list_t. For now, it suffices to know that appending to a NULL list creates a new linked list, and that a list must be deallocated with sdp_list_free when it is no longer needed.


typedef struct {
	uint32_t handle;
	sdp_list_t *pattern;
	sdp_list_t *attrlist;
} sdp_record_t;

The sdp_record_t data structure represents a single service record being advertised by another device. Its inner details aren't important, as there are a number of helper functions available to get information in and out of it. In this example, sdp_get_access_protos is used to extract a list of the protocols for the service record.


typedef struct sdp_data_struct sdp_data_t;
struct sdp_data_struct {
	uint8_t dtd;
	uint16_t attrId;
	union {
		int8_t    int8;
		int16_t   int16;
		int32_t   int32;
		int64_t   int64;
		uint128_t int128;
		uint8_t   uint8;
		uint16_t  uint16;
		uint32_t  uint32;
		uint64_t  uint64;
		uint128_t uint128;
		uuid_t    uuid;
		char     *str;
		sdp_data_t *dataseq;
	} val;
	sdp_data_t *next;
	int unitSize;
};

Finally, there is the sdp_data_t structure, which is ultimately used to store each element of information in a service record. At a high level, it is a node of a linked list that carries a piece of data (the val field). As a variable type data structure, it can be used in different ways, depending on the context. For now, it's sufficient to know that each protocol stack in the list of protocol sequences is represented as a singly linked list of sdp_data_t structures, and extracting the protocol and port information requires iterating through this list until the proper elements are found. The type of a sdp_data_t is specified by the dtd field, which is what we use to search the list.

4.4.1. sdpd - The SDP daemon

Every Bluetooth device typically runs an SDP server that answers queries from other Bluetooth devices. In BlueZ, the implementation of the SDP server is called sdpd, and is usually started by the system boot scripts. sdpd handles all incoming SDP search requests. Applications that need to advertise a Bluetooth service must use inter-process communication (IPC) methods to tell sdpd what to advertise. Currently, this is done with the named pipe /var/run/sdp. BlueZ provides convenience functions written to make this process a little easier.

Registering a service with sdpd involves describing the service to advertise, connected to sdpd, instructing sdpd on what to advertise, and then disconnecting.

4.4.2. Describing a service

Describing a service is essentially building the service record that was parsed in the previous examples. This involves creating several lists and populating them with data attributes. Example 4-9 shows how to describe a service application with UUID 0xABCD that runs on RFCOMM channel 11, is named ``Roto-Rooter Data Router", provided by ``Roto-Rooter", and has the description ``An experimental plumbing router"

Example 4-9. Describing a service


#include <bluetooth/bluetooth.h>
#include <bluetooth/sdp.h>
#include <bluetooth/sdp_lib.h>

sdp_session_t *register_service()
{
    uint32_t service_uuid_int[] = { 0, 0, 0, 0xABCD };
    uint8_t rfcomm_channel = 11;
    const char *service_name = "Roto-Rooter Data Router";
    const char *service_dsc = "An experimental plumbing router";
    const char *service_prov = "Roto-Rooter";

    uuid_t root_uuid, l2cap_uuid, rfcomm_uuid, svc_uuid;
    sdp_list_t *l2cap_list = 0, 
               *rfcomm_list = 0,
               *root_list = 0,
               *proto_list = 0, 
               *access_proto_list = 0;
    sdp_data_t *channel = 0, *psm = 0;

    sdp_record_t *record = sdp_record_alloc();

    // set the general service ID
    sdp_uuid128_create( &svc_uuid, &service_uuid_int );
    sdp_set_service_id( record, svc_uuid );

    // 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 = sdp_data_alloc(SDP_UINT8, &rfcomm_channel);
    rfcomm_list = sdp_list_append( 0, &rfcomm_uuid );
    sdp_list_append( rfcomm_list, channel );
    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 );

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

4.4.3. Registering a service

Building the description is quite straightforward, and consists of taking those five fields and packing them into data structures. Most of the work is just putting lists together. Once the service record is complete, the application connects to the local SDP server and registers a new service, taking care afterwards to free the data structures allocated earlier.


    .
    .
    int err = 0;
    sdp_session_t *session = 0;

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

    // cleanup
    sdp_data_free( channel );
    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 );

    return session;
}

The special argument BDADDR_LOCAL causes sdp_connect to connect to the local SDP server (via the named pipe /var/run/sdp) instead of a remote device. Once an active session is established with the local SDP server, sdp_record_register advertises a service record. The service will be advertised for as long as the session with the SDP server is kept open. As soon as the SDP server detects that the socket connection is closed, it will stop advertising the service. sdp_close terminates a session with the SDP server.


sdp_session_t *sdp_connect( const bdaddr_t *src, const bdaddr_t *dst, uint32_t
        flags );
int sdp_close( sdp_session_t *session );

int sdp_record_register(sdp_session_t *sess, sdp_record_t *rec, uint8_t flags);