MicrocosmWorksInnovando y Arquitectando el Cosmos Digital
Acerca deContacto
MicrocosmWorksInnovando y Arquitectando el Cosmos Digital

Ofreciendo soluciones de TI que importan. Nos apasiona la tecnología, la seguridad y ayudar a las empresas a crecer a través de una infraestructura de TI confiable e innovadora.

[email protected]
+91 7011868196
New Delhi, India

Centro de Crecimiento de IA

Centro de IAInnovación para StartupsAcelerador Empresarial

Soluciones

Todas las SolucionesAplicaciones de Bienestar y FitnessPlataforma de Video con IADesarrollo de Agentes de IA

Recursos

PerspectivasGuías de la IndustriaPlanos de Casos de UsoPatrones de ArquitecturaEstudios de Caso

Compañía

Sobre NosotrosContactoNuestro Trabajo

Servicios

Consultoría DigitalInfraestructura en la NubeDesarrollo SaaSDesarrollo de IATecnología de Video
Desarrollo ERPPersonalización de ZohoDesarrollo de OdooIntegración de SalesforceDesarrollo de CRM Personalizado
Integración de QuickBooksSoluciones IoTDesarrollo de Blockchain
Consultoría de CiberseguridadSoporte IT - L3

© 2026 MicrocosmWorks. Todos los derechos reservados.

Política de PrivacidadTérminos de Servicio
Volver a Perspectivas
IoT Development

Building a Connected Dispenser App

Building a mobile app that pairs with and controls a connected dispenser device over BLE.

Mayank Joshi.webpMayank Chandra Joshi
•
September 7, 2026
•
Actualizado September 7, 2026
•
6 min read
ChatGPT Image Sep 7, 2026, 12_49_11 PM (1).webp
6 min read

Most companion apps are thin BLE remotes: tap a button, write a value, done. A dispenser can't be that thin — a dispense is a health event that must be recorded exactly once, the device speaks a custom binary protocol rather than a standard GATT profile, and "don't miss a dose" can't depend on the phone being awake. Here's how we built the path from a tap to durable, reconcilable state, as part of our wellness & fitness app development work.

At a Glance

Domain Connected wellness — smart supplement dispensing

Core Technologies React Native + Expo, react-native-ble-plx, Zustand, NestJS, MongoDB 8 (transactions)

Key Capabilities Custom BLE command/telemetry protocol, SMP/CBOR firmware updates, transactional dispense recording, timezone-aware reminders

Status Entering Phase 2 of development

The Challenge

Our client was creating a connected wellness product: a device that dispenses supplements, controlled from a phone. The conventional way to build the companion app — and the way most are built — is as a thin remote that writes a characteristic and trusts the result. That works for a light bulb, and almost not at all for a dispenser.

We knew that going in, which is why we didn't build it that way. The limits of the thin-remote model are structural, not tuning problems:

  • A dispense has to be recorded exactly once. It decrements physical stock, counts toward a daily dose, and feeds nutrient analytics. A double-count or a lost write corrupts all three.
  • The device speaks a custom protocol, not a profile. Commands are framed binary packets; there's no off-the-shelf characteristic that means "dispense one tablet."
  • Commands and telemetry don't share a shape. The app writes compact binary commands but reads back JSON telemetry (battery, charging, cartridge id, water quantity) — over different characteristics.
  • Firmware updates ride the same connection. The device has to be upgradable in the field, over a separate management service, without a second tool.
  • Reminders can't live on the device. "You haven't dosed today" is a question about time zones, history, and de-duplication — a server concern, not a phone timer.

The root cause was simple: a dispense isn't a button press, it's a transaction — both on the wire and in the database. A model that treats it as a fire-and-forget write can't make either half reliable.

Our Solution

We split responsibilities so each layer does what it's best at: the phone orchestrates, the device executes a custom protocol defined through our IoT application development practice, and the cloud is the transactional system of record — with reminders handled entirely server-side.

Architecture

  • React Native + Expo + Zustand — a BluetoothStore owns the BLE connection, dispense flow, and telemetry, persisted to AsyncStorage.
  • react-native-ble-plx — scanning, connect/reconnect, and characteristic read/write.
  • A custom command layer — builds framed binary commands for the device, and uses CBOR over an SMP service for firmware updates.
  • NestJS API — dispense and schedule endpoints, built on the same SaaS application development foundation we use across client products; records each dispense inside a MongoDB transaction.
  • MongoDB 8 (Mongoose) — the system of record, plus write-time roll-up collections that downstream analytics read.
  • NestJS Schedule crons + Expo push (queued via ActiveMQ/STOMP) — timezone-aware reminders, de-duplicated through Mongo logs.
  • Sentry (mobile) — production error tracking in the app.


architecture-ble-dispenser.webp

A Custom Wire Protocol

The hard part of any BLE product is that the device doesn't speak a standard language. Ours exposes a command/telemetry service and a separate firmware (SMP) service. The app connects with react-native-ble-plx, then subscribes to telemetry — which arrives as JSON on the 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 });

});

 

A dispense is a compact framed binary command — a start marker, a command id, a length, the cartridge byte, a read/write flag, and an end marker — base64-encoded and written to the command characteristic. (The literal UUIDs and frame markers are redacted here.)

// 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),
  
);

 

The app waits for the device's acknowledgement (it matches the command id back out of the response frame) before it treats the dispense as real — and only then records it server-side. Firmware updates use the same connection but a different language: chunked CBOR payloads over the SMP service.

From Tap to Record

A confirmed dispense becomes durable state through one endpoint — POST /dispense/tablet/:cartridgeId — which writes inside a single MongoDB transaction so the half-dozen effects either all happen or none do:


// 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

});

 

That single transaction updates the dispense log, the monthly roll-up, physical stock, the daily dose count, and any pending reminder — which is exactly why a dispense has to be a transaction, not a write.

Reminders Are a Server Concern

Once a dispense is durable, the questions a phone timer couldn't answer become routine — and they're answered by cron jobs, not the app:

  • Has this user dosed today, in their time zone?
  • Is the cartridge empty (replace) or the stock empty (reorder), or both (discontinuation)?
  • Have they drifted away from a health area they used to take?

Three @nestjs/schedule services handle these — a schedule/dose reminder, a cartridge-state reminder, and a cross-health inactivity reminder. Each resolves the user's local time with date-fns-tz, checks Mongo for whether they've dosed today, de-duplicates against a log collection, and enqueues an Expo push via ActiveMQ. The device is never involved.

Results

Designing for "a dispense is a transaction" instead of a thin-remote write changed what the system can guarantee:

  • Exactly-once recording. A dispense updates the log, roll-ups, stock, and daily dose atomically — no double counts, no partial writes.
  • A documented device protocol, including field upgrades. Commands and telemetry are first-class, and firmware updates ship over the same connection via SMP/CBOR.
  • Reminders that are correct, not approximate. Timezone-aware, history-aware, and de-duplicated server-side — so they fire whether or not the app is open.
  • Analytics it can trust. Because every dispense rolls up at write time, the data feeding charts and adherence is always consistent with the log.

The API, crons, and push queue run on infrastructure managed through our cloud infrastructure services, so the same reliability guarantees hold as the device fleet and user base scale.

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)


Read more from our team

1. Syncing Apple Health & Health Connect

2. Personalized Recipe Search: Retrieval That Knows What You Should Eat Next

3. Optimizing Channel Logo for Different Video Resolutions

 

IoTBLEMobile AppConnectivity
Mayank Joshi.webp

Sobre el Autor

Mayank Chandra Joshi

AI & Cloud Solutions Expert at MicrocosmWorks

Building innovative AI-powered solutions and helping businesses transform through cutting-edge technology.

¿Desea saber más?

Contáctenos para discutir cómo podemos ayudarle a implementar estas soluciones para su negocio.

Ponte en Contacto

Comments (0)

Share your thoughts and join the conversation

Leave a Comment

Your email will not be published

No comments yet

Be the first to share your thoughts!