Skip to content

A spot measurement the ring reports as failed is stored and shown as a good reading #74

Description

@foureight84

Split out of #66 so it isn't lost when that closes with the 2.9.0 release.

Reported by @Albabit on v2.9.0+52-rc1, YCBT ring (Ale-Hop2211 E1C7):

  • Off the finger entirely, the app reported a successful reading of 88 and 85 bpm, and 98 % SpO₂.
  • His own Python client against the same ring, same situations, shows the ring declaring failure plainly:
    • Off the finger: t+1.1s 04 0e {00 02} — status 02, failure, no samples at all.
    • On the finger with contact coming and going: plausible samples 86–88 bpm from t+13.1s, then t+20.2s 04 0e {00 02}.

86–88 is exactly the range the app showed as a successful reading.

What the code actually does

The failure chain reads correct end to end, so his diagnosis — that scoping the failure check to the legs each run starts stopped consulting it — is not what the diff did:

  • YCBTDecoder.measurementResultEvents maps 04 0e {00 02} to MeasurementComplete(mode = 0x00, success = false).
  • YCBTDriver.ingest passes it through (isSupported has else -> true).
  • RingSyncCoordinator calls spot.noteCompleted(mode, false); the gate records FAILED.
  • measureHR's poll loop reads completed != nullaborted = trueresult = null, hrState = FAILED, and nothing is published.
  • The Vitals HR button's failure predicate is coordinator.hrState == failedState.

That chain also predates #71 (it came in with #59/#60), so this is not a regression introduced by the split-buttons work.

Two candidate mechanisms

  1. The live-sample gate reopens before the ring stops streaming. measureHR's finally calls engine?.stopHeartRate(), which only enqueues the stop, then immediately reopens the gate. Every 04 13 sample arriving between the reopen and the ring honouring the stop passes EventPersistenceSubscriber with the gate open and is stored as an ordinary live heart-rate row. On an aborted run those are precisely the junk samples. This fits the moved-around case.
  2. Nothing was stored by the measurement at all, and the card is showing the newest row the next history sync pulled in — indistinguishable to the user.

Neither explains the off-the-finger case, where the ring sent no samples and the app still showed 88 and 85.

Why nothing caught it

RingSyncCoordinator takes concrete RingBLEClient and PulseLoopDatabase, so the measurement legs have no test harness. 9da8532 added failure-verdict coverage to SpotMeasurementGateTest, but that exercises the gate class in isolation — the coordinator's abort branch is still executed by no test. Building that harness is probably the first move here.

Next

Waiting on a diagnostics export from @Albabit taken straight after a failed measurement (asked for in #66 (comment)): the timestamps of the heart-rate rows stored around that minute, and whether they carry the spot flag.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions