Zum Hauptinhalt Zur Navigation Zur Suche

Golem.de programmiert: BluetoothLE im Eigenbau

Fitness-Armbänder, Heimautomation und Überwachung: BluetoothLE scheint der omnipotente Funkstandard zu sein. Golem.de zeigt, wie mit wenig Aufwand BluetoothLE erkundet und genutzt werden kann.
/ Alexander Merz
31 Kommentare undefined News folgen (öffnet im neuen Fenster)
Raspberry Pi mit Bluetooth-Dongle und Breadboard (Bild: Golem.de)
Raspberry Pi mit Bluetooth-Dongle und Breadboard Bild: Golem.de

Vom Mobile World Congress haben einige Kollegen interessante, BluetoothLE-basierte Gadgets mitgebracht. Apples iBeacon-Konzept hat Aufmerksamkeit erregt. Selbst für diejenigen, die keine eigenen BluetoothLE-Projekte realisieren wollen, können ein paar Grundkenntnisse und Werkzeuge zur Analyse seiner Umgebung in Zukunft ganz hilfreich sein. Deshalb haben wir das Thema aufgegriffen – und sind durch Zufall auf eine Bibliothek gestoßen, mit der mit geringem Aufwand ein BluetoothLE-Projekt umgesetzt werden kann.

Bluetooth vs. BluetoothLE

Die Begriffe Bluetooth und BluetoothLE werden selten genau unterschieden. Die beiden Buchstaben LE für Low Energy bedeuten aber nicht einfach nur ein paar zusätzliche oder andere Spezifikationen im Protokoll im Vergleich zum sogenannten Classic Bluetooth. Bei der Anwendungsentwicklung ist ebenfalls einiges verschieden.

Bluetooth LE ausprobiert
Bluetooth LE ausprobiert (02:56)

Auch wer sich nur ein wenig mit Bluetooth beschäftigt hat, wird bereits von Profilen gehört haben: Dabei handelt es sich, vereinfacht gesprochen, um Definitionen, welche Methoden ein angesprochenes Bluetooth-Gerät bereitstellen und in welchem Format Nutzdaten ausgetauscht werden. Mittlerweile gibt es mehr als 25 Profile für die verschiedensten Anwendungsfälle. Am meisten genutzt und von fast jedem modernen Smartphone unterstützt werden A2D zum Übertragen von Musik und HFS für Headsets. Mit dem Profil HID werden Tastaturen und Mäuse an einen Computer angebunden.

Die grundsätzliche Natur einer Classic -Bluetooth-Verbindung ist unabhängig von diesen Profilen. In der Spezifikation steht explizit die Formulierung, dass für einen Nutzer eine Bluetooth-Verbindung zwischen zwei Geräten einem virtuellen Kabel gleicht. Das hat mehrere Konsequenzen: Zum einen ist eine Bluetooth-Verbindung damit immer exklusiv. Ein Smartphone kann also nicht Musik zu verschiedenen Bluetooth-Lautsprechern gleichzeitig streamen, sondern stets nur zu einem Empfänger. Zum anderen erfordert damit die Bluetooth-Kommunikation auch immer einen aufwendigen Kopplungs- wie Verbindungsprozess und erhält auf Protokollebene eine kontinuierliche Übertragung aufrecht.

Die erste Konsequenz ist unpraktisch für das Internet-of-Things, die zweite schraubt den Leistungs- und damit Energiebedarf eines Bluetooth-Gerätes in die Höhe.

Deswegen macht BluetoothLE vieles anders. Es gibt nur ein Profil, genannt Generic Attribute , kurz GATT . Bei diesem Profil stellt ein Bluetooth-Gerät keine Methoden bereit, sondern repräsentiert nur einen Schlüssel-Wert-Speicher. Die Kommunikation besteht also darin, Werte zu setzen oder zu schreiben. Das Bluetooth-Gerät muss dabei keine permanente Verbindung aufrechterhalten; damit entfällt auch die Notwendigkeit, die Kopplung und die Verbindung zu verwalten, wenn es nicht anderweitig notwendig ist. Dadurch sind aber bestimmte Anwendungen mit BluetoothLE nicht umsetzbar – wie zum Beispiel das Streaming von Inhalten.

Das GATT-Profil

Wie bereits geschrieben, kann mit diesem Profil ein Bluetooth-Gerät als einfacher Schlüssel-Wert-Speicher beschrieben werden. Ein Temperatursensor kann zum Beispiel unter einem konkreten Schlüssel die Temperatur zur Verfügung stellen und unter einem weiteren Schlüssel die Maßeinheit wie Celsius oder Fahrenheit.

In der Praxis ist GATT allerdings komplizierter. Es fängt damit an, dass der Speicher hierarchisch organisiert ist. Auf oberster Ebene liegen einer oder mehrere Services. Ein Service definiert ein oder mehrere zusammengehörige Characteristics – dabei handelt es sich um die angebotenen Schlüssel. Über einen Schlüssel kann nun, je nach Geräten, ein Wert gelesen oder geschrieben werden, oder auch beides.

Warum das so kompliziert ist

Durch die hierarchische Struktur soll die Batterie geschont werden. Die Funkübertragung von Daten verbraucht viel Energie, deshalb sollte die Menge der zu übertragenden Daten möglichst minimal sein.

Nehmen wir ein Fitness-Armband, das die Temperatur des Trägers und dessen Blutdruck misst, dazu noch nett vibriert, wenn ein Anruf oder eine SMS auf dem gekoppelten Smartphone ankommt.

Mit Classic Bluetooth senden die Geräte dabei Inquiry Messages aus. Auf diese Weise findet das Smartphone das Armband. Dann erfolgen die Kopplung und die Verbindungsaufnahme. Als nächstes fragt das Smartphone über das Service Discovery Protocol ( SDP ), welche Profile mit welchen Spezifikationseigenschaften auf dem Gerät überhaupt zur Verfügung stehen. Bis der erste Temperaturwert übermittelt wurde, sind also schon einige Bluetooth-Datenpakete durch die Luft geflogen. Beim Thema SDP gilt es übrigens aufzupassen: Bei Classic Bluetooth sind mit Services im Namen des Protokolls tatsächlich in erster Linie die Profile gemeint, nicht die Services des GATT-Profils.

Bei BluetoothLE gibt es ebenfalls noch eine Inquiry Message, die Antwort darauf kann aber bereits die angebotenen Services des Armbandes enthalten. Das Smartphone kann also schon an diesem Punkt entscheiden, ob eine weitere Kommunikation sinnvoll ist. Wäre GATT "flach" organisiert, müssten dafür tatsächlich sämtliche Schlüssel-Identifikationen übermittelt werden – oder es wäre eine zusätzliche SDP-Abfrage fällig. Aber selbst der damit einhergehende Verbindungs- und Übermittlungsprozess ist bei Bluetooth-LE sparsamer als bei Bluetooth Classic.

iBeacon

Beim Thema Kommunikationsaufbau bei Bluetooth möchten wir iBeacon nicht ganz unerwähnt lassen. Wie oben angesprochen, schicken BluetoothLE-Geräte Inquiry Messages aus. Das Format als solches ist standardisiert – über EIF ( Extended Inquiry Format ) können Geräteproduzenten aber ein eigenes Subformat implementieren. Apple tut das bei iBeacons und packt standardkonform in die Inquiry Message gleich eine eindeutige Identifizierung des jeweiligen iBeacons. Außerdem ist die Sendestärke der Bluetooth-Geräte ebenfalls Teil der Inquiry-Nachricht.

Damit kann eine App errechnen, in welcher Entfernung sich welche iBeacons befinden. In Kombination mit der Registrierung der iBeacons und deren Position bei Apple kann eine App damit zielgenau eine Person bespielen. Wohlgemerkt: Das alles spielt sich ohne einen weiteren Verbindungsaufbau über Bluetooth ab, die eigentliche Logik läuft komplett über das Smartphone und die Server bei Apple. Das iBeacon muss keine Services bereitstellen.

Begriffsverwirrung

Bevor wir zu praktischen Fingerübungen kommen, gilt es noch, ein wenig Sprachpräzisierung zu betreiben. Die Bluetooth-Spezifikation verwendet diverse Begriffe, um zu beschreiben, wie Geräte miteinander in Beziehung stehen. Da ist von Server – Client, Observer – Observable, Controller – Target, Master – Slave, Source – Sink oder Central – Peripheral die Rede. Diese Begriffspaare sind zumeist profilspezifisch. Da aber viele APIs, Programme und Geräte mehrere Profile unterstützen, und aufgrund sprachlicher Gewohnheiten, werden die Begriffe gerne wild durcheinandergeworfen.

Beim GATT-Profil wird von einem "Peripheral"-Modul gesprochen, um das Daten erhebende beziehungsweise ausführende Gerät zu beschreiben. Im obigen Beispiel wäre es das Armband. Das "Central"-Modul ist hingegen das Gerät, welches das eigentliche Programm und die Logik ausführt, also zumeist ein Smartphone oder ein Computer. Parallel ist auch die Verwendung von GATT-Server für das Peripheral- und GATT-Client für das Central-Modul üblich.

Erste Versuche

Das Henne-Ei-Problem: Wer einen Sender und Empfänger für irgendetwas implementieren will, müsste eigentlich beides zur gleichen Zeit programmieren – und riskiert etwas zu bauen, das auch nur in dieser Kombination funktioniert, nicht aber mit anderen Programmen.

Deswegen verwenden wir erst einmal existierende Hardware und Programme, um später beides durch eigene Entwicklungen zu ersetzen. Auch wer nur etwas über seine eigenen Bluetooth-Geräte lernen will, ohne gleich selbst zu programmieren, wird in diesem Abschnitt fündig.

Die Hardware, die wir anschauen wollen, ist das Alcatel Smartband. Mit der dazugehörigen Android-App kann es derzeit die Schritte des Trägers zählen.

Statt der App nutzen wir aber LightBlue(öffnet im neuen Fenster) , um auf das Armband zuzugreifen. Dieses Programm für OS X unterstützt den Roh-Zugriff auf BluetoothLE-Geräte. Eine vergleichbare App ist nRF Master Control Panel(öffnet im neuen Fenster) für Android-Smartphones.

Die Anwendung

Für beide Programme gilt: starten, warten bis BluetoothLE-Geräte erkannt wurden und dann einfach verbinden. Erst jetzt wird es etwas kryptisch, denn nun werden die Services des Gerätes angezeigt. Die Services werden über 16 und 128 Bit lange Universally Unique Identifier (UUID) identifiziert. Services mit kurzen, 16 Bit langen UUID sind in der GATT-Spezifikation(öffnet im neuen Fenster) von Bluetooth definiert.

Einige Services geben Auskunft über allgemeine Eigenschaften der Geräte, zum Beispiel Informationen zum Hersteller oder dem Batteriestand, andere sind anwendungsspezifisch – zum Beispiel ein Service für Temperaturangaben von einem Temperatursensor. Diese Art von Service ist noch am ehesten mit den klassischen Profilen von Bluetooth vergleichbar, denn die Eigenschaften und Datenformate sind vorgegeben. Neben den UUIDs gibt die Spezifikation auch noch die entsprechende Bezeichnung vor, die uns die nRF-App direkt anzeigt, Lightblue leider nicht.

Schauen wir uns den Service mit der UUID 0x180F an, dem Batteriestatus. Nach einem Klick werden uns die Characteristics des Services angezeigt, also die Schlüssel des jeweiligen Services.

Hier finden wir das Characteristic mit der UUID 0x2A19 – laut Spezifikation repräsentiert es den Batterieladezustand in Prozent. Damit wir ihn aber tatsächlich sehen, müssen wir bei Lightblue noch einen Button drücken beziehungsweise bei nRF den Download-Pfeil berühren. Je nach Eigenschaft des Characteristics können wir verschiedene Aktionen über das Programm mit ihm durchführen: einen Wert hineinschreiben ( Write ), einen Wert auslesen ( Read ) oder ein Characteristic abonnieren( Notify bzw. Subscribe ). Diese Abonnement-Funktion werden wir uns später noch anschauen.

BluetoothLE lässt Produzenten die freie Wahl, welche dieser vordefinierten Services ein Gerät anbietet. Die Services 0x1800 ( Generic Access ) und 0x180A ( Geräte-Informationen ) sind aber praktisch immer implementiert, um sich gegenüber Gerätenutzern eindeutig identifizieren zu können.

Herstellerspezifische Services

Ein Blick in die Bluetooth-Spezifikation verrät uns, dass es keinen vordefinierten Service für die Funktion "Schritte zählen" im Armband gibt. Diesen Teil übernimmt in irgendeiner Form der Service mit dem 128-Bit-Schlüssel. Solche langen Schlüssel kommen immer zum Einsatz, wenn ein Gerät eigene Services und Schlüssel implementieren will, die nicht vom Standard abgedeckt sind.

Dieser Service besitzt ein Write-Characteristic und ein Read- und Notify-Characteristic. Vermutlich muss an das Write-Characteristic ein bestimmter Kommandowert oder eine Reihe von programmspezifischen Kommandos übergeben werden, worauf das Armband über das Notify-Characteristic das anrufende Programm mit den Daten versorgt. Das ist aber an dieser Stelle nur eine Vermutung. Um diese zu verifizieren, müssten wir tatsächlich die Roh-Kommunikation zwischen Armband und der zugehörigen Original-App verfolgen.

Das ist aber noch einmal ein ganzes Stück aufwendiger und würde endgültig den Rahmen dieses Artikels sprengen.

Noble und Bleno

Die Kosten für hardware-basierte Bluetooth-Entwicklerkits sind gerade in den vergangenen Monaten massiv gefallen, Adafruit hat zum Beispiel vor kurzem ein BluetoothLE-Modul(öffnet im neuen Fenster) für gerade einmal 20 Dollar vorgestellt.

Aber wer sowieso in einem Projekt einen Raspberry Pi oder vergleichbare Linux-basierte Boards einsetzt, könnte auch einen noch billigeren Bluetooth-Dongle einsetzen und muss nicht so maschinennah programmieren. Soweit die Theorie, doch leider ist der Bluetooth-Stack unter Linux namens BlueZ(öffnet im neuen Fenster) ein Trauerspiel – mehr dazu auf der letzten Seite des Artikels.

Durch Zufall sind wir aber auf zwei Bibliotheken gestoßen: Noble(öffnet im neuen Fenster) , um einen Rechner als Central-Modul in Skriptform zu nutzen, und Bleno(öffnet im neuen Fenster) , um ein Peripheral-Modul ebenfalls als Skript zu implementieren. Beide stammen vom gleichen Entwickler – und setzen auf Node.js auf. Sie funktionieren derzeit unter Linux und OS X. Unter Linux setzen sie direkt auf die HCI-Schnittstelle für Bluetooth auf und umgehen damit BlueZ, den Linux-Bluetooth-Stack, weitgehend. Mit diesen Bibliotheken und dem Wissen der vorangegangenen Seiten ist es recht einfach, mit BluetoothLE zu spielen.

Im folgenden Projekt werden wir sowohl ein Peripheral- als auch ein Central-Modul umsetzen. Das Peripheral-Modul wird über eine LED und einen Taster verfügen. Das Central-Modul wird den Zustand des Tasters überwachen, und wenn er gedrückt wird, die LED anschalten. Und er wird sie wieder ausschalten, wenn der Taster losgelassen wird.

Das Peripheral-Modul

Die Funktion des Peripheral wird ein Raspberry Pi übernehmen, an ihn schließen wir über dessen GPIO-Pins eine LED und einen Taster an. Dann entwickeln wir einen BluetoothLE-Service mit zwei Characterics: einen um die LED an- und auszuschalten und einen zweiten zum Auslesen des Taster-Status.

Dazu müssen wir mit Hilfe von Bleno den Service sowie die Characteristics implementieren und die notwendige Logik, um auf Schreib- beziehungsweise Leseanfragen zu reagieren. Das vollständige Javascript-Skript für Node.js kann von unserem Webserver unter http://www.golem.de/projekte/btle/bt_peripheral.js heruntergeladen werden.

Wir beginnen damit, Bleno in unserem Skript einzubinden und zwei Event-Handler zu registrieren.

        var bleno = require('bleno'); bleno.on('stateChange', function(state) {  if (state === 'poweredOn') {    bleno.startAdvertising('led', []);  } else {    bleno.stopAdvertising();  } }); bleno.on('advertisingStart', function(error) {  if (!error) {    bleno.setServices([ new LedService()    ]);  } });

Der Event 'stateChange' wird aufgerufen, wenn der Bluetooth-Dongle von Bleno angesprochen werden kann – oder auch nicht mehr. Wenn der Dongle bereit ist, versetzt startAdvertising() ihn in einem Modus, in dem er kontinuierlich auf sich aufmerksam macht. Der erste Parameter dieser Methode ist der Name, unter dem das Peripheral für andere Bluetooth-Geräte sichtbar ist.

Der zweite Event wird ausgelöst, wenn das Advertising erfolgreich gestartet werden konnte. Jetzt kann unser eigener Service initialisiert werden. Die Klasse LedService ist abgeleitet von bleno.PrimaryService . Im Konstruktor der Klasse setzen wir die UUID des Service und initialisieren die beiden Characteristics des Services.

        function LedService() {  LedService.super_.call(this, {    uuid: '5dfeb700bb8711e393430002a5d5c51b',    characteristics: [      new LedReadCharacteristic()      , new LedWriteCharacteristic()    ]  }); } util.inherits(LedService, bleno.PrimaryService);

Die UUID können wir selbst festlegen, sie sollte laut Bluetooth-Spezifikation 128 Bit lang sein. Eine einfache Generierungsmöglichkeit gibt es auf der Webseite der ITU(öffnet im neuen Fenster) .

Tasterwert lesen und senden

Das erste Characteristic LedReadCharacteristic liefert den Zustand des Tasters zurück. Diese Klasse wird von bleno.Characteristic abgeleitet.

        function LedReadCharacteristic() {  LedReadCharacteristic.super_.call(this, {    uuid: '6ee494e0bb8711e3891a0002a5d5c51b',    properties: ['read', 'notify'],    descriptors: [      new BlenoDescriptor({        uuid: '792e3fa0bb8711e390640002a5d5c51b',        value: 'Led read'      })    ]  }); } util.inherits(LedReadCharacteristic, bleno.Characteristic);

Auch hier vergeben wir wieder eine UUID, diese kann sich von der UUID des Services ableiten, muss das aber nicht. Der Descriptor-Eintrag ist eine optionale Textbeschreibung des Characteristic. Entscheidend für ein Characteristic sind aber dessen Eigenschaften, sie werden im properties -Eintrag angegeben. Die Eigenschaft "read" drückt aus, dass ein Central-Modul einen Wert vom Characteristic lesen kann.

Nun kann aber der Taster jederzeit gedrückt werden, ein Central-Modul müsste also ständig unser Peripheral abfragen. Um das zu vermeiden, setzen wir auch die Eigenschaft "notify". Ein Central-Modul kann dann dieses Characteristic abonnieren. Das bedeutet: Das Peripheral schickt von sich aus einen Wert an das Central-Modul – in unserem Fall, wenn sich der Zustand des Tasters ändert.

Für beide Eigenschaften müssen wir je eine Methode definieren, deren Logik sich allerdings deutlich unterscheidet.

        LedReadCharacteristic.prototype.onReadRequest = function(offset, callback) {  if (offset) {    callback(BlenoCharacteristic.RESULT_ATTR_NOT_LONG, null);  } else {  if(null != buttonValue) { callback(BlenoCharacteristic.RESULT_SUCCESS, buttonValue);    };    } };

Die Methode onReadRequest wird von Bleno aufgerufen, wenn ein Central-Modul einen Wert lesen will – und muss demzufolge einen Wert zurückliefern. Der Wert wird über eine Callback-Methode übermittelt, die unserer Methode als zweiter Parameter übergeben wird.

Diese Callback-Methode wird mit zwei Argumenten aufgerufen: dem Status der Antwort und dem eigentlichen zu übermittelnden Wert, Letzterer in Form eines Buffer-Objektes von Node.js. Zu der Frage, woher dieser Wert in der globalen Variable buttonValue kommt, und zur Aufgabe des Objekts kommen wir weiter unten.

Die offset -Variable hat für unsere Aufgabe keine weitere Bedeutung. Zum Verständnis: Bluetooth überträgt die Daten paketweise, wobei diese Pakete sehr klein sind. Um größere Datenmengen zu übertragen, müssen diese aufgeteilt werden. Deswegen kann die onReadMethode mit aufsteigenden Offsets aufgerufen werden, um die Portionen eines einzelnen Wertes – zum Beispiel bei einer langen Zeichenkette – stückweise zu übertragen. Da wir aber hier grundsätzlich nur einen einzelnen Wert von 8 Bit Länge übertragen, signalisieren wir bei Offsets größer 0, dass keine weiteren Datenstücke mehr vorhanden sind.

Für die Notify-Eigenschaft müssen wir erst einmal recht wenig tun:

        LedReadCharacteristic.prototype.onSubscribe = function(maxValueSize, updateValueCallback) { onButtonStateCallback = updateValueCallback; };

Die Variable updateValueCallback verweist auf eine Callback-Funktion, die wir aufrufen müssen, wenn es gilt, ein Central-Modul über einen neuen Wert zu informieren. Wir speichern diese Funkion in einer globalen Variable.

Den Zustand des Tasters erfragt alle 200 Millisekunden eine Funktion des Peripheral-Skriptes. Dazu nutzt es normale Dateioperationen und das dateibasierte Interface für die GPIO-Pin unter /sys/class/gpio . Der Artikel Hardware, die sich freut geht darauf intensiv ein, hier verwenden wir ein ähnliches Konzept, aber eben unter Node.js und Javascript statt Python.

Im Kern lesen wir den Wert der Datei /sys/class/gpio/gpio21/value ein, an Pin 21 ist der Taster angeschlossen. Ist der Taster gedrückt, erhalten wir "1", ansonsten "0". Diesen Zahlwert in einer Zeichenkette wandeln wir um in einen Integerwert, den wir wiederum einem Buffer-Objekt übergeben:

        var sc = fs.readFileSync('/sys/class/gpio/gpio21/value', 'utf-8'); var buffer = new Buffer(1); buffer.writeUInt8(parseInt(sc),0); buttonValue = buffer;

Der Grund für diese anscheinend umständliche Logik liegt in Node.js. Das kann zwar wunderbar mit UTF-8 umgehen, benötigt aber Hilfe bei binären Daten und Strömen. Dafür gibt es die Buffer-Klasse(öffnet im neuen Fenster) . Da der Bluetooth-Treiber mit binären Datenströmen arbeitet und bei einer schwachtypisierten Sprache wie Javascript der Datentyp im Skript nicht unbedingt eindeutig ist, müssen wir die Daten entsprechend über Buffer-Objekte kapseln.

Jedes Mal, wenn der Status des Tasters ermittelt wurde, wird verglichen, ob es eine Änderung zum vorherigen Status gab. Ist das der Fall, wird die Callback-Funktion des Notify aufgerufen. Dieser Methode muss lediglich der zu liefernde Wert als Buffer-Objekt übergeben werden.

        function buttonStateChanged() { if(null != onButtonStateCallback) { onButtonStateCallback(buttonValue); } }

LED an- und ausschalten

Der Code, mit dem ein Central-Modul einen Wert dem Peripheral übergeben kann, unterscheidet sich nicht wesentlich vom bisher gezeigten. Auch hier müssen wir das Characteristic definieren, setzen als Eigenschaft "write" und definieren eine zugehörige onWriteRequest -Methode:

        function LedWriteCharacteristic() {  LedWriteCharacteristic.super_.call(this, {    uuid: '6ee494e0bb8711e3891a0002a5d5c51c',    properties: ['write'],    descriptors: [      new BlenoDescriptor({        uuid: '792e3fa0bb8711e390640002a5d5c51c',        value: 'Led write'      })    ]  }); } util.inherits(LedWriteCharacteristic, BlenoCharacteristic); LedWriteCharacteristic.prototype.onWriteRequest = function(data, offset, withoutResponse, callback) { if (offset) {    callback(this.RESULT_ATTR_NOT_LONG); } var value = data.readUInt8(0); switchLed(value); };

Die Funktion switchLed() kapselt die Logik, um die LED an- bzw. auszuschalten. Ist der übergebene Wert 0, wird der LED-Pin auf logisch LOW geschaltet, jeder Wert über 0 schaltet ihn auf logisch HIGH – die LED erhält Strom und leuchtet.

Damit haben wir die wesentliche Logik für unser Peripheral-Modul. Um die LED zu schalten oder den Taster abzufragen, kann Lightblue oder nRF verwendet werden. Wenn das fehlerlos klappt, können wir den nächsten Schritt gehen.

Das Central-Modul

Die Bibliothek Noble funktioniert ganz ähnlich wie Bleno. Es beginnt mit der Suche nach BluetoothLE-Geräten und dem Aufruf unserer Funktion discover() , wenn das Gerät gefunden wurde.

        var noble = require('noble'); noble.on('discover', discover); noble.startScanning([], false);

Der startScaning() -Methode kann ein Array mit Service-UUIDs gegeben werden, um gezielt nur jene Peripheral-Module zu finden, die auch mit dem Central-Skript verwendet können. Darauf verzichten wir. Der zweite Parameter bestimmt, ob gefundene Peripherals mehrfach der discover() -Funktion gemeldet werden sollen ( false ) oder nur einmal ( true ).

        function discover(peripherial) {    peripherial.connect(function(error) {        if(error) {           return;        }        peripherial.discoverServices([], function(error, services) {            if(error) { return;    }   for(var i = 0, l = services.length; l > i; i++) {       var service = services[i];       if(SERVICE == service.uuid) {          handleService(service);       }     }        });  }); };

Die discover() -Methode ist recht übersichtlich. Findet Noble ein Peripheral, übergibt es der Methode ein entsprechendes Objekt. Über dieses Objekt verbinden wir uns mit ihm per connect() und fragen die verfügbaren Services ab. Haben wir eine Liste der Services erhalten, suchen wir nach dem Service, dessen UUID unserem oben definierten LedService entspricht. Ist die Suche erfolgreich, wird unsere Funktion handleService() aufgerufen.

Einen Tick schneller und energiesparender

Auch wenn diese Zeilen nicht allzu schwierig sind – es geht noch einfacher: Wenn das Peripheral den Service als Teil der Erkennung ausgibt, kann das Central-Modul bereits an dieser Stelle filtern. Im Peripheral-Skript muss die UUID im Array der Methode bleno.startAdvertising('led', ['Die_UUID']) angegeben werden. Dann kann im Central-Skript noble.startScanning(['Die_UUID'], false) ebenfalls die UUID des Services im Array angegeben werden und wir wissen sofort, ob es unser Peripheral ist. Eine Verbindung zu jedem erkannten Gerät und der nachfolgende Test auf die Services kann dann entfallen, es ist nur eine Verbindung zu unserem Gerät notwendig.

In unserer aktuellen Implementierung verzichten wir darauf, um die API besser vorzustellen.

Die Characteristics abfragen

Zurück zu unserer Central-Implementierung und der handleService() -Funktion: Wenn wir die Characteristics abfragen, speichern wir die entsprechenden Objekte, da wir sie im Folgenden unterschiedlich behandeln müssen.

Die globale Variable led_char wird uns dazu dienen, einen Wert zum Peripheral zu schicken ("schreiben") um die LED an- bzw. auszuschalten. Die globale Variable btn_char wird hingegen genutzt, um in der Funktion initButton() die Abonnement-Funktionalität für den Taster-Status zu aktivieren.

        function handleService(service) {   service.discoverCharacteristics([], function(error, characteristics) {      for(var i = 0, l = characteristics.length; i > l; i++) {         switch(characteristics[i].uuid) {            case LED_UUID :       led_char = characteristics[i];       break;    case BTN_UUID :       btn_char = characteristics[i];       initButton();       break;         }       }   }); }

Die aufgerufene Methode initButton ist keine Hexerei. Darin wird btn_char.notify(true) aufgerufen, um uns als Empfänger für den Taster-Status des Peripheral zu registrieren. Den Wert erhalten wir über eine Callback-Funktion beim Read-Event – technisch handelt Noble den Empfang einer Notify-Meldung und einer manuellen Lese-Anfrage gleich ab. Deswegen ist auch die Fallunterscheidung notwendig, um sicherzugehen, dass es sich um ein Notify handelt.

        function initButton() {   btn_char.notify(true);   btn_char.on('read', function(value, isNotification) { if(isNotification) {           setLed(value); }   }); }

Die Methode setLed() macht nun nichts anderes, als den empfangenen Wert vom Notify über led_char.write() an das Peripheral zu schicken. Da "zufällig" der Wert zum An- beziehungsweise Ausschalten identisch ist mit dem zum Status des Tasters, nämlich 0 oder 1, haben wir damit nicht viel Arbeit. Ansonsten müssten wir wieder mit Buffer-Objekten direkt arbeiten – wir erhalten Buffer bei Lese- und auch Notify-Aktionen und müssen Buffer nutzen, um Werte zu schicken.

        function setLed(value) {Öster led_char.write(value, false); }

Das vollständige Central-Skript kann unter http://www.golem.de/projekte/btle/bt_central.js herunter geladen werden.

Troubleshooting

Voraussetzung für den Einsatz der Bibliotheken ist ein BluetoothLE-fähiger Kernel, ab Kernel-Version 3.10 ist das üblicherweise der Fall. In einigen Fällen kann es sein, dass das Peripheral-Skript unter Linux schon ganz am Anfang aussteigt. Schuld daran ist ein – vermutlich – falsch compilierter Bluez-Daemon. Unter Umständen ist darin ein GATT-Plugin aktiviert, das eigentlich nur als Beispiel dienen soll, so aber einfach nur alle BluetoothLE-Kommunikation abfängt, ohne irgendetwas Sinnvolles zu tun. Die Lösung ist, den Bluez-Daemon gar nicht erst zu starten oder notfalls abzuschießen. Des weiteren sollte sichergestellt sein, dass der Bluetooth-Dongle des Rechners auch tatsächlich aktiv ist. Das geht auf der Kommandozeile mit hciconfig hci0 up .

Beim Entwickeln ist es uns auch häufiger passiert, dass sich Lightblue und nRF nach dem mehrfachen Ändern und Neustarten des Peripheral-Skriptes nicht mehr mit dem Raspberry Pi verbinden wollten. Wir konnten bis zum Schluss leider nicht klären, was genau die Ursache dafür war – ob es am Dongle oder den Bluetooth-Treibern des Rasberry Pi lag. Die Bleno-Bibliothek glauben wir als Ursache ausschließen zu können. Nur ein Reset des Raspberry Pi löste die Blockade. Im Dauertest mit unserem Central-Skript trat dieses Problem nicht auf.

Warum wir nicht BlueZ verwendet haben

Um die Jahreswende kam uns die Idee, nach dem Erfolg von Laika, dem freundlichen Bürohund , doch einmal ein Projekt mit Bluetooth – nicht BluetoothLE – zu machen. Unsere Idee hatte nichts mit Raketentechnik zu tun. An einen Raspberry Pi oder Intel Galileo wollten wir per GPIO-Pins ein paar Taster hängen, welche die Steuerung (zumindest Start und Stop) eines Musikplayers auf einem Smartphone übernehmen sollten.

Wir wussten zu dem Zeitpunkt nicht, dass wir damit einen Trip in die Programmiererhölle gebucht hatten. Auf dem Papier sah es ganz trivial aus. Es gibt unter Linux einen Bluetooth-Stack, genannt Bluez(öffnet im neuen Fenster) , dazu eine API und Tools. Des weiteren gibt es ein definiertes Bluetooth-Profil namens AVRCP, das genau unsere gewünschte Funktion umsetzt – und auch in Bluez implementiert ist.

In der Praxis ist Bluez aber schlicht eine Katastrophe. Wir könnten einen mehrseitigen Artikel darüber schreiben. Aber es spricht schon Bände, dass es bis auf eine Handvoll kurzer API-Beschreibungen keinerlei Dokumentation oder Einführungen gibt – nicht einmal gegen Geld von Dritten. Google hilft auch nicht weiter, Zehntausende von Suchergebnissen liefern im Prinzip immer die gleichen "magischen" fünf Kommandozeilenaufrufe, um ein Headset unter Linux zum Laufen zu bringen. Bluez wird übrigens seit fast zehn Jahren entwickelt!

Um es kurz zu machen: Bluez ist für Anwendungsentwickler praktisch unbrauchbar, wenn sie nicht bereit sind, die Bluetooth-Spezifikation(öffnet im neuen Fenster) durchzuarbeiten (Core-Spezifikation V4.0: 2.302 Seiten) und zusätzlich viel Zeit und Leidensfähigkeit mitzubringen, um die (Un-)Logik von Bluez halbwegs zu verstehen. Bluez ist nichts für ein Hobbyprojekt.


Relevante Themen