Skip to content

G7SensorKit on the watch, including but not limited to directAuth capability - #80

Merged
ps2 merged 19 commits into
LoopKit:next-devfrom
jeremybarnum:sportmode/next-dev
Oct 24, 2026
Merged

ps2 merged 19 commits into
LoopKit:next-devfrom
jeremybarnum:sportmode/next-dev

Conversation

@jeremybarnum

@jeremybarnum jeremybarnum commented Oct 1, 2026 •

Copy link
Copy Markdown

Lets the watch read the G7 directly. It replaces #65, rebased on current next-dev, from my fork so maintainers can push edits. Two upstream commits are kept as they were.

While it is possible for the Loop Watch App to read G7 BG analogously to how the phone does it, doing so under the conditions in which SportMode is used increases the probability of triggering a known Dexcom problem that results in losing BG for both Loop and Dexcom D2W on the watch. Therefore, this build adopts directAuth only on the watch, with a fix to avoid the Dexcom problem.

What's in it:

  • A watchOS target, and the watch's own way of reaching the sensor between readings.
  • Hand-off: the phone passes the watch the sensor's identity and pairing code (exportConfiguration() / init(adopting:localState:), from LoopKit's new protocols), replacing receivePairingCode. Where a watch-only #if was really about "this manager was set up by another device", it's now a run-time check instead.
  • A manager set up by another device keeps its sensor until that device says the sensor changed. The kit already knows each sensor's own session length (10 or 15 days), so Loop's old watch-side age check, and its 15.5-day bound, are no longer needed.
  • Faster pairing (Pete's commit): about 145 ms down to 2.5 ms per key calculation.
  • One #if os(watchOS) remains: it creates the watch's way of reaching the sensor, which lives in a watch-only file.

Changes on the phone even without the Loop PR:

  • The faster pairing code runs on the phone too.
  • Some Bluetooth calls hop to the manager queue asynchronously instead of synchronously, and there's more logging.
  • The G7 settings screen shows a "Watch Direct Read" (pairing code) section whenever a paired watch has Loop installed.

Needs LoopKit/LoopKit#626; merge that first. Builds against stock next-dev with it, and the kit's 176 tests pass.

Update: direct to watch lands first

Jeremy agreed that direct to watch (DTW) lands ahead of Sport Mode; the Sport Mode PRs get updated after. Two commits on top:

Watch acquisition: fixes from a week on a watch (5db6301, with @EJM0 and @kristinkand)

  • Missed bursts are counted from when acquisition started, not only from the last reading, and a bootstrap scan stops at its cap.
  • A connect or disconnect that state restoration hands back half-finished is cancelled and looked at again after 2 s and 7 s. A restored close keeps the 35 s wait; a restored connect lodges at once.
  • The 35 s wait after a reading stays. A real G7 keeps advertising for ~26 s when a display is missing. A watch that connects into that tail gets a link that fails to establish (bluetoothd reason 762). watchOS counts those, and five in about 5.8 h make it connect only above −70 dBm, which on a wrist usually means never. We confirmed this on a watch: with the wait set to 0, two 762s took it to the −70 floor. CoreBluetooth's start-delay option doesn't help on the watch.

Rebond a reconnect, explain an unfinished pairing, a Read on Apple Watch setting (651c2a4)

The Loop side is LoopKit/Loop#2605. It runs the G7 manager on the stock Loop watch app, behind the toggle.

Tested on an Apple Watch Series 9 with a real G7 and the phone's Bluetooth off: the watch read every window in the background. 196 tests pass, and the watchOS target builds.

Jeremy Barnum and others added 4 commits October 1, 2026 09:37
- watchOS target: G7SensorKit-watchOS framework target and shared scheme,
  deployment target 10.6 to match LoopKit, and the iOS target's SDKROOT
  pinned to iphoneos so the watchOS target cannot satisfy an iOS dependency.
  next-dev's Share sources are members of the watch target too, because
  G7CGMManager uses them.
- Watch acquisition, all under #if os(watchOS) in G7BluetoothManager: a
  daemon-held connect request re-lodged after each reading, with the process
  held across the sensor's tail; a bootstrap scan for first contact, a
  sensor change, three missed bursts, or a user Reconnect; radio census
  lines for diagnosis. Phone acquisition is unchanged.
- Pairing code: entered on the phone once per sensor (a settings section
  shown only when a paired watch has the app) and synced to the watch in
  the CGM state.
- The watch declares display type .watch.
- An adopted sensor latches its activation date on the first reading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- G7P256Field: fixed-width 4 x 64-bit Montgomery field with a 4-bit window
  scalar multiply, behind G7P256.add and G7P256.multiply. A scalar
  multiplication goes from 145 ms to 2.5 ms in an unoptimized Debug build.
- The sensor drops a key exchange after about four seconds without a reply.
  exchangeKeys computes rounds 1 and 2 before the first request and round 3
  before requesting the sensor's, logs the compute times, and checks the
  sensor's advisory proofs only after replying.
- Watch discovery with no known peripheral identifier scans until the sensor
  is found instead of for one capped pass, and publishes
  G7WatchDirectRead.isSearching and searchingNote. Scan state resets when
  Bluetooth powers off so the next poweredOn restarts it.
- G7CGMManager.defaultDisplayType (watch on watchOS) backs every initializer
  and G7Sensor, so a freshly created manager on the watch no longer
  authenticates in the phone's slot.
- G7CGMManager.sensorSessionLengthIsKnown says whether sensorEndsAt comes
  from the sensor's reported session length.
- The adopted peripheral is reported by G7BluetoothManager, recorded in the
  sensor's credentials, and saved to G7CGMManagerState.peripheralIdentifier,
  which is already restored into the Bluetooth layer at launch. The separate
  rememberedPeripheralID in user defaults is gone; an existing value migrates
  into state once, and a freshly created manager discards it.
- The arm's last-reading clock is in memory, seeded at launch from the
  state's latestReadingTimestamp.
- The missing-code and searching signals are G7CGMManager.watchNeedsCodeFor
  and watchIsSearching, with G7CGMManager.watchStatusDidChange, instead of
  G7WatchDirectRead statics backed by user defaults.
- The re-lodge arm is fixed at holdApp; the diagnostics toggle and its key
  are removed.
- The legacy G7Lab.* keys are removed on first launch.
Configuration sharing (LoopKit's DeviceConfigurationSharing):
- The export carries the sensor's identity, activation and pairing code; the
  key, peripheral handle, readings and Share account stay with the exporter.
  It changes only when the sensor or its code does, so a host can rebuild on
  change.
- init(adopting:) replaces receivePairingCode(_:phoneSensorID:). A manager
  built from an export reads directly with the passed sensor and code,
  records configuredByAnotherController, and asks the sensor for its version
  so the session length is known before the first relaunch.
- Watch build conditionals that were really about role are run-time checks
  on configuredByAnotherController: forcing direct mode at restore (now in
  the state's decode), latching the activation date from the first reading,
  and the code-replacement prompt when a sensor changes while eavesdropping
  (UI-controlled managers only). The pairing-code status and entry API is no
  longer phone-only. The display slot, the watch acquisition arm's API and
  the one-time defaults migration stay platform conditionals.
- A passed manager keeps its sensor at the end of a suspected-session-end
  grace period, whether the period ends while the app runs or ended while it
  was not running: the exporter decides when the sensor ended, and its next
  export brings the replacement. Deleting it retracts its lifecycle alerts
  and lets go of the sensor without recording a sensor end or archiving it.
- The unused reacquireForNewSensor path is removed.

Cleanups:
- Comments state intent in a line or two, without issue numbers or history
  dates, and the false ones are corrected (the pairing code reaches the
  watch in the export). G7Authenticator's advisory comment is back to stock;
  the budget-warning log lines lose their banners.
- The grid-delay re-lodge arm has a neutral name, which is also its raw
  value; nothing stores it.
- The watch simulator's x86_64 exclusion is dropped. It came in for a
  dependency that no longer exists.

Tests: G7DeviceHandoffTests (11), and deleting a passed manager retracts
every lifecycle alert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jeremy Barnum and others added 11 commits October 1, 2026 17:01
- Only earlier test builds wrote those keys; no upstream install has them.
- Removes two watch conditionals in G7CGMManager and the helper behind them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Drop the duplicate-connect guard in handleDiscoveredPeripheral. It was
  written for the removed ride-only mode, whose connection events fired
  ~10/s; the arm's lodged flag already blocks a second connect, and the
  active peripheral manager is reused rather than re-created.
- Drop the grid-delay re-lodge arm (Relodge, RelodgePlan, nextFire,
  gridDelay, fireOffset, lead) and the lodge's startDelay parameter. The
  re-lodge has been fixed to the 35 s hold; relodgeHold(sinceLinkUp:)
  keeps that decision unchanged.
- The reading clock now serves only the 3-missed-bursts check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- isScanning and isConnected are stock again: a synchronous read on the
  manager's queue. The watch-only 50 ms bound existed for a watch screen that
  read them on the main thread; that screen now shows the status the CGM
  manager already publishes (searching, code needed, last reading) instead.
- Drop the user's "Reconnect sensor" action (G7CGMManager.reconnectG7,
  G7Sensor.reconnect, G7BluetoothManager.reconnect). It was a manual way
  out of a stuck first connection; the scan after three missed readings
  covers the same case without a button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Role (configuredByAnotherController), carried in the sensor's credentials:
- A manager passed its configuration keeps the peripheral handle it finds,
  including when CoreBluetooth swaps the peripheral object; handles are per
  device, so the export cannot carry one. A manager set up through its own
  UI keeps the identifier it paired with, as before.
- Display slot: a manager set up through its own UI claims .phone; one
  passed its configuration claims the secondary (.watch) slot, so it never
  takes the slot of the controller that passed it. defaultDisplayType and
  its platform conditional are gone; the UI initializers default to .phone
  again.

Reading mode:
- Reading directly with no pairing code and no key connects to nothing,
  not even the sensor's own peripheral: such a link closes unauthenticated.
  This extends the existing no-code, no-handle check. Reaching the sensor
  that way sets the code-needed status. Every path that sets up direct
  reading through the UI stores a code, so a UI-configured manager never
  meets this.

Plumbing, compiled everywhere:
- The bluetooth manager's delegate callbacks (log line, can-authenticate,
  adopted peripheral, status changed) keep their empty defaults without a
  platform conditional.
- The state-restoration relaunch line goes to the device log.
- Neutral names: needsCodeForSensor, isSearchingForSensor and the
  G7CGMManager.statusDidChange notification replace the watch-prefixed ones.

Tests: the display slot by role; only a passed sensor keeps the peripheral
it finds; direct reading without a code or key cannot authenticate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- G7WatchAcquisition.swift, a member of G7SensorKit-watchOS only, holds the
  arm: its state, the hold-then-connect re-lodge, the restored-as-connecting
  cancel, the first-contact and three-missed-readings scan, the reading
  clock, and the glance notes. The constants and pure decisions stay on
  G7WatchAcquisition, so the watch tests reach them unchanged.
- G7BluetoothManager reaches it through G7AcquisitionArm: scan, found,
  connected, disconnected, failed, radio down, restored, and the reading
  clock. The arm is created once in init behind a single `#if os(watchOS)`;
  the iOS build has no arm and runs the stock steps exactly as before.
  This is the kit's one platform conditional.
- The reading-clock call sites in G7Sensor and G7CGMManager are plain calls
  that do nothing without an arm.
- A connect the daemon refuses at once is retried after 30 s, every time.
  The stop-after-two rule guarded the grid-delay arm's fractional start
  delay, which is gone.
- Members the arm uses lose `private`; nothing else about them changes.

Tests: the iOS target runs the stock acquisition.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The adopting init gains the localState parameter from LoopKit's contract
  and ignores it; G7 keeps no local state yet.

Tests: G7SensorKit 176 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bench, 2026-10-04: discovery took 4 min 51 s, about one sensor broadcast
cycle, and it then asks for the system Bluetooth pairing. Discovery
happened with the screen off, so the note asks only that Loop stay open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The watch arm's picture of its connect request (`lodged`) is updated only
by Bluetooth callbacks. One that never comes leaves nothing lodged and
nothing scheduled to notice: the missed-burst check lives in scan(), which
runs at launch, poweredOn, a forget or after a bootstrap pass.

2026-10-05 08:08:55: an app update relaunched the watch with its own
pending connect restored as .connecting. The arm cancelled it as foreign
(the 09-16 stale-request rule); the cancel took effect in bluetoothd (a
force-quit relaunch at 09:27 restored no request) but no callback or
.disconnected state reached the app, so the 2-s follow-up stood down and
no connect was requested for 79 minutes. The force-quit recovered it only
because launch runs scan(): 16 bursts missed → bootstrap pass → link at the
next broadcast. Earlier restores-as-connecting (09-17, 09-18) recovered
because their cancels were answered at once.

recheck() runs what scan() runs at launch on any wake the host reports:
connected or a recent request standing → nothing; nothing standing →
lodge; three bursts missed → a bootstrap pass. A pass in progress already
owns acquisition. G7CGMManager.recheckAcquisition() reaches it; on the
phone there is no arm and it does nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The system's Bluetooth pairing prompt appears only the first time a watch
meets a sensor. Bench 2026-10-04: the new sensor at 23:41 drew a "Just
Works" pairing request (sensor bond=2); the same sensor re-found at 00:06
after an install that wiped the app's data did not (bond=1, lePaired 1),
because the system's pairing outlives the app. While searching the app
cannot tell which case it is in, so the note says "if".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@EJM0

EJM0 commented Oct 17, 2026

Copy link
Copy Markdown

G7 reconnect fails with 050102 after the system Bluetooth bond is lost

When it happens
The sensor is already paired, so the shared key is stored, but the OS-level Bluetooth bond is gone. This happens when the sensor is forgotten in Settings › Bluetooth, or when watchOS/iOS drops its bonds. On the next connection the stored key still passes the challenge, but the sensor reports bond=2 (bond required):

Authenticating with the saved key
Challenge: the sensor's answer verified
Challenge: verdict auth=1 bond=2
Authentication failed: Unexpected response during reconnect: 050102

The sensor then closes the link. This repeats on every connection, and the only way out is a full re-pair of the sensor.

Cause
In G7Authenticator.run, a stored-key reconnect only succeeds on auth=1 rdict throws unexpectedResponse(step: "reconnect"), including auth=1bond=2`, where the key is valid and only the bond is missing.

Fix
Throw only when the reconnect is not authenticated. If the key is acceptedrun the same certificate exchange, key challenge and bond request that afirst pairing runs:

-        if !didExchangeKeys {
+        // Authenticated but not bonded (bond=2) on a reconnect: the key is still good, only the system bond is gone.
+        // The certificate phase bonds again, as on a pairing.
+        if !didExchangeKeys, !status.isAuthenticated {
             throw G7AuthenticatorError.unexpectedResponse(
                 step: "reconnect",

Result (tested on Apple Watch + G7)

Challenge: verdict auth=1 bond=2
Running the certificate exchange (auth=1 bond=2)
...
Bonded (0801)
Sensor didRead G7GlucoseMessage(glucose: 196, ...)  + backfill

The sensor bonds again without re-entering the pairing code, and readings and backfill resume right away. A stored key the sensor rejects (auth≠1) still fails as before.

Jeremy Barnum and others added 4 commits October 21, 2026 16:23
Applies the review of LoopKit #626 more widely: drops
isConfiguredByAnotherController and the unused sensorSessionLengthIsKnown,
and makes members only the kit reads internal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From EJM0's and kristinkand's port of this branch to Trio, where
each closed a gap of half an hour or more without readings:

- Misses count from the manager's start at the earliest. A fresh
  manager counted from a long-stale saved reading and went straight to
  a scan pass instead of lodging by its known identifier.
- A wake past the scan cap ends a pass the app was suspended in; its
  timer never ran, and every wake was turned away until the app opened.
- A peripheral restored as connecting or disconnecting, with no request
  or close of ours, is cancelled and replaced by our own connect, with
  the process held for the looks. After a restored close the connect
  waits out the sensor's advertising tail like any other close. Our
  cancel of a restored connect had no link, so the new connect goes in
  at once rather than after a tail wait.

Not taken: lodging when the app leaves the screen, and handing the wait
to CBConnectPeripheralOptionStartDelayKey. Both connect into the tail,
which the sensor drops and watchOS counts against the watch, and the
start-delay option doesn't seem to work on the watch. The tail
clearance now says so.

The watch notes are localizable and no longer name the app.

Co-Authored-By: elias maurer <eliasjosefmaurer@gmail.com>
Co-Authored-By: kristinkand <163904051+kristinkand@users.noreply.github.com>
…tch setting

- A reconnect the sensor answers authenticated but not bonded (05 01 02
  after the system bond was lost) runs the certificate phase with the
  saved key and bonds again, without the code (EJM0's fix). If that
  fails, the key is dropped and the next connection pairs with the
  code, as LoopKit#98 does.
- A pairing whose bond the sensor never confirmed, because the system
  pairing request couldn't show with the app in the background, used to
  surface later as a wrong pairing code. When that key is refused, the
  new Sensor Pairing Not Finished alert says to keep the app open and
  accept the request; it is retracted at the next reading (kristinkand's
  fix, worded for any app).
- A Read on Apple Watch setting in the G7 settings, offered when a
  paired watch has the app installed or while it is on. The manager
  reports it as isConfigurationSharingEnabled, so the host passes the
  sensor to its watch only while it is on; the footer says the watch
  takes the sensor's watch slot. Eavesdropping, the code entry the
  watch needs sits under the setting.
- The settings card counts down to the next reading in minutes and
  seconds, instead of counting up the minutes since the last; once a
  reading is a minute late it shows the minutes since, as before.
- The setting, the new-sensor alert and the status text are
  localizable.

Co-Authored-By: elias maurer <eliasjosefmaurer@gmail.com>
Co-Authored-By: kristinkand <163904051+kristinkand@users.noreply.github.com>
kristinkand added a commit to kristinkand/G7SensorKit that referenced this pull request Oct 24, 2026
…with the code

From Pete Schwamb's LoopKit#80 (b97ad44, after LoopKit#98): a stored-key reconnect the sensor
doesn't answer as authenticated, or whose certificate phase fails while bonding again (05 01 02 after the
system bond was lost), throws storedKeyNotBonded. That counts as a stale key: it's dropped while a code is
at hand, and the next connection pairs with it. Before, the first case was an unexpectedResponse that kept
the key, and the second failed on every connection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
kristinkand added a commit to kristinkand/G7SensorKit that referenced this pull request Oct 24, 2026
… the screen

Pete Schwamb's take in LoopKit#80 (8efdffc): lodging as the app leaves the screen connects into
the sensor's advertising tail, which the sensor drops and watchOS counts against the watch, and the start
delay doesn't seem to work on the watch. Our tests agree it costs readings: after a start-delay connect, 4 of
5 gaps on 23 October and 2 of 68 bursts on 24 October.

Leaving the screen with a hold running now keeps the hold: it waits out the tail, and if watchOS ends it
first, its end lodges at once. With no hold and no request standing, it still lodges a plain connect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
kristinkand added a commit to kristinkand/G7SensorKit that referenced this pull request Oct 24, 2026
From Pete Schwamb's LoopKit#80 (b97ad44): until the next reading is due, the settings card
counts down to it in minutes and seconds; once a reading is a minute late, it shows the minutes since the
last, as before.

Co-Authored-By: Pete Schwamb <pete@schwamb.net>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
kristinkand pushed a commit to kristinkand/Trio that referenced this pull request Oct 24, 2026
…e app leaves the screen

The reader's own copy of the G7 code gets the two changes from Pete Schwamb's LoopKit/G7SensorKit#80 that
the kit forks get too:
- A stored-key reconnect the sensor doesn't answer as authenticated, or whose certificate phase fails while
  bonding again, throws storedKeyNotBonded: the key is dropped while a code is at hand, and the next
  connection pairs with it (before: an unexpectedResponse that kept the key).
- Leaving the screen with a hold running keeps the hold, which waits out the sensor's tail; if watchOS ends
  it first, its end lodges at once. No connect into the tail, no start delay: after a start-delay connect,
  4 of 5 reading gaps on 23 October and 2 of 68 bursts on 24 October went missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
kristinkand pushed a commit to kristinkand/Trio that referenced this pull request Oct 24, 2026
…t-dev; no start delay after a loop

kristinkand/G7SensorKit untethered-fallback 5e32eb7 (test branch only):
- a refused rebond drops the key, the next connection pairs with the code (#80, #98)
- no start delay, no connect into the sensor's tail as the app leaves the screen (#80)
- #98: calibration replies decoded (accepted high no longer shows as refused), a queued calibration older
  than 5 minutes is dropped; #83: the paired sensor's warm-up time on Sensor Paired; #93: why no scan, logged
- the G7 settings count down to the next reading (#80)

The untethered loop no longer hands the sensor's wait to bluetoothd when it's done (Elias's 160bd1a): the
sensor keeps its own hold, as that function's note foresaw if link-ups dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
kristinkand pushed a commit to kristinkand/Trio that referenced this pull request Oct 24, 2026
…e app leaves the screen

The reader's own copy of the G7 code gets the two changes from Pete Schwamb's LoopKit/G7SensorKit#80 that
the kit forks get too:
- A stored-key reconnect the sensor doesn't answer as authenticated, or whose certificate phase fails while
  bonding again, throws storedKeyNotBonded: the key is dropped while a code is at hand, and the next
  connection pairs with it (before: an unexpectedResponse that kept the key).
- Leaving the screen with a hold running keeps the hold, which waits out the sensor's tail; if watchOS ends
  it first, its end lodges at once. No connect into the tail, no start delay: after a start-delay connect,
  4 of 5 reading gaps on 23 October and 2 of 68 bursts on 24 October went missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
kristinkand pushed a commit to kristinkand/Trio that referenced this pull request Oct 24, 2026
…G7SensorKit#80)

kristinkand/G7SensorKit untethered-pr-plus 3f066c6, on top of untethered-plus 4259a99:
- a refused rebond drops the key, the next connection pairs with the code (Pete Schwamb, #80, #98)
- no start delay, no connect into the sensor's tail as the app leaves the screen (#80)

The untethered loop no longer hands the sensor's wait to bluetoothd when it's done (Elias's 160bd1a): the
sensor keeps its own hold, as that function's note foresaw if link-ups dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
kristinkand pushed a commit to kristinkand/Trio that referenced this pull request Oct 24, 2026
…e app leaves the screen

The reader's own copy of the G7 code gets the two changes from Pete Schwamb's LoopKit/G7SensorKit#80 that
the kit forks get too:
- A stored-key reconnect the sensor doesn't answer as authenticated, or whose certificate phase fails while
  bonding again, throws storedKeyNotBonded: the key is dropped while a code is at hand, and the next
  connection pairs with it (before: an unexpectedResponse that kept the key).
- Leaving the screen with a hold running keeps the hold, which waits out the sensor's tail; if watchOS ends
  it first, its end lodges at once. No connect into the tail, no start delay: after a start-delay connect,
  4 of 5 reading gaps on 23 October and 2 of 68 bursts on 24 October went missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
kristinkand pushed a commit to kristinkand/Trio that referenced this pull request Oct 24, 2026
…t-dev; no start delay after a loop

kristinkand/G7SensorKit untethered-fallback 5e32eb7 (test branch only):
- a refused rebond drops the key, the next connection pairs with the code (#80, #98)
- no start delay, no connect into the sensor's tail as the app leaves the screen (#80)
- #98: calibration replies decoded (accepted high no longer shows as refused), a queued calibration older
  than 5 minutes is dropped; #83: the paired sensor's warm-up time on Sensor Paired; #93: why no scan, logged
- the G7 settings count down to the next reading (#80)

The untethered loop no longer hands the sensor's wait to bluetoothd when it's done (Elias's 160bd1a): the
sensor keeps its own hold, as that function's note foresaw if link-ups dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
kristinkand pushed a commit to kristinkand/Trio that referenced this pull request Oct 24, 2026
…e app leaves the screen

The reader's own copy of the G7 code gets the two changes from Pete Schwamb's LoopKit/G7SensorKit#80 that
the kit forks get too:
- A stored-key reconnect the sensor doesn't answer as authenticated, or whose certificate phase fails while
  bonding again, throws storedKeyNotBonded: the key is dropped while a code is at hand, and the next
  connection pairs with it (before: an unexpectedResponse that kept the key).
- Leaving the screen with a hold running keeps the hold, which waits out the sensor's tail; if watchOS ends
  it first, its end lodges at once. No connect into the tail, no start delay: after a start-delay connect,
  4 of 5 reading gaps on 23 October and 2 of 68 bursts on 24 October went missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
kristinkand pushed a commit to kristinkand/Trio that referenced this pull request Oct 24, 2026
…G7SensorKit#80)

kristinkand/G7SensorKit untethered-pr-plus 3f066c6, on top of untethered-plus 4259a99:
- a refused rebond drops the key, the next connection pairs with the code (Pete Schwamb, #80, #98)
- no start delay, no connect into the sensor's tail as the app leaves the screen (#80)

The untethered loop no longer hands the sensor's wait to bluetoothd when it's done (Elias's 160bd1a): the
sensor keeps its own hold, as that function's note foresaw if link-ups dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
kristinkand added a commit to kristinkand/Trio that referenced this pull request Oct 24, 2026
… as the app leaves the screen

kristinkand/G7SensorKit untethered-pr 01e84bb, on top of EJM0's 369f306:
- a refused rebond drops the key, the next connection pairs with the code (Pete Schwamb, LoopKit/G7SensorKit#80, #98)
- leaving the screen with a hold running keeps the hold past the sensor's tail (#80)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
kristinkand pushed a commit to kristinkand/Trio that referenced this pull request Oct 24, 2026
…e app leaves the screen

The reader's own copy of the G7 code gets the two changes from Pete Schwamb's LoopKit/G7SensorKit#80 that
the kit forks get too:
- A stored-key reconnect the sensor doesn't answer as authenticated, or whose certificate phase fails while
  bonding again, throws storedKeyNotBonded: the key is dropped while a code is at hand, and the next
  connection pairs with it (before: an unexpectedResponse that kept the key).
- Leaving the screen with a hold running keeps the hold, which waits out the sensor's tail; if watchOS ends
  it first, its end lodges at once. No connect into the tail, no start delay: after a start-delay connect,
  4 of 5 reading gaps on 23 October and 2 of 68 bursts on 24 October went missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
kristinkand pushed a commit to kristinkand/Trio that referenced this pull request Oct 24, 2026
…e app leaves the screen

The reader's own copy of the G7 code gets the two changes from Pete Schwamb's LoopKit/G7SensorKit#80 that
the kit forks get too:
- A stored-key reconnect the sensor doesn't answer as authenticated, or whose certificate phase fails while
  bonding again, throws storedKeyNotBonded: the key is dropped while a code is at hand, and the next
  connection pairs with it (before: an unexpectedResponse that kept the key).
- Leaving the screen with a hold running keeps the hold, which waits out the sensor's tail; if watchOS ends
  it first, its end lodges at once. No connect into the tail, no start delay: after a start-delay connect,
  4 of 5 reading gaps on 23 October and 2 of 68 bursts on 24 October went missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
kristinkand pushed a commit to kristinkand/Trio that referenced this pull request Oct 24, 2026
…t-dev; no start delay after a loop

kristinkand/G7SensorKit untethered-fallback 5e32eb7 (test branch only):
- a refused rebond drops the key, the next connection pairs with the code (#80, #98)
- no start delay, no connect into the sensor's tail as the app leaves the screen (#80)
- #98: calibration replies decoded (accepted high no longer shows as refused), a queued calibration older
  than 5 minutes is dropped; #83: the paired sensor's warm-up time on Sensor Paired; #93: why no scan, logged
- the G7 settings count down to the next reading (#80)

The untethered loop no longer hands the sensor's wait to bluetoothd when it's done (Elias's 160bd1a): the
sensor keeps its own hold, as that function's note foresaw if link-ups dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
kristinkand pushed a commit to kristinkand/Trio that referenced this pull request Oct 24, 2026
…e app leaves the screen

The reader's own copy of the G7 code gets the two changes from Pete Schwamb's LoopKit/G7SensorKit#80 that
the kit forks get too:
- A stored-key reconnect the sensor doesn't answer as authenticated, or whose certificate phase fails while
  bonding again, throws storedKeyNotBonded: the key is dropped while a code is at hand, and the next
  connection pairs with it (before: an unexpectedResponse that kept the key).
- Leaving the screen with a hold running keeps the hold, which waits out the sensor's tail; if watchOS ends
  it first, its end lodges at once. No connect into the tail, no start delay: after a start-delay connect,
  4 of 5 reading gaps on 23 October and 2 of 68 bursts on 24 October went missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
kristinkand added a commit to kristinkand/Trio that referenced this pull request Oct 24, 2026
… as the app leaves the screen

kristinkand/G7SensorKit untethered-pr 01e84bb, on top of EJM0's 369f306:
- a refused rebond drops the key, the next connection pairs with the code (Pete Schwamb, LoopKit/G7SensorKit#80, #98)
- leaving the screen with a hold running keeps the hold past the sensor's tail (#80)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
kristinkand pushed a commit to kristinkand/Trio that referenced this pull request Oct 24, 2026
…e app leaves the screen

The reader's own copy of the G7 code gets the two changes from Pete Schwamb's LoopKit/G7SensorKit#80 that
the kit forks get too:
- A stored-key reconnect the sensor doesn't answer as authenticated, or whose certificate phase fails while
  bonding again, throws storedKeyNotBonded: the key is dropped while a code is at hand, and the next
  connection pairs with it (before: an unexpectedResponse that kept the key).
- Leaving the screen with a hold running keeps the hold, which waits out the sensor's tail; if watchOS ends
  it first, its end lodges at once. No connect into the tail, no start delay: after a start-delay connect,
  4 of 5 reading gaps on 23 October and 2 of 68 bursts on 24 October went missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
kristinkand pushed a commit to kristinkand/Trio that referenced this pull request Oct 24, 2026
…G7SensorKit#80)

kristinkand/G7SensorKit untethered-pr-plus 3f066c6, on top of untethered-plus 4259a99:
- a refused rebond drops the key, the next connection pairs with the code (Pete Schwamb, #80, #98)
- no start delay, no connect into the sensor's tail as the app leaves the screen (#80)

The untethered loop no longer hands the sensor's wait to bluetoothd when it's done (Elias's 160bd1a): the
sensor keeps its own hold, as that function's note foresaw if link-ups dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
kristinkand pushed a commit to kristinkand/Trio that referenced this pull request Oct 24, 2026
…e app leaves the screen

The reader's own copy of the G7 code gets the two changes from Pete Schwamb's LoopKit/G7SensorKit#80 that
the kit forks get too:
- A stored-key reconnect the sensor doesn't answer as authenticated, or whose certificate phase fails while
  bonding again, throws storedKeyNotBonded: the key is dropped while a code is at hand, and the next
  connection pairs with it (before: an unexpectedResponse that kept the key).
- Leaving the screen with a hold running keeps the hold, which waits out the sensor's tail; if watchOS ends
  it first, its end lodges at once. No connect into the tail, no start delay: after a start-delay connect,
  4 of 5 reading gaps on 23 October and 2 of 68 bursts on 24 October went missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
EJM0 pushed a commit to EJM0/G7SensorKit that referenced this pull request Oct 24, 2026
…with the code

From Pete Schwamb's LoopKit#80 (b97ad44, after LoopKit#98): a stored-key reconnect the sensor
doesn't answer as authenticated, or whose certificate phase fails while bonding again (05 01 02 after the
system bond was lost), throws storedKeyNotBonded. That counts as a stale key: it's dropped while a code is
at hand, and the next connection pairs with it. Before, the first case was an unexpectedResponse that kept
the key, and the second failed on every connection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
(cherry picked from commit 30ec7a1)
EJM0 pushed a commit to EJM0/Trio that referenced this pull request Oct 24, 2026
…s with the code

A stored-key reconnect the sensor doesn't answer as authenticated, or whose bonding again with the saved key
fails (05 01 02 after the system bond was lost), now counts as a stale key: it's dropped while a pairing code is
at hand, and the next connection pairs with the code. Before, the first case kept the key and the second failed
on every connection.

From Pete Schwamb's LoopKit/G7SensorKit#80 (b97ad44), as kristinkand's untethered-pr-plus has it (30ec7a1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0135v4JApHewuwGjWKiSPDNY
@ps2
ps2 marked this pull request as ready for review October 24, 2026 17:52
@ps2
ps2 merged commit 0c43b91 into LoopKit:next-dev Oct 24, 2026
JoelonGit pushed a commit to JoelonGit/LoopWorkspace that referenced this pull request Oct 24, 2026
Points LoopKit, G7SensorKit and Loop at their next-dev merges of
LoopKit/LoopKit#626, LoopKit/G7SensorKit#80 and LoopKit/Loop#2605.
Loop also picks up LoopKit/Loop#2606.
kristinkand added a commit to kristinkand/G7SensorKit that referenced this pull request Oct 25, 2026
From Pete Schwamb's LoopKit#80 (b97ad44): until the next reading is due, the settings card
counts down to it in minutes and seconds; once a reading is a minute late, it shows the minutes since the
last, as before.

Co-Authored-By: Pete Schwamb <pete@schwamb.net>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
(cherry picked from commit 946c850)
kristinkand added a commit to kristinkand/Trio that referenced this pull request Oct 25, 2026
…the watch

Untethered mode builds on his Sport Mode for Loop (LoopKit/Loop#2545), the watch's G7 reader copies his
watch acquisition from G7SensorKit on the watch (LoopKit/G7SensorKit#80), and Extended Untethered Mode's
minimal screen follows his Sport Mode glance. Named in the untethered docs' credits (with Elias Maurer and
Pete Schwamb), the watch G7 README, the acknowledgements, and next to the code that follows his.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
kristinkand added a commit to kristinkand/Trio that referenced this pull request Oct 25, 2026
…the watch

Untethered mode builds on his Sport Mode for Loop (LoopKit/Loop#2545), the watch's G7 reader copies his
watch acquisition from G7SensorKit on the watch (LoopKit/G7SensorKit#80), and Extended Untethered Mode's
minimal screen follows his Sport Mode glance. Named in the untethered docs' credits (with Elias Maurer and
Pete Schwamb), the watch G7 README, the acknowledgements, and next to the code that follows his.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
kristinkand added a commit to kristinkand/Trio that referenced this pull request Oct 25, 2026
…the watch

Untethered mode builds on his Sport Mode for Loop (LoopKit/Loop#2545), the watch's G7 reader copies his
watch acquisition from G7SensorKit on the watch (LoopKit/G7SensorKit#80), and Extended Untethered Mode's
minimal screen follows his Sport Mode glance. Named in the untethered docs' credits (with Elias Maurer and
Pete Schwamb), the watch G7 README, the acknowledgements, and next to the code that follows his.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
kristinkand added a commit to kristinkand/Trio that referenced this pull request Oct 25, 2026
…the watch

Untethered mode builds on his Sport Mode for Loop (LoopKit/Loop#2545), the watch's G7 reader copies his
watch acquisition from G7SensorKit on the watch (LoopKit/G7SensorKit#80), and Extended Untethered Mode's
minimal screen follows his Sport Mode glance. Named in the untethered docs' credits (with Elias Maurer and
Pete Schwamb), the watch G7 README, the acknowledgements, and next to the code that follows his.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
kristinkand added a commit to kristinkand/Trio that referenced this pull request Oct 25, 2026
…the watch

Untethered mode builds on his Sport Mode for Loop (LoopKit/Loop#2545), the watch's G7 reader copies his
watch acquisition from G7SensorKit on the watch (LoopKit/G7SensorKit#80), and Extended Untethered Mode's
minimal screen follows his Sport Mode glance. Named in the untethered docs' credits (with Elias Maurer and
Pete Schwamb), the watch G7 README, the acknowledgements, and next to the code that follows his.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
kristinkand added a commit to kristinkand/Trio that referenced this pull request Oct 25, 2026
…the watch

Untethered mode builds on his Sport Mode for Loop (LoopKit/Loop#2545), the watch's G7 reader copies his
watch acquisition from G7SensorKit on the watch (LoopKit/G7SensorKit#80), and Extended Untethered Mode's
minimal screen follows his Sport Mode glance. Named in the untethered docs' credits (with Elias Maurer and
Pete Schwamb), the watch G7 README, the acknowledgements, and next to the code that follows his.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
kristinkand added a commit to kristinkand/Trio that referenced this pull request Oct 25, 2026
…the watch

Untethered mode builds on his Sport Mode for Loop (LoopKit/Loop#2545), the watch's G7 reader copies his
watch acquisition from G7SensorKit on the watch (LoopKit/G7SensorKit#80), and Extended Untethered Mode's
minimal screen follows his Sport Mode glance. Named in the untethered docs' credits (with Elias Maurer and
Pete Schwamb), the watch G7 README, the acknowledgements, and next to the code that follows his.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgnnHLJbhxmQaEjQNYR5VZ
EJM0 pushed a commit to EJM0/Trio that referenced this pull request Oct 25, 2026
…the watch

After kristinkand's 56cb282 on feature/untethered-pr-plus. Untethered mode
builds on his Sport Mode for Loop (LoopKit/Loop#2545), the watch acquisition
in this branch's G7SensorKit follows his (LoopKit/G7SensorKit#80), and
Extended Untethered Mode's minimal screen follows his Sport Mode glance. Here
in the acknowledgements and next to the glance; the docs and the copied G7
reader that the original also credits him in aren't on this branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0135v4JApHewuwGjWKiSPDNY
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.

3 participants