grig-teo:~$
← back to articles

Talking to a Smart Ring That Has No Manual: Reverse-Engineering My COLMI R10

Wrong protocol guesses, a connection that lied about being connected, endianness disasters, phantom steps, and a temperature sensor hiding inside a health-check stream — how I turned a $30 undocumented ring into a live health pipeline.

Talking to a Smart Ring That Has No Manual

I wanted my vitals — heart rate, blood oxygen, stress, sleep, steps, temperature — flowing continuously from a $30 COLMI R10 ring into my own app, my own server, and the public health page on this site. COLMI publishes no SDK and no protocol documentation, and mainstream fitness apps can't read the ring because it doesn't use the standard Heart Rate Service. So the only way in was reverse engineering.

First contact

On paper the setup is simple: the ring speaks a Nordic-UART-style custom service with two characteristics, one you write to and one that notifies back. My first client was built on optimistic assumptions lifted from a sibling model's reverse-engineering notes — a 16-byte packet, a command byte, a checksum. Every packet I sent was ignored, because my assumptions were wrong in three places at once: the command sat at the wrong offset, the checksum used the wrong modulus, and the opcodes were invented. The ring answered with silence, which is the only answer hardware ever gives to a wrong question.

The cure was humility. I stopped guessing and read three years of other people's work: the colmi_r02_client Python project, the community protocol reference at colmi.puxtril.com, and Gadgetbridge's COLMI support classes. The real frame: opcode at byte zero, fourteen payload bytes, checksum equal to the sum of the first fifteen bytes modulo 256. My first correct packet was a battery request, and 57% came back. I stared at that number like it was a letter from the device.

The connection that lied

Then came the strangest bug of the project: the app said “Connected” and showed nothing — no battery, no readings, no traffic. It turns out iOS keeps BLE links alive on your app's behalf, and when you come back, CoreBluetooth hands you an already-connected peripheral without firing the callbacks your setup code depends on. My service discovery never ran, so the link had no channels at all — a phone line with both ends off the hook.

Closing every path took three separate fixes: rediscover services on state restoration, discover when adopting a system-held connection, and a watchdog that force-drops zombie links after ten silent seconds. I also added a one-line breadcrumb trail to the UI — “adopting system-held ring → discovering services → channels ready” — because debugging Bluetooth without visibility is guesswork, and I was done guessing.

One more rule of the jungle: a ring serves exactly one master. Whenever QRing held the connection, my scan spun forever. Now the app detects that, asks you to close the other app, and recycles the scan every two minutes until the ring is free.

Teaching the ring to talk about everything

With the basics flowing, I mapped the whole vocabulary: realtime heart-rate and SpO2 streams; 15-minute activity slots for steps, calories and distance; 30-minute stress and HRV interval logs; hourly SpO2 history; and sleep sessions — the last two over a second, “big data” BLE service whose frames arrive chopped across notifications and have to be reassembled before they mean anything.

Two streams fought back. Starting the realtime stress or HRV stream put the ring's firmware into a catatonic state — still “connected”, answering nothing. I dropped those streams (their histories work fine) and added a silence watchdog: three minutes without a frame and the app disconnects and starts over.

Every unit is a lie until proven otherwise

The most dangerous bugs were not crashes — they were plausible numbers. The live step counter decoded little-endian when it was big-endian, turning 43 steps into 2.8 million. The calorie history multiplied by ten when the ring actually reports centi-calories: one 23-step slot proudly claimed 1,430 kcal — a full day of eating, burned in fifteen minutes of sitting still. The ring's live calorie total includes basal metabolism, so the card quietly showed an entire day's burn by noon. And on the server, every three-minute sync appended the same 15-minute slots again, inflating my step count eightfold until the database learned to upsert instead of append.

My rule now: never trust a unit until it's checked against physics. Three hundred steps should be roughly 200 meters and roughly 12 kcal. If the math breaks reality, the decoder is wrong — not the world.

The temperature hiding in a health check

QRing showed body temperature — 36.7 °C — but no documentation anywhere said how to get it. I probed every undocumented data type I could find; the ring politely returned empty frames. The break came from starting the ring's health-check stream and simply watching the bytes. Frame after frame: one byte held my heart rate, two bytes held beat intervals that matched it to the millisecond, and in the middle sat a big-endian milli-degree value reading 32.85 °C that slowly cooled through the evening — skin temperature, exactly where physics says a finger should sit. QRing's 36.7 is that skin value plus a core-temperature compensation. The sensor was never hiding; the documentation was.

What still isn't solved

Sleep. The ring reports zero recorded nights through the same channel every other metric uses — and, embarrassingly, that's probably accurate: I haven't spent a full night wearing it yet. Tomorrow morning will tell whether the pipeline is complete or the hunt continues.

What I keep from this project: a protocol is a conversation, not a document. You ask a question, the device answers in bytes, and every wrong assumption costs you a day of silence. But the first frame that parses — the first honest 57% — feels like the device deciding to trust you back.

Talking to a Smart Ring That Has No Manual: Reverse-Engineering My COLMI R10 | grig-teo