Dieses Tutorial bietet einen allgemeinen Ansatz zum Abrufen von Daten von Bluetooth Low Energy-Geraeten, wobei ein Herzfrequenz- und Blutsauerstoffmessgeraet als Beispiel verwendet wird.

Fuer dieses Tutorial verwende ich:

  • Medisana PM100 Connect
  • iPhone 11 Pro Max
  • iOS 14.4
  • MacBook Pro, 13-Zoll, 2016
  • macOS Big Sur 11.2.2
  • Xcode 12.4

Obwohl sich dieses Tutorial auf ein bestimmtes BLE-Geraet konzentriert, sollte es einfach an andere BLE-Geraete angepasst werden koennen.

Medisana PM100 Connect

Der vollstaendige Projektcode kann hier heruntergeladen werden.

Einfuehrung

Lange Zeit habe ich nach einem erschwinglichen Herzfrequenzmesser und Pulsoximeter gesucht, das sich per Bluetooth mit einem Smartphone verbindet.

Schliesslich fand ich den “PM100 Connect” von Medisana. Der Ausgangspunkt dieses Tutorials ist der CBCentralManager, der “ein Objekt ist, das Peripheriegeraete scannt, entdeckt, verbindet und verwaltet.”

Lass uns eines erstellen und verdrahten.

var centralManager: CBCentralManager?
centralManager = CBCentralManager(delegate: self, queue: nil)

Bei der Instanziierung muss ein Objekt bereitgestellt werden, das als Delegate fungiert.

Dieser Delegate muss centralManagerDidUpdateState(_ central: CBCentralManager) implementieren, damit die App pruefen kann, ob das Geraet, auf dem sie laeuft, tatsaechlich Bluetooth unterstuetzt.

func centralManagerDidUpdateState(_ central: CBCentralManager) {
	guard central.state ==.poweredOn else {
		logger.debug("No Bluetooth available")
  return
	}
	central.scanForPeripherals(withServices: nil, options: nil)
}

Wenn Bluetooth verfuegbar ist, scannt die App nach Peripheriegeraeten in der Naehe.

Du musst eine zusaetzliche Delegate-Methode implementieren, um festzustellen, welche Peripheriegeraete verfuegbar sind.

func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
	logger.debug("Found peripheral: \(peripheral.description)")
}

Beim Ausfuehren mit eingeschaltetem PM100 entdeckte ich schnell, dass es sich als “e-Oximeter” identifiziert.

Verbinden wir uns damit.

centralManager?.connect(peripheral, options: nil)

Wenn die Verbindung hergestellt wird, wird dies in dieser Delegate-Methode gemeldet:

func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
	logger.debug("Connected to peripheral: \(peripheral.description)")
}

Fertig! Wir haben uns erfolgreich mit dem PM100 verbunden (oder jedem anderen Geraet, das du fuer dieses Tutorial verwendest). Im naechsten Schritt gehen wir durch die Schritte, die erforderlich sind, um alle Daten zu entdecken, die das Geraet senden kann.

Services und Characteristics

Um herauszufinden, was das Bluetooth-Geraet (das Peripheriegeraet) anbietet, entdecke zuerst seine Services:

peripheral.discoverServices(nil)

Siehe die Dokumentation fuer CBService. Ein Service ist “eine Sammlung von Daten und zugehoerigen Verhaltensweisen, die eine Funktion oder ein Feature eines Geraets erfuellen.”

Implementieren Sie die entsprechende CBPeripheralDelegate-Methode, die aufgerufen wird, wenn Services entdeckt werden.

func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
	peripheral.services?.forEach { service in
		logger.debug("Discovered service: \(service.description)")
	}
}

Der PM100 bietet vier Services:

Discovered service: <CBService: 0x2816b8700, isPrimary = YES, UUID = Device Information>
Discovered service: <CBService: 0x2816b82c0, isPrimary = YES, UUID = Battery>
Discovered service: <CBService: 0x2816b84c0, isPrimary = YES, UUID = 1822>
Discovered service: <CBService: 0x2816b8300, isPrimary = YES, UUID = 0000FEE8-0000-1000-8000-00805F9B34FB>

Fuer jeden Service koennen wir die sogenannten Characteristics entdecken:

peripheral.discoverCharacteristics(nil, for: service)

Wir kommen dem Abrufen der Herzfrequenz- und Sauerstoffdaten mit CBCharacteristic naeher. Schauen wir uns die Dokumentation an:

“CBCharacteristic und seine Unterklasse CBMutableCharacteristic repraesentieren weitere Informationen ueber den Service eines Peripheriegeraets. Insbesondere repraesentieren CBCharacteristic-Objekte die Characteristics des Services eines entfernten Peripheriegeraets. Eine Characteristic enthaelt einen einzelnen Wert und eine beliebige Anzahl von Deskriptoren, die diesen Wert beschreiben. Die Eigenschaften einer Characteristic bestimmen, wie Sie den Wert einer Characteristic verwenden koennen und wie Sie auf die Deskriptoren zugreifen.”

Wir muessen die Delegate-Methode implementieren, um alle entdeckten Characteristics zu empfangen:

func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
	service.characteristics?.forEach { characteristic in
		logger.debug("Discovered characteristic: \(characteristic.description) for service \(service.uuid)")
	 }
}

Jetzt muessen wir die Bluetooth-Spezifikation konsultieren, um festzustellen, dass wir den Wert der Characteristic 2A5F innerhalb des Service 1822 wollen. Das koennen Sie auch googeln.

Wir moechten benachrichtigt werden, wenn das Geraet Herzfrequenz- und Sauerstoffdaten sendet. Daher muessen wir Folgendes aufrufen:

if (characteristic.uuid == CBUUID(string: "2A5F")) {
	peripheral.setNotifyValue(true, for: characteristic)
}

Rufen Sie schliesslich die Werte ab, indem Sie die letzte Delegate-Methode implementieren:

func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
	guard characteristic.service.uuid CBUUID(string: "1822"),
			 characteristic.uuid CBUUID(string: "2A5F"),
			 let data = characteristic.value else {
	    return
	}
 
	let numberOfBytes = data.count
	var byteArray = [UInt8](repeating: 0, count: numberOfBytes)
	(data as NSData).getBytes(&byteArray, length: numberOfBytes)
 
	logger.debug("Data: \(byteArray)")
}

Durch den Vergleich der Werte im byteArray mit der Anzeige meines PM100 war es einfach festzustellen, welcher Wert der Herzfrequenz und welcher der Sauerstoffsaettigung entsprach:

let oxygenation = byteArray[1]
let heartRate = byteArray[3]

Egal, welches Bluetooth-Geraet Sie interessiert, der Ansatz zur Entdeckung seiner Services und Characteristics ist immer derselbe.

Eigentlich sind wir noch nicht ganz fertig…

Beispielanwendung

Der vollstaendige Projektcode kann hier heruntergeladen werden.

Dieser Download enthaelt einen Xcode-Workspace zum Erstellen der voll funktionsfaehigen Herzfrequenzmesser-iOS-Anwendung.

Beispielanwendung

Vielen Dank fuers Lesen!

Ressourcen