8000 fix(cbio): refuse and surface a restarted sensor counter by shroominic · Pull Request #215 · shroominic/OpenGlucose · GitHub
[go: up one dir, main page]

Skip to content

fix(cbio): refuse and surface a restarted sensor counter - #215

Closed
shroominic wants to merge 1 commit into
feature/cbio-gs1from
fix/cbio-counter-restart
Closed

shroominic wants to merge 1 commit into
feature/cbio-gs1from
fix/cbio-counter-restart

Conversation

@shroominic
Copy link
Copy Markdown
Owner

Why

The fresh GS1 capture read raw index 1..32 where the earlier one read 1..10969, which looked like the sensor's archive counter restarting. It did not: every capture stamps the same index with the same counter.

capture index range read first record's own counter implied counter at index 1
2026-09-18T12:57 1..32 1789070220 1789070220
2026-09-18T10:48 7537..10969 1789522380 1789070220
2026-09-18T10:25 1..1520 1789070220 1789070220

Index 7537 sits exactly 7536 minutes after index 1 on the same 60 s-per-index line, so the counter is monotonic and the sensor was neither replaced nor reset. The depth difference is the harness: _runSession sends one 06 08 and waits a fixed CBIO_STREAM_SECONDS window, so 10,969 records or 32 records arrive depending on throughput, and both report completed.

The archive, however, could not have told the difference. CbioHistoryArchive.ingest keyed on index alone:

  • a fully overlapping batch returned duplicate with no flag, so a genuinely new cycle would be dropped silently;
  • a partially overlapping batch appended its new positions after the old ones, stapling two unrelated stretches onto one numbering;
  • contiguous stayed true in both cases.

What changed

  • cbio_history_archive.dart: ingest compares an incoming record's own counter against the record already held for that index. The sensor stamps a position once when it produces it, and the captures show that stamp is stable across sessions, so a differing counter is a restart rather than a retransmission. The batch is refused (nothing merged, nothing overwritten), contiguous clears, and the archive reports CbioArchiveIngestStatus.counterRestart with sawCounterRestart and counterRestartIndex.
  • cbio_glucose_session.dart: publishes cbio.raw.counter-restart index=<n> records=<n> contiguous=false as a warning and refreshes sync state, so a restart is visible instead of looking like a sensor that stopped producing records.
  • Tests: three archive tests and one session test.

The anchor needed no change: deriveCbioIndexTimeAnchor already covers only the unbroken 60 s run back from the anchored position (cbio_index_time_anchor_test.dart, "covers only the unbroken counter step back from the anchor"), so a restarted stretch is never timestamped.

RED to GREEN

Library reverted with the new tests kept:

Error: Member not found: 'counterRestart'.
Error: The getter 'sawCounterRestart' isn't defined for the type 'CbioHistoryArchive'.

With the change: archive tests 16/16 including the three new ones, and the session test asserts the history stays [1, 2, 3] with the original values while a counter-restart warning is published.

Gates

make format-check (0 changed), make lint (no issues), make test-unit (app 541 passed, cgm_cbio 133 passed / 2 skipped), plus dart analyze clean in packages/cgm_cbio. Nothing merges to main; units stay unverified; no vendor bytes or addresses anywhere.

Not in this change

The app resumes history at resumeOffset + 1, so a restart is observed only when an overlapping batch actually arrives. Probing for it on every connection costs an extra read and can pull the whole archive back, so it deserves its own change rather than riding along here.

Closes #214.

The archive keyed records by index and treated an index it already held as a
retransmission, whatever counter that record carried. A sensor whose numbering
restarts re-uses those indexes for a different stretch of time, so the new cycle
was absorbed as a duplicate -- silently, because a full overlap reported
duplicate and set no flag -- while any positions above the old newest index
would have been stapled onto the old numbering.

The sensor stamps each position once, when it produces that record, and the
three captured sessions agree on the stamp for every shared index (index 1 is
the same counter in all of them). A position that returns carrying a different
counter therefore cannot be a retransmission. Ingest now refuses that batch,
keeps the record it already held, stays non-contiguous for the rest of the
session, and reports the counterRestart status with the lowest index involved.
The session publishes it as a warning, so a restarted counter reads as a
restarted counter instead of a sensor that stopped producing records.

The anchor was already fail-closed here: it covers only the unbroken 60 s run
back from the anchored position, so a restarted stretch is never timestamped.

Still open and deliberately not changed: the app resumes history above the
newest index it holds, so a restart is only observed when an overlapping batch
arrives. Probing for it costs a read on every connection and belongs in its own
change.
@shroominic
Copy link
Copy Markdown
Owner Author

Carried into #209 (head e6f6f35) as 14626f6 — applied on the consolidation tip with no conflicts.

Claims verified against the branch, not taken from the description. I ran a temporary probe against the pre-fix tree and then against the fix, on the same worktree (ec83adc → 14626f6), driving CbioHistoryArchive.ingest with hand-built 08 frames:

case before after
full overlap, same indexes, different counter duplicate, contiguous=true, no flag, held records kept — a real new cycle would be dropped silently counterRestart, contiguous=false, sawCounterRestart=true, counterRestartIndex=1, held records unchanged
partial overlap (1..3 then 3..5 on a different counter) accepted, contiguous=true, records spliced [1:1000, 2:1060, 3:1120, 4:800000060, 5:800000120] counterRestart, contiguous=false, counterRestartIndex=3, records unchanged [1:1000, 2:1060, 3:1120] — nothing spliced
non-overlap control (1..3 then 4..6) accepted, contiguous=true accepted, contiguous=true, no false positive

The refusal path returns before any mutation of _byIndex, _batchCounts, _gapDetected or _outOfOrder, so "nothing merged, nothing overwritten" holds literally. The anchor claim also holds: _coveredFromIndex breaks the covered range when the counter step is not cbioRecordStepSeconds, so a restarted stretch is never timestamped (its own test names that case). The session publishes cbio.raw.counter-restart index=<n> records=<n> contiguous=false as a warning.

One precision note, recorded rather than changed: counterRestartIndex is the lowest index of the refused batch, not the lowest conflicting index. When a batch's lowest index matches the stored counter and a higher one conflicts, the reported index is the batch's lowest. The getter's first sentence says exactly that; the trailing clause of its doc comment reads as if it were filtered. Behavior is safe either way — the batch is refused whole — so this is a doc nuance, not a defect.

Closing as carried. Carrying PR: #209.

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.

1 participant

0