Bluetooth LE – GATT profile

Almost 9 years ago, I wrote a series of blog posts about Bluetooth and the J2ME based “Java APIs for Bluetooth Wireless Technology” (JABWT). JABWT and J2ME haven’t survived the advent of the mobile age, but Bluetooth has evolved quite well: The 2010 introduced Low Energy standard highly optimizes battery performance by sacrifing data throughput and latency. The standard has seen a rapid adoption rate: iOS supports Bluetooth LE since Version 5.0 and Android since Version 4.3. The official page shows an overview of supported devices and peripherals.

Bluetooth LE is not backward compatible to classical Bluetooth profiles or protocols. Instead of a stream based protocol (like RFCOMM) it provides a high level data exchange profile called GATT (Generic Attribute Profile) which stores data using hierarchical structured attributes. Conceptually GATT defines two roles: client and server. Typically a smartphone acts as a client while a peripheral (e.g. a temperature sensor) plays the server role. The server groups attributes into services, each of which can contain zero or more characteristics. A characteristic is a container for user data in a specific format (e.g. uint_32). So called “properties” define whether the characteristic is readable, writable and observable.

A characteristic may contain zero or more descriptors which store metadata. An important descriptor is the CCCD (Client Characteristic Configuration Descriptor) which enables or disables server-initiated updates. So the client can decide if he wants to get notified when the data of the characteristic changes.

The services and characteristics are identified with UUIDs. In order to optimize space in the data packages the Bluetooth specification supports shortened 16 and 32 bit UUIDs for predefined standard UUIDs. Otherwise custom 128 bit UUIDs have to be used. But still, the Bluetooth SIG offers the possibility to register own 16 bit UUIDs (see this registration form).

The following picture shows the (slightly simplified) structure of the standardized Battery Service which exposes the level of a battery within a device.

Battery Service
The service provides only one characteristic offering a CCCD descriptor so that  clients may register for notifications. The format type is set to uint8 because the possible values of the characteristic range from 0 to 100. Due to the fact that the service and the characteristic is standardized by the Bluetooth SIG, 16 bit UUIDs are used.

The simplicity of the GATT profile was surely a key factor for the success of Bluetooth LE, but when you start building an application you’ll encounter some small glitches. For example the maximum amount of services and characteristics may be very limited on some chipsets (e.g. max. 7 characteristics per service). Although the specification defines a maximum size of 512 bytes for a characteristic value, some hardware implementations only allow a fraction of that size. So you should prototype your service very early against the Bluetooth LE chip you want to use.

JABWT (JSR-82) Teil 5: Discovery Agent und DiscoveryListener

Ausgangspunkt für die Suche von Bluetoothgeräten ist die Klasse DiscoveryAgent. Sie erlaubt
die Suche nach Geräten und Diensten. Dabei wird die Suche im Hintergrund ausgeführt und blockiert nicht die Applikation. Wird ein Gerät oder ein Dienst gefunden, dann wird dies über ein Listener, dem DiscoveryListener, mitgeteilt.

Der Entwickler implementiert diesen Listener und übergibt ihn beim Start der Suche. Die JABWT-Implementierung ruft die Methoden dieses Listeners auf und übermittelt gefundene Geräte (Klasse RemoteDevice) bzw. Dienste (Interface ServiceRecord) als Übergabeparameter. Mit Hilfe dieser Objekte erhält man den Connection String (den wir ja nicht mehr hartcodieren möchten 😉 )

Im nachfolgenden Beispiel wird zunächst eine Suche nach Geräten mit der major device class “peripheral” ausgeführt. Anschliessend wird auf einem gefundenen Gerät die Suche nach dem RFCOMM-Dienst gestartet.

Beispielcode:

  1. ...
  2. DiscoveryListener listener      = new DiscoveryListener() {
  3.  
  4.  public void deviceDiscovered(RemoteDevice remoteDevice, DeviceClass deviceClass) {
  5.         // Ein Bluetoothgerät wurde gefunden.
  6.         // Filterung nach Major Device Class "Peripheral"
  7.         if ((deviceClass.getMajorDeviceClass() & 0x500) != 0) {
  8.                 // Speichern des remoteDevice in eine Liste
  9.         }
  10.         ...
  11.     }
  12.     public void servicesDiscovered(int i, ServiceRecord[] sr) {
  13.         // Ein oder mehrere Dienste wurden gefunden.
  14.         int reqSecurity = ServiceRecord.NOAUTHENTICATE_NOENCRYPT;
  15.         boolean mustBeMaster = false;
  16.         String connectionURL = sr[0].getConnectionURL(reqSecurity, mustBeMaster);
  17.         ...
  18.     }
  19.  
  20.     public void serviceSearchCompleted(int transactionID, int responseCode) {
  21.         // Abschluss der Suche nach Diensten.
  22.         if (responseCode == DiscoveryListener.SERVICE_SEARCH_ERROR) {
  23.                 // Suche war nicht erfolgreich -> Fehlerbehandlung
  24.         }
  25.         ...
  26.     }
  27.    
  28.     public void inquiryCompleted(int discType) {
  29.         // Abschluss der Suche nach Geräten.
  30.         if (discType == DiscoveryListener.INQUIRY_ERROR) {
  31.                 // Suche war nicht erfolgreich -> Fehlerbehandlung
  32.         }
  33.         ...
  34.     }
  35. }
  36.  
  37. LocalDevice local               = LocalDevice.getLocalDevice();
  38. DiscoveryAgent agent            = local.getDiscoveryAgent();
  39.  
  40. // Suche nach Geräten starten
  41. agent.startInquiry(DiscoveryAgent.GIAC, listener);
  42. ...
  43.  
  44. // Suche nach RFCOMM Dienst (0x0003) auf einem bereits gefundenen Gerät starten
  45. UUID[] uuidSet = new UUID[] { new UUID(0x0003) };
  46. int[] attrSet  = null; // null, weil uns nur die Default Attribute interessieren
  47. agent.searchServices(attrSet, uuidSet, remoteDevice, listener);

JABWT (JSR-82) Teil 4: Grundlagen für Erkennung von Diensten und Geräten

Wie kann man ein Bluetoothgerät zur Laufzeit finden? Das Bluetoothprotokoll unterschiedet zwischen Erkennung von Diensten (Services) und Geräten (Devices). Um eine gezielte Suche zu ermöglichen, wird jedes Bluetoothgerät mit folgenden Informationen gekennzeichnet:

  • major service class (“Positioning”, “Telephony” oder “Networking”)
  • major device class (Grobe Klassifizierung des Geräts z.B. “Peripheral” oder “Audio/Video”)
  • minor device class (Genauere Spezifikation der “major device class” z.B. “Keyboard”)

Da ein Gerät mehrere Dienste anbieten kann, sind mehrere “major service class” Einträge möglich.

Es gibt zwei Typen der Gerätesuche (“Inquiry”): “General/Unlimited Inquiry” und “Limited Dedicated Inquiry”. Die “Limited Dedicated Inquiry” liefert als Suchergebnis nur Geräte, die nur für eine begrenze Zeitdauer gefunden werden können (z.B. PDAs). Ein Bluetooth-Gerät kann sich also in drei verschiedenen Modi befinden: “general”, “limited” oder “not discoverable”.

Im nächsten Teil stelle ich den DiscoveryAgent und den DiscoveryListener vor, mit denen im JABWT die Suche nach Geräten und Diensten ausgeführt wird.

P.S.: Etwas ärgerlich finde ich, dass die offiziellen Bluetooth Spezifikationen nur mit einem sog. Adopter Account runtergeladen werden können. Und den bekommt man nur wenn man das “Bluetooth Trademark Licence Agreement” unterzeichnet(!).

Das Bild zeigt die Registrationsvorschriften von bluetooth.org: '...It is your responsibility to print, sign, and return the Bluetooth Trademark License Agreement (BTLA) to: ...'

JABWT (JSR-82) Teil 3: RFCOMM Beispiel

Das nachfolgende Codebeispiel zeigt sehr vereinfacht einen Zugriff auf einen GPS Empfänger.

Zunächst wird über die Connector Klasse ein StreamConnection Objekt geholt, mit Hilfe dessen ein InputStream geöffnet wird. Über diesen Stream schickt der GPS Empfänger laufend NMEA-Messages, die die aktuelle Position angeben. Unser Beispielcode interpretiert die ersten ankommenden 256 Bytes als String. Das ist ausreichend um mindestens 1 NMEA-Message komplett zu empfangen. Anschliessend wird die Verbindung im finally Block geschlossen.

  1. import java.io.IOException;
  2. import java.io.InputStream;
  3. import javax.microedition.io.Connector;
  4. import javax.microedition.io.StreamConnection;
  5. ...
  6.  
  7. StreamConnection conn   = null;
  8. InputStream is          = null;
  9. String message          = null;
  10. try {
  11.         conn = (StreamConnection) Connector.open ("btspp://008049323FAE:1",
  12.                                                   Connector.READ);
  13.         is = conn.openInputStream();
  14.         byte buffer[] = new byte[256];
  15.         is.read(buffer);
  16.         message = new String(buffer);
  17.  
  18. } catch (IOException io) {     
  19.         // handle exception
  20. } finally {
  21.         if (conn != null) {
  22.                 try {
  23.                         conn.close();
  24.                 } catch (IOException ignored) {}
  25.         }
  26.        
  27.         if (is != null) {
  28.                 try {
  29.                         is.close();
  30.                 } catch (IOException ignored) {}
  31.         }
  32.        
  33. }

Dieser Code ist natürlich in dieser einfachen Form noch nicht “produktionstauglich”. Z.B. würde man den Bluetooth Zugriff in einem separaten Thread oder besser in einem TimerTask auslagern. Zudem ist im Beispiel die Bluetoothadresse noch hartcodiert. Wie man die Adresse zur Laufzeit findet, seht ihr im nächsten Teil 😉

JABWT (JSR-82) Teil 2: RFCOMM

RFCOMM emuliert eine serielle Schnittstelle zwischen zwei Bluetoothgeräten und
ist deswegen die “einfachste” API in der JABWT Welt. Für die Verwendung des Protokolls wurden keine neuen Klassen definiert. Stattdessen wird das General Connection Framework (GCF) aus der CLDC-Spezifikation verwendet. Das GCF abstrahiert Verbindungen in einer
hierarchischen Struktur (Connection, StreamConnection, HttpConnection, usw.).

Für das Aufbauen von unterschiedlichen Verbindungen ist die Connector Klasse
zuständig. Sie gibt je nach übergebener URL das passende Connection Object zurück
(z.B. HttpConnection bei URL mit dem Schema “http:“). Für eine RFCOMM-Verbindung lautet das Schema “btspp”.

Beispiel für eine Clientverbindung:
btspp://008049323FAE:1
schema:bluetooth address:server channel

Der Server Channel identifiziert eindeutig einen Bluetoothdienst und ist vergleichbar mit einer TCP/IP Portnummer. Zusätzlich können mit der URL auch Parameter übergeben werden. Für eine Clientverbindung sind folgende Parameter möglich:

  • authenticate (true oder false) Gibt an, ob das Remotedevice authentifiziert werden muss.
  • master (true oder false) Gibt an, dass das Gerät “Master” der Verbindung sein muss.
  • encrypt (true oder false) Gibt an, dass die Bluetoothverbindung verschlüsselt sein muss.

Beispiel mit Parameter:
btspp://008049323FAE:1;master=true;authenticate=true

So, genug für heute. Im nächsten Teil gibts Beispiel-Code 🙂

JABWT (JSR-82) – Teil 1: Bluetooth Protokolle

JABWT (“Java APIs for Bluetooth wireless technology”) ist der Java-Standard (JSR-82) für Bluetooth Applikationen. Mittlerweile wird dieser Standard von vielen aktuellen Mobiltelefonen unterstützt. In dieser Artikelserie geht es um einen sehr groben Überblick über die Möglichkeiten dieser API.

Als Einstieg kurz die wichtigsten Bluetooth Protokolle :

Bluetooth Protokolle: L2CAP, RFCOMM, OBEX und SDP

L2CAP (Logical Link Control and Adaptation Protocol) – Jede Bluetooth Datenverbindung (Ausnahme: Bluetooth Audio) verwendet das L2CAP Protokoll als Basis. Dieses Protokoll verwendet das HCI (Host Controller Interface) und abstrahiert den eigentlichen Zugriff auf den Bluetooth Controller.

RFCOMM (Radio Frequency Communication) simuliert eine oder mehrere RS-232 Serialport Verbindungen zwischen zwei Geräten. Die Daten werden in Form von Streams ausgetauscht.

OBEX (Object Exchange Protokoll) baut auf RFCOMM auf und ist als Alternative zu HTTP konzipiert worden. Ein OBEX Request (und auch die Response) kann in kleinere Teile zerlegt und verarbeitet werden. Einsatzzweck ist z.B. das Drucken von Dokumenten (Das Basic Printing Profile basiert auf OBEX)

SDP (Service Discovery Protokoll) erlaubt das Suchen und Inspizieren von Bluetooth Diensten.