Dieses Tutorial bietet einen allgemeinen Ansatz zur Extraktion von Daten aus Bluetooth Low Energy (BLE)-Geraeten, am Beispiel eines Herzfrequenz- und Blutsauerstoffmonitors.

Fuer dieses Tutorial verwende ich:

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

Medisana PM100 Connect

Einfuehrung

Lange Zeit habe ich nach einem erschwinglichen Blutsauerstoffmessgeraet gesucht, das sich per Bluetooth mit einem Smartphone verbinden kann.

Endlich hatte ich Glueck und fand das “PM100 Connect” von Medisana.

Das Unternehmen behauptet, es habe eine App namens “VitaDock”, die Daten vom Geraet erfasst und anzeigt.

Um es kurz zu machen: Ich konnte das Geraet nicht mit dieser App zum Laufen bringen. Beim Lesen von Bewertungen auf Amazon entdeckte ich, dass ich nicht der Einzige mit Problemen war.

Ich war frustriert und kurz davor, es zurueckzugeben, als mir klar wurde, dass dies eine ausgezeichnete Gelegenheit war, Apples “Core Bluetooth”-Framework und die Funktionsweise von Bluetooth Low Energy-Geraeten zu lernen.

Es war Zeit, in den Code einzutauchen und sich die Haende schmutzig zu machen - oder die Herzfrequenz zu erhoehen, die am Ende dieses Tutorials mit dem folgenden Code gemessen werden kann!

Voraussetzungen

Um dem Geraet die Nutzung von Bluetooth zu ermoeglichen, muessen zwei Schluessel in der Info.plist gesetzt werden.

  • Privacy - Bluetooth Always Usage Description
  • Privacy - Bluetooth Peripheral Usage Description

Info.plist

CentralManagers und Peripherals

Der Einstiegspunkt dieses Tutorials ist der CBCentralManager, der “ein Objekt ist, das nach Peripheriegeraeten sucht, diese erkennt, sich mit ihnen verbindet und sie verwaltet.”

Lass uns einen erstellen und verbinden.

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

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

Dieser Delegate muss centralManagerDidUpdateState(_ central: CBCentralManager) implementieren, damit die App pruefen kann, ob das Geraet, auf dem die App 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, wird die App nach Peripheriegeraeten in ihrer Naehe suchen.

Eine weitere Delegate-Methode muss implementiert werden, um zu bestimmen, welche Peripheriegeraete verfuegbar sind.

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

Mit eingeschaltetem PM100 entdeckte ich schnell, dass sein Name “e-Oximeter” ist.

Lass uns eine Verbindung herstellen.

centralManager?.connect(peripheral, options: nil)

Wenn die Verbindung hergestellt wurde, wird diese Delegate-Methode aufgerufen.

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

Fertig! Du hast erfolgreich eine Verbindung zum PM100 (oder einem anderen in diesem Tutorial verwendeten Geraet) hergestellt. Als Naechstes werden wir durchgehen, wie man alle Daten entdeckt, die das Geraet senden kann.

Services und Characteristics

Um zu sehen, was das Bluetooth-Peripheriegeraet bietet, entdecke zuerst seine Services:

peripheral.discoverServices(nil)

In der Dokumentation siehe CBService. Ein Service wird beschrieben als “Eine Sammlung von Daten und zugehoerigen Verhaltensweisen, die eine Funktion oder ein Feature eines Geraets erfuellen.”

Implementiere die entsprechende CBPeripheralDelegate-Methode, die aufgerufen wird, wenn ein Service entdeckt wird.

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

Der PM100 hat 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 sind sehr nah daran, die Herzfrequenz- und Sauerstoffsaettigungsdaten mit CBCharacteristic abzurufen. Beziehe dich auf die Dokumentation:

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

Wir muessen eine Delegate-Methode implementieren, um alle entdeckten Characteristics abzurufen:

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

Ein wenig Recherche in der Bluetooth-Spezifikation zeigt, dass wir den Wert der Characteristic 2A5F des Service 1822 wollen. Du kannst das auch googeln.

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

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

Rufe schliesslich die Werte ab, indem du die verbleibende Delegate-Methode implementierst:

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 Vergleichen der Werte des byteArray mit dem Display meines PM100 war es einfach zu bestimmen, welcher Wert der Herzfrequenz und welcher der Sauerstoffsaettigung entspricht:

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

Wir sind fertig! Egal fuer welches Bluetooth-Geraet du dich interessierst, der Ansatz sollte derselbe sein, um seine Services und Characteristics zu entdecken.

Eigentlich sind wir noch nicht ganz fertig…

Beispielanwendung

Wenn du eine voll funktionsfaehige SwiftUI-Anwendung benoetigst, die die in diesem Tutorial gezeigten Herzfrequenz- und Sauerstoffsaettigungsdaten anzeigt, stelle ich den kompletten Xcode-Workspace zum Download bereit.

Beispielanwendung

Danke fuers Lesen und danke fuer deine Unterstuetzung!

Ressourcen