Hablar con un anillo inteligente que no tiene manual: ingeniería inversa de mi COLMI R10
Suposiciones erróneas sobre el protocolo, una conexión que mentía sobre estar conectada, desastres de endianness, pasos fantasma y un sensor de temperatura escondido dentro de un flujo de control de salud: así convertí un anillo sin documentación de 30 dólares en un pipeline de salud en vivo.
Hablar con un anillo inteligente que no tiene manual
Quería que mis constantes vitales —frecuencia cardíaca, oxígeno en sangre, estrés, sueño, pasos, temperatura— fluyeran de forma continua desde un anillo COLMI R10 de 30 dólares hacia mi propia aplicación, mi propio servidor y la página pública de salud de este sitio. COLMI no publica ningún SDK ni documentación del protocolo, y las aplicaciones de fitness convencionales no pueden leer el anillo porque no usa el servicio estándar Heart Rate Service. Así que la única vía de entrada era la ingeniería inversa.
Primer contacto
Sobre el papel, la configuración es sencilla: el anillo habla un servicio personalizado al estilo Nordic UART con dos características, una en la que se escribe y otra que notifica de vuelta. Mi primer cliente se construyó sobre suposiciones optimistas tomadas de las notas de ingeniería inversa de un modelo hermano: un paquete de 16 bytes, un byte de comando, un checksum. Todos los paquetes que envié fueron ignorados, porque mis suposiciones fallaban en tres puntos a la vez: el comando estaba en el desplazamiento equivocado, el checksum usaba el módulo equivocado y los opcodes eran inventados. El anillo respondió con silencio, que es la única respuesta que el hardware da a una pregunta equivocada.
El remedio fue la humildad. Dejé de adivinar y leí tres años del trabajo de otras personas: el proyecto Python colmi_r02_client, la referencia comunitaria del protocolo en colmi.puxtril.com y las clases de soporte de COLMI en Gadgetbridge. La trama real: el opcode en el byte cero, catorce bytes de carga útil y un checksum igual a la suma de los primeros quince bytes módulo 256. Mi primer paquete correcto fue una petición de batería, y respondió un 57 %. Me quedé mirando ese número como si fuera una carta del dispositivo.
La conexión que mentía
Después llegó el error más extraño del proyecto: la aplicación decía «Connected» y no mostraba nada: ni batería, ni lecturas, ni tráfico. Resulta que iOS mantiene vivos los enlaces BLE en nombre de tu aplicación y, cuando vuelves, CoreBluetooth te entrega un periférico ya conectado sin disparar las llamadas de retorno de las que depende tu código de configuración. El descubrimiento de servicios nunca llegó a ejecutarse, así que el enlace no tenía ningún canal: una línea de teléfono con los dos extremos descolgados.
Cerrar todas las vías exigió tres correcciones distintas: redescubrir los servicios al restaurarse el estado, descubrir los servicios al adoptar una conexión retenida por el sistema y un watchdog que fuerza la caída de los enlaces zombi tras diez segundos de silencio. También añadí a la interfaz un rastro de una sola línea —«adoptando anillo retenido por el sistema → descubriendo servicios → canales listos»—, porque depurar Bluetooth sin visibilidad es adivinar, y yo ya había dejado de adivinar.
Una regla más de la selva: un anillo sirve exactamente a un dueño. Siempre que QRing retenía la conexión, mi escaneo giraba sin fin. Ahora la aplicación lo detecta, te pide que cierres la otra aplicación y reinicia el escaneo cada dos minutos hasta que el anillo queda libre.
Enseñar al anillo a hablar de todo
Con lo básico ya fluyendo, mapeé todo el vocabulario: flujos de frecuencia cardíaca y SpO2 en tiempo real; franjas de actividad de 15 minutos para pasos, calorías y distancia; registros de intervalos de estrés y HRV de 30 minutos; historial de SpO2 por horas; y sesiones de sueño, estas dos últimas a través de un segundo servicio BLE de «big data» cuyas tramas llegan troceadas entre notificaciones y hay que reensamblar antes de que signifiquen algo.
Dos flujos se resistieron. Iniciar el flujo de estrés o de HRV en tiempo real dejaba el firmware del anillo en un estado catatónico: seguía «conectado», pero no respondía a nada. Descarté esos flujos (sus históricos funcionan bien) y añadí un watchdog de silencio: tres minutos sin una sola trama y la aplicación se desconecta y vuelve a empezar.
Toda unidad es mentira hasta que se demuestre lo contrario
Los errores más peligrosos no eran caídas: eran números verosímiles. El contador de pasos en vivo decodificaba en little-endian cuando era big-endian, y convertía 43 pasos en 2,8 millones. El historial de calorías multiplicaba por diez cuando el anillo en realidad informa en centicalorías: una franja de 23 pasos presumía de 1.430 kcal, un día entero de comida quemado en quince minutos sentado sin moverme. El total de calorías en vivo del anillo incluye el metabolismo basal, así que la tarjeta mostraba en silencio el gasto de todo un día antes del mediodía. Y en el servidor, cada sincronización de tres minutos volvía a añadir las mismas franjas de 15 minutos, inflando mi recuento de pasos ocho veces hasta que la base de datos aprendió a hacer upsert en lugar de añadir.
Mi regla ahora: no fiarse de una unidad hasta contrastarla con la física. Trescientos pasos deberían ser más o menos 200 metros y más o menos 12 kcal. Si las cuentas contradicen la realidad, el decodificador está equivocado, no el mundo.
La temperatura escondida en un control de salud
QRing mostraba la temperatura corporal —36,7 °C—, pero ninguna documentación indicaba cómo obtenerla. Sondeé todos los tipos de datos sin documentar que pude encontrar; el anillo me devolvía cortésmente tramas vacías. El avance llegó al iniciar el flujo de control de salud del anillo y limitarme a observar los bytes. Trama tras trama: un byte contenía mi frecuencia cardíaca, dos bytes contenían intervalos entre latidos que coincidían con ella al milisegundo y, en medio, había un valor big-endian en milésimas de grado que marcaba 32,85 °C y se enfriaba lentamente con la tarde: temperatura de la piel, exactamente donde la física dice que debería estar un dedo. El 36,7 de QRing es ese valor de piel más una compensación de temperatura central. El sensor nunca estuvo escondido; lo que estaba escondido era la documentación.
Lo que aún no está resuelto
El sueño. El anillo informa de cero noches registradas por el mismo canal que usan todas las demás métricas y, para mi vergüenza, probablemente sea cierto: todavía no he pasado una noche entera llevándolo puesto. Mañana por la mañana se verá si el pipeline está completo o si la caza continúa.
Lo que me llevo de este proyecto: un protocolo es una conversación, no un documento. Tú haces una pregunta, el dispositivo responde en bytes y cada suposición equivocada te cuesta un día de silencio. Pero la primera trama que se parsea —el primer 57 % honesto— se siente como si el dispositivo decidiera confiar en ti a su vez.