Conversația cu un inel inteligent fără manual: inginerie inversă a COLMI R10
Presupuneri greșite despre protocol, o conexiune care mintea, dezastre de endianness, pași fantomă și un senzor de temperatură ascuns într-un flux health-check — cum am transformat un inel nedocumentat de 30 de dolari într-un pipeline live de sănătate.
Conversația cu un inel inteligent fără manual
Îmi doream valorile mele vitale — puls, oxigen din sânge, stres, somn, pași, temperatură — curgând continuu de pe un inel COLMI R10 de 30 de dolari în aplicația mea, pe serverul meu și pe pagina publică de sănătate a acestui site. COLMI nu publică niciun SDK și nicio documentație de protocol, iar aplicațiile de fitness obișnuite nu pot citi inelul, pentru că nu folosește serviciul standard Heart Rate. Așa că singura cale a fost ingineria inversă.
Primul contact
Pe hârtie e simplu: inelul vorbește printr-un serviciu personalizat de tip Nordic UART cu două caracteristici — una pe care scrii și una care notifică înapoi. Primul client l-am construit pe presupuneri optimiste luate din notițele de reverse-engineering ale unui model înrudit: pachet de 16 octeți, octet de comandă, sumă de control. Fiecare pachet trimis a fost ignorat, pentru că presupunerile erau greșite în trei locuri deodată: comanda stătea la offset-ul greșit, suma de control folosea modulul greșit, iar opcodurile erau inventate. Inelul răspundea cu tăcere — singurul răspuns pe care hardware-ul îl dă la o întrebare greșită.
Lecția a fost umilința. Am încetat să mai ghicesc și am citit trei ani de muncă a altora: proiectul Python colmi_r02_client, referința de protocol colmi.puxtril.com și clasele de suport COLMI din Gadgetbridge. Cadrul real: opcode pe octetul zero, paisprezece octeți de payload, suma de control egală cu suma primilor cincisprezece octeți modulo 256. Primul pachet corect a fost o cerere de baterie, și 57% a venit înapoi. M-am uitat la cifra asta ca la o scrisoare de la dispozitiv.
Conexiunea care mintea
Apoi a venit cel mai ciudat bug al proiectului: aplicația arăta „Conectat” și nimic — fără baterie, fără valori, fără trafic. Se dovedește că iOS ține conexiunile BLE în viață pentru aplicația ta, iar la întoarcere CoreBluetooth îți dă un periferic deja conectat fără a apela callback-urile de care depinde codul tău. Descoperirea serviciilor nu rula, așa că legătura nu avea canale — o linie telefonică cu ambele capete închise.
Pentru a închide toate căile au fost nevoie de trei fixuri: re-descoperirea serviciilor la restaurarea stării, descoperire la adoptarea unei conexiuni ținute de sistem și un watchdog care taie legăturile zombie după zece secunde de liniște. Am adăugat și o linie de cronologie în interfață — „adoptare conexiune de sistem → descoperire servicii → canale gata” — pentru că depanarea Bluetooth pe întuneric este ghicitoare, iar cu ghicitul am terminat.
Încă o regulă a junglei: inelul servește un singur stăpân. De fiecare dată când QRing ținea conexiunea, scanarea mea se învârtia în gol. Acum aplicația detectează asta, îți spune să închizi cealaltă aplicație și reciclează scanarea la fiecare două minute până când inelul se eliberează.
Învățând inelul să povestească totul
Odată ce bazele au mers, am cartografiat tot vocabularul: fluxuri de puls și SpO2 în timp real; sloturi de activitate de 15 minute pentru pași, calorii și distanță; jurnale de stres și HRV la intervale de 30 de minute; istoricul SpO2 pe ore; și sesiunile de somn — ultimele două printr-un al doilea serviciu BLE „big data”, ale cărui cadre vin tăiate pe mai multe notificări și trebuie reasamblate înainte să însemne ceva.
Două fluxuri s-au împotrivit. Pornirea fluxului de stres sau HRV băga firmware-ul inelului într-o stare catatonică — încă „conectat”, dar nu mai răspundea la nimic. Am renunțat la ele (istoricele lor funcționează) și am adăugat un watchdog al tăcerii: trei minute fără cadre și aplicația reconectează de la zero.
Fiecare unitate minte până la proba contrarie
Cele mai periculoase bug-uri nu sunt căderile, ci numerele plauzibile. Contorul de pași a fost decodat little-endian când era big-endian, transformând 43 de pași în 2,8 milioane. Istoricul de calorii era înmulțit cu zece, deși inelul raportează centi-calorii: un slot de 23 de pași anunța mândru 1.430 kcal — rația unei zile întregi, arsă în cincisprezece minute de stat jos. Totalul live de calorii al inelului include metabolismul bazal, așa că cartonașul arăta discret arderea unei zile întregi încă de la prânz. Iar pe server, fiecare sincronizare la trei minute adăuga din nou aceleași sloturi de 15 minute, umflând pașii de opt ori — până când baza de date a învățat upsert în loc de append.
Regula mea acum: nu avea încredere într-o unitate până nu e verificată cu fizica. Trei sute de pași înseamnă cam 200 de metri și vreo 12 kcal. Dacă matematica încalcă realitatea, decodorul greșește, nu lumea.
Temperatura ascunsă într-un health check
QRing arăta temperatura corpului — 36,7 °C — dar nicăieri nu scria cum se obține. Am sondat toate tipurile de date nedocumentate pe care le-am găsit; inelul a returnat politicos cadre goale. Descoperirea a venit pornind fluxul „health check” al inelului și privind pur și simplu octeții. Cadru după cadru: un octet ținea pulsul meu, doi octeți țineau intervale care se potriveau cu el la milisecundă, iar la mijloc stătea o valoare big-endian în miligrade: 32,85 °C, răcindu-se încet pe parcursul serii — temperatura pielii, exact unde spune fizica că ar trebui să fie. 36,7 din QRing este aceeași temperatură a pielii plus o compensare de temperatură corporală. Senzorul nu se ascundea niciodată — documentația se ascundea.
Ce rămâne nerezolvat
Somnul. Inelul raportează zero nopți înregistrate pe același canal pe care îl folosesc toate celelalte metrici — și, rușinos de zis, probabil e adevărat: încă nu am dormit o noapte întreagă purtându-l. Dimineața de mâine va spune dacă pipeline-ul e complet sau vânătoarea continuă.
Ce păstrez din acest proiect: un protocol este o conversație, nu un document. Pune o întrebare, dispozitivul răspunde în octeți, și fiecare presupunere greșită te costă o zi de tăcere. Dar primul cadru care se parsează — primii 57% adevărați — te face să simți că dispozitivul a decis să se încreadă și el în tine.