La mayoría de las aplicaciones complementarias son mandos remotos BLE delgados: toca un botón, escribe un valor y listo. Un dispensador no puede ser tan simple: una dispensación es un evento de salud que debe registrarse exactamente una vez, el dispositivo utiliza un protocolo binario personalizado en lugar de un perfil GATT estándar, y "no perder una dosis" no puede depender de que el teléfono esté encendido. Así es como construimos el camino desde un toque hasta un estado duradero y reconciliable, como parte de nuestro trabajo de desarrollo de aplicaciones de bienestar y fitness.
En resumen
Dominio Bienestar conectado — dispensación inteligente de suplementos
Tecnologías principales React Native + Expo, react-native-ble-plx, Zustand, NestJS, MongoDB 8 (transacciones)
Capacidades clave Protocolo BLE personalizado de comando/telemetría, actualizaciones de firmware SMP/CBOR, registro transaccional de dispensación, recordatorios con reconocimiento de zona horaria
Estado Entrando en la Fase 2 de desarrollo
El Desafío
Nuestro cliente estaba creando un producto de bienestar conectado: un dispositivo que dispensa suplementos, controlado desde un teléfono. La forma convencional de construir la aplicación complementaria —y la forma en que la mayoría se construyen— es como un control remoto delgado que escribe una característica y confía en el resultado. Eso funciona para una bombilla, y casi no sirve para un dispensador.
Sabíamos eso de antemano, por eso no lo construimos de esa manera. Los límites del modelo de control remoto delgado son estructurales, no problemas de ajuste:
- Una dispensación debe registrarse exactamente una vez. Reduce el stock físico, cuenta para una dosis diaria y alimenta el análisis de nutrientes. Un doble conteo o una escritura perdida corrompe los tres.
- El dispositivo utiliza un protocolo personalizado, no un perfil. Los comandos son paquetes binarios enmarcados; no existe una característica estándar que signifique "dispensar una tableta".
- Los comandos y la telemetría no comparten una forma. La aplicación escribe comandos binarios compactos pero lee telemetría JSON (batería, carga, ID del cartucho, cantidad de agua) — a través de diferentes características.
- Las actualizaciones de firmware utilizan la misma conexión. El dispositivo debe ser actualizable en el campo, a través de un servicio de gestión separado, sin una segunda herramienta.
- Los recordatorios no pueden residir en el dispositivo. "No has tomado tu dosis hoy" es una cuestión de zonas horarias, historial y deduplicación — una preocupación del servidor, no un temporizador del teléfono.
La causa principal era simple: una dispensación no es una pulsación de botón, es una transacción — tanto en el cable como en la base de datos. Un modelo que la trata como una escritura de "disparar y olvidar" no puede hacer que ninguna de las dos partes sea fiable.
Nuestra Solución
Dividimos las responsabilidades para que cada capa haga lo que mejor sabe hacer: el teléfono orquesta, el dispositivo ejecuta un protocolo personalizado definido a través de nuestra práctica de desarrollo de aplicaciones IoT, y la nube es el sistema transaccional de registro — con recordatorios gestionados completamente en el servidor.
Arquitectura
- React Native + Expo + Zustand — un BluetoothStore gestiona la conexión BLE, el flujo de dispensación y la telemetría, persistidos en AsyncStorage.
- react-native-ble-plx — escaneo, conexión/reconexión y lectura/escritura de características.
- Una capa de comandos personalizada — construye comandos binarios enmarcados para el dispositivo y utiliza CBOR sobre un servicio SMP para actualizaciones de firmware.
- API NestJS — endpoints de dispensación y programación, construidos sobre la misma base de desarrollo de aplicaciones SaaS que usamos en todos los productos del cliente; registra cada dispensación dentro de una transacción de MongoDB.
- MongoDB 8 (Mongoose) — el sistema de registro, además de colecciones de resumen en tiempo de escritura que leen los análisis posteriores.
- Crons de NestJS Schedule + Expo push (en cola a través de ActiveMQ/STOMP) — recordatorios con reconocimiento de zona horaria, deduplicados a través de los logs de Mongo.
- Sentry (móvil) — seguimiento de errores en producción en la aplicación.

Un protocolo de comunicación personalizado
La parte difícil de cualquier producto BLE es que el dispositivo no utiliza un lenguaje estándar. El nuestro expone un servicio de comando/telemetría y un servicio de firmware (SMP) separado. La aplicación se conecta con react-native-ble-plx, luego se suscribe a la telemetría — que llega como JSON en la característica de notificación:
// 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 });
});
Una dispensación es un comando binario enmarcado compacto — un marcador de inicio, un ID de comando, una longitud, el byte del cartucho, un indicador de lectura/escritura y un marcador de finalización — codificado en base64 y escrito en la característica de comando. (Los UUID literales y los marcadores de trama se han omitido aquí.)
// 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),
);
La aplicación espera el reconocimiento del dispositivo (hace coincidir el ID de comando de vuelta fuera del marco de respuesta) antes de considerar la dispensación como real — y solo entonces la registra en el servidor. Las actualizaciones de firmware utilizan la misma conexión pero un lenguaje diferente: cargas útiles CBOR fragmentadas sobre el servicio SMP.
Del toque al registro
Una dispensación confirmada se convierte en un estado duradero a través de un único endpoint — POST /dispense/tablet/:cartridgeId — que escribe dentro de una única transacción de MongoDB para que la media docena de efectos ocurran todos o ninguno:
// 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
});
Esa única transacción actualiza el registro de dispensación, el resumen mensual, el stock físico, el recuento de dosis diarias y cualquier recordatorio pendiente — que es exactamente la razón por la que una dispensación tiene que ser una transacción, no una escritura.
Los recordatorios son una preocupación del servidor
Una vez que una dispensación es duradera, las preguntas que un temporizador de teléfono no podía responder se vuelven rutinarias — y son respondidas por trabajos cron, no por la aplicación:
- ¿Ha tomado este usuario su dosis hoy, en su zona horaria?
- ¿Está el cartucho vacío (reemplazar) o el stock vacío (reordenar), o ambos (interrupción)?
- ¿Se han alejado de un área de salud que solían atender?
Tres servicios de @nestjs/schedule gestionan esto — un recordatorio de horario/dosis, un recordatorio de estado del cartucho y un recordatorio de inactividad de salud cruzada. Cada uno resuelve la hora local del usuario con date-fns-tz, verifica en Mongo si han tomado su dosis hoy, deduplica contra una colección de logs y pone en cola una notificación Expo push a través de ActiveMQ. El dispositivo nunca interviene.
Resultados
Diseñar para "una dispensación es una transacción" en lugar de una escritura remota delgada cambió lo que el sistema puede garantizar:
- Registro exactamente una vez. Una dispensación actualiza el registro, los resúmenes, el stock y la dosis diaria de forma atómica — sin dobles conteos ni escrituras parciales.
- Un protocolo de dispositivo documentado, incluyendo actualizaciones en campo. Los comandos y la telemetría son de primera clase, y las actualizaciones de firmware se envían a través de la misma conexión mediante SMP/CBOR.
- Recordatorios que son correctos, no aproximados. Con reconocimiento de zona horaria, de historial y deduplicados en el servidor — para que se activen independientemente de si la aplicación está abierta.
- Análisis en los que se puede confiar. Dado que cada dispensación se resume en el momento de la escritura, los datos que alimentan los gráficos y la adherencia son siempre consistentes con el registro.
La API, los crons y la cola de notificaciones se ejecutan en una infraestructura gestionada a través de nuestros servicios de infraestructura en la nube, por lo que las mismas garantías de fiabilidad se mantienen a medida que la flota de dispositivos y la base de usuarios escalan.
Pila tecnológica: React Native · Expo · TypeScript · react-native-ble-plx · Zustand · CBOR (firmware SMP) · NestJS · MongoDB 8 · Mongoose (transactions) · @nestjs/schedule · ActiveMQ / STOMP · Expo Server SDK · JWT · AWS S3 · AWS SES · Sentry (mobile)
Lea más de nuestro equipo
1. Sincronización de Apple Health y Health Connect
2. Búsqueda de recetas personalizada: recuperación que sabe qué deberías comer a continuación
3. Optimización del logotipo del canal para diferentes resoluciones de video

