Karamihan sa mga companion app ay manipis na BLE remote: pindutin ang isang button, isulat ang isang halaga, tapos na. Hindi maaaring ganoon kanipis ang isang dispenser — ang isang pagdispensa ay isang kaganapan sa kalusugan na dapat maitala nang eksaktong isang beses, ang device ay gumagamit ng custom na binary protocol sa halip na isang standard na GATT profile, at ang "huwag palampasin ang isang dosis" ay hindi maaaring umasa sa pagkagising ng telepono. Narito kung paano namin binuo ang daan mula sa isang pagpindot patungo sa matibay at nagkakasundong estado, bilang bahagi ng aming wellness & fitness app development na gawain.
Sa Isang Sulyap
Domain Connected wellness — smart supplement dispensing
Core Technologies React Native + Expo, react-native-ble-plx, Zustand, NestJS, MongoDB 8 (transactions)
Mga Pangunahing Kakayahan Custom BLE command/telemetry protocol, SMP/CBOR firmware updates, pagtatala ng transactional dispense, mga paalala na may kaalaman sa time zone
Katayuan Pumapasok sa Phase 2 ng pagpapaunlad
Ang Hamon
Ang aming kliyente ay gumagawa ng isang connected wellness product: isang device na nagdi-dispense ng mga supplement, na kinokontrol mula sa isang telepono. Ang kinaugaliang paraan upang buuin ang companion app — at ang paraan kung paano karamihan ay ginagawa — ay bilang isang manipis na remote na nagsusulat ng isang characteristic at nagtitiwala sa resulta. Gumagana iyon para sa isang light bulb, at halos hindi talaga para sa isang dispenser.
Alam namin iyon bago pa man simulan, kaya hindi namin ito binuo sa ganoong paraan. Ang mga limitasyon ng thin-remote model ay structural, hindi problema sa pag-tune:
- Ang isang dispense ay dapat maitala nang eksaktong isang beses. Binabawasan nito ang pisikal na stock, binibilang sa pang-araw-araw na dosis, at nagbibigay ng data para sa nutrient analytics. Ang isang double-count o nawawalang write ay sumisira sa lahat ng tatlo.
- Ang device ay gumagamit ng custom protocol, hindi isang profile. Ang mga command ay framed binary packets; walang off-the-shelf characteristic na nangangahulugang "mag-dispense ng isang tablet."
- Hindi magkatulad ang hugis ng mga command at telemetry. Ang app ay nagsusulat ng compact binary commands ngunit nagbabasa pabalik ng JSON telemetry (battery, charging, cartridge id, water quantity) — sa magkaibang characteristics.
- Ang mga update ng firmware ay gumagamit ng parehong koneksyon. Ang device ay dapat na ma-upgrade sa field, sa pamamagitan ng isang hiwalay na management service, nang walang pangalawang tool.
- Hindi maaaring manatili ang mga paalala sa device. Ang "Hindi ka pa nagdosis ngayon" ay tanong tungkol sa time zones, history, at de-duplication — isang server concern, hindi isang phone timer.
Ang pangunahing sanhi ay simple: ang isang dispense ay hindi isang pagpindot ng button, ito ay isang transaction — pareho sa wire at sa database. Ang isang modelo na tinatrato ito bilang isang fire-and-forget write ay hindi makapagpaparamdam ng pagiging maaasahan sa alinman sa dalawang bahagi.
Ang Aming Solusyon
Hinati namin ang mga responsibilidad upang ang bawat layer ay gawin ang pinakamahusay na kaya nito: ang telepono ang nag-o-orchestrate, ang device ang nagpapatupad ng custom protocol na tinukoy sa pamamagitan ng aming IoT application development practice, at ang cloud ang transactional system of record — na may mga paalala na ganap na pinangangasiwaan sa server-side.
Arkitektura
- React Native + Expo + Zustand — isang BluetoothStore nagmamay-ari ng BLE connection, dispense flow, at telemetry, na inililigtas sa AsyncStorage.
- react-native-ble-plx — pag-scan, connect/reconnect, at characteristic read/write.
- Isang custom command layer — bumubuo ng framed binary commands para sa device, at gumagamit ng CBOR sa ibabaw ng isang SMP service para sa firmware updates.
- NestJS API — mga dispense at schedule endpoint, binuo sa parehong SaaS application development na pundasyon na ginagamit namin sa lahat ng produkto ng kliyente; nagtatala ng bawat dispense sa loob ng isang MongoDB transaction.
- MongoDB 8 (Mongoose) — ang system of record, dagdag pa ang write-time roll-up collections na binabasa ng downstream analytics.
- NestJS Schedule crons + Expo push (queued via ActiveMQ/STOMP) — mga paalala na may kaalaman sa time zone, na-de-duplicate sa pamamagitan ng Mongo logs.
- Sentry (mobile) — pagsubaybay sa production error sa app.

Isang Custom Wire Protocol
Ang mahirap na bahagi ng anumang BLE product ay ang device ay hindi gumagamit ng standard na wika. Ang aming device ay naglalabas ng command/telemetry service at isang hiwalay na firmware (SMP) service. Kumokonekta ang app sa react-native-ble-plx, pagkatapos ay nagsu-subscribe sa telemetry — na dumarating bilang JSON sa notify characteristic:
// 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 });
});
Ang isang dispense ay isang compact framed binary command — isang start marker, isang command id, isang length, ang cartridge byte, isang read/write flag, at isang end marker — na base64-encoded at isinulat sa command characteristic. (Ang mga literal na UUID at frame marker ay inalis dito.)
// 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),
);
Naghihintay ang app ng kumpirmasyon mula sa device (tugma ito sa command id mula sa response frame) bago nito ituring na totoo ang dispense — at doon lang ito itatala sa server-side. Ang mga update ng firmware ay gumagamit ng parehong koneksyon ngunit ibang wika: chunked CBOR payloads sa ibabaw ng SMP service.
Mula Pindot Hanggang Pagtala
Ang isang kumpirmadong dispense ay nagiging durable state sa pamamagitan ng isang endpoint — POST /dispense/tablet/:cartridgeId — na nagsusulat sa loob ng isang solong MongoDB transaction upang ang kalahating dosenang epekto ay mangyari lahat o wala sa mga ito ang mangyari:
// 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
});
Ang isang transaction na iyon ay nag-a-update sa dispense log, monthly roll-up, physical stock, daily dose count, at anumang pending reminder — na siyang eksaktong dahilan kung bakit ang isang dispense ay dapat isang transaction, hindi isang write.
Ang Mga Paalala ay Isang Isyu ng Server
Kapag ang isang dispense ay durable, ang mga tanong na hindi masagot ng phone timer ay nagiging karaniwan — at sinasagot ang mga ito ng cron jobs, hindi ng app:
- Nagdosis na ba ang user na ito ngayon, sa kanilang time zone?
- Walang laman ba ang cartridge (palitan) o walang stock (muling mag-order), o pareho (pagtigil)?
- Nalayo na ba sila sa isang lugar ng kalusugan na dati nilang pinapangalagaan?
Tatlong @nestjs/schedule services ang nangangasiwa sa mga ito — isang schedule/dose reminder, isang cartridge-state reminder, at isang cross-health inactivity reminder. Bawat isa ay inaayos ang lokal na oras ng user gamit ang date-fns-tz, tinitingnan ang Mongo kung nagdosis na sila ngayon, ini-de-duplicate laban sa isang log collection, at nag-e-enqueue ng Expo push sa pamamagitan ng ActiveMQ. Hindi kailanman kasali ang device.
Mga Resulta
Ang pagdidisenyo para sa "ang isang dispense ay isang transaction" sa halip na isang thin-remote write ay nagpabago sa kayang garantiyahan ng system:
- Eksaktong isang beses na pagtatala. Ang isang dispense ay nag-a-update sa log, roll-ups, stock, at daily dose nang atomically — walang double counts, walang partial writes.
- Isang dokumentadong device protocol, kasama ang field upgrades. Ang mga command at telemetry ay first-class, at ang mga update ng firmware ay ipinapadala sa parehong koneksyon sa pamamagitan ng SMP/CBOR.
- Mga paalala na tama, hindi tinatantya. Timezone-aware, history-aware, at na-de-duplicate sa server-side — kaya nagpa-fire ang mga ito bukas man o hindi ang app.
- Analytics na mapagkakatiwalaan. Dahil ang bawat dispense ay nagro-roll up sa write time, ang data na nagpapakain sa mga chart at adherence ay palaging pare-pareho sa log.
Ang API, crons, at push queue ay tumatakbo sa infrastructure na pinamamahalaan sa pamamagitan ng aming cloud infrastructure services, kaya ang parehong reliability guarantees ay mananatili habang lumalaki ang device fleet at user base.
Technology 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)
Magbasa pa mula sa aming team
1. Syncing Apple Health & Health Connect
2. Personalized Recipe Search: Retrieval That Knows What You Should Eat Next

