8000 feat(cbio): anchor the GS1 record index to the clock the app set by shroominic · Pull Request #197 · shroominic/OpenGlucose · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/testing/cbio-gs1-app-live.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,33 @@ protocol does not establish.
per stored minute. It carries no epoch, so the app does not invent a
wall-clock time from it.

## Clock anchor

The counter stays epoch-less; what the app can offer instead is the one
reference it establishes itself. The session writes the sensor clock once per
session (`06 03 LE32(epoch)`), and the surface reads an index as a clock only
when the sensor's own newest record stamp agrees with the app's clock to within
`cbioAnchorTolerance` (3 minutes):

- the anchor is the newest stored position plus that position's own stamp, and
every older position steps back 60 s per index, as the vendor layout reports
the counter (`base + 60 * i`);
- the range the anchor speaks for stops at the first hole or counter jump, so a
position the archive cannot support keeps no timestamp;
- the anchor is published in the snapshot metadata
(`cgm.cbio.clock.anchorIndex`, `…anchorEpochSeconds`,
`…anchorCoveredFrom`, `…referenceEpochSeconds`), and the hero states the
provenance: `Sensor clock set by this app · latest 09:05 (±1 min)`;
- when the clock was never written, or the sensor's stamps disagree with the
app's clock (the +7 h 28 m counter of #146), no anchor is published and the
hero says `Sensor clock unsynced · ordered by sensor index, not by clock`
instead of a placeholder time.

The timestamps therefore inherit the clock this app set on the sensor and its
drift, which the surface states as the ±1 minute of one stored record. The
capture above predates the anchor and shows the unsynced copy; a device
re-capture of the anchored copy is still pending.

## How to reproduce

```bash
Expand Down
8 changes: 8 additions & 0 deletions openhealth/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1177,6 +1177,14 @@ class _DashboardHeroCardState extends State<_DashboardHeroCard> {
color: const Color(0xFFC7E4DD),
),
),
const SizedBox(height: 6),
Text(
cbioClockStateText(snapshot, reading: latest),
key: const ValueKey<String>('cbioClockState'),
style: theme.textTheme.bodySmall?.copyWith(
color: const Color(0xFFC7E4DD),
),
),
],
if (latest?.isDisplayProvisional == true) ...<Widget>[
const SizedBox(height: 6),
Expand Down
43 changes: 43 additions & 0 deletions openhealth/lib/src/session_presentation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,49 @@ String cbioStoredRangeText(Iterable<CgmReading> readings) {
'sensor minutes ${positions.first}–${positions.last}';
}

/// The index-to-clock anchor a GS1 session published, or null when it has none.
CbioIndexTimeAnchor? cbioAnchorForSnapshot(CgmSessionSnapshot snapshot) =>
isCbioSnapshot(snapshot)
? CbioIndexTimeAnchor.fromMetadata(snapshot.metadata)
: null;

/// The clock state of a GS1 surface.
///
/// The record index is a wall clock only where the session holds an anchor for
/// it: the clock this app set on the sensor, confirmed against the sensor's own
/// newest record stamp. Without one the line names the ordering the surface can
/// stand behind instead of a placeholder time, and with one it states the
/// reference and the minute it can be trusted to.
/// [reading] is the same reading the surface shows as the latest, so the clock
/// line and the value above it always describe one position.
String cbioClockStateText(
CgmSessionSnapshot snapshot, {
CgmReading? reading,
DateTime? now,
}) {
if (!isCbioSnapshot(snapshot)) {
return '';
}
final anchor = cbioAnchorForSnapshot(snapshot);
if (anchor == null) {
return 'Sensor clock unsynced · ordered by sensor index, not by clock';
}
final latest = reading ?? snapshot.latestReading;
// The line repeats the reading's own stamp, so a position the publisher left
// untimed is never given a clock the anchor does not cover.
if (latest == null ||
clampedDisplayRecordedAt(latest.recordedAt, now: now) == null) {
return 'Sensor clock set by this app · this position has no anchored time';
}
return 'Sensor clock set by this app · latest '
'${readingTimeText(latest, now: now)} (${_anchorUncertaintyText(anchor)})';
}

String _anchorUncertaintyText(CbioIndexTimeAnchor anchor) {
final minutes = anchor.uncertainty.inMinutes;
return minutes >= 1 ? '±$minutes min' : '±${anchor.uncertainty.inSeconds} s';
}

/// The live driver rebuilds this diagnostic from its in-memory packet counter
/// on each snapshot. Retained glucose history or a saved NFC state is not proof
/// that the current Bluetooth session received verified packets.
Expand Down
Loading
Loading
0