Every Bluetooth application is unique, but there are a number of tasks that are common to many applications. This section presents several of these tasks and describes how to perform them using the Bluetooth and Bluetooth UI API.
Much of the code illustrating these tasks is drawn from the sample applications available in /Developer/Examples/Bluetooth on Mac OS X version 10.2 and later. The samples are also included in the SDK, available at http://developer.apple.com/hardware/bluetooth/. Examine these sample applications to see how the individual tasks in this section fit into a complete application.
Providing a New Service
Using Delegates to Receive Asynchronous Messages
Performing Device Inquiries
If you’re providing a new Bluetooth service, you must make that service available through the local SDP database. This ensures that your service is visible to potential clients performing SDP service searches. Then, you wait for a client to request your service.
There are five steps in providing a service:
Define the service.
Generate a UUID for the service (or, if you’re providing a predefined service, use the UUID defined in the profile).
Add the service definition to the SDP database.
Register for notification of the opening of the incoming channel assigned to the service.
When finished providing the service, remove the service definition from the SDP database.
The following sections describe these five steps in detail. API reference for the functions in this section is available at Device Drivers Bluetooth Documentation or on disk from /Developer/Documentation/DeviceDriversIOKit/DeviceDriversIOKit.html.
As described in “Objects in Bluetooth Connections,” you define a service by creating a dictionary in which each key-value pair, or property, corresponds to a service attribute. Apple makes it easy to add new services to the system by supporting the importation of a plist file that contains the dictionary. Thus, instead of building up a potentially complex dictionary in code, you can use the Property List Editor application to create it. Then, you use the Bluetooth API to load the dictionary into your application. Figure 3-1 shows a portion of the dictionary that describes the RFCOMM Chat Server service.
Each property in the dictionary corresponds to one of the many service attributes defined by the Bluetooth specification (or to one of the attributes defined in the service’s profile). The property’s key is the attribute ID, and the value is the attribute’s data value. For example, the first property in the dictionary in Figure 3-1 describes the service record handle, a 32-bit number that uniquely identifies the service within the server. The third key-value pair describes the Bluetooth protocols the service needs.
Each attribute key is a string that must begin with a hexadecimal number representing the attribute’s ID. These IDs are defined in the file BluetoothAssignedNumbers.h, available in the Bluetooth framework. If you choose, you can add other characters to the key string, but only after a space following the hexadecimal ID number. For example, each key in the dictionary above displays the name of the attribute after the ID number and a space.
Each attribute value contains the information that describes the attribute. The Bluetooth specification defines several data types that identify the different types of data the attribute values can contain. For example, an unsigned integer is type 1, and a sequence of data elements is type 6. Apple has mapped these data types onto Foundation classes such as NSNumber and NSArray. In turn, these classes correspond to native property list types, such as integer and array. This chain of correspondence makes it easy to translate a dictionary in a plist file into a service record object that represents your service.
As you can see in Figure 3-1, however, the attribute values differ significantly from one another. This is because different attribute-value types can be described in different ways. Formally, an attribute value is described by a combination of three components:
The size of the data
A description of the data type
The data itself
This set of information is neatly captured in a three-property dictionary in which each property key names a component and each corresponding value holds the information. An example of such a dictionary is inside the value of the protocol descriptor list key shown in Figure 3-1:
<dict> |
<key>DataElementSize</key> |
<integer>1</integer> |
<key>DataElementType</key> |
<integer>1</integer> |
<key>DataElementValue</key> |
<integer>3</integer> |
</dict> |
However, to make it easier to create an attribute dictionary in a plist file, Apple provides some shortcuts for common data types:
If the attribute’s value is of type string, data, or array, you do not need to provide a data-element size property. This is because the Mac OS X Bluetooth system will infer the data type from the dictionary type and calculate the size from the data itself.
If the attribute’s value is a string or an unsigned, 32-bit integer, no three-property dictionary is required to describe it. The value of the service record handle key in Figure 3-1 is an example of an unsigned, 32-bit integer value.
If the value is of the nil type (type 0), neither the data-element size property nor the data-element value property is required.
If the data type is either unsigned integer or signed two’s complement integer, you can use the data property list type to hold the value. In this case, the numeric data is read into the service record in network-byte order (most significant byte first). No data-element size property is needed for these data types when you use the data property list type.
If the attribute’s value is a UUID, you can use the data property list type to hold it. The Mac OS X Bluetooth system will infer the data type and calculate the size from the value.
If the attribute’s value is a list, such as the protocol-descriptor list attribute in Figure 3-1, you use the array property list type to represent it. Each array member describes a member of the list. Because each array member is itself a data element, it must conform to the guidelines for attribute values.
At this time, there are two service-attribute properties you do not have to place in your service dictionary:
Service record handle
RFCOMM channel ID
Because there is a single name space for service record handles and channel IDs, the Bluetooth system assigns these when your application imports the dictionary. If you do include these properties in your service-attribute dictionary, the Bluetooth system attempts to use them. If the values you specify are already in use, the Bluetooth system assigns others instead.
The service attributes discussed so far are defined by the Bluetooth specification. They are made available through the service-discovery process so potential clients can make informed choices. In addition to these attributes, Apple defines optional local attributes that control the local behavior of the service. These attributes are not visible to remote clients. At this time, Apple defines two local attributes:
Persistent
Target application
You specify these attributes in a special property with the key LocalAttributes, placed at the root level of your service’s attribute dictionary. The value of the LocalAttributes key must be a dictionary whose members are individual local attributes.
The persistent attribute (identified by the key Persistent) accepts a Boolean type. A value of TRUE indicates that the service should persist beyond the application that initiated it and across system reboots. When you use this attribute to designate a service as persistent, the service-record handle is automatically saved. It is used to restore the service whenever the associated Bluetooth hardware is present. It’s essential that your application save its own copy of a persistent service’s record handle, too. This is because the only way to programmatically remove a persistent service is to pass the record handle to the IOBluetoothRemoveServiceWithRecordHandle function. (For information on how to force the removal of a service whose handle you don’t know, see “Removing a Service Without a Handle.”)
By default, the absence of the Persistent attribute causes the service to be transient. (Note that the absence of the local attributes property as a whole also means the service is considered transient.) This means that the service is automatically removed when the client application terminates. If you choose, you can remove a transient service before the client application exits by calling IOBluetoothRemoveServiceWithRecordHandle.
The second local attribute specifies an application that should be launched when a remote device attempts to connect to the service. This happens when a remote device tries to open an L2CAP or RFCOMM channel of the type specified in the service’s service record. The key of this local attribute is TargetApplication, and the value must be a string containing the absolute path to the target application’s executable file. If no TargetApplication attribute is present (or if the local attributes dictionary is absent), no special action is taken when a remote device connects to the service. In this case, it’s the application’s responsibility to watch for the connection and take the appropriate steps.
To generate a UUID for your service, you can use the command-line utility uuidgen. Simply type uuidgen on the command line to receive a unique 128-bit value in the form of a hyphen-punctuated ASCII string, as in this example:
% uuidgen |
4302FA6D-089B-11D8-96C4-0030656F08FE |
You then use this UUID to identify your service. In the unlikely event you need a new UUID each time your code executes, you can use a Core Foundation function to generate it. Listing Listing 3-1 shows how to do this.
Listing 3-1 Generating a new UUID in code
#include <CoreFoundation/CoreFoundation.h> |
int main() |
{ |
CFUUIDRef uuid; |
CFStringRef string; |
uuid = CFUUIDCreate( NULL ); |
string = CFUUIDCreateString( NULL, uuid ); |
CFShow( string ); |
} |
After you’ve described your service’s attributes in a plist file, you’re ready to use it in your application. The following is an outline of the steps you take to make your service available:
Create a dictionary and initialize it with the contents of your plist file.
Create an SDP service record that contains the attributes in your dictionary.
Preserve the newly created service-record handle. This is required if your service is persistent or if you plan to terminate a transient service before your application closes.
If necessary, preserve the RFCOMM or L2CAP channel the system assigns to your service.
The code in Listing 3-2 shows how to implement these steps. It assumes that you already defined an attribute dictionary in a plist file and know the file’s path. For brevity’s sake, only limited error handling is shown.
Listing 3-2 Making a new service available
- (BOOL)publishService |
{ |
NSString *dictionaryPath = nil; |
NSString *serviceName = nil; |
NSMutableDictionary *sdpEntries = nil; |
// Create a string with the new service name. |
serviceName = [NSString stringWithFormat:@"%@ My New Service", [self |
localDeviceName]]; |
// Get the path for the dictionary we wish to publish. |
dictionaryPath = [[NSBundle mainBundle] |
pathForResource:@"MyServiceDictionary" ofType:@"plist"]; |
if ( ( dictionaryPath != nil ) && ( serviceName != nil ) ) |
{ |
// Initialize sdpEntries with the dictionary from the path. |
sdpEntries = [NSMutableDictionary |
dictionaryWithContentsOfFile:dictionaryPath]; |
if ( sdpEntries != nil ) |
{ |
IOBluetoothSDPServiceRecordRef serviceRecordRef; |
[sdpEntries setObject:serviceName forKey:@"0100 - ServiceName*"]; |
// Create a new IOBluetoothSDPServiceRecord that includes both |
// the attributes in the dictionary and the attributes the |
// system assigns. Add this service record to the SDP database. |
if (IOBluetoothAddServiceDict( (CFDictionaryRef) sdpEntries, |
&serviceRecordRef ) == kIOReturnSuccess) |
{ |
IOBluetoothSDPServiceRecord *serviceRecord; |
serviceRecord = [IOBluetoothSDPServiceRecord |
withSDPServiceRecordRef:serviceRecordRef]; |
// Preserve the RFCOMM channel assigned to this service. |
// A header file contains the following declaration: |
// IOBluetoothRFCOMMChannelID mServerChannelID; |
[serviceRecord getRFCOMMChannelID:&mServerChannelID]; |
// Preserve the service-record handle assigned to this |
// service. |
// A header file contains the following declaration: |
// IOBluetoothSDPServiceRecordHandle mServerHandle; |
[serviceRecord getServiceRecordHandle:&mServerHandle]; |
// Now that we have an IOBluetoothSDPServiceRecord object, |
// we no longer need the IOBluetoothSDPServiceRecordRef. |
IOBluetoothObjectRelease( serviceRecordRef ); |
} |
} |
} |
} |
The system assigns a particular RFCOMM channel to your service, and your application needs to know when a client is opening that channel. To do this, you register for a channel-open notification. Listing 3-3 shows how to do this, using the RFCOMM channel ID you saved when you added your service dictionary (as shown in Listing 3-2).
Listing 3-3 Registering for a channel-open notification
// Register for a notification so we get notified when a client opens |
// the channel assigned to our new service. |
// A header file contains the following declaration: |
// IOBluetoothUserNotification *mIncomingChannelNotification; |
mIncomingChannelNotification = [IOBluetoothRFCOMMChannel |
registerForChannelOpenNotifications:self |
selector:@selector(newRFCOMMChannelOpened:channel:) |
withChannelID:mServerChannelID |
direction:kIOBluetoothUserNotificationChannelDirectionIncoming]; |
When your application is ready to stop providing your service, you must remove it from the SDP database so it is no longer available to potential clients. To do this, you use the service record handle you saved when you first added your service dictionary to the database. In addition, you should unregister for the channel open notifications for which you registered earlier. Listing 3-4 shows how to perform these tasks.
Listing 3-4 Preparing to stop providing a service
- (void)stopProvidingService |
{ |
if ( mServerHandle != 0 ) |
{ |
// Remove the service. |
IOBluetoothRemoveServiceWithRecordHandle( mServerHandle ); |
} |
// Unregister the notification. |
if ( mIncomingChannelNotification != nil ) |
{ |
[mIncomingChannelNotification unregister]; |
mIncomingChannelNotification = nil; |
} |
mServerChannelID = 0; |
} |
If you’ve created a persistent service and your application crashes before you’re able to save the service-record handle, you can remove it manually, as the following steps show.
Note: Following these steps results in the loss of all cached Bluetooth information on your system, such as device names and paired-device information. This should only be done as a last resort and never by an end user.
Enable the root user.
To do this, open Directory Utility (located in /Applications/Utilities), click the lock to make changes, and choose Edit > Enable Root User. (Note that you should disable the root user when you are not using it, to ensure the security and stability of your system.)
Open the Terminal application and log in as root.
Change directory to /var/root/Library/Preferences.
Remove the blued.plist file.
Important: The blued.plist file is an internal implementation detail. You should not write code that depends on its location or existence.
Beginning in Mac OS X version 10.2.5, you can use delegates in your Objective-C application to receive asynchronous messages sent by L2CAP and RFCOMM channels. These messages include notifications of incoming data and channel status changes. When an L2CAP or RFCOMM channel is opened, the client of the channel uses the setDelegate: method to designate a delegate. It’s often convenient for the client to make itself the delegate, as in this example:
[newRFCOMMChannel setDelegate:self] |
If you choose to employ a delegate to receive asynchronous messages, you must implement at least the incoming data delegate method. Other delegate methods, such as those that receive channel status messages, are optional. The header files IOBluetoothL2CAPChannel.h and IOBluetoothRFCOMMChannel.h (both located in the Bluetooth framework) define informal protocols that describe the available delegate methods. For example, as a client of an RFCOMM channel, in addition to the rfcommChannelData:data:length: method, you can implement any of the following delegate methods:
rfcommChannelOpenComplete:status:
rfcommChannelClose:
rfcommChannelControlSignalChanged:
rfcommChannelFlowControlChanged:
rfcommChannelWriteComplete:refcon:status:
rfcommChannelQueueSpaceAvailable:
The IOBluetoothDeviceInquiry class is designed to allow Bluetooth-supported inquiries while avoiding the worst of the negative consequences associated with the device inquiry process. The class does this by limiting the amount of time spent on inquiries within a certain time period.
Your application, too, must bear some of the responsibility for the smooth operation of the device inquiry process. In particular:
You should not try to circumvent the restriction of inquiries by calling start on the IOBluetoothDeviceInquiry object multiple times in quick succession. After you call start, the inquiry may take several seconds to begin and calling start many times in a row does not change this. Therefore, you should call start only once to begin an inquiry process, recognizing that the inquiry may not begin as soon as you expect it to. If you implement the deviceInquiryStarted delegate method, you will be able to tell when the inquiry process has begun to search for devices.
You can shorten the length of time the IOBluetoothDeviceInquiry object spends on the inquiry process, but you should still not initiate multiple inquiries within that length of time.
You must not initiate your own remote device-name requests while this object is performing a device inquiry or from the delegate methods you implement. If you do this, you could deadlock your process.
If you need to perform your own name requests on remote devices, do so only after you have stopped the IOBluetoothDeviceInquiry object.
You can tell the IOBluetoothDeviceInquiry object to perform name requests on the remote devices it finds by calling the setUpdateNewDeviceNames method. You can then retrieve the information after your deviceInquiryDeviceFound delegate method is invoked.
By default, the IOBluetoothDeviceInquiry object performs the broadest possible inquiry, searching for devices of any major and minor device class that report any major service class. If you choose, you can use the setSearchCriteria method to restrict the inquiry process to consider only devices that report a specific service class or that belong to a specific major or minor device class. The BluetoothAssignedNumbers.h header file (located in the Bluetooth framework) lists the device and service class values you can use. It is not recommended that you do this, however, because not all devices identify themselves or their services in a standard manner. If you perform a restricted device inquiry, you can miss devices you might be interested in.
Last updated: 2007-12-11