Einführung in die BLE-Entwicklung mit iOS

Erste Schritte mit Core Bluetooth ab iOS 6
Kommentare

Core Bluetooth hat für typische Anwendungsfälle aus den Bereichen Healthcare, Heimvernetzung, Entertainment, Security und Sport/Fitness Einzug in iOS 5 gehalten. Selbst Themen wie abstandsabhängige Kommunikation zwischen Geräten werden mit dem relativ neuen Standard adressiert. In diesem Zuge entwickelt sich Bluetooth LE zu einer echten Alternative zu NFC und wird selbst für Szenarien wie mobiles Bezahlen oder die Spieleindustrie interessant.

Das in iOS 5 eingeführte Core-Bluetooth-Framework deckt die meisten Aspekte der Bluetooth-4.0-Low-Energy-(LE-)Spezifikation ab. Konnte man mit iOS 5 nur auf Bluetooth-LE-Geräte zugreifen, die Daten bereitstellen, so wurde mit iOS 6 das API erweitert, damit sich iOS-Geräte selbst als Datenlieferant anbieten können. Leider unterstützen nur die neueren iOS-Geräte und Macs die Bluetooth-LE-(BLE-)Spezifikation. Dazu gehören das iPhone 4S, iPhone 5, der Mac Mini, das „neue“ iPad, MacBook Air und MacBook Pro. Seit iOS 6 gibt es auch eine BLE-Unterstützung des iPhone-Simulators, die allerdings in der Praxis aufgrund der verschiedenen BLE Dongles recht problematisch ausfällt. Apple wird den Support des Simulators voraussichtlich in nächsten iOS-Versionen wieder einstellen. Alternativ zu echten iOS-Devices gibt es auch diverse Bluetooth-LE-Development-Kits, die Sensordaten wie Gyroscope, Temperatur, Accelerometer und Ähnliches anbieten. Am Bekanntesten ist momentan vermutlich das Sensor-Tag von Texas Instruments, das man für nur 25 US-Dollar erwerben kann. Das Tüfteln mit diesen externen Sensoren macht wirklich viel Spaß und beflügelt manch eine Idee im Kontext des „Internet of Things“ und der „Machine to Machine“-Vernetzung. Es liegt durchaus nahe, dass BLE ebenfalls die Grundlage für die viel spekulierte „iWatch“ bilden wird. Im Gegensatz zu WLAN oder dem klassischen Bluetooth ist Bluetooth LE nicht besonders gut geeignet, um große Datenmengen wie Bilder, Videos oder Dokumente zu übertragen, da sich nur geringe Transferraten realisieren lassen. Die Stärken von BLE liegen in der geringeren Sendeleistung – so kann ein BLE-Gerät, ausgestattet mit einer kleinen Knopfbatterie, leicht über mehrere Monate betrieben werden. Das wäre mit herkömmlichem Bluetooth oder WLAN undenkbar. Weiter ist es möglich, BLE-Geräte ad hoc miteinander zu verbinden, ohne mühselige Pairing-Orgien durchzuführen. Ein explizites Pairing ist nur für verschlüsselte Übertragungen notwendig.

Bluetooth LE verlangt keine Konformität zu MFI (Made for iPhone), sondern man kann auch ohne Zertifizierung eigene Peripherals erstellen, bzw. verwenden.

Die wichtigsten Frameworkklassen

Schauen wir uns die wichtigsten Klassen aus dem Core-Bluetooth-(CB-)Framework genauer an. Grundsätzlich wird zwischen Central und Peripheral unterschieden. Das gesamte Framework dreht sich um diese beiden Komponenten und regelt anhand von Delegate Callbacks den Austausch von Informationen und Daten zwischen ihnen. Eine Analogie findet man in Client-/Serverarchitekturen, bzw. im Provider-/Subscriber-Muster. Das Central übernimmt dabei die Rolle des Clients/Subscriber, der Daten abfragt bzw. benötigt. Das Peripheral enthält Daten, die angeboten werden, und spielt somit die Rolle eines Servers/Provider. Wie bereits erwähnt, kann ab iOS 6 ein Gerät sowohl Central als auch Peripheral sein, jedoch leider nicht beides zur selben Zeit! Die Central-Komponente wird durch die CBCentralManager-Klasse und die Peripheral-Komponente durch die CBPeripheralManager-Klasse repräsentiert. CBCentralManager-Objekte werden verwendet, um entdeckte oder verbundene Peripherals, die durch CBPeripheral-Objekte repräsentiert werden, zu verwalten. Der CBCentralManager dient insbesondere dazu, nach Peripherals zu scannen, diese zu entdecken und sich mit ihnen zu verbinden. Anders herum wird auf der Seite eines Peripherals durch ein CBCentral-Objekt der Client repräsentiert, der sich mit dem Peripheral verbunden hat. Man kann sich das so vorstellen, dass ein Peripheral nach außen hin über Broadcasts bekannt gibt, Daten anzubieten. In diesem Angebot können so genannte Services enthalten sein. Auf der Gegenseite scannt ein Central die Umgebung nach vorhandenen Services. Sobald ein gesuchter Service gefunden wird, kann der Central eine Verbindung zum Peripheral aufbauen. Ist diese Verbindung erfolgreich aufgebaut, können die beiden Geräte Daten untereinander austauschen. Die Daten für den Austausch sind in Services organisiert, die wiederum aus verschiedenen so genannten Characteristics bestehen. Eine Characteristic ist im Grunde ein definierter Attributtyp, der einen logischen Wert enthält. Zahlreiche Services und Characteristics sind mittlerweile standardisiert, sodass auch herstellerunabhängig beispielsweise ein Heartrate-Monitor durch einen gleichen Service oder zumindest gleiche Characteristics seine Daten anbietet. Auf der Central-Seite wird ein Service durch eine CBService-Klasse repräsentiert. Jeder dieser Services enthält eine Liste von Characteristics, jeweils repräsentiert durch eine CBCharacteristics-Klasse. Auf gleiche Weise werden Services und Characteristics auf Seiten des Peripherals durch ihre erzeugbaren, bzw. veränderbaren Pendants, CBMutableService und CBMutableCharacteristic umgesetzt. Sowohl ein Service als auch eine Characteristic müssen eindeutig durch eine UUID identifizierbar sein. Das Core-Bluetooth-Framework stellt hierfür die Hilfsklasse CBUUID bereit. In Abbildung 1 wird der Zusammenhang der Kernkomponenten nochmals illustriert.

Abb. 1: Die wichtigsten Kernkomponenten in Core Bluetooth

Im Folgenden werden anhand eines einfachen Beispiels die grundlegenden Mechanismen von Core Bluetooth in der Anwendung vorgestellt. Dazu erstellen wir ein Peripheral, das einen Service anbietet, sowie ein Central, das sich ab einer bestimmten räumlichen Nähe zum Peripheral verbindet und die bereitgestellten Daten abruft. Idealerweise probiert man das Beispiel mit zwei BLE-fähigen iOS-Geräten aus. In diesem Beispiel kapseln wir die Client-, bzw. Serverfunktionalität in zwei eigene Klassen (BizCardServer => Peripheral und BizCardClient => Central).

Aufmacherbild: Steps of success von Shutterstock / Urheberrecht: CGinspiration

[ header = Seite 2: Ein eigenes Peripheral bauen ]

Ein eigenes Peripheral bauen

Die im Folgenden aufgeführten Schritte sind notwendig, um ein Peripheral zu erstellen:

• Einen CBPeripheralManager erzeugen und starten
• Einen CBMutableService mit CBMutableCharacteristic erzeugen
• Service bekanntgeben (advertise)
• Mit der Central verbinden und Daten austauschen
• Im Xcode-Projekt muss als Allererstes das CoreBluetooth.framework unter LINKED FRAMEWORK AND LIBRARIES eingebunden werden

In unserem BizCardServer (Listing 1) importieren wir die Core-Bluetooth-Framework-Header (<CoreBluetooth/CoreBluetooth.h>). Damit wir auf die Callbacks in der Rolle als Peripheral reagieren können, müssen wir das entsprechende CBPeripheralManagerDelegate-Protokoll implementieren. Zusätzlich legen wir Properties für den Peripheral-Manager, den zu erstellenden Service und die beinhaltende Characteristic an. Ebenso benötigen wir für das Beispiel ein dataToSend-Property, das die anzubietenden Daten in einem NSData Object hält, sowie eine sendDataIndex als Hilfszähler, um auch Datenmengen versenden zu können, die nicht in einem einzigen „Chunk“ übertragen werden können, sondern auf mehrere Datenpakete aufgeteilt werden müssen.

#import "BizCardServer.h"
#import <CoreBluetooth/CoreBluetooth.h>

#define NOTIFY_MTU      20

@interface BizCardServer ()  <CBPeripheralManagerDelegate>

@property (nonatomic, strong) CBPeripheralManager *peripheralManager;
@property (nonatomic, strong) CBMutableService *cardService;
@property (nonatomic, strong) CBMutableCharacteristic *cardCharacteristic;


@property (strong, nonatomic) NSData *dataToSend;
@property (nonatomic, readwrite) NSInteger sendDataIndex;

@end

In der init-Methode initialisieren wir den Peripheral-Manager. Das erste Argument setzt das delegate (in unserem Fall self). Im zweiten Argument kann eine Queue angegeben werden, falls zum Beispiel der Peripheral-Manager nicht im Main Thread laufen soll. Wir erzeugen in unserem Beispiel eine eigene BLE_Queue-Queue mittels Grand Central Dispatch und übergeben diese als Argument (Listing 2).

// First create the peripheral manager to act as server (manager) for data
-(id)init
{
  if (self = [super init]) {
    _peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:dispatch_queue_create("BLE_Queue", nil)];
  }

  return self;
}

Sobald der Peripheral-Manager initialisiert wurde, müssen wir seinen Zustand überprüfen (Listing 3). Dies ist notwendig, um sicherzustellen, dass unsere App auch tatsächlich auf einem Bluetooth-LE-fähigen Gerät läuft. Dazu implementieren wir die Delegate-Methode peripheralManagerDidUpdateState:. Hier können wir auch ggf. passende Infomeldungen an den User weitergeben, falls Bluetooth im System nicht aktiviert wurde oder nicht verfügbar ist.

// Check if Bluetooth LE is available and setup service
- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral
{
  switch (peripheral.state) {
    case CBPeripheralManagerStatePoweredOn:
      [self setupService];
      break;
    default:
      NSLog(@"Peripheral Manager did update state");
      UIAlertView *alert = [[UIAlertView alloc] initWithTitle:
        @"Bluetooth Info" message:@"Bluetooth LE is not available or activated"
        delegate:nil
        cancelButtonTitle:@"OK"
        otherButtonTitles:nil, nil];
      
; break; } }

Im Falle des Status CBPeripheralManagerStatePoweredOn wissen wir, dass BLE verfügbar ist, und wir können unseren anzubietenden Service erzeugen ([self setupService], Listing 4). Die weiteren validen States findet man in der Core-Bluetooth-Framework-Referenzdokumentation.

// Construct a new service and publish it
- (void)setupService
{
  // Create characteristic
  self.cardCharacteristic = [[CBMutableCharacteristic alloc] 
                 nitWithType:[CBUUID
                 UUIDWithString:kCardCharacteristicUUID]
                 properties:CBCharacteristicPropertyNotiy
                 value:nil
                 permissions:CBAttributePermissionsReadable];

  // Create service and add characteristic
  self.cardService = [[CBMutableService alloc] initWithType:
                     [CBUUID UUIDWithString:kCardServiceUUID] primary:YES];

  // Set the characteristic for service
  self.cardService.characteristics = @[self.cardCharacteristic];

  // Publish service
  [self.peripheralManager addService:self.cardService];
}

Jeder Service und jede Characteristic wird durch eine 16- bzw. 128-Bit-UUID identifiziert. Im Falle von eigenen Anwendungen muss man eine 128-Bit-UUID verwenden und sollte dafür sorgen, dass es keine Kollisionen mit anderen UUIDs von anderen Services bzw. Characteristics gibt. Die 16-Bit-UUIDs werden durch die Bluetooth-SIG vergeben. Als Hilfe kann man im OSX-Terminal einfach das Kommando uuidgen aufrufen, um eigene eindeutige Identifier zu erzeugen.

In unserem Fall benötigen wir zwei UUIDs: eine für den Service und eine für die Characteristic (Listing 5).

#ifndef BizCardService_h
#define BizCardService_h


#define kCardServiceUUID          @"E07F0AE8-00BB-47F5-94F4-4A7174EA3032"
#define kCardCharacteristicUUID   @"9D534E8B-2153-4F6D-B2B0-8C68665A7274"

#endif

In Listing 4 sieht man die Erzeugung eines Service. Als Erstes wird jedoch eine Characteristic erzeugt und die CBUUID, die unserer selbst generierten UUID entspricht, als ersten Parameter der initWithType:properties:value:permissions-Methode übergeben. Als drittes Argument wird nil übergeben, was so viel bedeutet, wie dass sich der Wert für diese Characteristic dynamisch ergibt und nicht schon statisch feststeht. Das entspricht dem generellen Vorgehen beim Erzeugen von dynamischen Daten. Falls der Wert jedoch sowieso immer konstant bleibt, spricht natürlich auch nichts dagegen, diesen hier fest anzugeben. Der zweite Parameter der init-Methode bestimmt, in welcher Form der Wert verwendet werden soll. Ein paar der wichtigsten Möglichkeiten sind im Folgenden aufgelistet:

• CBCharacteristicPropertyBroadcast: Erlaubt den Broadcast eines Characteristic-Werts durch Verwendung des characteristic configuration descriptor.
• CBCharacteristicPropertyRead: Erlaubt das Lesen des Characteristic-Werts.
• CBCharacteristicPropertyWriteWithoutResponse: Ermöglicht das Schreiben des Characteristic-Werts, jedoch ohne Antwort.
• CBCharacteristicPropertyWrite: Ermöglicht das Schreiben des Characteristic-Werts.
• CBCharacteristicPropertyNotify: Ermöglicht die Benachrichtigung bei Änderungen des Characteristic-Werts ohne Antwort.

Des Weiteren sind Konstanten, bzw. Parameter für Verschlüsselung und Signierung von Characteristic-Wertänderungen verfügbar. Für diese weiterführenden Themen findet man die Details in der Core-Bluetooth-Developer-Dokumentation. Das letzte Argument der init-Methode sind die Lese-, Schreibe- und Verschlüsselungs- Restriktionen für Attribute. Mögliche Werte sind:

• CBAttributePermissionsReadable
• CBAttributePermissionsWriteable
• CBAttributePermissionsReadEncryptionRequired
• CBAttributePermissionsWriteEncryptionRequired

Nachdem die Characteristic erzeugt wurde, wird der Service erstellt. Auch dieser erhält eine CBUUID, die wir zuvor selbst generiert haben. Diesem Service fügen wir eine Liste der Characteristics hinzu, um die hierarchische Struktur aufzubauen (Abb. 2). Am Ende der Methode sagen wir dem Peripheral-Manager, dass dieser Service dem Peripheral hinzugefügt werden soll.

Abb. 2: Struktur von Services und Characteristics

[ header = Seite 3: Fortsetzung – Ein eigenes Peripheral bauen ]

Durch das Hinzufügen des Service zum Peripheral-Manager wird der nächste Delegate Callback peripheralManager:didAddService:error ausgelöst. Oft wird an dieser Stelle das Bekanntmachen (Advertising) des Service gestartet. In unserem Beispiel wollen wir das explizit durch eine Nutzeraktion starten können und haben dazu eine eigene Methode startAdvertisingBizCardService und stopAdvertisingBizCardService erzeugt, die unsere BizCardServer-Klasse auch im Header File publik macht (in Listing 6 nicht explizit gezeigt).

// When service is added start advertising it via Bluetooth LE
- (void)peripheralManager:(CBPeripheralManager *)peripheral
        didAddService:(CBService *)service
        error:(NSError *)error
{
  if (error == nil) {
    NSLog(@"Service Setup (not advertised yet!)");
  } else {
    NSLog(@"Error adding service: %@", [error localizedDescription]);
  }
}

// Public method 
- (void)startAdvertisingBizCardService
{
  // Start advertising service
  [self.peripheralManager startAdvertising:
    @{CBAdvertisementDataLocalNameKey:@"BCS",
    CBAdvertisementDataServiceUUIDsKey  :
    @[[CBUUID UUIDWithString:kCardServiceUUID] ] }];
}

// Public method
- (void)stopAdvertisingBizCardService
{
  // Stop advertising service
  [self.peripheralManager stopAdvertising];
}

Das Advertising erfolgt durch periodische Broadcasts des Peripherals. Um die initiale Erkennung zu beschleunigen, aber auch gleichzeitig die Batterie zu schonen, werden nach dem Starten des Advertisings die Pakete in kurzen Zeitabständen versendet und nach einiger Zeit werden diese Intervalle automatisch vergrößert. Das gleiche Vorgehen trifft auf das Scanning nach Devices durch das Central zu. Befindet sich die App im Vordergrund, erfolgt das Advertising des Peripherals sowie das Scanning auf Central-Seite mit höherer Intensität, als wenn sich die App im Hintergrund befindet. Man sollte also beachten, dass sich die Zeiträume für das Entdecken und Verbinden von BLE-Geräten im Background verlängern. Core Bluetooth verfügt über ein Caching der Characteristics (nicht der Werte), um das Entdecken und Analysieren der Services zu beschleunigen.

Das Advertise-Paket, das maximal 31 Bytes an Daten/Informationen enthalten kann, liefert neben der Service-UUID auch eine Information zur Signalstärke (RSSI), die verwendet werden kann, um die Distanz zwischen den Geräten zu bestimmen. Es handelt sich dabei um einen ungenauen Wert, der lediglich helfen kann, zwischen nah, mittelweit und ganz weit zu unterscheiden, wobei die Grenzen je nach Geräten und „Wetterlage“ und Umgebungsparametern (z. B. Luftfeuchtigkeit, Temperatur, elektromagnetische Störquellen) schwanken können.

Nachdem der Peripheral-Manager begonnen hat, den Service bekannt zu machen (Advertising). empfängt er über seine Delegate-Methoden die Nachricht peripheralManager:didStartAdvertising:. Sobald sich ein Central mit dem Peripheral verbunden hat, bekommt man den centralDidConnect-Callback des CBPeripheralManagerDelegate. Üblicherweise gefolgt von Read Requests für den Wert einer Characteristic, die durch den Callback didReceiveReadRequest signalisiert werden. Der CBPeripheralManager antwortet in der Regel mit respondToRequest auf die Anfragen und liefert somit die gewünschten Daten, bis der Central die Verbindung beendet, was durch einen Aufruf des centralDidDisconnect aus dem Delegate-Protokoll auf Seiten des Peripherals sichtbar wird.

Eine andere, oftmals bessere Möglichkeit, ist es, den sog. Subscription/Notify-Mechanismus zu verwenden, bei dem ein Central über angebotene Daten und deren Änderungen automatisch benachrichtigt wird. Hat sich ein Central für einen Service, bzw. für eine Characteristic subscribed (siehe BizCardClient), wird dies über einen Callback der peripheralManager:central:didSubscribeToCharacteristic: Delegate-Methode bekannt gemacht. An dieser Stelle kann dann der dynamische Inhalt erstellt und per updateValue:forCharacteristic:onSubscribedCentrals: versendet werden. Sollte die Größe der Daten 20 Bytes überschreiten (NOTIFY_MTU), können diese nicht mit einem Mal übertragen werden und der Datenstrom muss in Häppchen („Chunk“) übertragen werden. In unserem Beispiel wird ein EOM als Signal für das Ende einer Nachricht gesendet (Listing 7). Das Zerteilen der Nachricht entspricht weitestgehend dem BTLE-Beispielprojekt von Apple, wobei es hier natürlich auch „schönere“ Wege gibt, um diesen Mechanismus zu realisieren.

// Recognize when the central unsubscribes
- (void)peripheralManager:(CBPeripheralManager *)peripheral
        central:(CBCentral *)central
        didUnsubscribeFromCharacteristic:(CBCharacteristic *)characteristic
{
  NSLog(@"Central unsubscribed from characteristic");
}

// Catch when someone subscribes to our characteristic, then start sending them data
- (void)peripheralManager:(CBPeripheralManager *)peripheral
        central:(CBCentral *)central
        didSubscribeToCharacteristic:(CBCharacteristic *)characteristic
{

  if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:kCardCharacteristicUUID]]) {
    NSLog(@"Central subscribed to kCardCharacteristicUUID");
    
    // Create some sampel data --> could be provided dynamically!
    self.dataToSend = [@"{'firstName' : 'Peter', 'lastName' :
      'Pan','organization' : 'New kids on the block', 'location' :
      'Neverland','contact' : 'pp@neverland.org'}"
      dataUsingEncoding:NSUTF8StringEncoding];

    // Reset index
    self.sendDataIndex = 0;

    // Start sending data
    [self sendData];
  }
}

// Sends the next amount of data to the connected central
- (void)sendData
{
  // First up, check if we're meant to be sending an EOM
  static BOOL sendingEOM = NO;

  if (sendingEOM) {
    // send EOM
    if ([self.peripheralManager updateValue:
        [@"EOM" dataUsingEncoding:NSUTF8StringEncoding]
        forCharacteristic:self.cardCharacteristic
        onSubscribedCentrals:nil]) {
      // Mark it as sent
      sendingEOM = NO;
      NSLog(@"Sent: EOM");
    } else {
      // It didn't send, so we'll exit and wait for
      // peripheralManagerIsReadyToUpdateSubscribers to call sendData again
      return;
    }
  }
  // We're not sending an EOM, so we're sending data
  // Data left to send?
  if (self.sendDataIndex >= self.dataToSend.length) {
    return; // No data left.  Do nothing
  }

  // There's data left, so send until the callback fails, or we're done.
  BOOL didSend = YES;

  // Make the next chunk
  while (didSend) {
    // How big should chunk be
    NSInteger amountToSend = self.dataToSend.length - self.sendDataIndex;

    // Can't be longer than 20 bytes
    if (amountToSend > NOTIFY_MTU) {
      amountToSend = NOTIFY_MTU;
    }

    // Copy out the data we want
    NSData *chunk = [NSData dataWithBytes:
                     self.dataToSend.bytes+self.sendDataIndex 
                     length:amountToSend];

    // Send next chunk of data
    if ( [self.peripheralManager updateValue:
          chunk forCharacteristic:self.cardCharacteristic 
          onSubscribedCentrals:nil] ) {

      NSString *stringFromData = [[NSString alloc] initWithData:
                                  chunk encoding:NSUTF8StringEncoding];
      NSLog(@"Sent: %@", stringFromData);

      // Update our index
      self.sendDataIndex += amountToSend;

      // Was it the last one?
      if (self.sendDataIndex >= self.dataToSend.length) {
        // It was - send an EOM
        // Set this so if the send fails, we'll send it next time
        sendingEOM = YES;
        // Send EOM
        if ([self.peripheralManager updateValue:
            [@"EOM" dataUsingEncoding:NSUTF8StringEncoding]
            forCharacteristic:self.cardCharacteristic
            onSubscribedCentrals:nil]) {
          // we're all done
          sendingEOM = NO;
          NSLog(@"Sent: EOM");
        } else {
          return;
        }
      }
    } else {
      // Didn't work - drop out and wait for the callback
      return;
    }
  }
}
// This callback comes in when the PeripheralManager is ready to send the next chunk of data. This is to ensure that packets will arrive in the order they are sent.
- (void)peripheralManagerIsReadyToUpdateSubscribers:
        (CBPeripheralManager *)peripheral
{
  // Start sending again
  [self sendData];
}

Die Interaktion zwischen Central und Peripheral sowie der Datenaustausch können auch mit entsprechenden BackgroundModes in der info.plist im Hintergrund durchgeführt werden. Apple empfiehlt jedoch, die Apps so zu gestalten, dass das Scannen und Verbinden über ein Start/Stop-Konzept für einen Nutzer aktiv und sichtbar umgesetzt wird, da ansonsten durch die Hintergrundtätigkeiten der Anwendung der Energie- und Datenbedarf des Geräts schnell sehr hoch ausfallen kann, was wiederum zu einer schlechten „User Experience“ führt.

[ header = Seite 4: Ein Central bauen ]

Ein Central bauen 

Nachdem wir mit unserem Peripheral als Server-Daten anbieten, müssen wir noch ein Central als Client erstellen, der diese Daten konsumiert. Dazu implementieren wir in unserem Central (gekapselt durch BizCardClient) das CBCentralManagerDelegate– sowie das CBPeripheralDelegate-Protokoll, um die entsprechenden Callbacks für das Set-up des Centrals, den Verbindungsaufbau und die Interaktion mit dem Peripheral zu erhalten.

Als Properties merken wir und einen CBCentralManager sowie das verbundene CBPeripheral und ebenfalls ein NSMutableData-Property, das die empfangenen Daten aufnimmt. Analog zum Peripheral zuvor wird in der init-Methode der Central-Manager erzeugt, self als Delegate gesetzt und eine GCD-Queue angegeben, in der das Central ablaufen soll, da ansonsten bei nil die Main Queue verwendet wird. Sobald der Central-Manager initialisiert ist, kann sein Status überprüft werden, um ebenso sicherzustellen, dass das Gerät BLE unterstützt. Wie beim Peripheral erfolgt dies durch die Implementierung einer Delegate-Methode centralManagerDidUpdateStatus: (Listing 8). Oft wird hier bereits der Scan nach Peripherals gestartet. In unserem Beispiel haben wir diesen Schritt um den Scan zu starten in eine eigene public-Methode verlagert, um den Scan durch den User direkt start- und stopbar zu machen. Die scanForPeripheralsWithServices:options-Methode wird verwendet, um das Scanning nach Advertisements des Peripherals zu starten. Als Argument kann eine Liste der spezifischen Services (UUIDs) mitgegeben werden, um die Suche einzuschränken.

#import "BizCardClient.h"
#import <CoreBluetooth/CoreBluetooth.h>

#define kUpperProximityRSSILimit -15
#define kLowerProximityRSSILimit -32

@interface BizCardClient ()  <CBCentralManagerDelegate, CBPeripheralDelegate>

@property (nonatomic, strong) CBCentralManager *centralManager;
@property (nonatomic, strong) CBPeripheral *connectedPeripheral;
@property (nonatomic, strong) NSMutableData *data;

@end

@implementation BizCardClient

// First create the central manager to act as client (manager) for data
-(id)init
{
  if (self = [super init]) {
    _centralManager = [[CBCentralManager alloc] initWithDelegate:self
                        queue:dispatch_queue_create("BLE_Queue", nil)];
    _data = [[NSMutableData alloc] init];
  }

  return self;
}

- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
  switch (central.state) {
    case CBCentralManagerStatePoweredOn:
      //[self scan]; // We'll make the scanning startable by the user
      NSLog(@"Central Manager ready to scan for services");
      break;
    default:
      NSLog(@"Central Manager did change state");
      break;
  }
}

- (void)scan
{
  [self.centralManager scanForPeripheralsWithServices:
    @[ [CBUUID UUIDWithString:kCardServiceUUID] ]
  options:@{CBCentralManagerScanOptionAllowDuplicatesKey : @YES }];
}

Wenn ein Peripheral entdeckt wird, erfolgt ein Aufruf des Delegates central:didDiscoverPeripheral:advertismentData:RSSI: mit den Advertisement-Daten und dem RSSI-Signalstärkewert (Received Signal Strength Indicator). Letzterer kann verwendet werden, um sich nur mit einem Gerät zu verbinden, das sich in einem gewünschten Abstand befindet (Listing 9).

- (void)centralManager:(CBCentralManager *)central 
        didDiscoverPeripheral:(CBPeripheral *)peripheral
        advertisementData:(NSDictionary *)advertisementData
        RSSI:(NSNumber *)RSSI {

  // Reject any where the value is above reasonable range
  // Reject if the signal strength is too low to be close enough (Close is around -22dB)
  if (RSSI.integerValue > kUpperProximityRSSILimit || RSSI.integerValue < kLowerProximityRSSILimit) {
    NSLog(@"Too far or too close RSSI: %@", RSSI);
    return;
  }
  
  // Stops scanning for peripheral if close enough
  [self.centralManager stopScan];

  if (self.connectedPeripheral != peripheral) {
    self.connectedPeripheral = peripheral;
    NSLog(@"Connecting to peripheral %@", peripheral);
    NSLog(@"Advertisement Data %@ : %@",
      [advertisementData objectForKey:CBAdvertisementDataLocalNameKey],
      [advertisementData objectForKey:CBAdvertisementDataServiceUUIDsKey]);
    NSLog(@"RSSI: %@", RSSI);

    // Connects to the discovered peripheral
    [self.centralManager connectPeripheral:peripheral options:nil];
  }
}

- (void)centralManager:(CBCentralManager *)central
        didConnectPeripheral:(CBPeripheral *)peripheral
{
  NSLog(@"didConnectPeripheral");

  // Clears the data
  [self.data setLength:0];
  // Set peripheral delegate
  peripheral.delegate = self;
  // Asks peripheral to discover service
  [peripheral discoverServices:@[ [CBUUID UUIDWithString:kCardServiceUUID] ]];
}

- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
{
  if (error) {
    NSLog(@"Error connecting to peripheral: %@", [error localizedDescription]);
    [self cleanup];
    return;
  }
}

- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
{
  if (error) {
    NSLog(@"Error disconnecting peripheral: %@", [error localizedDescription]);
  }
  [self cleanup];
  self.connectedPeripheral = nil;
  // Disconnected ==> start scanning again
  //[self scan];
}

[ header = Seite 5: Fortsetzung – Ein Central bauen ]

Hat man ein passendes Peripheral gefunden, wird eine Verbindung über die Central-Manager-Methode connectPeripheral:options: hergestellt. Mit dem Optionsparameter kann gesteuert werden, ob das Connect und Disconnect mit einem Peripheral zu weiteren Notifications/Alerts führen soll. Dieser Connect-Aufruf mündet in einen Delegate Callback didConnectPeripheral:. Ist eine Verbindung zu dem Peripheral hergestellt, wird dieses mittels discoverServices: mit optionaler Liste der Service-UUIDs als Parameter gefragt, die zum Peripheral gehörenden Services zu finden (Listing 9). Analog zum Entdecken des Peripherals wird ein gefundener Service durch einen asynchronen Delegate Callback peripheral:didDiscoverService: und eine gefundene Characteristic durch den Delegate Callback peripheral:didDiscoverCharacteristics: signalisiert. In letzterem Fall sieht man im Beispielcode (Listing 10), dass die von uns gewünschte (und vom Peripheral bereitgestellte) Characteristic durch setNotifyValue:forCharacteristic: „subscribed“ wird.

- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
{
  if (error) {
    NSLog(@"Error discovering service: %@", [error localizedDescription]);
    [self cleanup];
    return;
  }

  for (CBService *service in peripheral.services) {
    NSLog(@"Service found with UUID: %@", service.UUID);

    // Discovers the characteristics for a given service
    if ([service.UUID isEqual:[CBUUID UUIDWithString:kCardServiceUUID]]) {
      [peripheral discoverCharacteristics:@[[CBUUID UUIDWithString:kCardCharacteristicUUID]]
      forService:service];
    }
  }
}

- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
{
  if (error) {
    NSLog(@"Error discovering characteristic: %@", [error localizedDescription]);
    [self cleanup];
    return;
  }
  
  if ([service.UUID isEqual:[CBUUID UUIDWithString:kCardServiceUUID]]) {
    for (CBCharacteristic *characteristic in service.characteristics) {
      if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:kCardCharacteristicUUID]]) {
        // Subscribe to characteristic
        [peripheral setNotifyValue:YES forCharacteristic:characteristic];
      }
    }
  }
}

Dadurch erfolgt bei Änderungen des Werts auf Seiten des Peripherals in unserem Central ein Callback der Delegate-Methode peripheral:didUpdateNotificationStateForCharacteristic:error: sowie ein Aufruf von peripheral:didUpdateValueForCharacteristic:error: , der die empfangenen Chunks für die „subscribed“ Characteristic entgegennimmt und in unserem Beispiel zu einem gesamten NSData-Objekt zusammenfügt, bis das EOM als Terminierungssignal empfangen wird (Listing 11). Im Beispiel trennen wir danach auch die Verbindung zum Peripheral mittels cancelPeripheralConnection: und „unsubscriben“ uns von der Characteristic.

// This callback lets us know more data has arrived via notification on the characteristic
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
  if (error) {
    NSLog(@"Error discovering characteristics: %@", [error localizedDescription]);
    [self cleanup];
    return;
  }

  NSString *stringFromData = [[NSString alloc] initWithData:characteristic.value encoding:NSUTF8StringEncoding];

  // Have we got everything we need?
  if ([stringFromData isEqualToString:@"EOM"]) {

    // We have, so show the data
    NSString *receivedData = [[NSString alloc] initWithData:self.data encoding:NSUTF8StringEncoding];
    NSLog(@"Received Data so far: %@", receivedData);

    // Cancel our subscription to the characteristic
    [peripheral setNotifyValue:NO forCharacteristic:characteristic];

    // and disconnect from the peripehral
    [self.centralManager cancelPeripheralConnection:peripheral];

    dispatch_async(dispatch_get_main_queue(), ^{
      UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Received" message:@"Data arrived" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
      
; }); } // Otherwise, just add the data on to what we already have [self.data appendData:characteristic.value]; // Log it NSLog(@"Received: %@", stringFromData); } // The peripheral letting us know whether our subscribe/unsubscribe happened or not - (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error { if (error) { NSLog(@"Error changing notification state: %@", error.localizedDescription); } // Exit if it's not the transfer characteristic if (![characteristic.UUID isEqual:[CBUUID UUIDWithString:kCardCharacteristicUUID]]) { return; } // Notification has started if (characteristic.isNotifying) { NSLog(@"Notification began on %@", characteristic); } // Notification has stopped else { // so disconnect from the peripheral NSLog(@"Notification stopped on %@. Disconnecting", characteristic); [self.centralManager cancelPeripheralConnection:peripheral]; } }

Damit haben wir auch schon alle Schritte durchgeführt, um Daten zwischen zwei BLE-Geräten abhängig von der Distanz zueinander auszutauschen.

Wie man sieht …

Wie man sieht, macht es das Core-Bluetooth-Framework einem Entwickler sehr einfach, die Funktionalität in eigene Apps zu integrieren und auch verschiedenste bereits vorhandene BLE-Endgeräte anzubinden, oder vielleicht sogar auch als „Bastler“ mit z. B. Raspberry Pi und Bluetooth LE Dongle eigene externe Sensoren und Steuer-/Regelmechanismen zu entwerfen. Die zahlreichen „Kaskaden“ über die diversen Delegate-Aufrufe durchzuführen, macht den Code leider nicht sehr übersichtlich, aber hier gibt es bereits erste Frameworks, die versuchen, durch Block-APIs zu verbessern. Die Bluetooth-LE-Technologie und Core Bluetooth als Framework stehen gemessen an den Möglichkeiten sicher noch ganz am Anfang und haben ein enormes Potenzial für weitere fantasievolle Anwendungsfälle. Unserer Meinung nach handelt es sich dabei um ein Zukunftsthema, dass man als iOS-Entwickler auf jeden Fall auf dem „Radar“ behalten sollte.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -