Skip to content

Emit calibrations as manual glucose; stop marking display-only readings user-entered - #102

Merged
ps2 merged 2 commits into
LoopKit:next-devfrom
ETolboom:g7-calibration-manual-glucose
Oct 25, 2026
Merged

ps2 merged 2 commits into
LoopKit:next-devfrom
ETolboom:g7-calibration-manual-glucose

Conversation

@ETolboom

@ETolboom ETolboom commented Oct 25, 2026 •

Copy link
Copy Markdown

What

Two calibration-related fixes:

  1. Emit entered calibrations as manual glucose readings. calibrate(glucose:at:) now also emits the entered meter value as a user-entered NewGlucoseSample, timestamped at the time it was taken, so apps can store it as the manual blood glucose reading it is. Loop stores it like a fingerstick entered in the UI; Trio charts user-entered samples as MBG (fix: store calibrations as manual fingersticks nightscout/Trio#1472). The sample is emitted at entry, before and regardless of the sensor's answer: the fingerstick is a measurement in its own right even when the sensor later refuses the calibration. Its sync identifier lives in its own namespace so it can never dedup-collide with sensor readings.

  2. Stop marking display-only readings as user-entered. Live and backfill readings now carry wasUserEntered: false; isDisplayOnly already carries the "shifted for display while settling a calibration" meaning. The old wasUserEntered = glucoseIsDisplayOnly mapping is a G5-era idiom that mislabels device readings as manual entries.

Also included: the G7CalibrationBoundsMessage.lastGlucose documentation now matches what the field actually holds (the stored value of the most recent calibration, from any display; never the source for what this app entered, since a record can be stale or another display's).

Why

A Trio tester reported: entered a fingerstick of 72 as a G7 calibration, and two cycles later Trio showed 61 as an MBG: the sensor's post-calibration settling value, not the entered one. Two bugs combined:

  • The entered calibration was never emitted, so 72 was never recorded.
  • The sensor flags the readings it emits while settling a calibration as display-only (secondary algorithm state byte, bit 0x10), and the old mapping turned those into user-entered samples, which Trio stores as MBG.

Evidence

Field logs from the reporter's device

Logs from the tester's Trio install (pre-fix build), calibration of 163 mg/dL entered at 17:45:57:

17:45:57  Calibration 163 mg/dL entered; queued for the sensor's next connection
          (nothing stored as a calibration at entry time)
17:46:45  live EGV glucose=165, flag byte 0x0f (normal)
17:46:45  calibration sent; sensor replies 34 0001 00 (accepted)
17:46:45  backfill: glucose=165 flag 0x0f, then glucose=163 flag 0x1f
          (glucoseIsDisplayOnly: true)
17:46:45  Trio: "Display-only reading ..., storing it" -> "Storing 1 calibrations"
17:51:45  same backfill record re-delivered -> "Storing 1 calibrations" again
17:56:45  Calibration bounds: lastGlucose:163 processing:completeHigh

This shows both bugs in one trace: nothing is recorded when the user enters the calibration, and the display-only-flagged backfill reading produced by the accepted calibration is what gets stored as a "calibration" (MBG), twice, because backfill re-delivers it on the next cycle. Here the settling value happened to equal the entered 163; in the original report (entered 72, MBG 61) it did not.

Supplementary: packet capture

The flag behavior is also visible on the wire, from a PacketLogger capture of a calibration session against a real sensor:

00:32:43  EGV glucose=161  byte18=0x0f (in-range, normal)
00:37:42  calibration 146 sent; accepted (34 0001 00)
00:37:42  EGV glucose=154  byte18=0x1f (bgTriggered, display-only)

The reading right after an accepted calibration carries a transitional value (154, between the old 161 and the entered 146) flagged display-only. A control capture where the calibration got a different response (34 0003 06) shows the next reading keeping 0x0f, so the flagging tracks the accepted-calibration path.

The kit's own test suite has carried such a frame since 2022: testG7MessageDataWithCalibration decodes byte 18 = 0x1f (bgTriggered) with a live glucose value. The new tests pin both behaviors: the entered calibration is emitted as a user-entered sample at entry time, and a display-only reading arrives as isDisplayOnly: true, wasUserEntered: false.

What changes for Loop

The two fixes have very different blast radius.

Fix 2 (settling readings no longer marked user-entered) is dosing-neutral. Nothing in the dosing path filters on wasUserEntered: momentum (GlucoseMath.linearMomentumEffect) and insulin counteraction effects (counteractionEffects) already exclude these readings via isDisplayOnly, and the prediction start uses the latest sample regardless of origin. What changes is reporting and staleness:

  • CGM staleness monitor: GlucoseStore.getLatestCGMGlucose is the only consumer that filters wasUserEntered == NO; settling readings now count as CGM data, so a calibration no longer makes glucose look stale during its settling window.
  • HUD: settling readings render as CGM values with a trend arrow instead of as "manual glucose".
  • Nightscout: uploaded as sensor entries (sgv) instead of meter (mbg); isCalibration still follows isDisplayOnly.
  • HealthKit: no longer written with HKMetadataKeyWasUserEntered.

Fix 1 (the entered calibration is reported to the app) does affect dosing, identically to a fingerstick entered in the Loop UI. The sample joins the glucose history handed to the algorithm, which never filters on wasUserEntered:

  • Prediction start: while the calibration is the most recent sample, the forecast starts from the meter value instead of the current sensor value (entered 72 vs sensor 61, in the reporter's case).
  • Momentum: the sample joins the 15-minute regression (same provenance as the sensor stream, so hasSingleProvenance still holds). In the common accepted-calibration case this is moot: the settling readings in that window are display-only, which already disables momentum via containsCalibrations(). If the sensor rejects the calibration there are no settling readings, and the entered value does move the regression slope.
  • Retrospective correction: counteractionEffects does not filter user-entered samples, so with RC enabled the calibration pairs with neighboring sensor readings (same provenance, more than 4 minutes apart) and shifts the observed discrepancy around calibration time.

Whether dosing should consume the reported calibration is an app-level policy decision: the kit reports the event, the app decides what to do with it. Loop can exclude wasUserEntered samples from its prediction input if it wants to dose purely on sensor data; Trio deliberately charts and doses on MBG entries (nightscout/Trio#1472), where this is the requested behavior.

Tests

Full G7SensorKit suite green (198 tests) against LoopKit next-dev.

calibrate(glucose:at:) now also emits the entered meter value as a
user-entered NewGlucoseSample, timestamped when it was taken, so the app
can store it as the manual blood glucose reading it is (Trio charts
user-entered samples as MBG, per nightscout/Trio#1472).

The bounds answer's lastGlucose is no source for it: captured traces
(xDrip4iOS, DexcomG7CalibrationTests) show the field stores the most
recent calibration's value through and beyond processing, but a record
can be stale or another display's. Its documentation is corrected to
match those traces (the DexKit layout's lastEGV name notwithstanding),
and G7CalibrationRecord is documented as the keeper of the entered value.
@ETolboom
ETolboom force-pushed the g7-calibration-manual-glucose branch from 1db9e1e to cc8a0f7 Compare October 25, 2026 18:19
@ETolboom
ETolboom marked this pull request as ready for review October 25, 2026 18:28
A reading the sensor shifts for display while settling a calibration is
a device reading: isDisplayOnly carries that. Marking it user-entered
mislabels it as a manual entry downstream (stored as MBG in Trio,
excluded from CGM staleness in Loop, typed meter on Nightscout upload,
HKMetadataKeyWasUserEntered in HealthKit).
@ETolboom ETolboom changed the title Emit entered calibrations as manual glucose readings Emit calibrations as manual glucose; stop marking display-only readings user-entered Oct 25, 2026
@ps2
ps2 merged commit 4d24c0c into LoopKit:next-dev Oct 25, 2026
@ETolboom
ETolboom deleted the g7-calibration-manual-glucose branch October 25, 2026 22:14
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.

2 participants