Sebagian besar aplikasi pendamping adalah remote BLE yang tipis: ketuk tombol, tulis nilai, selesai. Dispenser tidak bisa setipis itu — distribusi adalah peristiwa kesehatan yang harus dicatat tepat sekali, perangkat berbicara dengan protokol biner khusus daripada profil GATT standar, dan "jangan lewatkan dosis" tidak bisa bergantung pada ponsel yang terjaga. Inilah cara kami membangun jalur dari ketukan ke status yang tahan lama dan dapat direkonsiliasi, sebagai bagian dari pengembangan aplikasi kebugaran & kesehatan kami.
Sekilas
Domain Kesehatan terhubung — distribusi suplemen pintar
Teknologi Inti React Native + Expo, react-native-ble-plx, Zustand, NestJS, MongoDB 8 (transaksi)
Kemampuan Utama Protokol perintah/telemetri BLE khusus, pembaruan firmware SMP/CBOR, pencatatan distribusi transaksional, pengingat yang sadar zona waktu
Status Memasuki Fase 2 pengembangan
Tantangan
Klien kami sedang membuat produk kesehatan terhubung: perangkat yang mendistribusikan suplemen, dikendalikan dari ponsel. Cara konvensional untuk membangun aplikasi pendamping — dan cara kebanyakan dibangun — adalah sebagai remote tipis yang menulis karakteristik dan mempercayai hasilnya. Itu bekerja untuk bola lampu, dan hampir tidak sama sekali untuk dispenser.
Kami tahu itu sejak awal, itulah sebabnya kami tidak membangunnya dengan cara itu. Batasan model remote tipis adalah struktural, bukan masalah penyetelan:
- Distribusi harus dicatat tepat sekali. Ini mengurangi stok fisik, dihitung sebagai dosis harian, dan memberi makan analitik nutrisi. Penghitungan ganda atau penulisan yang hilang merusak ketiganya.
- Perangkat berbicara dengan protokol khusus, bukan profil. Perintah dibingkai paket biner; tidak ada karakteristik siap pakai yang berarti "distribusi satu tablet."
- Perintah dan telemetri tidak berbagi bentuk. Aplikasi menulis perintah biner ringkas tetapi membaca kembali telemetri JSON (baterai, pengisian daya, id kartrid, jumlah air) — melalui karakteristik yang berbeda.
- Pembaruan firmware menggunakan koneksi yang sama. Perangkat harus dapat ditingkatkan di lapangan, melalui layanan manajemen terpisah, tanpa alat kedua.
- Pengingat tidak bisa berada di perangkat. "Anda belum mendosis hari ini" adalah pertanyaan tentang zona waktu, sejarah, dan deduplikasi — masalah server, bukan timer ponsel.
Penyebab utamanya sederhana: distribusi bukanlah penekanan tombol, melainkan transaksi — baik di kabel maupun di database. Model yang memperlakukannya sebagai penulisan fire-and-forget tidak dapat membuat salah satu dari keduanya dapat diandalkan.
Solusi Kami
Kami membagi tanggung jawab sehingga setiap lapisan melakukan apa yang terbaik: ponsel mengorkestrasikan, perangkat menjalankan protokol khusus yang didefinisikan melalui praktik pengembangan aplikasi IoT kami, dan cloud adalah sistem pencatatan transaksional — dengan pengingat yang sepenuhnya ditangani di sisi server.
Arsitektur
- React Native + Expo + Zustand — sebuah BluetoothStore memiliki koneksi BLE, aliran distribusi, dan telemetri, disimpan ke AsyncStorage.
- react-native-ble-plx — pemindaian, sambung/kembali sambung, dan baca/tulis karakteristik.
- Lapisan perintah khusus — membangun perintah biner berbingkai untuk perangkat, dan menggunakan CBOR melalui layanan SMP untuk pembaruan firmware.
- NestJS API — endpoint distribusi dan jadwal, dibangun di atas fondasi pengembangan aplikasi SaaS yang sama yang kami gunakan di seluruh produk klien; mencatat setiap distribusi di dalam transaksi MongoDB.
- MongoDB 8 (Mongoose) — sistem pencatatan, ditambah koleksi roll-up waktu tulis yang dibaca analitik hilir.
- NestJS Schedule crons + Expo push (diantrikan melalui ActiveMQ/STOMP) — pengingat yang sadar zona waktu, dideduplikasi melalui log Mongo.
- Sentry (mobile) — pelacakan kesalahan produksi di aplikasi.

Protokol Kabel Khusus
Bagian tersulit dari produk BLE mana pun adalah bahwa perangkat tidak berbicara dalam bahasa standar. Perangkat kami menampilkan layanan perintah/telemetri dan layanan firmware (SMP) terpisah. Aplikasi terhubung dengan react-native-ble-plx, kemudian berlangganan telemetri — yang tiba sebagai JSON pada karakteristik pemberitahuan:
// BluetoothStore.ts — react-native-ble-plx + Zustand
const device = await bleManager.connectToDevice(deviceId, { autoConnect: true });
await device.discoverAllServicesAndCharacteristics();
// Telemetri: JSON melalui karakteristik pemberitahuan (UUID adalah konstanta sisi aplikasi).
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 });
});
Distribusi adalah perintah biner berbingkai ringkas — penanda awal, id perintah, panjang, byte kartrid, bendera baca/tulis, dan penanda akhir — dikodekan dalam base64 dan ditulis ke karakteristik perintah. (UUID literal dan penanda bingkai disembunyikan di sini.)
// Bentuk bingkai: [SOF] [CMD] [LEN] [DATA] [RW] [EOF] — dibangun oleh layanan perintah.
const command = SNBCommandService.buildDispenseNutritionCommand(cartridgeId); // CMD = DISPENSE
await bleManager.writeCharacteristicWithResponseForDevice(
device.id, CH_SERVICE_UUID, RX_UUID,
SNBCommandService.uint8ArrayToBase64(command),
);
Aplikasi menunggu pengakuan perangkat (itu mencocokkan id perintah dari bingkai respons) sebelum memperlakukan distribusi sebagai nyata — dan hanya kemudian mencatatnya di sisi server. Pembaruan firmware menggunakan koneksi yang sama tetapi dengan bahasa yang berbeda: payload CBOR yang dipecah-pecah melalui layanan SMP.
Dari Ketukan ke Pencatatan
Distribusi yang dikonfirmasi menjadi status tahan lama melalui satu endpoint — POST /dispense/tablet/:cartridgeId — yang menulis di dalam satu transaksi MongoDB sehingga setengah lusin efek baik semuanya terjadi atau tidak sama sekali:
// dispense.service.ts — catat tepat sekali, secara atomik
await session.withTransaction(async () => {
await this.dispensed.create([{ cartridgeId, cartridgeModalId, dispensedBy: userId, dispensedAt }], { session });
// roll-up waktu tulis: totalDaysConsumed hanya bertambah pada dosis pertama hari itu
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 }); // tandai IS_MISSED
});
Transaksi tunggal tersebut memperbarui log distribusi, roll-up bulanan, stok fisik, hitungan dosis harian, dan pengingat yang tertunda — yang merupakan alasan mengapa distribusi harus menjadi transaksi, bukan penulisan.
Pengingat Adalah Masalah Server
Setelah distribusi menjadi tahan lama, pertanyaan yang tidak bisa dijawab oleh timer ponsel menjadi rutin — dan dijawab oleh pekerjaan cron, bukan aplikasi:
- Apakah pengguna ini telah mendosis hari ini, di zona waktu mereka?
- Apakah kartrid kosong (ganti) atau stok kosong (pesan ulang), atau keduanya (penghentian)?
- Apakah mereka telah menjauh dari area kesehatan yang biasa mereka ambil?
Tiga layanan @nestjs/schedule menangani ini — pengingat jadwal/dosis, pengingat status kartrid, dan pengingat ketidakaktifan lintas kesehatan. Masing-masing menyelesaikan waktu lokal pengguna dengan date-fns-tz, memeriksa Mongo apakah mereka telah mendosis hari ini, dideduplikasi terhadap koleksi log, dan mengantrikan push Expo melalui ActiveMQ. Perangkat tidak pernah terlibat.
Hasil
Merancang untuk "distribusi adalah transaksi" alih-alih penulisan remote tipis mengubah apa yang dapat dijamin oleh sistem:
- Pencatatan tepat sekali. Distribusi memperbarui log, roll-up, stok, dan dosis harian secara atomik — tidak ada penghitungan ganda, tidak ada penulisan parsial.
- Protokol perangkat yang didokumentasikan, termasuk peningkatan lapangan. Perintah dan telemetri adalah kelas satu, dan pembaruan firmware dikirim melalui koneksi yang sama melalui SMP/CBOR.
- Pengingat yang benar, bukan perkiraan. Sadar zona waktu, sadar sejarah, dan dideduplikasi di sisi server — sehingga mereka berfungsi apakah aplikasi terbuka atau tidak.
- Analitik yang dapat dipercaya. Karena setiap distribusi digulung pada waktu penulisan, data yang memberi makan grafik dan kepatuhan selalu konsisten dengan log.
API, cron, dan antrian push dijalankan pada infrastruktur yang dikelola melalui layanan infrastruktur cloud kami, sehingga jaminan keandalan yang sama berlaku saat armada perangkat dan basis pengguna meningkat.
Tumpukan Teknologi: React Native · Expo · TypeScript · react-native-ble-plx · Zustand · CBOR (SMP firmware) · NestJS · MongoDB 8 · Mongoose (transaksi) · @nestjs/schedule · ActiveMQ / STOMP · Expo Server SDK · JWT · AWS S3 · AWS SES · Sentry (mobile)
Baca lebih lanjut dari tim kami
1. Sinkronisasi Apple Health & Health Connect
2. Pencarian Resep yang Dipersonalisasi: Pengambilan yang Tahu Apa yang Harus Anda Makan Selanjutnya
3. Mengoptimalkan Logo Saluran untuk Resolusi Video yang Berbeda

