fix(crp): stop claiming SpO2 the R11 has no sensor for, and ask the ring instead - #40
fix(crp): stop claiming SpO2 the R11 has no sensor for, and ask the ring instead#40foureight84 wants to merge 1 commit into
Conversation
…ing instead zaggash's 2026-07-25 capture (build 30) is the first from a shipped R11, and it settles three things we had been guessing at. The R11 has no SpO2 hardware. COLMI's own spec sheet lists two sensors — an STK8321 accelerometer and a Vcare VC30F *heart rate* unit — and pulse oximetry needs a second wavelength the VC30F hasn't got. The capture agrees: every spot SpO2 answered with the 0xFF no-reading sentinel, every all-day SpO2 frame came back all-zero. We were showing a Measure button that could never succeed. So SpO2 leaves CRPCoordinator.capabilities (the unconditional floor) and becomes a bitmapGatedCapabilities entry, granted only when the ring itself claims it. The vendor exposes exactly the read-back for that — querySupportSpO2Type (2/37) answers NOT_SUPPORT / SLEEP_OXYGEN / TIMING_OXYGEN — so CRPSyncEngine asks on connect and CRPDecoder.decodeSpO2Support turns a real type into the grant, via the same additive SupportFunctions path YCBT's 02 01 bitmap already uses. A unit that genuinely has the sensor earns SpO2 back without us hardcoding either way. The monitor-state read-backs come along too (2/6 HR, 2/7 HRV, 2/8 SpO2, 2/45 stress, 2/21 temp; each reports the configured interval, 0 = off). Stress, temperature and firmware all went 23-sent/0-answered on his ring, and these replies are what will tell "the monitor is switched off" apart from "this ring lacks the sensor" in the next capture. Temperature history was on the wrong opcode: q.b(2,48) is the vendor's querySleepState (d1/b.java:650), while real temp history is i0.b(day, frameIndex) = q.c(2,22,[day,idx]). That is why 48 never answered. Fixed to 2/22; its sample layout is still unconfirmed by a non-empty capture, so the reply stays an ack rather than inventing values. Finally, group 3 / cmd 7 is a measurement-failed signal, not a wear signal. 32 pushes in the capture, every one [00], never once [01], several seconds after a good HR reading — it lands ~2 ms before the 0xFF sentinel. As an abort it is reliable (every measure that saw one produced no reading; every measure that didn't produced one) and worth keeping: it turns a 60 s SpO2 dead-wait into a ~4.5 s failure. As "put the ring on" copy it was wrong, blaming how the user wears a ring for a vital it cannot measure. WearEvidence now holds the rule — a real bpm can only be read off skin, so a recent HR sample vouches for contact and downgrades the message to the generic failure. AGENTS.md is corrected to match; its previous note read [00] as "ring not worn". 780 tests pass.
|
Closing — the central premise is falsified by an earlier capture. zaggash sent a 2026-07-23 diagnostics export (build 26) from the same COLMI R11, and it contains a real SpO2 reading: What actually happens: SpO2 is slow and contact-sensitive. Of three attempts in that capture, the one that succeeded took 48 seconds of silence before answering; the two that failed both drew a The Still valid, moving to a clean branch:
The wear-state fast-fail itself stays as it shipped: the |
Diagnosed from zaggash's 2026-07-25 diagnostics export — the first capture from a shipped build (v1.0.0+30) on his COLMI R11.
The R11 has no SpO2 hardware
COLMI's own spec sheet lists exactly two sensors: an STK8321 accelerometer and a Vcare VC30F — heart rate. Pulse oximetry needs a second wavelength the VC30F hasn't got. It advertises HR, sleep, activity and stress, and never mentions SpO2.
The capture agrees:
0xFFno-reading sentinel, every attemptWe were showing a Measure button that could never succeed.
Ask the ring instead of hardcoding
SPO2/MANUAL_SPO2leaveCRPCoordinator.capabilities(the unconditional floor) and becomebitmapGatedCapabilitiesentries. The vendor exposes exactly the read-back needed —querySupportSpO2Type(2/37) answersNOT_SUPPORT/SLEEP_OXYGEN/TIMING_OXYGEN— soCRPSyncEngineasks on connect andCRPDecoder.decodeSpO2Supportturns a real type into the grant, through the same additiveSupportFunctionspath YCBT's02 01bitmap already uses.A unit that genuinely has the sensor earns SpO2 back. One that doesn't never gets it. Neither answer is hardcoded.
The monitor-state read-backs come along too —
2/6HR,2/7HRV,2/8SpO2,2/45stress,2/21temp, each reporting the configured interval (0= off). Stress, temperature and firmware all went 23-sent / 0-answered on his ring; these replies are what will distinguish "the monitor is switched off" from "this ring lacks the sensor" in the next capture.Temperature history was on the wrong opcode
q.b(2,48)is the vendor'squerySleepState(d1/b.java:650). Real temperature history isi0.b(day, frameIndex)=q.c(2,22,[day,idx]), the same shape as the other timing histories. That is why2/48never answered. Fixed to2/22— its sample layout is still unconfirmed by a non-empty capture, so the reply stays an ack rather than inventing values.group 3 / cmd 7is a measurement-failed signal, not a wear signal32 pushes in the capture, every one
[00], never once[01]— several arriving seconds after a good HR reading. It lands ~2 ms before the0xFFsentinel.WearEvidencenow holds the rule: a real bpm can only be read off skin, so a recent HR sample vouches for contact and downgrades the message to the generic failure.Also validated by this capture
The multi-frame follow-up, which was the open hardware question from the rc3 work: HR asked frames (0,0)+(0,1) and got both; HRV asked (0,0)…(0,3) and got all four. HR history decoded 27 readings at 00:10–11:35 local (46–104 bpm), HRV 11 readings (30–56 ms), sleep 12 records across light/deep/REM — so local-midnight anchoring is correct with no UTC drift.
Testing
TZ=UTC ./gradlew testDebugUnitTest— 780 tests, 0 failures. New coverage:WearEvidenceTest(6 tests for the HR-as-wear-witness rule, incl. expiry and a clock-jump guard) and three decoder/coordinator tests assertingNOT_SUPPORTgrants nothing, types 1/2 grant SpO2 back, and SpO2 is genuinely absent from the floor while HR stays unconditional.Reviewer notes
AGENTS.mdis corrected — its previous note readg3c7 [00]as "ring not worn".2/37and state replies, and confirm whether stress/temp are off or absent.