Die meisten Begleit-Apps sind schlanke BLE-Fernbedienungen: Taste tippen, Wert schreiben, fertig. Ein Spender kann nicht so einfach sein – eine Abgabe ist ein Gesundheitsereignis, das genau einmal aufgezeichnet werden muss, das Gerät spricht ein benutzerdefiniertes Binärprotokoll anstatt eines Standard-GATT-Profils, und „keine Dosis vergessen“ darf nicht davon abhängen, dass das Telefon wach ist. Hier erfahren Sie, wie wir den Weg vom Tippen zu einem dauerhaften, abgleichbaren Zustand aufgebaut haben, als Teil unserer Entwicklung von Wellness- & Fitness-Apps.
Auf einen Blick
Domäne Vernetzte Wellness — intelligente Nahrungsergänzungsmittel-Abgabe
Kerntechnologien React Native + Expo, react-native-ble-plx, Zustand, NestJS, MongoDB 8 (transactions)
SchlĂĽsselfunktionen Benutzerdefiniertes BLE-Befehls-/Telemetrie-Protokoll, SMP/CBOR-Firmware-Updates, transaktionsbasierte Abgabeaufzeichnung, zeitzonenbewusste Erinnerungen
Status Eintritt in Phase 2 der Entwicklung
Die Herausforderung
Unser Kunde entwickelte ein vernetztes Wellness-Produkt: ein Gerät, das Nahrungsergänzungsmittel abgibt und über ein Telefon gesteuert wird. Die konventionelle Art, die Begleit-App zu entwickeln – und die Art, wie die meisten Apps gebaut werden – ist eine schlanke Fernbedienung, die eine Characteristic schreibt und dem Ergebnis vertraut. Das funktioniert für eine Glühbirne, aber fast gar nicht für einen Spender.
Das wussten wir von Anfang an, weshalb wir es nicht auf diese Weise gebaut haben. Die Grenzen des Thin-Remote-Modells sind strukturell, nicht Abstimmungsprobleme:
- Eine Abgabe muss genau einmal aufgezeichnet werden. Sie reduziert den physischen Bestand, zählt zu einer Tagesdosis und speist Nährstoffanalysen. Eine doppelte Zählung oder ein verlorener Schreibvorgang korrumpiert alle drei.
- Das Gerät spricht ein benutzerdefiniertes Protokoll, kein Profil. Befehle sind gerahmte Binärpakete; es gibt keine Standard-Characteristic, die „eine Tablette abgeben“ bedeutet.
- Befehle und Telemetrie teilen sich keine Form. Die App schreibt kompakte Binärbefehle, liest aber JSON-Telemetrie (Batterie, Ladezustand, Patronen-ID, Wassermenge) zurück – über verschiedene Characteristics.
- Firmware-Updates nutzen dieselbe Verbindung. Das Gerät muss im Feld über einen separaten Verwaltungsdienst ohne ein zweites Werkzeug aufrüstbar sein.
- Erinnerungen können nicht auf dem Gerät gespeichert werden. „Sie haben heute noch keine Dosis genommen“ ist eine Frage zu Zeitzonen, Historie und Deduplizierung – eine Server-Angelegenheit, kein Telefon-Timer.
Die Grundursache war einfach: Eine Abgabe ist kein Knopfdruck, sondern eine Transaktion – sowohl im Datenverkehr als auch in der Datenbank. Ein Modell, das dies als Fire-and-Forget-Schreibvorgang behandelt, kann keine der beiden Hälften zuverlässig machen.
Unsere Lösung
Wir haben die Verantwortlichkeiten aufgeteilt, sodass jede Schicht das tut, worin sie am besten ist: Das Telefon orchestriert, das Gerät führt ein benutzerdefiniertes Protokoll aus, das durch unsere IoT-Anwendungsentwicklungspraxis definiert ist, und die Cloud ist das transaktionsbasierte System der Aufzeichnung – wobei Erinnerungen vollständig serverseitig gehandhabt werden.
Architektur
- React Native + Expo + Zustand — ein BluetoothStore verwaltet die BLE-Verbindung, den Abgabe-Workflow und die Telemetrie, die in AsyncStorage gespeichert werden.
- react-native-ble-plx — Scannen, Verbinden/Wiederverbinden und Characteristic lesen/schreiben.
- Eine benutzerdefinierte Befehlsschicht — erstellt gerahmte Binärbefehle für das Gerät und verwendet CBOR über einen SMP-Dienst für Firmware-Updates.
- NestJS API — Abgabe- und Zeitplan-Endpunkte, aufgebaut auf derselben SaaS-Anwendungsentwicklungsbasis, die wir bei allen Kundenprodukten verwenden; zeichnet jede Abgabe innerhalb einer MongoDB-Transaktion auf.
- MongoDB 8 (Mongoose) — das System der Aufzeichnung, plus Write-Time-Rollup-Sammlungen, die nachgeschaltete Analysen lesen.
- NestJS Schedule Crons + Expo Push (über ActiveMQ/STOMP in die Warteschlange gestellt) — zeitzonenbewusste Erinnerungen, dedupliziert über Mongo-Logs.
- Sentry (mobile) — Produktionsfehlerverfolgung in der App.

Ein benutzerdefiniertes Drahtprotokoll
Der schwierige Teil eines jeden BLE-Produkts ist, dass das Gerät keine Standardsprache spricht. Unseres stellt einen Befehls-/Telemetrie-Dienst und einen separaten Firmware (SMP)-Dienst bereit. Die App verbindet sich mit react-native-ble-plx und abonniert dann Telemetrie – die als JSON auf der Notify Characteristic ankommt:
// BluetoothStore.ts — react-native-ble-plx + Zustand
const device = await bleManager.connectToDevice(deviceId, { autoConnect: true });
await device.discoverAllServicesAndCharacteristics();
// Telemetry: JSON over the notify characteristic (UUIDs are app-side constants).
bleManager.monitorCharacteristicForDevice(device.id, CH_SERVICE_UUID, TX_UUID, (_err, c) => {
if (!c?.value) return;
const t = JSON.parse(base64ToString(c.value));
// { battery_percentage, charging_status, cartridge_id, water_qty, sequence, timestamp }
set({ batteryPercentage: Number(t.battery_percentage), chargingStatus: t.charging_status });
});
Eine Abgabe ist ein kompakter gerahmter Binärbefehl – ein Start-Marker, eine Befehls-ID, eine Länge, das Patronen-Byte, ein Lese-/Schreib-Flag und ein End-Marker – base64-kodiert und auf die Befehls-Characteristic geschrieben. (Die wörtlichen UUIDs und Frame-Marker sind hier unkenntlich gemacht.)
// Frame shape: [SOF] [CMD] [LEN] [DATA] [RW] [EOF] — built by the command service.
const command = SNBCommandService.buildDispenseNutritionCommand(cartridgeId); // CMD = DISPENSE
await bleManager.writeCharacteristicWithResponseForDevice(
device.id, CH_SERVICE_UUID, RX_UUID,
SNBCommandService.uint8ArrayToBase64(command),
);
Die App wartet auf die Bestätigung des Geräts (sie gleicht die Befehls-ID aus dem Antwort-Frame ab), bevor sie die Abgabe als real betrachtet – und erst dann serverseitig aufzeichnet. Firmware-Updates nutzen dieselbe Verbindung, aber eine andere Sprache: gechunkte CBOR-Payloads über den SMP-Dienst.
Vom Tippen zur Aufzeichnung
Eine bestätigte Abgabe wird zu einem dauerhaften Zustand über einen Endpunkt – POST /dispense/tablet/:cartridgeId –, der innerhalb einer einzigen MongoDB-Transaktion schreibt, sodass die halbe Dutzend Effekte entweder alle eintreten oder keiner:
// dispense.service.ts — record exactly once, atomically
await session.withTransaction(async () => {
await this.dispensed.create([{ cartridgeId, cartridgeModalId, dispensedBy: userId, dispensedAt }], { session });
// write-time roll-up: totalDaysConsumed only increments on the first dose of the day
await this.monthly.findOneAndUpdate(
{ userId, cartridgeModalId, month, year },
{ $inc: { totalTablets: 1, ...(firstDoseToday ? { totalDaysConsumed: 1 } : {}) } },
{ upsert: true, session },
);
await this.cartridge.updateOne({ cartridgeId }, { $inc: { tablets: -1 } }, { session });
await this.dailyDose.updateOne({ userId, dateLocal }, { $inc: { tabletsTaken: 1 } }, { upsert: true, session });
await this.notifications.cancelPending(userId, 'SUPPLEMENT_REMINDER', { session }); // mark IS_MISSED
});
Diese einzelne Transaktion aktualisiert das Abgabeprotokoll, die monatliche Zusammenfassung, den physischen Bestand, die tägliche Dosisanzahl und alle ausstehenden Erinnerungen – genau deshalb muss eine Abgabe eine Transaktion sein, kein Schreibvorgang.
Erinnerungen sind eine Server-Angelegenheit
Sobald eine Abgabe dauerhaft ist, werden die Fragen, die ein Telefon-Timer nicht beantworten konnte, zur Routine – und sie werden von Cron-Jobs beantwortet, nicht von der App:
- Hat dieser Benutzer heute, in seiner Zeitzone, eine Dosis genommen?
- Ist die Patrone leer (ersetzen) oder der Bestand leer (nachbestellen), oder beides (Einstellung)?
- Haben sie sich von einem Gesundheitsbereich entfernt, den sie frĂĽher beachtet haben?
Drei @nestjs/schedule Dienste kümmern sich darum – eine Zeitplan-/Dosis-Erinnerung, eine Patronenstatus-Erinnerung und eine bereichsübergreifende Inaktivitäts-Erinnerung. Jeder Dienst ermittelt die lokale Zeit des Benutzers mit date-fns-tz, prüft in Mongo, ob der Benutzer heute eine Dosis genommen hat, dedupliziert anhand einer Protokollsammlung und reiht eine Expo-Push-Nachricht über ActiveMQ ein. Das Gerät ist nie involviert.
Ergebnisse
Die Gestaltung nach dem Prinzip „eine Abgabe ist eine Transaktion“ anstelle eines Thin-Remote-Schreibvorgangs hat die Garantien des Systems verändert:
- Genau einmalige Aufzeichnung. Eine Abgabe aktualisiert das Protokoll, die Zusammenfassungen, den Bestand und die Tagesdosis atomar – keine doppelten Zählungen, keine teilweisen Schreibvorgänge.
- Ein dokumentiertes Geräteprotokoll, einschließlich Feld-Upgrades. Befehle und Telemetrie sind erstklassig, und Firmware-Updates werden über dieselbe Verbindung via SMP/CBOR gesendet.
- Erinnerungen, die korrekt, nicht annähernd sind. Zeitzonenbewusst, historiebewusst und serverseitig dedupliziert – sodass sie ausgelöst werden, unabhängig davon, ob die App geöffnet ist oder nicht.
- Analysen, denen man vertrauen kann. Da jede Abgabe zum Zeitpunkt des Schreibvorgangs zusammengefasst wird, sind die Daten, die Diagramme und die Einhaltung speisen, immer konsistent mit dem Protokoll.
Die API, Crons und Push-Warteschlange laufen auf Infrastruktur, die über unsere Cloud-Infrastruktur-Dienste verwaltet wird, sodass die gleichen Zuverlässigkeitsgarantien auch bei Skalierung der Geräteflotte und Benutzerbasis gelten.
Technologie-Stack: React Native · Expo · TypeScript · react-native-ble-plx · Zustand · CBOR (SMP firmware) · NestJS · MongoDB 8 · Mongoose (transactions) · @nestjs/schedule · ActiveMQ / STOMP · Expo Server SDK · JWT · AWS S3 · AWS SES · Sentry (mobile)
Lesen Sie mehr von unserem Team
1. Synchronisierung von Apple Health & Health Connect
2. Personalisierte Rezeptsuche: Abruf, der weiß, was Sie als Nächstes essen sollten
3. Optimierung des Senderlogos für verschiedene Videoauflösungen

