Each Bluetooth device supports one or more services, such as basic printing or generic telephony. Each service is identified by a UUIDsometimes a custom one, sometimes a standardized one. For example, the UUID for the basic printing service is 0x1122. (Normally, these are written using their short forms rather than the full 128-bit forms.)
Devices publish service records to tell other devices how to communicate with them. The Bluetooth specification lays out the exact structure and meaning of a service record in excruciating detail, but as usual Java encapsulates this in a much easier-to-use interface, javax.bluetooth.ServiceRecord.
A service record is essentially an indexed list of attributes. Attributes do not have names, only values. Given a ServiceRecord object, the getAttributeIDs( ) method returns an array of all the IDs of the attributes in the service record:
public int[] getAttributeIDs( )
You can then iterate through this list, passing each ID in turn to the getAttributeValue( ) method to retrieve each attribute:
public DataElement getAttributeValue(int attrID)
Bluetooth attributes can have a variety of types, such as string, UUID, boolean, URL, sequence, null, and several signed and unsigned integer types. Java represents each of these as a DataElement object. These types, and the Java types they map to, are summarized in Table 25-4.
Most of these types do not map precisely onto Java primitive types, so the Java Bluetooth API encapsulates them all in the javax.bluetooth.DataElement class. This class has three methods to read the value out of a DataElement as a long, boolean, or Object:
public long getLong( ) public boolean getBoolean( ) public Object getValue( )
The getdataType( ) method tells you the Bluetooth type of the DataElement object:
public int getDataType( )
The return value is one of the named constants found in the third column of Table 25-4. Once you know the type of value to expect, you can use one of the three getter methods to return the value as the corresponding Java object or primitive type. If you try to get a mismatched typefor example, an INT_4 as a boolean or a URL as a longthese methods throw a ClassCastException.
Data elements can also wrap two list types. DATSEQ is an ordered sequence of values. DATALT is a list of values from which any one should be chosen. For either of these two types, getValue( ) returns a java.util.Enumeration. In this case, the getSize( ) method returns the number of items in that enumeration:
public int getSize( )
The addElement( ) method appends a new item of one of the 18 Bluetooth types to a DATSEQ or DATALT:
public void addElement(DataElement element)
The insertElementAt( ) method inserts a new data element into the specified position in a DATSEQ or DATALT:
public void insertElement(DataElement element, int position)
The removeElement( ) removes the first occurrence of the specified new data element from the DATSEQ or DATALT:
public void removeElement(DataElement element)
The same element may appear in the list more than once. These methods all throw a ClassCastException if you attempt to use them on a DataElement that does not represent a DATALT or DATSEQ.
Like the remote devices themselves, the service records for a device are obtained via the DiscoveryAgent class. The simplest way to find a known service is to ask for it by UUID using the selectService( ) method:
public String selectService(UUID uuid, int security, boolean master) throws BluetoothStateException
This returns a connection string with the URL used to connect to the service, such as:
btspp://00904B2A88D6:1;authenticate=false;encrypt=false;master=false
The security argument is usually one of these three named constants, depending on what combination of authentication and encryption you desire:
Finally, the master argument is TRue if the client insists on being the master of the connection and false if it can act as the master or the slave.
For example, suppose you want to find a basic printing service. The UUID for this is 0x1122, so this code fragment locates one if theres one to be found:
UUID printingID = new UUID(0x1122); String url = agent.selectService( printingID, ServiceRecord.AUTHENTICATE_NOENCRYPT, false);
If it can locate the requested service, it returns null.
What if theres more than one available device that supports the relevant service? In this case, the results are implementation dependent, but usually one or another is returned. (The Avetana stack actually throws a custom checked exception here, which is not conformant with the specification.)
The searchServices( ) method asks a specific device what services it supports:
public int searchServices(int[] attrSet, UUID[] uuidSet, RemoteDevice device, DiscoveryListener listener) throws BluetoothStateException
This approach is somewhat more reliable if you might have more than one device that offers a given service. The uuidSet argument contains the UUIDs for all the protocols you e looking for. Table 25-5 lists the UUIDs (in 2-byte form) of the services you can request. The attrSet argument contains the list of attributes (in addition to the default attributes) whose information should be provided. device is the specific device to query for services, and listener is the listener to tell about any services that are found. The method returns an ID you can use if you later need to cancel the search, which you can do with the following method:
public boolean cancelServiceSearch(int transID)
|
Table 25-6 lists some of the attributes you can request. The first fiveServiceRecordHandle, ServiceClassIDList, ServiceRecordState, ServiceID, and ProtocolDescriptorListare always returned. The others need to be specifically requested.
Some of these IDs may vary depending on the profile. For instance, 0x0301 means "external network" in the Cordless Telephony Profile but "supported data stores" in the Synchronization Profile.
The ServiceRecord interface provides a number of setter and getter methods for inspecting and updating service records. By far the most important thing youll need from a ServiceRecord object is the connection string. This is the URL youll use to open a connection to the device:
public String getConnectionURL(int requiredSecurity, boolean mustBeMaster)
The security argument is one of these three named constants, depending on what combination of authentication and encryption you desire:
The master argument is true if the client insists on being the master of the connection or false if it can act as the master or the slave.
The value you get back is a complete GCF Bluetooth URL, such as:
btspp://00904B2A88D6:1;authenticate=false;encrypt=false;master=false
Once you have the URL, you can talk to the device using the methods of the last chapter.
You can add or remove parameters from the URL using substring operations. For instance, you could change the above URL to:
btspp://00904B2A88D6:1;authenticate=false;encrypt=false;master=true
However, getConnectionURL() is the only way to get the necessary protocol, address, and channel for the device.
The getAttributeIDs( ) method returns an array of the IDs of all the attributes this service possesses:
public int[] getAttributeIDs( )
You can retrieve one of these attributes with the getAttributeValue( ) method:
public DataElement getAttributeValue(int attrID)
This returns a DataElement object that wraps the Bluetooth object in a Java class, as described in Table 25-4.
Example 25-4 is a program that searches for all L2CAP (UUID 0x0100) services. When it finds one, it lists its URL. Notice that you have to explicitly start a search for each devices services. That is, Example 25-4 first starts a search for devices as previously seen in Example 25-3. When a device is found, it searches that device for services using searchServices( ). For each service, it requests all attributes that might be present.
import java.io.IOException; import javax.bluetooth.*; public class BluetoothServicesSearch implements DiscoveryListener { private DiscoveryAgent agent; private final static UUID L2CAP = new UUID(0x0100); public static void main(String[] args) throws Exception { BluetoothServicesSearch search = new BluetoothServicesSearch( ); search.agent = LocalDevice.getLocalDevice().getDiscoveryAgent( ); search.agent.startInquiry(DiscoveryAgent.GIAC, search); } public void deviceDiscovered(RemoteDevice device, DeviceClass type) { try { System.out.println("Found " + device.getFriendlyName(false) + " at " + device.getBluetoothAddress( )); } catch (IOException ex) { System.out.println("Found unnamed device " + " at " + device.getBluetoothAddress( )); } searchServices(device); } public final static int SERVICE_RECORD_HANDLE = 0X0000; public final static int SERVICE_CLASSID_LIST = 0X0001; public final static int SERVICE_RECORD_STATE = 0X0002; public final static int SERVICE_ID = 0X0003; public final static int PROTOCOL_DESCRIPTOR_LIST = 0X0004; public final static int BROWSE_GROUP_LIST = 0X0005; public final static int LANGUAGE_BASED_ATTRIBUTE_ID_LIST = 0X0006; public final static int SERVICE_INFO_TIME_TO_LIVE = 0X0007; public final static int SERVICE_AVAILABILITY = 0X0008; public final static int BLUETOOTH_PROFILE_DESCRIPTOR_LIST = 0X0009; public final static int DOCUMENTATION_URL = 0X000A; public final static int CLIENT_EXECUTABLE_URL = 0X000B; public final static int ICON_URL = 0X000C; public final static int VERSION_NUMBER_LIST = 0X0200; public final static int SERVICE_DATABASE_STATE = 0X0201; private void searchServices(RemoteDevice device) { UUID[] searchList = {L2CAP}; int[] attributes = {SERVICE_RECORD_HANDLE, SERVICE_CLASSID_LIST, SERVICE_RECORD_STATE, SERVICE_ID, PROTOCOL_DESCRIPTOR_LIST, BROWSE_GROUP_LIST, LANGUAGE_BASED_ATTRIBUTE_ID_LIST, SERVICE_INFO_TIME_TO_LIVE, SERVICE_AVAILABILITY, BLUETOOTH_PROFILE_DESCRIPTOR_LIST, DOCUMENTATION_URL, CLIENT_EXECUTABLE_URL, ICON_URL, VERSION_NUMBER_LIST, SERVICE_DATABASE_STATE}; try { System.out.println("Searching " + device.getBluetoothAddress( ) + " for services"); int trans = this.agent.searchServices(attributes, searchList, device, this); System.out.println("Service Search " + trans + " started"); } catch (BluetoothStateException ex) { System.out.println( "BluetoothStateException: " + ex.getMessage( ) ); } } public void servicesDiscovered(int transactionID, ServiceRecord[] record) { for (int i = 0; i < record.length; i++) { System.out.println("Found service " + record[i].getConnectionURL( ServiceRecord.NOAUTHENTICATE_NOENCRYPT, false)); } } public void serviceSearchCompleted(int transactionID, int responseCode) { switch (responseCode) { case DiscoveryListener.SERVICE_SEARCH_DEVICE_NOT_REACHABLE: System.out.println("Could not find device on search " + transactionID); break; case DiscoveryListener.SERVICE_SEARCH_ERROR: System.out.println("Error searching device on search " + transactionID); break; case DiscoveryListener.SERVICE_SEARCH_NO_RECORDS: System.out.println("No service records on device on search " + transactionID); break; case DiscoveryListener.SERVICE_SEARCH_TERMINATED: System.out.println("User cancelled search " + transactionID); break; case DiscoveryListener.SERVICE_SEARCH_COMPLETED: System.out.println("Service search " + transactionID + " complete"); break; default: System.out.println("Unexpected response code " + responseCode + " from search " + transactionID); } } public void inquiryCompleted(int transactionID) { System.out.println("Device search " + transactionID + " complete"); } } |
Most other Bluetooth protocols are built on top of L2CAP, so this program will probably find all the accessible devices. Heres what I got when I ran it on my system after making sure all devices were discoverable:
Found Earthmate Blue Logger GPS at 00904B2A88D6 Searching 00904B2A88D6 for services Service Search 1 started Found service btspp://00904B2A88D6:1;authenticate=false;encrypt=false;master=false Service search 1 complete Found elharos mouse at 000A95095A59 Searching 000A95095A59 for services Service Search 2 started Found service btl2cap://000A95095A59:11;authenticate=false;encrypt=false;master=false Found service btl2cap://000A95095A59:1;authenticate=false;encrypt=false;master=false Service search 2 complete Found WACOM Pen Tablet at 0013C2000D23 Searching 0013C2000D23 for services Service Search 3 started Found service btl2cap://0013C2000D23:11;authenticate=false;encrypt=false;master=false Found service btl2cap://0013C2000D23:1;authenticate=false;encrypt=false;master=false Service search 3 complete Device search 0 complete
You can see there are three devices on this system: a GPS unit, a pen tablet, and a mouse. The GPS unit has a single serial port (RFCOMM) connection, which well make use of in the next section. The mouse and the graphics tablet each have two L2CAP URLs, one for the control channel and one for the interrupt channel. This is the common pattern for HID devices.
More often, youll want to look for a particular service with a particular UUID. This normally happens asynchronously, but theres a maximum number of searches you can run at once. (The exact number varies from device to device but can be read from the bluetooth.sd.trans.max property.) Consequently, you need to keep track of the searches and cancel the ongoing searches when youve found what you e looking for. Example 25-5 demonstrates. The static BluetoothServiceFinder.getConnectionURL( ) method finds a service with a specified UUID. Well use this class again shortly.
import javax.bluetooth.*; import java.util.Vector; public class BluetoothServiceFinder implements DiscoveryListener { public static String getConnectionURL(String uuid) throws BluetoothStateException { BluetoothServiceFinder finder = new BluetoothServiceFinder(BluetoothReceiver.UUID); return finder.getFirstURL( ); } private DiscoveryAgent agent; private int serviceSearchCount; private ServiceRecord record; // Id rather use ArrayList, but Vector is more // commonly available in J2ME environments private Vector devices = new Vector( ); private String uuid; // Every search has an ID that allows it to be cancelled. // We need to store these so we can tell when all searches // are complete. private int[] transactions; private BluetoothServiceFinder(String serviceUUID) throws BluetoothStateException { this.uuid = serviceUUID; agent = LocalDevice.getLocalDevice().getDiscoveryAgent( ); int maxSimultaneousSearches = Integer.parseInt( LocalDevice.getProperty("bluetooth.sd.trans.max")); transactions = new int[maxSimultaneousSearches]; // We need to initialize the transactions list with illegal // values. According to spec, the transaction ID is supposed to be // positive, and thus nonzero. However, several implementations // get this wrong and use zero as a transaction ID. for (int i = 0; i < maxSimultaneousSearches; i++) { transactions[i] = -1; } } private void addTransaction(int transactionID) { for (int i = 0; i < transactions.length; i++) { if (transactions[i] == -1) { transactions[i] = transactionID; return; } } } private void removeTransaction(int transactionID) { for (int i = 0; i < transactions.length; i++) { if (transactions[i] == transactionID) { transactions[i] = -1; return; } } } private boolean searchServices(RemoteDevice[] devices) { UUID[] searchList = { new UUID(uuid, false) }; for (int i = 0; i < devices.length; i++) { if (record != null) { return true; } try { // don care about attributes int transactionID = agent.searchServices(null, searchList, devices[i], this); addTransaction(transactionID); } catch (BluetoothStateException ex) { } synchronized (this) { serviceSearchCount++; if (serviceSearchCount == transactions.length) { try { this.wait( ); } catch (InterruptedException ex) { // continue } } } } while (serviceSearchCount > 0) { // unfinished searches synchronized (this) { try { this.wait( ); } catch (InterruptedException ex) { // continue } } } if (record != null) return true; else return false; } private String getFirstURL( ) { try { agent.startInquiry(DiscoveryAgent.GIAC, this); synchronized (this) { try { this.wait( ); } catch (InterruptedException ex) { } } } catch (BluetoothStateException ex) { System.out.println("No devices in range"); } if (devices.size( ) > 0) { RemoteDevice[] remotes = new RemoteDevice[devices.size( )]; devices.copyInto(remotes); if (searchServices(remotes)) { return record.getConnectionURL( ServiceRecord.NOAUTHENTICATE_NOENCRYPT, false); } } return null; } // DiscoveryListener methods public void deviceDiscovered(RemoteDevice device, DeviceClass type) { devices.addElement(device); } public void serviceSearchCompleted(int transactionID, int responseCode) { removeTransaction(transactionID); serviceSearchCount--; synchronized (this) { this.notifyAll( ); } } public void servicesDiscovered(int transactionID, ServiceRecord[] records) { if (record == null) { record = records[0]; for (int i = 0; i < transactions.length; i++) { if (transactions[i] != -1) { agent.cancelServiceSearch(transactions[i]); } } } } public void inquiryCompleted(int discType) { synchronized (this) { this.notifyAll( ); } } } |