Skip to content

Add RawSensors Command - #2

Open
Sighery wants to merge 1 commit into
Puxtril:mainfrom
Sighery:main
Open

Add RawSensors Command#2
Sighery wants to merge 1 commit into
Puxtril:mainfrom
Sighery:main

Conversation

@Sighery

@Sighery Sighery commented Feb 7, 2026

Copy link
Copy Markdown

This allows for getting the raw SpO2, PPG and accelerometer data from the ring. Tested on a Colmi R02.

I'm no C dev so feel free to change my struct definitions/sample code to make it more idiomatic if needed 👍

@Sighery
Sighery force-pushed the main branch 2 times, most recently from b4392bc to 4ae3056 Compare February 7, 2026 18:15
@Puxtril

Puxtril commented Feb 8, 2026

Copy link
Copy Markdown
Owner

Thanks for submitting this! All the C syntax looks good to me. And the decode examples are a nice addition.

I'm tempted to list all 4 SPO2 values as big endian 16-bit integers (Seems like that would be the case), but I cannot find that request in the decompiled QRing app so I can't confirm that's the case. Are you able to confirm?

@Sighery

Sighery commented Feb 9, 2026

Copy link
Copy Markdown
Author

Hey, so, I'm not using the Android app at all. That's why I was interested in hearing how you reversed all these commands, because maybe I could find more stuff that would be useful to me. I used your website + the other repo I link in the PR as reference code for my project. Which is remote page turns on Kindles using the ring through Bluetooth for gestures. I can confirm that I am getting this data and parsing it on the Kindle:

There is some weird thing going on with the SpO2 and PPG data, so far in my short few tests I've done now (I honestly wasn't even parsing it before because I'm only interested in accelerometer data), the data itself seems stuck. But I've also noticed a similar thing with the accelerometer data a few times, where it wouldn't change no matter how much I moved the ring, but would after I reconnected. My guess is there's just something with my parsing code and/or Kindle Bluetooth stack which is not great (I also had to reverse that). But overall I can confirm the command exists, and it returns data after you request that command.

My project and relevant (Go) code is open source. Here is my parsing logic. Here some tests I've written with the data and expected parse result (honestly still not 100% sure I've implemented the parsing correctly, I still struggle with big-endian/little-endian and bit shift operations in my head). And here the relevant bit from the main function that enables/disables the raw sensors and signs up for notifications. Sadly there is no easy way to run/share this since Kindles use a super old kernel so it requires a whole annoying C toolchain to build anything, and then you need one of these devices since there's no public emulators (I'm sure Amazon have an internal one, but nothing the community has access to/has developed).

That being said, you should be able to get the data on your Colmi R03 as well with the bit of Python code from the reference repo, or really any BLE tool out there. As long as you get the packet response once you sign up for the notifications, you can pass that result into any bit of code on any language to do the bit shift calculations.

This allows for getting the raw SpO2, PPG and accelerometer data from
the ring. Tested on a Colmi R02.
@Puxtril

Puxtril commented Feb 15, 2026

Copy link
Copy Markdown
Owner

Sorry, I wasn't giving enough information in my response. I'm curious now that you mention, what the cause for the issue you mentioned, where the ring stops sending data to your kindle device. I'm also vaguely curious if those two SPO2 values are actually 16-bit, which would make more sense for the documentation.

I looked at the Android app in Jadx (package name com.qcwireless.ring). From what I remember, I was mainly looking at the com.oudmon.ble package, specifically the com.oudmon.ble.base.communication.req and com.oudmon.ble.base.communication.rsp packages for unpacking the data packets.

I actually couldn't find any command there with a CMD value larger than 127, because from what I can see, the most-signifigant bit in the command is an error bit. Maybe that's outdated information, but that's currently in my documentation page. So I'm wondering how your command 161 fits into the picture. I don't doubt the validity of this command, but it seems there's some disconnect here. I don't think there's another Android app that handles this communication. Maybe it's a feature of the ring firmware but not used by the app?

Regardless, I will try implementing this code on my end to see what I can read. With the issue of the ring not responding, how did you get that to trigger? Is it just sometimes after sending the 161 command, it doesn't send data back?

@Sighery

Sighery commented Feb 15, 2026

Copy link
Copy Markdown
Author

I'm honestly not quite sure how/if the error bit thing works. I can't say that I've observed it in my (very limited) testing. Perhaps it's used in the sub 128 commands? And they don't use it on the commands above that?

I briefly used this Android app, com.app.cq.ring. I only just paired the ring with the app to figure out if the app had any documentation on the camera gestures, but no luck. I also couldn't figure out any functionality in the UI to get raw sensor data, so my guess is that it's not used by the app. But then again this app of mine is different from yours.

What I can say for certain is that I haven't messed or touched the firmware on this ring at all since I got it. It runs exactly the same firmware it came in the box with (unless the Android app unilaterally updated it after pairing it, but I at least made sure not to go into the firmware update option at all).

For the command response, I did a poor job of explaining myself. It sends data back. It sends accelerometer, SpO2 and PPG notifications back. It doesn't seem to be a way to only sign up to a subtype of those, you just have to read the second byte to figure out the type of notification it is, and ignore the ones you're not interested in.

However, what I noticed is that in my testing, the SpO2 and PPG data never changed. For me, I've always been getting exactly these payloads:

SpO2: a1 01 06 45 05 e1 00 00 02 f0 01 00 00 00 00 c6 (which should map to {Spo2:1605 Spo2Max:225 Spo2Min:0 Spo2Diff:240})
PPG: a1 02 00 01 00 00 00 00 00 00 00 00 00 00 00 a4 (should map to {Ppg:1 PpgMax:0 PpgMin:0 PpgDiff:0})

I'm not 100% confident on my parsing code, so perhaps I need to change the uint8_t fields in the SpO2 parsing to uint16_t, but either way, the payload itself, for those two types, hasn't changed at all. I tried wearing the ring, also not wearing it, wearing it while moving around, wearing it while not moving around, and so far I've always gotten the same payload for the SpO2 and PPG data.

For accelerometer, I have gotten different payloads. I'm still not confident on my parsing code, but I can say the payloads at least change. However I've noticed sometimes the accelerometer data gets "stuck" and reports the same payload even if I'm moving the ring about? That's what I was mentioning that usually unsubscribing from the Read Characteristic, sending a 161 command disable to the Write Characteristic, and then another 161 command enable, and resubscribing again, would "fix" that and make it report new accelerometer payloads again.

But I'm not confident this is on the ring, and is not somehow on my Kindle Bluetooth stack which I've had to reverse/reimplement myself, so it might perfectly be that I just have bugs there, or I'm reusing buffers I shouldn't.


As an aside, do you know where the firmware files are hosted? And if they use any type of encryption? If I get my hands on one, I could try to look at it through Ghidra. I'm still terrible at reverse engineering, but perhaps I can find something that shines a light on the command error bit stuff.

@Puxtril

Puxtril commented Feb 17, 2026

Copy link
Copy Markdown
Owner

I implemented some rudimentary parsing code for these values. A couple things to note:

Acceleration: I wasn't able to replicate the issue of the data getting "stuck", but I only ran it for 2 minutes.

Different values: I was able to get different data for the SPO2 and PPG values. During the sensor enabled command, I just wave my hand around while the LEDs are flashing and they update slightly. This is some example parsed values:

PPG Data
PPG: VAL[11741] MAX[11840] MIN[11476] DIFF[]
PPG: VAL[11697] MAX[11840] MIN[11476] DIFF[]
PPG: VAL[11627] MAX[11840] MIN[11476] DIFF[]
PPG: VAL[11635] MAX[11840] MIN[11476] DIFF[]
PPG: VAL[11626] MAX[11840] MIN[11476] DIFF[]
PPG: VAL[11632] MAX[11840] MIN[11476] DIFF[]
PPG: VAL[11624] MAX[11840] MIN[11476] DIFF[]
PPG: VAL[11637] MAX[11840] MIN[11476] DIFF[]
PPG: VAL[11628] MAX[11840] MIN[11476] DIFF[]
PPG: VAL[11648] MAX[11840] MIN[11476] DIFF[]
PPG: VAL[11626] MAX[11840] MIN[11476] DIFF[]
SPO2 Data
SPO2 Parsed: VAL[211] MAX[225] MIN[87] DIFF[]
SPO2 Parsed: VAL[228] MAX[225] MIN[87] DIFF[]
SPO2 Parsed: VAL[232] MAX[225] MIN[87] DIFF[]
SPO2 Parsed: VAL[209] MAX[225] MIN[87] DIFF[]
SPO2 Parsed: VAL[217] MAX[225] MIN[87] DIFF[]
SPO2 Parsed: VAL[220] MAX[225] MIN[87] DIFF[]
SPO2 Parsed: VAL[172] MAX[225] MIN[87] DIFF[]
SPO2 Parsed: VAL[147] MAX[225] MIN[87] DIFF[]
SPO2 Parsed: VAL[143] MAX[225] MIN[87] DIFF[]
SPO2 Parsed: VAL[229] MAX[225] MIN[87] DIFF[]
SPO2 Parsed: VAL[224] MAX[225] MIN[87] DIFF[]
SPO2 Parsed: VAL[243] MAX[225] MIN[87] DIFF[]
SPO2 Parsed: VAL[241] MAX[225] MIN[87] DIFF[]
SPO2 Parsed: VAL[240] MAX[225] MIN[87] DIFF[]
SPO2 Parsed: VAL[240] MAX[225] MIN[87] DIFF[]
SPO2 Parsed: VAL[209] MAX[225] MIN[87] DIFF[]
SPO2 Parsed: VAL[227] MAX[225] MIN[87] DIFF[]
SPO2 Parsed: VAL[238] MAX[225] MIN[87] DIFF[]
SPO2 Parsed: VAL[235] MAX[225] MIN[87] DIFF[]
SPO2 Parsed: VAL[220] MAX[225] MIN[87] DIFF[]
SPO2 Parsed: VAL[244] MAX[225] MIN[87] DIFF[]

SPO2 16-bit: I couldn't test with my ring because it only sends back values < 255, so it only uses 1 byte. But I took another look at the data you sent, specifically this byte array: a1 01 06 45 05 e1 00 00 02 f0 01 00 00 00 00 c6. I figure your values should be 16-bit, so your interpretation of 1605/225/0 are instead 1605/1505/0, which make them a bit closer.
I was thinking 225 is significant because both our data contains that specific value, but it may just be coincidence. As for the Value being greater than the maximum, I think the SPO2 sensors on these rings are just a bit shit lol. Or the fact that taking SPO2 readings on the finger isn't the best location - wrist is apparently more accurate.

Firmware: I never looked at the firmware, only the Android app. However, there is a discord server, specifically 1 channel where research was exchanged on this ring. I searched the channel and found this Github repo, which may contain the SDK for this ring (hope you can read Chinese). If you want a link to the server, feel free to e-mail me and I'll send you a link.

@QuantumKuba

QuantumKuba commented Jun 21, 2026

Copy link
Copy Markdown

There must be a better way to do this. The ring can also trigger pure gesture controls in some way (they don't trigger the heart rate sensor nor any other sensors), the same way the QRing app is currently handling controlling their built in games and the camera shutter. Im currently doing some RE to try and find how that works.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants