Stale alerts on CGM delete, remove stuck-value detector, rename to Libre 3 / 3+ - #13
Open
ps2 wants to merge 5 commits into
Open
Stale alerts on CGM delete, remove stuck-value detector, rename to Libre 3 / 3+#13ps2 wants to merge 5 commits into
ps2 wants to merge 5 commits into
Conversation
The detector fired on any current-glucose word repeated across 3 advancing frames, which is ordinary flat glucose at 1 mg/dL resolution. A healthy 5-hour field capture produced 18 hits at 167, 165, 164, 131, 130 and 91, every one with the surrounding frame bytes and the sensor's historic series advancing normally. At that noise level it could never single out the hold it exists to catch, which pinned one value for the better part of an hour. Raise the threshold to 12 advancing frames, clearing the longest run observed in that capture, and repeat only every 5th frame afterwards so an hour-long hold costs ~10 lines instead of ~55. Run length alone still can't separate a hold from flat glucose, so every report now carries the sensor's committed 5-min historic series as a second opinion: its value, lifeCount, lag, drift since the run opened, and its gap from the pinned value. That series is produced independently and lands ~15 minutes behind, so once a run outlasts its own lag the two records describe the same minutes and should agree. When they don't, the live value is the suspect and the line is labelled STUCK-LATCH. When they do agree — which is what the false-low field report looked like — that is itself the finding, and points at the sensor rather than at our decode. Also report the step that ends a hold (flat glucose resumes by a point or two, a released hold jumps), and stop counting runs of error words, which are a different failure already surfaced by the quality-assessment path and never forwarded to Loop. Decision logic moves into a pure StuckGlucoseDetector so it can be tested directly. Replaying all 293 realtime frames from the field capture through it now emits nothing, down from 18 lines.
A user removed the Libre CGM after their sensor expired and switched back to a G7, then kept getting "sensor about to expire" alerts a week later, once per app launch. Nothing retracted the alerts on the deletion path. retractExpiryAlerts was reachable only from discardSensor, which only Replace Sensor calls; the Delete CGM button went straight to notifyDelegateOfDeletion and skipped the manager's delete entirely. Loop's AlertStore keeps alerts for the whole local-cache window (90 days in this build), and launch-time playback rebuilds any past-due delayed alert as .immediate and presents it again — so the three scheduled expiry alerts re-fired on every launch until acknowledged. delete now retracts and ends with notifyDelegateOfDeletion instead of a bare completion. That was wrong in the other direction too: overriding delete without re-issuing the notification meant the debug-menu delete tore down BLE but never actually removed the manager from Loop. Retraction covers every alert the manager can issue, not just the expiry set. Playback replays anything left unacknowledged and unretracted, so a standing sensorAttention or reconnectNeedsReScan notice produces the same symptom. They are listed in one place, allAlertIdentifiers, so a future alert can't be missed. discardSensor retracts the same set and clears the re-scan and sensor-attention state, since those notices belong to the sensor going away. Retraction is issued before the delegate notification and captures the delegate strongly, so it still lands once Loop releases the manager. retractAlert needs only the identifier, and both unschedules the pending user notification and records the retraction — which closes the launch-replay path and the rescheduleMutedAlerts path together.
The test target was wired up correctly but unreachable. The project had no
shared scheme, so its schemes were per-user and invisible at workspace level,
and building the project standalone can't resolve LibreCRKit, LoopKit or
LoopAlgorithm — those come from the workspace. Tests could only be verified
by extracting the type under test and running it as a script.
Add a shared LibreLoop.xcscheme carrying build and test actions, following
G7SensorKit's layout, so the tests run through the workspace:
xcodebuild -workspace LoopWorkspace.xcworkspace -scheme LibreLoop \
-destination 'platform=iOS Simulator,name=iPhone 17' test
With that in place, cover the deletion bug that prompted this: a stale
sensor-expiry alert re-firing on every launch for a week after the CGM was
removed. LibreLoopAlertRetractionTests asserts that both exit paths — delete
and discardSensor — retract every identifier in allAlertIdentifiers, that
delete still notifies the delegate, and that the identifier list stays
complete. Verified against the pre-fix delete: it fails on both the missing
retractions and the missing delegate notification.
15 tests pass.
Every realtime frame already logs the full decrypted plaintext along with the decoded word, DQ error, sensor condition and actionability, which is what is needed to review a frozen-value report. Classifying the anomaly in the app on top of that earns nothing, so remove the detector, its type and its tests. Also trim the comments on the alert-retraction change.
Covers both strings Loop uses to identify the plugin: the bundle display name in the Add CGM picker, and localizedTitle on the configured-CGM settings row. Screen titles, alert body text and the HKDevice name keep the plain "FreeStyle Libre 3": the slash reads badly in prose and truncates in a nav bar, and the HKDevice name is written into existing HealthKit samples.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three small fixes.
1. Stale alerts after the CGM is deleted
A user removed the Libre CGM after their sensor expired and switched back to a G7, then kept getting "sensor about to expire" alerts a week later — once per app launch.
Nothing retracted the alerts on the deletion path:
retractExpiryAlertswas reachable only fromdiscardSensor, which only Replace Sensor calls.notifyDelegateOfDeletion, skippingLibreLoopCGMManager.deleteentirely.AlertStorekeeps alerts for the whole local-cache window (90 days in this build), and launch-time playback rebuilds any past-due.delayedalert as.immediateand presents it again (StoredAlert.swift:158-166,AlertManager.swift:515). So the scheduled expiry alerts re-fired on every launch until acknowledged.deletenow retracts and ends withnotifyDelegateOfDeletioninstead of a barecompletion(). That was wrong in the other direction too — overridingdeletewithout re-issuing the notification meant the debug-menu delete tore down BLE but never actually removed the manager from Loop.Retraction covers every alert the manager can issue, not just the expiry set: playback replays anything left unacknowledged and unretracted, so a standing
sensorAttentionorreconnectNeedsReScannotice produces the same symptom.discardSensorretracts the same set.Workaround for anyone already affected: tap OK on the alert rather than dismissing it. That records acknowledgement and drops it from playback permanently.
Covering this needed the test target to actually run, so it also adds a shared
LibreLoop.xcscheme. The target was wired up correctly but unreachable — the project had no shared scheme, so its schemes were per-user and invisible at workspace level, and building the project standalone can't resolve LibreCRKit / LoopKit / LoopAlgorithm.2. Remove the stuck-value detector
Every realtime frame already logs the full decrypted plaintext plus the decoded word, DQ error, sensor condition and actionability — enough to review a frozen-value report directly. Classifying the anomaly in the app on top of that earns nothing, so the
STUCK:run-length check is gone.3. Rename to "FreeStyle Libre 3 / 3+"
Both strings Loop uses to identify the plugin: the bundle display name in the Add CGM picker, and
localizedTitleon the configured-CGM settings row.Screen titles, alert body text and the
HKDevicename keep the plain "FreeStyle Libre 3" — the slash reads badly in prose and truncates in a nav bar, and theHKDevicename is written into existing HealthKit samples.Testing
Loopscheme, iPhone 17 simulator).LibreLoopAlertRetractionTestsassert that both exit paths retract every identifier inallAlertIdentifiers, thatdeletestill notifies the delegate, and that the identifier list stays complete. Verified against the pre-fixdelete— it fails on both the missing retractions and the missing delegate notification.Not runtime-verified end to end: confirming the alert fix on device needs a paired sensor, deleting the CGM, and checking that no alert appears on subsequent launches.