02. Sep 2026 · Hardware und Software

Die App auf den zweiten Prototyp vorbereiten.

Die App kennt jetzt die neue Platine. Beim Vorbereiten kamen drei Dinge heraus, die die Firmware noch nicht melden kann.

Die HydroGuard-iPhone-App mit Hydrations-Index, Herzfrequenz, Bioimpedanz, Temperatur und Luftfeuchtigkeit
Die iPhone-App im Simulator. Die Daten dahinter sind simuliert, deshalb zeigt die App oben rechts ihre eigene „Sim“-Markierung.

Die Platine misst. Die App speichert, was sie gemessen hat, berechnet daraus einen Hydrations-Index und zeigt ihn an. Dazwischen liegt ein binäres Protokoll. Es legt fest, wie ein Messframe aussieht und welche Messgröße auf welcher Kanalnummer liegt.

Die App weiß nicht, welcher Mikrocontroller einen Frame geschickt hat. Sie weiß, dass Kanal 0x0001 die Impedanz in Milliohm ist. Deshalb lassen sich Platine und App getrennt voneinander entwickeln.

Von der Haut zum Telefon

Vier ElektrodenÄußeres Paar speist
Inneres Paar misst
MAX30001Bioimpedanz-
Frontend
ESP32-C6Liest, bündelt,
sendet
iPhoneSpeichert jeden Frame,
rechnet
Von der Haut bis zum Telefon. Der MAX30001 braucht zusätzlich einen stabilen 32,768-kHz-Takt, den ein eigener Oszillator auf der Platine liefert.

Die Messung beginnt an vier Druckknöpfen auf einem elastischen Band. Das äußere Paar schickt einen kleinen Wechselstrom von etwa 40 kHz durch den Oberarm. Das innere Paar misst die Spannung, die dieser Strom erzeugt. Die Trennung der beiden Aufgaben hält den Widerstand zwischen Metall und Haut aus dem Spannungswert heraus, und genau der macht eine einfache Zwei-Elektroden-Messung unzuverlässig.

Diese analoge Arbeit übernimmt der MAX30001. Dafür braucht er einen stabilen Takt, deshalb bekommt er auf der Platine einen eigenen 32,768-kHz-Oszillator, statt sich einen vom Mikrocontroller zu leihen. Für den Ersatzweg vom Controller gibt es ein Footprint, das aber bewusst unbestückt bleibt.

Der MAX30001 schickt von sich aus nichts. Er zieht eine einzelne Interruptleitung, INTB, um zu melden, dass ein Wert bereitliegt. Der Controller sieht das und liest das Ergebnis über SPI, vier Leitungen mit 1 MHz. Bewusst langsam: Die Werte kommen 64-mal pro Sekunde, es gibt also keinen Grund, den Bus schnell laufen zu lassen, solange wir dort noch Fehler suchen.

Das SPI-Gespräch

Jeder Austausch mit dem MAX30001 hat dieselbe Form: zweiunddreißig Bit, nicht mehr und nicht weniger. Ein Kommandobyte, danach drei Datenbytes. Die Register des Chips sind alle 24 Bit breit, drei Byte sind also genau ein Register.

Das Kommandobyte ist die sieben Bit lange Registeradresse um eins nach oben geschoben, und das unterste frei gewordene Bit sagt, ob gelesen oder geschrieben wird. Um das Statusregister an Adresse 0x01 zu lesen, schickt der Controller 0x03. Zum Schreiben 0x02. Lesen heißt: das Kommando schicken und danach drei beliebige Bytes, während der Chip auf der anderen Leitung seine Antwort heraustaktet.

CMDAdresse + R/W
D23:16Daten
D15:8Daten
D7:0Daten

Immer zweiunddreißig Bit. Die Register sind 24 Bit breit, drei Byte sind also genau ein Register.

A6
A5
A4
A3
A2
A1
A0
R/W
A6–A0 · sieben AdressbitsR/W · 1 liest, 0 schreibtStatus an 0x01 → 0x03 senden
Ein Austausch mit dem MAX30001. Die Adresse wird um eins nach oben geschoben, das unten frei werdende Bit wählt Lesen oder Schreiben.

Der Controller fragt nicht ständig nach, ob ein Wert bereitliegt. Dafür ist INTB da. Geht die Leitung auf Low, liest der Controller das Statusregister, um herauszufinden warum, denn dafür kommen mehrere Gründe infrage. Bit 19 bedeutet, dass der Bioimpedanz-FIFO seine Schwelle erreicht hat und es etwas abzuholen gibt.

Dann liest er den FIFO. Jeder Wert bringt in seinen untersten drei Bit eine Kennung mit, und die ist der eigentlich nützliche Teil: gültig, außerhalb des Bereichs, letzter verfügbarer Wert, FIFO leer, FIFO übergelaufen. Der Messwert sagt in denselben acht Bit, die ihn transportieren, wie weit man ihm trauen darf. Dieser Gedanke hält sich bis zum Telefon durch, wo jeder Datensatz eigene Flags trägt.

Den FIFO zu lesen ist nicht folgenlos. Jeder Lesezugriff schiebt den Zeiger weiter und verbraucht einen Wert. Es ist damit das eine Register, das die Bring-up-Werkzeuge nicht in einer Schleife auslesen dürfen, wie sie es mit Konfigurationsregistern tun. Dieser Unterschied steht im Treiber und bleibt nicht dem überlassen, der gerade debuggt.

Es gibt ein Register, dessen obere Bits immer dasselbe feste Muster zurückgeben. Es zu prüfen ist der Weg, auf dem der Controller sich vergewissert, dass der Bus richtig verdrahtet ist, bevor er irgendetwas anderem darauf traut. Genau das hieß „der MAX30001 antwortete über SPI“ in der ersten Bring-up-Notiz.

Was Bluetooth Low Energy hier macht

Der Controller sendet einen Wert nicht, sobald er ihn hat. Bei 64 Werten pro Sekunde würde ein Bluetooth-Paket pro Messwert den Akku leeren und das Telefon zuschütten, ohne dass jemand etwas davon hätte.

Stattdessen bietet die Platine einen Bluetooth-Dienst mit mehreren Charakteristiken an, also den einzelnen Werten, mit denen ein Client umgehen kann. Einige werden beim Verbinden einmal gelesen, etwa die Geräteinformationen und die Liste der Sensoren, die tatsächlich vorhanden sind. Zwei werden beschrieben, um die Platine um die Historie zu bitten. Der Rest sind Notify-Charakteristiken: Das Telefon abonniert sie einmal, danach schickt die Platine von sich aus, sobald sie etwas hat.

Die laufenden Messwerte gehen gebündelt über eine davon hinaus. Mehrere Kanäle kommen mit einem gemeinsamen Zeitstempel in einen Frame, und diese Frames gehen etwa sechzehnmal pro Sekunde raus. Das Telefon abonniert und wartet dann einfach.

Ein Frame, Byte für Byte

Hier ein Frame des ersten Prototyps, so wie er ankommt. Vierundsechzig Bytes: sechzehn Byte Kopf, danach sechs Datensätze zu je acht Byte. Alles Little-Endian.

0000  A1 01 10 17 2A 00 00 00 40 E2 01 00 00 00 00 00
0010  01 00 3C F0 00 00 60 01  02 00 E0 2E 00 00 58 05
0020  03 00 40 19 01 00 5E 01  05 00 12 7A 00 00 63 03
0030  07 00 18 10 00 00 64 01  09 00 64 00 00 00 64 01

Der Kopf weist sich aus und beschreibt das Bündel. A1 markiert den Anfang eines Frames, 01 ist die Protokollversion, 10 die Kopflänge von sechzehn Byte. 17 sind Flags: Live-Daten, enthält echte Daten, enthält auch simulierte Daten, Uhr ist synchronisiert. 2A ist die laufende Nummer, zweiundvierzig, an der die App einen verlorenen Frame erkennt. Die letzten acht Byte sind die Gerätezeit, 123456 ms seit dem Start.

Jeder Datensatz danach besteht aus Kanalnummer, vorzeichenbehaftetem Wert, einem Qualitätsbyte und eigenen Flags:

BytesKanalWertSagt
01 00 3C F0 00 00 60 01Impedanz61 500 mΩEcht, Qualität 96
02 00 E0 2E 00 00 58 05Atmung12 000Aus der Impedanz abgeleitet
03 00 40 19 01 00 5E 01Herzfrequenz72 000 mbpmEcht, Qualität 94
05 00 12 7A 00 00 63 03Temperatur31,25 °CSimuliert
07 00 18 10 00 00 64 01Akku4 120 mVEcht
09 00 64 00 00 00 64 01Kontaktqualität100 %Echt

Der interessante Datensatz ist die Temperatur. Ihre Flags sagen simuliert, und das stimmt: Der erste Prototyp hat gar keinen Temperatur- oder Feuchtigkeitssensor. Statt den Kanal wegzulassen, schickt die Platine einen plausiblen Wert und gibt in denselben acht Byte zu, dass sie ihn erfunden hat. Die App reicht das bis auf den Bildschirm durch, statt ihn stillschweigend als Messwert auszugeben.

Was die App damit macht

Sie behält jeden empfangenen Frame, holt nach einer Lücke die Historie nach und berechnet den Hydrations-Index auf dem Telefon statt auf der Platine.

Der Screenshot oben stammt aus dem Simulator, nicht von einer Platine. Ein iPhone-Simulator hat kein Bluetooth, deshalb läuft die App auf einer aufgezeichneten Sitzung. Die App sagt das selbst: Die kleine Markierung „Sim“ in der Ecke erscheint immer dann, wenn die angezeigten Daten simuliert sind, über denselben Mechanismus wie der simulierte Temperatur-Datensatz.