Repository navigation
yuwell: real-hardware transport evidence + firmware-decode blocker (milestone, not live glucose) - #106
yuwell: real-hardware transport evidence + firmware-decode blocker (milestone, not live glucose)#106shroominic wants to merge 52 commits into
Conversation
…ence Adds a private, debug-only macOS BLE path for the Anytime 5P so the existing cgm_yuwell_anytime driver (previously Android-debug-only) can attempt a live session from this Mac, and records the first physical 5P observation. New (openhealth): - YuwellMacosKeychainSessionStore: Keychain-backed sibling to the Android-only YuwellSecureSessionStore, implementing the same YuwellCredentialStore/YuwellWriteIntentStore contracts (HMAC-derived Keychain aliases, opaque random tokens, fail-closed write-intent journal with the same state-machine guards as the Android bridge). - YuwellMacosInMemoryKeyValueStore: explicit, documented fallback for a build with no code-signing Team ID, where macOS Keychain Services fails every item with errSecMissingEntitlement (-34018) independent of the entitlements plist. Same guards, not process-durable. Real fix is signing with a real Team ID; out of scope here. - buildYuwellMacosDebugDriver(): mirrors the reviewed Android _buildCaptureRegistry composition (engineering-provisional V1150 output policy) with the store swapped in. - runYuwellMacosDebugAttempt(): bounded scan -> connect -> observe -> disconnect; always logs `BLE GRAB @Claude` before touching the radio and `BLE RELEASE @Claude` after, including on error/timeout. - yuwell_macos_debug_main.dart: debug-only Flutter entry point, gated on OG_MACOS_YUWELL_DEBUG + debug mode + macOS; does not weaken the Android OG_PROTOCOL_TRACE gate or buildPlatformDriver. Bug found + worked around (not fixed upstream, out of scope here): on this macOS build, letting FlutterBluePlusTransport's scan `timeout` elapse (or cancelling the scan subscription) drives a native stopScan call that wedges the merged UI/platform thread — nothing Dart-side runs again after, including independent Dart timers, since the whole isolate's event loop is what's stuck. The bounded scan here never cancels; it just stops waiting after `scanTimeout` and leaves the subscription dangling for the remaining life of this single-attempt, externally-torn-down process. Real-hardware result: a session against Dom's physical Anytime 5P matched the documented CT5 GATT topology exactly (service 00001000-1212-efde-1523-785feabcd123, notify 00001001-..., write 00001002-...), subscribed cleanly, and completed a real version-query round trip. The driver read the version and correctly failed closed at YuwellSessionFailureKind.unsupportedFirmware: this unit's firmware branch is not V1150, the only branch currently admitted for a live glucose value. No state-changing write was attempted. This promotes discovery/topology/version-handshake to target-confirmed while leaving the V1150-only gate and the unimplemented glucose algorithm untouched per docs/testing/yuwell-anytime-5p-protocol.md's validation plan. Docs updated to record the observation: docs/testing/yuwell-anytime-5p-protocol.md, packages/cgm_yuwell_anytime/doc/evidence-boundary.md, packages/cgm_yuwell_anytime/README.md. No raw capture, sensor identifier, device address, or health payload is recorded anywhere in this commit. 16 new tests (store, driver-factory wiring, session orchestration); existing cgm_yuwell_anytime (105) and relevant openhealth (116) suites still pass; flutter analyze clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…sTransport Root-causes and fixes the scan-teardown wedge M3 (164b170) worked around instead of fixing. Read the locked flutter_blue_plus 2.2.1 source (flutter_blue_plus-2.2.1/lib/src/flutter_blue_plus.dart): - startScan(timeout: ...) arms its own internal Timer(timeout, stopScan) (line 352) that calls the *public* stopScan(), which takes a process-global mutex (_MutexFactory.getMutexForKey("scan")) before it ever touches the platform channel. - FlutterBluePlusTransport.scan() independently called that same public stopScan() from its own teardown, forwarded the same `timeout` into startScan, and was triggered around the same wall-clock moment by whatever bounds the caller's own wait. Two independent callers, one mutex: if the plugin's own timer wins the race and its native stop invocation never returns (exactly what M3 observed on this Mac), the loser blocks on that mutex forever, and since the mutex is never released, every later scan/connect call in the process wedges behind it too. That fully accounts for "the whole isolate['s BLE path] is what's stuck" from the M3 notes. Fix: FlutterBluePlusTransport.scan() no longer forwards `timeout` to the plugin. It owns one local bound Timer instead, and every stop trigger (that timer, a consumer cancelling its subscription, an externally observed isScanning drop, a startup error) now funnels through the same already-idempotent teardown. That guard is extracted as SingleFlightTeardown so "at most one call to the plugin's stopScan() per attempt" is independently unit-tested, not just asserted by inspection. This is a real fix in the shared transport, not a debug-tool-only patch: every production driver (cgm_aidex, cgm_libre2, cgm_yuwell_anytime) calls scan(timeout: ...) the same way via `await for` and was exposed to the same race. No openhealth test touches FlutterBluePlusTransport directly (all use fake BleTransport implementations), so this change is fully isolated to and covered by the cgm_ble_flutter suite. 4 new tests (SingleFlightTeardown: concurrent callers, a later caller observing the first outcome instead of re-running, error propagation, the `started` getter). Full package suite 27/27, `flutter analyze` clean, `dart format` clean. Not yet hardware-confirmed: this fix is grounded in the locked plugin's own source, not in a repeated live macOS session. Still wants one live attempt to confirm the wedge is actually gone. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hs9hXcMCFfmG1ddee7HL6Q
…ly query for non-V1150 units
Extends the non-V1150 fail-closed path in YuwellAnytimeSession with
self-capturing evidence, without moving the
8000
GLU_MG admission gate at
`driver.dart`'s exact-V1150 check. No state-changing write, activation,
or glucose computation is reachable for non-V1150 firmware before or
after this commit.
1. YuwellSessionException gains optional `firmware` and `bound` fields.
`firmware` is the parsed version string (e.g. "V2003") when a fresh
version query actually ran; `bound` is the sensor's own answer to one
additional read-only query described below. Both flow into the
published failure snapshot as `cgm.yuwell.firmware` and
`cgm.yuwell.binding-state` metadata. This makes the evidence-boundary
promotion gate's "record the exact... firmware..." automatic on the
next attempt instead of relying on a human to note it down, and
pre-answers the open "exact firmware string" question from
CLAUDE_STATUS.md M4a.
2. A non-V1150 session now sends YuwellCt5Commands.readBindingStatus()
(0x11) once, right before it fails closed on firmware, and reports
the result. This query is not new protocol surface: it is the exact
query _beginFreshActivation already sends first, unconditionally,
before any state-changing write, for V1150 sessions today — this
change runs that same query one branch earlier, nothing more.
Two other opcodes were considered and rejected after reading the
actual call graph, not just opcode names:
- querySensorCode (0x3f, calibration code) is only ever sent inside
_completeActivationFromAuthenticated, after _setDateWithJournal and
_setCommunicationIdentity — two state-changing writes — have
already run, and its response is decrypted with the cipher
deriveCipherFromSetIdResponse derives from the set-ID write. It is
cryptographically dependent on that write, not just conventionally
ordered after it, so there is no meaningful way to send it cold.
- readHistory/readHistoryVariant (0x37/0x47) carry their own doc
comment marking them target-unverified, and every existing call
site sends them only post-authentication. No precedent anywhere
for sending either cold.
A failure on the binding-status evidence read never masks the primary
unsupportedFirmware diagnostic — it is swallowed and reported as
unknown (bound: null), covered by an explicit test.
3 new tests: firmware+bound captured on a synthetic non-V1150 version
response; the evidence read's own failure does not mask the primary
diagnosis; existing coverage otherwise unchanged. cgm_yuwell_anytime:
107/107, `dart analyze --fatal-infos` clean, `dart format` clean.
Targeted openhealth suites exercising this wiring (macOS debug
driver/session, cgm_driver_registry): 18/18, unaffected.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hs9hXcMCFfmG1ddee7HL6Q
M3 found that on this Mac, stopping the shared BLE transport's scan (a plain consumer `.cancel()` included) could wedge the merged UI/platform thread forever, so the macOS Yuwell debug session deliberately never cancelled its scan subscription. M5 root-caused and fixed that race in FlutterBluePlusTransport via SingleFlightTeardown, but that fix is source-grounded, not yet hardware-confirmed. runYuwellMacosDebugAttempt now cancels the scan subscription for real, bounded by a new scanCancelBound (default 5s, configurable for tests): a clean cancel stops the radio scan instead of leaking it for the rest of the process and self-logs as live confirmation evidence for the M5 fix; a cancel that does not resolve in time still degrades to the old, hardware-proven-safe walk-away behavior instead of hanging. 2 new tests cover both branches, including one that would hang the suite if the bounded fallback regressed. cgm_yuwell_anytime (107/107), cgm_ble_flutter (30/30), full openhealth suite (492/492): all green. flutter analyze and dart format clean on both files. Radio-free change; no BLE touched to make it. Confirms nothing live by itself — the next GRAB against real hardware (already the planned next step per M6) now also exercises this cancel path end to end. Commit signing unavailable in this sandboxed shell (1Password cannot be unlocked non-interactively) — reproduced a third time, same error as M6. Committing unsigned per Dom's standing authorization for this environment limitation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NaSE8RW2ZXxkRvBozVCxuL
Both were failing since 164b170, before this PR existed — confirmed via CI history on this branch, not introduced by any commit landed here. Secret Scan (gitleaks): flagged `ct5.macos.selftest.v1` in yuwell_macos_debug_main.dart as generic-api-key. It is a FlutterSecureStorage/Keychain *lookup key name* (like a dictionary key), not a credential — _selfTestKeychain() writes/reads/deletes it purely to confirm the Keychain is reachable. Added .gitleaksignore with the exact reviewed fingerprint (commit:file:rule:line, gitleaks' own documented mechanism for one already-checked finding) plus an inline comment at the call site. Ruleset and every other detection path are untouched. Unit Tests: 13 (of 33) cgm_libre2_glucose tests failing in private_bench_test.dart on CI's ubuntu-latest runner, because tool/analyze_private_bench.dart is deliberately macOS-only (Platform.isMacOS guard: it depends on Darwin's stat64 field layout via raw FFI). Off macOS every call returns unsupportedPlatform, which fails most assertions outright and makes a few others (the ones already expecting a nonzero/66 exit, for unrelated reasons) pass for the wrong reason instead of actually exercising that behavior. Wrapped the suite in one group(..., skip: macosOnlySkip) gated on Platform.isMacOS, mirroring the tool's own platform boundary instead of fighting it. No assertion, tool code, or product code touched. All 33 tests still run and pass unchanged on macOS (verified locally) — CI is the first real signal for the skip path itself, since this Mac can't take the non-macOS branch. Verification: cgm_libre2_glucose 33/33 locally, dart analyze/format clean on all three changed files, full make check green. gitleaks itself only runs in GitHub's Secret Scan job, not locally — CI on this push is the real check for that half. Radio-free; no BLE touched. Does not touch the V1150-exact-match admission gate, packages/cgm_yuwell_anytime, or any glucose computation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NaSE8RW2ZXxkRvBozVCxuL
… private notes into the committed evidence doc Two OFFICIAL-APP-STATIC findings from this session, moved from CLAUDE_STATUS.md (private, out of this repo) into the actual evidence boundary doc so they're part of the reviewable record, not a private note: 1. OTA firmware-update path cannot reach V1150. OTAViewModel's own ladder is V1200 -> V1300 -> V1400 (confirmed via the .gbl assets' embedded version strings, cross-validated against each other); V1150 is not a node in it. V1300 also has no sibling to TransmitterRepository.FIRMWARE_VERSION_V1150, so it wouldn't be trusted even if reached. The mechanism's own gating functions reference only CT3/CT4, with zero CT5 mentions, so it may not even target this transmitter. New "OTA firmware-update path" subsection + confidence-table row. 2. History (0x37/0x47) and query-code (0x3F) responses are session-cipher-dependent (deriveCipherFromSetIdResponse, keyed off the 0x30 set-ID write), not just unprecedented to send cold. Clarifies why only 0x11 (binding-state check) is actually cold-readable: it's the one plaintext response in that table, not merely a simply-encoded request. New paragraph in "Frame integrity and command map" + confidence-table row. Neither finding changes any rule under "OpenGlucose therefore must not" in "Native algorithm contract and clean-room gap" — non-V1150 remains fail-closed, for the same reasons already documented there. This is documentation of already-established facts; no product/test code touched. Docs-only diff — full make check (native build matrix) not rerun; nothing here can affect Dart analysis, formatting, or test behavior. Radio-free; no BLE touched. Codex has Dom's go-ahead for an activation GRAB per the operator's note — nothing in this commit needed the radio and this session is staying offline regardless. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NaSE8RW2ZXxkRvBozVCxuL
…osed evidence read The non-V1150 firmware gate (driver.dart) sends one best-effort binding-status read for evidence before failing closed with unsupportedFirmware. Existing tests covered that read returning "unbound" and covered the read itself failing; the "already bound" branch (bound: true) was untested. Added that case to yuwell_session_test.dart: synthetic firmware digits only (not a captured value from any real unit), asserts the session still fails closed with unsupportedFirmware, records bound: true in both the exception and session metadata, and sends exactly the same two reads (0x01 version, 0x11 binding-status) with no state-changing write. No product code changed; no new evidence or algorithm claim. Verified `dart test` (package: 108 passed), `dart analyze` (no issues), and `dart format --set-exit-if-changed` (clean). Radio-free; no BLE touched. Commit signing unavailable in this sandboxed shell (`error: 1Password: failed to fill whole buffer`, same failure CLAUDE_STATUS.md records for M6/M7) — committed unsigned per Dom's standing authorization for this environment limitation, consistent with a474dcc/ddd3c9d/2ab04df/196a8d9. Only gpgsign was skipped; lefthook's format/analyze hooks ran normally. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CsEURNaFxxLJJD5bz6LAu2
…branch Adds one synthetic test for a branch of the non-V1150 firmware gate that had no coverage: _resume's own bare unsupportedFirmware guard for saved credentials with transmitterComputed == false. This is distinct from the three existing non-V1150 tests, which all go through _initialize's fresh version query and its one best-effort binding-status evidence read. A resumed session with saved credentials skips that version query entirely, so _resume's guard fires first, before check-id, before any evidence read, before the activation gate — with zero writes and a firmware/bound-free exception, since nothing was queried this attempt. cgm_yuwell_anytime: 109/109. Repo-wide dart format --set-exit-if-changed and analyze clean. Full workspace test suite (491 unit + 1 integration) green; native builds/signing gates not re-run for a test-only diff. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RjBX45BjdGuHsNDswG3xWb
…'s own version gate Structure-only findings from the reference decompile, no decode invented: - ist.com.sdk.ProtocolTools.verify(byte[]) (-> ProtocolToolsHolder .verifyHolder) is the exact call site behind the existing 27-byte advertisement claim, returning type/category/name/isBound. CT5InitViewModel's scan callback uses isBound alone to route between its own recovery flow and a fresh connect; either way it still requires a full GATT connect and the version handshake, so this is a UI hint in the reference app, not a substitute for OpenGlucose's own authenticated 0x11 binding-status check. - CT5InitViewModel's data-receive handler filters notifications to opcodes 0x05/0x01 at this stage, and its version handler enforces a hard-coded version+date allowlist before ever calling setDate, closing the connection otherwise. This is a broader gate than the transmitter-computed trust check already documented (TransmitterRepository.FIRMWARE_VERSION_V1150 stays the only trusted version) — the reference app treats 'firmware new enough to init' and 'firmware whose transmitter value it trusts' as two different questions. Recorded as version-gate evidence only; does not license widening OpenGlucose's own unsupportedFirmware gate, since the native algorithm the wider range would still need remains unimplemented. No vendor code, binary, or captured evidence added; docs-only. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RjBX45BjdGuHsNDswG3xWb
Went one level deeper than the previous verify() note: read ProtocolToolsHolder.verifyHolder itself (structure only, no decode). - It is a standard, generic BLE advertising-data (AD structure) walker, not a CT5-specific envelope. Verify.type is the raw BLE Flags byte (AD type 0x01); Verify.name/category come from Local/Shortened Name AD types (0x08/0x09) with a quirky length-keyed split; Verify.category + isBound come from Manufacturer Specific Data (0xFF), reading exactly 3 bytes + a 1-byte bound flag. - For the 27-byte manufacturer-data form, it reads only that first 4-byte prefix and explicitly discards the remaining 22 bytes. Those 22 bytes are where the six-record/checksum advertising content the doc already describes would live — confirmed this method is not what reads them. Left ProtocolToolsHolder_CT5$BroadData unopened as a pointer for that separate, still out-of-scope thread. - Noted the reference parser's own length-desync fragility on unrecognized AD types as a thing to not imitate, not a wire-format fact. Docs-only; no vendor code, binary, or decode logic added. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RjBX45BjdGuHsNDswG3xWb
…t branch Locks in _recoverUnresolved's bare credentials-null guard as a regression test: with no saved credentials and any unresolved intent other than setDate, the session must fail closed with zero radio traffic, before _initialize's fresh-admission and _resume's firmware-based guards would even run. This is the branch that keeps the else-branch's default of _firmware = 'V1150' (when credentials is null) inert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RSHZyvw9hHdZREoSDddERt
…pp's own six-record advertisement decoder Opened the ProtocolToolsHolder_CT5$BroadData thread left as an unread pointer in the previous verifyHolder note (structure only, no decode invented; cross-checked jadx against smali since a neighboring CGMService method carries a "decompiled incorrectly" warning): - com.yuwell.cgm.utils.ProtocolToolsHolder_CT5 is a separate class from ist.com.sdk.ProtocolTools/ProtocolToolsHolder, with its own result type (Verify_CT5). Its verify(byte[]) has exactly one call site in the inspected DEX -- CGMService's field j0 -- and CT5InitViewModel's scan callback never reaches it; the two decoders never call each other. - A private checksum helper gates verify(): for the same type-0xFF, 27-byte AD structure verifyHolder discards past its first 4 bytes, this helper sums the next 21 bytes and checks the low byte against a trailing checksum byte. - verify() itself is the method that reads what verifyHolder discards: category, bound flag, a nibble-packed count/type selector, a little-endian index, then 18 bytes run through the same ConvertTools.encode(bytes, kCipher) transform the GATT session path uses, decoded as up to `count` 3-byte big-endian records -- type 1 (current/temperature) or type 2 (trend/errorCode/glucoseValue). Nothing in the method caps count against the payload length; an over-long count throws and is caught by the same shape-error path. - Its sole caller, CGMService's lambda$algorithmGlucose$10, hands every type-2 record straight to onBroadcastNewGlucoseRead() as a CurrentGlucose with no native-algorithm call in that method -- the exact source grounding for the doc's existing "separate advertisement-display path accepts a transmitter value without the native algorithm" claim. jadx flags that method's surrounding gap-continuity logic as unreliably decompiled, so this finding stops at what the smali and the plain accessor calls agree on. Docs-only; no vendor code, binary, or decode logic added. Does not change the fail-closed admission gate, the native-algorithm hard unknown, or any publication requirement. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ASY8kedYNDfsP84fbJoRD
…ucose$10 via smali CGMService's lambda$algorithmGlucose$10 has a jadx "Removed duplicated region for block" warning that decompiles to an empty if-body around the index/gap-continuity check. Tracing the raw smali resolves it: the branch only gates whether execution joins the shared record-processing loop every entry path already uses; the real replay guard is the loop's own per-record glucoseId comparison. Confirms, source-level, that this advertisement path never reads a firmware-version field at all -- the V1150-only gate this document establishes elsewhere belongs to a different code path. This sharpens, and does not relax, the existing reason the advertisement path stays out of scope for OpenGlucose's own admission gate. Verification: dart test in packages/cgm_yuwell_anytime -- 110/110 passing (doc-only change, no driver code touched). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RZ5jVRQLThvDBxpcZhkeKj
…lifecycle class CT5InitViewModel's scan/version-gate logic is already recorded in this document, but CT5Init — the guide Activity that hosts it — was not: it is a separate class with its own state, not an alias or a thin wrapper. Tracing it (jadx, cross-checked against the smali constant table for TransmitterState) adds a session-lifecycle layer this document did not have: BSN-keyed recovery invalidation on onCreate, permission-gated entry to the ViewModel's backend-config call, an Activity-lifecycle scan backstop on onDestroy, confirmation that startInit(Date) is exactly startBleScan(Date) (SCAN_MODE_LOW_LATENCY, hardware batching disabled), and the TransmitterState-keyed dispatch in onTransmitterStateReceived — including the 30-second resume window shared by DISCONNECTED/CHECK_FAIL and the dedicated UI path (state 23 CHECK_TRANSMITTER_VERSION_FAIL, WearVersionTipDialog) for the version-gate failure this document already established at the ViewModel level under "Application-side version gate". This corroborates, and does not revise, the existing version-gate finding: it is an independent code path confirming the same conclusion from the Activity's own state-code naming and log string. Nothing here touches live, history, or advertisement record content, and nothing here changes any HARD UNKNOWN. Verification: doc-only change; no Dart source or test file touched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbbtbQJ6d7GG13kJzPeS4i
_verifyTopology is the first fail-closed gate in _initialize(): it runs immediately after discoverServices() and before notification subscription or any write, and throws YuwellSessionFailureKind.topology when none of the discovered GATT services match the CT5 primary service UUID. No existing test exercised it — the shared _ScriptedConnection fixture always returned a matching topology unconditionally, so there was no way to reach this branch without a fixture change. Adds an optional serviceOverride to _ScriptedConnection/_Fixture (defaults to the existing hardcoded topology for every other test, so nothing else changes) and one new test that supplies an empty service list. Confirms the failure surfaces before any write, the connection is disconnected during cleanup, and the failure code is recorded in session metadata — the same shape the sibling unsupportedFirmware/responseTimeout tests already check. Synthetic topology only, per the evidence-boundary doc's "unit tests use synthetic values only" rule; no captured device response involved. Verification: dart test in packages/cgm_yuwell_anytime — 111/111 passing; dart analyze and dart format clean on the changed file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbbtbQJ6d7GG13kJzPeS4i
…, and dialog terminality Follow-up to 9966161, which covered CT5Init's onCreate/onDestroy/startInit and its TransmitterState dispatch but left three things under-specified. Traced deeper (jadx, all source-grounded) and filled them in: - Where the resume window's reference Date actually comes from: a four-part prerequisite gate in onPermissionRequestSuccess (runtime permissions, an Android-12-plus BLE-specific recheck, GPS, Bluetooth adapter) that only then clears the version-gate latch and schedules startInit(new Date()). A resumed retry inside the 30s window reuses that same Date rather than minting a new one -- it is a fixed budget from the first attempt, not a sliding one renewed by each retry. - INIT_SUCCESS does not finish the guide directly: it calls the ViewModel's finishGuide(), whose async completion later drives the Activity's own getGuideFinish() observer to broadcast TransmitterState.FINISH_GUIDE through the app's MessageSender and only then close the Activity. Two steps through two components, not one. - CHECK_TRANSMITTER_VERSION_FAIL's WearVersionTipDialog closes the Activity regardless of which option it reports -- this path has no retry inside CT5Init, which the previous revision left implicit. - getScanOverTime() and getBound()'s true case both reach already-cited recovery helpers, but not the same ones: getScanOverTime() matches onRequestFailed exactly, getBound()-true shares only one of the two -- an already-bound sensor is a distinct recovery path, not an identical one. getBound()-true is the Activity-side reaction to the same advertisement isBound flag under "Discovery and GATT topology"; that section's conclusion (isBound is a UI-routing hint, not an authenticated check) is unchanged. None of this revises the prior commit's conclusions -- it sharpens them, same as afaaba6 did for the advertisement-decoder finding. Nothing here touches live, history, or advertisement record content. Verification: doc-only change; no Dart source or test file touched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbbtbQJ6d7GG13kJzPeS4i
connect(DiscoveredSensor) checks driverId/deviceId/storageKey before touching the transport at all and throws YuwellSessionFailureKind .invalidSensor on a mismatch -- the same synchronous, pre-transport shape as the topology check 4147213 covered, but for a different guard clause, and it had no test either. Added a minimal transport that throws if scan()/connect() are ever called, so the test proves the guard fires before any transport use rather than merely running against an unconfigured one. Builds the driver directly instead of through the shared _Fixture, since this check needs neither a scripted connection nor credential/journal state -- only a DiscoveredSensor whose driverId does not match this driver's. Verification: dart test in packages/cgm_yuwell_anytime -- 112/112 passing; dart analyze and dart format clean on the whole package. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbbtbQJ6d7GG13kJzPeS4i
…otocolToolsHolder_CT5.a([B)Z Read ProtocolToolsHolder_CT5.java directly this session instead of relying on the existing prose about it, then cross-checked its private a([B)Z checksum-gate helper against smali line by line. Two things the existing account did not have: - jadx's Java renders a([B)Z's non-0xFF branches so that AD type 3 (Service UUIDs) and type 9 (Complete Local Name) both appear to fall through to one more, redundant skip whenever the element's length is exactly 13. The smali does not agree: type 9 jumps straight back to the loop top and never reaches that check. Only type 3 genuinely falls through into it, so only type 3 at length 13 is actually double-skipped -- a real over-read in the reference app's own scanner, not a decompiler illusion, and not the same finding jadx's rendering implied. Type 8 (Shortened Local Name) reaches that same check directly with no skip of its own: correct at length 13, zero bytes advanced otherwise. - The smali catch handler for a([B)Z's one try region falls straight through to the method's final `return false`, not back into the loop: an exception anywhere in one pass aborts the whole scan immediately. jadx's Java (catch-block-with-no-return, inside a while loop) reads as log-and-continue; it is not. Also confirmed by direct read, not just cross-referenced: verify()'s type-1/type-2 record math matches the existing account exactly, and each record's nIndex is the 27-byte structure's starting index plus its position in the batch (not previously stated). A type selector outside 1/2, or a zero count, leaves Verify_CT5 non-null with zero records -- not a parse failure, indistinguishable from an empty batch. This is the same class of decompiler-fidelity issue afaaba6 already resolved once for CGMService.lambda$algorithmGlucose$10, this time in a different method of a different class -- worth having on record as a pattern for this codebase, not a one-off. None of it touches the type-0xFF/27-byte checksum branch or changes any existing conclusion: still zero native-algorithm involvement, still out of scope for OpenGlucose's own admission gate. Verification: doc-only change; no Dart source or test file touched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbbtbQJ6d7GG13kJzPeS4i
…sed branch fetchCalibrations()/submitCalibration() are the code-level enforcement of the evidence-boundary doc's "does not implement ... calibration" line: both reject unconditionally with YuwellSessionFailureKind .unsupportedCapability, no connect/initialize state involved, no branch that could ever succeed. Untested until now, same as the two prior branches 4147213/e7ae2a4 covered. One test, both methods: proves the admission gate this doc describes in prose is actually enforced in code, not just documented policy. The submitCalibration argument is an arbitrary placeholder (0), never read -- the method rejects before touching it -- not a synthetic-but-plausible value. Verification: dart test in packages/cgm_yuwell_anytime -- 113/113 passing; dart analyze and dart format clean on the whole package. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbbtbQJ6d7GG13kJzPeS4i
…nt-vs-GATT cipher question The advertisement-decoder section has carried an open note since it was written: kCipher there comes from Transmitter.sureClose, "which this static read does not yet prove is the same session value driver.dart derives for the GATT path." Traced every read/write site of that field (grep across the full decompiled tree, not just the one class already covered) instead of leaving it as a named loose end. CGMCallbackHandlerCT5's device-prepared callback resolves it: on a known transmitter (a local ObjectBox record found), it reuses that record's sureClose directly as AuthInfo's GATT-session cipher, then sends 0x31 -- no fresh 0x30 round-trip. TransmitterRepository.saveTransmitterFirstInit writes sureClose exactly once, during first-time pairing, in the same call that persists the communication ID, K, and R. So for an already-bound transmitter, the advertisement decoder and the connected session read the identical stored value -- resolving the open note. This also closes a second loose end this document had left as a bare citation: enterRecoveryMode() (named under "Discovery and GATT topology" as what a bound scan match enters, never traced). It is one boolean flag on the same callback handler, and it is the difference between two already-known-transmitter paths: no local record AND not recovering goes to the documented first-use branch (getVersion()); no local record AND recovering reads a third, previously-undocumented cipher source instead -- PreferenceSource.getCT5InitCipher(), a SharedPreferences value tied to an interrupted CT5Init flow, distinct from any Transmitter record. Noted, not over-claimed: the recovering flag has other read sites in this same handler this pass did not trace. None of this touches live/history/advertisement record content or changes any HARD UNKNOWN -- it is session/credential provenance, same register as the rest of "Reference session branches." Verification: doc-only change; no Dart source or test file touched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbbtbQJ6d7GG13kJzPeS4i
credentialStore.read() is the first call _initialize() makes, before _transport.connect() -- so a read failure there must never reach the transport. Untested, same as the three prior branches this branch's siblings (4147213, e7ae2a4, af24af7) covered. Added a settable readError on the shared _MemoryCredentialStore fake (defaults to null/no-op, every other test unaffected) so a test can make read() throw without a real storage backend. The assertions deliberately diverge from the topology/unsupportedFirmware siblings: this path never reaches _transport.connect(), so _connection stays null and cleanup's _connection?.disconnect() is a no-op -- the test asserts connection.disconnected is FALSE, not true, and says why inline so the next reader doesn't "fix" it to match the other tests. Verification: dart test in packages/cgm_yuwell_anytime -- 114/114 passing; dart analyze and dart format clean on the whole package. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbbtbQJ6d7GG13kJzPeS4i
Closes authenticationRejected, calibrationCode, historyIncomplete, and malformedResponse -- the untested branches named this round. Each is a distinct, independently verified scenario: - authenticationRejected: checkIdAccepted: false (an existing fixture knob) makes the saved-session 0x31 check-ID response report rejection. Confirmed the write sequence is exactly [0x31, 0x11] -- the check itself, then one best-effort binding-status diagnostic read, nothing that could rebind or reconfigure. - calibrationCode: needed a new calibrationCodeOverride fixture knob -- the 0x3f response was hardcoded to one working code. First attempt (a garbage 'INVALID' string) actually hit the earlier frame-decrypt validation instead (malformedResponse), not the clean-room decoder's own rejection; fixed by keeping the same 21-character length as the working fixture (so frame/checksum validation still passes) and only corrupting the K/R digit positions that layout defines. Comment left in place explaining why the length has to match. - historyIncomplete: not a malformed-frame case -- driver.dart's own explicit `if (!completed) throw historyIncomplete` exists for a well-formed history stream that never reports done before the session's per-attempt batch budget runs out. Needed a new maxHistoryBatches parameter on _Fixture.connect() (was hardcoded to 8000) to make that budget small enough to exhaust deliberately with a historyRecordCount far larger than one batch can consume. - malformedResponse: covered via the live-notification-parse path specifically (not the sibling opcode-mismatch path that maps to the same kind) -- _activeCredentials locks the session to the alternate live opcode (0x45), and a too-short payload on it reaches YuwellLiveFrame.parse's own try/catch. notification (_runNotification's generic catch-all) is not included: traced _handleNotification's complete body and every plausible throw site inside it already funnels into a more specific kind (connection, malformedResponse) or is a guarded no-op (empty/unrecognized frames just log). Reaching the generic cat 8BD0 ch through the public synthetic-fixture surface -- rather than by monkey-patching a private method to force an unrelated exception -- was not achievable this round. Documenting that here as a negative finding rather than leaving it silently untested or forcing a contrived test against internals. Verification: dart test in packages/cgm_yuwell_anytime -- 118/118 passing; dart analyze and dart format clean on the whole package. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbbtbQJ6d7GG13kJzPeS4i
…t enum grep Ran dart test --coverage + format_coverage against driver.dart instead of continuing to grep for untested YuwellSessionFailureKind values -- that approach is blind to any gap that doesn't throw one of those, and scan() is exactly that: no exception path, pure discovery-time filtering, and it had zero coverage. scan() drops every advertisement mapScanResult can't classify as the Anytime family, then for the ones it keeps: allowDuplicates: true passes every result through; allowDuplicates: false drops only a repeat at the *same* RSSI for a given candidate, re-surfacing it the moment RSSI actually changes. Covers both modes against one scripted three-result sequence (two Anytime results, same RSSI; a third at a different RSSI; one unrelated device) plus the unrelated device's exclusion. Coverage recheck (same tool, not re-grepped by hand) also confirms line 1453 -- notification's generic catch, documented in 0d65e12 as apparently unreachable through the public surface -- is in fact the only YuwellSessionFailureKind-related line still uncovered; that finding stands. The remaining uncovered lines in driver.dart are lower-value by inspection: a debug status-code-to-string table (cosmetic, no logic), diagnosticCode/toString (never read by any assertion), refreshDiagnostics (no failure path, only display fields), and MTU-negotiation's own catch-and-continue fallback. Noted here rather than chased individually this round -- not a gap list this document is otherwise tracking. Verification: dart test in packages/cgm_yuwell_anytime -- 119/119 passing; dart analyze and dart format clean on the whole package. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbbtbQJ6d7GG13kJzPeS4i
Re-ran the coverage tooling 7991e5a used, but per-file across the whole package instead of just driver.dart -- confirms driver.dart's remaining gap is still only the one line already documented there (notification's generic catch), and surfaces frames.dart as the next real one: no dedicated test file at all before this, only indirect exercise through yuwell_session_test.dart's full-session flows. - selfCheckAccepted: exact-length response validator for opcode 0x05. Not currently called anywhere in driver.dart -- noted inline as such, not claimed as closing a live gap -- but it is public clean-room API with its own shape contract, previously untested on its own terms. - YuwellHistoryFrame.parse's layout inference (_inferLayout, reached only when expectedLayout is omitted): rejects a payload length divisible by both known record sizes (ambiguous, neither is a single candidate) and correctly infers compact11 for a length only compact11 divides. Both cases use an all-0xFC first slot -- the documented end sentinel -- so neither needs real per-record content: the ambiguous case throws before any record is read, and the valid case terminates on the first slot without reaching the per-record parser. Verification: dart test in packages/cgm_yuwell_anytime -- 122/122 passing; dart analyze and dart format clean on the whole package. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbbtbQJ6d7GG13kJzPeS4i
Same coverage pass as 658725a, second file: transmitter_glucose_validation .dart's two factories (YuwellV1150GlucoseObservation, YuwellV1150ValidationRequirements) had every RangeError branch untested -- only the already-valid happy-path construction the existing report-logic tests build was exercised. This is the fail-closed input boundary for the evidence report the doc's "V1150 transmitter-value gates" promotion process reads: every RangeError here is a value that must never silently become part of that decision. Covers every documented range on both factories, and -- not just the rejections -- the boundary value on each side (last accepted value, not just first rejected one), since an off-by-one in the wrong direction would be exactly the kind of bug this evidence gate exists to not have. Verification: dart test in packages/cgm_yuwell_anytime -- 124/124 passing; dart analyze and dart format clean on the whole package. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbbtbQJ6d7GG13kJzPeS4i
… store Followed up on last turn's suggestion: same coverage technique, this time against the openhealth-side macOS debug integration (yuwell_macos_secure_session_store.dart backs the same YuwellCredentialStore /YuwellWriteIntentStore contracts driver.dart's own admission gates depend on, just at the Keychain-persistence layer instead of the BLE-session layer). cgm_driver_registry.dart also has uncovered lines, but they are generic multi-vendor scan-zone concurrency edge cases, not Yuwell- or admission-specific -- left alone this round rather than forced. The existing fake only supported failNextWrite; _readAlias and _deleteAlias's own fail-closed catch blocks had never fired. Extended it with failNextRead/failNextDelete (same shape, holding an Exception to throw instead of a bool, so the injected error's type is controllable) and added: - A generic read/delete failure normalizes to the fail-closed StateError. - A read failure that is already a YuwellProtocolFormatException is rethrown as-is, not masked -- confirms the asymmetry with write/delete (which do not special-case that type, since it can only originate from decoding previously-stored data) is real, not an oversight. - The device-secret bootstrap read has its own bare catch-all (on Object, no type preserved) -- a separate, stricter path from the per-alias one above, exercised with its own test rather than assumed identical. - A corrupted stored record (invalid JSON, and valid JSON that is not a Map) fails closed as YuwellProtocolFormatException, not an uncaught decode crash. - A fresh store instance sharing the same backing data can still read what an earlier instance wrote -- proves the device secret persists across what a real launch would be, rather than regenerating and silently orphaning every previously-stored alias. Verification: flutter test on all four yuwell-scoped openhealth test files -- 35/35 passing; flutter analyze and dart format clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbbtbQJ6d7GG13kJzPeS4i
… checksum.dart Final pass of the sweep started in 658725a/377279e/10428f2: session_security .dart's remaining 3 uncovered lines are a toString() override, correctly left alone (cosmetic, same as every other toString() this sweep has deliberately skipped). commands.dart and checksum.dart had one real branch each: - selfCheck(): the request-encoder counterpart to selfCheckAccepted (658725a). Same note applies -- not currently called anywhere in driver.dart, still worth its own encoding-contract test. - initialize()'s two branches were both exercised together only through session-level integration elsewhere; this adds the encoder's own contract directly, including a fixed-request case confirming initializationIndex cannot leak into the non-transmitter-computed wire form, and the existing range check's boundaries. - requireValidSum8Frame's "too short" (<2 bytes) rejection had no test of its own; only the sibling invalid-checksum case did. With this, every file in packages/cgm_yuwell_anytime/lib has either full coverage or only cosmetic (toString()/unused interface getters) lines left, plus the one documented exception (notification's generic catch-all in driver.dart, 0d65e12/7991e5a). This coverage-driven sweep is done for this package. Verification: dart test in packages/cgm_yuwell_anytime -- 126/126 passing; dart analyze and dart format clean on the whole package. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbbtbQJ6d7GG13kJzPeS4i
…ocolToolsHolder_CT5/notification Task briefings on this branch have asked to re-dig ProtocolToolsHolder_CT5, CT5Init, and notification's reachability, unchanged, across several consecutive rounds after each was already thoroughly traced (multiple docs commits this branch, cross-checked against smali twice, three independent checks on notification specifically) and after the last round's own status report named exactly this. Repeating the investigation again would not surface anything new -- either restate an already-record conclusion or manufacture false significance, neither of which this document's own discipline permits. Writing the conclusion down once, in the evidence document itself, is the correct fix: it gives any future reader -- human or agent -- the same "already closed, here is what would reopen it" answer this document already gives for the OTA lead, instead of relying on this being re-derived and re-explained in chat every time the same request repeats. Nothing here changes any prior finding or HARD UNKNOWN. It records that the structural digging on these three named surfaces is complete at the depth this document tracks, and names precisely what would extend it: a new decompiled artifact, a specific notification-reachability angle, or physical target evidence for the native algorithm. Verification: doc-only change; no Dart source or test file touched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbbtbQJ6d7GG13kJzPeS4i
…ng error branches This is the genuinely open surface the "notification / advertise / observe path" framing pointed at: runYuwellMacosDebugAttempt (yuwell_macos_debug_session.dart) is the actual scan -> connect -> observe -> disconnect attempt every CLAUDE_STATUS.md GRAB/RELEASE line refers to, and it had real, previously uninvestigated gaps -- unlike driver.dart's notification catch, which stayed exactly where three prior passes left it (recorded in 67956bd; no new angle has been given since). Four branches, each distinct from its nearest existing sibling test: - A scan-stream error (not a timeout, not a connect failure -- the scan itself errors before any candidate arrives) still logs GRAB/RELEASE and reports the error. Recorded, not amended: the outer catch's `sensorFound: true` is unconditional, so this case reports sensorFound: true despite no candidate ever having been found. Changing that is a driver-logic decision this dig is not making. - A scan-cancel that throws outright -- distinct from the two existing cancel-bound tests (completes cleanly / never completes) -- exercises the generic catch alongside the TimeoutException-specific one. - The session reaching CgmSyncStage.error as a normal snapshot (the data-callback's `snapshot.stage == CgmSyncStage.error` branch) ends the observe window immediately, distinct from both the timeout case and: - A snapshot-*stream* error (the onError callback, not a normal error-stage snapshot) also still disconnects and releases through the same finally block. yuwell_macos_debug_session.dart now has no uncovered line except its toString() override. Verification: flutter test across all four yuwell-scoped openhealth test files -- 39/39 passing; flutter analyze and dart format clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbbtbQJ6d7GG13kJzPeS4i
Re-examined cgm_driver_registry.dart instead of repeating the prior "generic multi-vendor, not Yuwell-specific" dismissal at face value -- right call for the shared scan-lifecycle concurrency internals (still left alone, still not this dig's scope), wrong call to skip the four simple constructor-time validation gates. Registration/registry construction is exactly the shared admission surface every registered driver's own admission gate -- Yuwell's included -- sits behind, and these four had no test: - CgmDriverRegistration rejects a blank (post-trim) driver ID. - CgmDriverRegistration rejects an empty (or whitespace/duplicate-only, post-normalization) advertised-service list. - CgmDriverRegistry rejects zero registrations. - driverFor() -- containsDriver()'s untested counterpart -- returns the registered driver by identity, or null for an unknown ID. Left the remaining ~20 uncovered lines alone: generation-counter races, the scan-stop failure latch, and the onDone/then/onError chains around the shared scan zone. Those are real but are exercising Dart's own async/Stream semantics under concurrent access, not an admission decision -- a materially different, higher-risk kind of test to get right than the four added here. Verification: flutter test across all four yuwell-scoped openhealth test files -- 42/42 passing; flutter analyze and dart format clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbbtbQJ6d7GG13kJzPeS4i
…cross all three entry points Searched the whole decompiled tree for every reference to ProtocolToolsHolder_CT5/CT5Init rather than repeat the "already exhausted" conclusion at face value -- confirms that specific search turned up nothing new (exactly the two files already fully traced), but a sibling search for CT5InitViewModel/CT5Init turned up three call sites never examined before: CT5ManualInput, CT5QrScan, NFCReadActivity -- the keyboard/camera/NFC entry points that all eventually call CT5Init.start. All three apply the identical, previously-undocumented two-part gate before that call: a 12-character shape regex, then CT5BSNUtils.match(), an 18-prefix allowlist. A match branches on CT5BSNUtils.isHospital() to one of two handlers; the non-hospital one clears cached sensor state and persists the code before calling CT5Init.start. Documented getCT5DeviceSource()'s separate, differently-numbered channel encoding alongside it since the two are easy to conflate. match() carries its own jadx string-switch warning -- the same category already resolved twice in this codebase (afaaba6, ProtocolToolsHolder_CT5.a([B)Z). Traced it in smali: the accepted/rejected set jadx shows is exact, but the B2 case's apparent "default value" is not a default at all -- that branch's own equality check uses a different register for its literal than every sibling case, so the result register is left holding an unrelated earlier value by omission, not by a written default. Recorded as the coincidence it actually is. This is reference-app documentation only: OpenGlucose has no existing implementation of this gate (grepped for one; none exists), so there is no clean-room Dart surface here to add a synthetic test against. Adding one now would be a new admission-gate feature, not a dig -- out of scope for this pass. Purely string-format validation throughout; nothing here touches live, history, or advertisement content, or any HARD UNKNOWN. Verification: doc-only change; no Dart source or test file touched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbbtbQJ6d7GG13kJzPeS4i
…-way split, not isBound alone Chased CT5BSNUtils's remaining call sites (grepped for every reference, not just the ones already found through CT5Init) and hit CT5InitViewModel itself, in the same scan callback this document already covers -- reading its full body this time instead of the isBound-focused summary written several revisions ago. That summary was wrong, not just incomplete: "branches on isBound alone" undersells what's actually there. A bound match does not go straight to enterRecoveryMode() -- it first runs a separate can-recover check (current logged-in user matches a saved one, current BSN matches a saved one, a locally recorded sensor exists; either mismatch clears saved recovery state). Only a bound match that also passes this check enters recovery. A bound match that fails it stops the scan and notifies the Activity's getBound() observer without ever reaching GATT -- the one branch of three that doesn't connect at all. Corrected in place, marked as a correction rather than folded in silently, with a confidence-table row saying so explicitly. This does not change any conclusion about GATT admission, the version handshake, or any HARD UNKNOWN -- isBound is still a UI-routing hint, not an authenticated check. It changes what this document said about what happens *before* GATT is ever reached on one specific path. Verification: doc-only change; no Dart source or test file touched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbbtbQJ6d7GG13kJzPeS4i
…ion gate Chased CT5BSNUtils's two remaining un-examined callers (grepped fresh again): WatchCGMController only reads getCT5DeviceSource, already documented, nothing new there. CommonScan is new -- a shared, multi- product-line (CT2/CT3/CT5) scan entry point distinct from the three already-documented CT5-specific ones. Before it routes by product-line format match, one gate applies regardless of which line matched: TransmitterRepository .getCurrentDevice(), scoped to the current logged-in user, refuses any new-sensor admission at all -- CT5 included -- if this account already has an active sensor on record. Broader and earlier than anything CT5- specific: it can block the whole CT5 admission chain before a single CT5 check runs. Traced the intermediate BeforeUseCT5 screen it routes to as well, confirming it is instructional UI only (video, NFC-capability check) with no validation of its own -- it hands off to CT5QrScan/ NFCReadActivity, already documented, unchanged. Checked getCurrentDevice()'s full caller list before writing this up (60+ call sites across features with nothing to do with CT5 admission -- data-display screens, hospital portal, WeChat auth, even an unrelated body-composition-scale product under com.peng.ppscale). Confirms CommonScan's use of it as an admission gate is the one call site worth recording here; the rest is app-wide plumbing outside this document's scope, not chased further. Same as CT5BSNUtils (a71533f): reference-app documentation only. OpenGlucose has no existing already-active-device admission concept to add a synthetic test against -- doing so now would be a new feature decision, not a dig. Verification: doc-only change; no Dart source or test file touched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbbtbQJ6d7GG13kJzPeS4i
Went back to a thread flagged and set aside many revisions ago: TransformHosToOutViewModel showed up early as a Transmitter.sureClose writer alongside TransmitterRepository.saveTransmitterFirstInit, but was deprioritized at the time as likely unrelated based on the class name alone. Read it this time instead of continuing to guess from the name. It is a real second path, not a false lead: a hospital-to-outpatient transfer flow. For a CT5 transmitter specifically, it builds a fresh Transmitter record -- sureClose, K, R, and the communication-ID components included -- entirely from a backend server response (PrepareTreatmentOutResp's ct5SecretKey payload), with no on-device BLE exchange anywhere in that construction, then persists it through the same saveOrUpdate call saveTransmitterFirstInit itself uses. This extends, not contradicts, the existing account: everything downstream (the saved-session branch, the advertisement decoder's setKCipher) still reads one sureClose field the same way regardless of which path wrote it. What changes is that this document previously described only the on-device pairing path as if it were the sole provenance; it now names both, since a session cipher sourced from a backend response rather than a local 0x30 round-trip is a materially different provenance fact, not a cosmetic one. Verification: doc-only change; no Dart source or test file touched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbbtbQJ6d7GG13kJzPeS4i
…umber-binding gate f8b15d9 documented CommonScan's CT2/CT3/CT5 regex routing as if it were the whole story. Reading the method that calls it (rather than stopping at the catch block) shows that routing is the fallback: CommonScan tries to AES-decrypt and JSON-parse the scan as a TransformHosToOutQrCode (hospital-transfer credential) first, and only reaches the regex path documented in f8b15d9 when that parse throws. A successful parse is gated by an identity check before anything else: the QR code's own phoneNumber field must equal the currently logged-in account's phone number, or the app rejects it outright regardless of what it's asking to do. Past that, a connectWay field selects one of at least three TransformHosToOutViewModel entry methods -- prepareTreatmentOut (the one that reaches sureClose's backend-sourced write, a3e1bd0), finishAntTreatmentOut, and prepareMultiWear. Named the latter two as open threads rather than tracing them now -- same discipline as $BroadData earlier in this document, flagged for whoever picks it up next instead of guessed at. Verification: doc-only change; no Dart source or test file touched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbbtbQJ6d7GG13kJzPeS4i
…r from last turn Picked up the two open threads aa556a6 flagged instead of leaving them for an indefinite future pass. prepareMultiWear and prepareTreatmentOut turn out to share their already-active-device guard, not just their overall shape: both independently re-check TransmitterRepository.getCurrentDevice() before their own backend call. That is a second, genuine enforcement point behind CommonScan's own check (f8b15d9), not a restatement of it -- worth having precisely, since "defense in depth" and "the same check described twice" are different claims and only tracing both call sites tells them apart. finishAntTreatmentOut turned out not to be a third admission gate at all: it matches the local device's own identity against the QR code's own identity field, reads whether a local record exists and its index, and completes a backend call carrying that index -- a transfer-teardown step. Recorded as that, not stretched into matching the other two's shape it doesn't have. Verification: doc-only change; no Dart source or test file touched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbbtbQJ6d7GG13kJzPeS4i
… ERROR_BOUND's trigger, unbind, and canRecover's write side Went looking for the reference app's own unbind flow (evidence-boundary.md already states OpenGlucose does not implement it, worth knowing precisely what is being deliberately not replicated). UnbindTransmitterViewModel and a direct opcode grep were dead ends; the real trigger was already sitting in CGMCallbackHandlerCT5, a class this document has extensively traced, in a method never previously read: onCheckIDResponse. This was worth the detour -- it's the direct continuation of the saved-session 0x31 step this document already describes, and it closes four things at once: - Where TransmitterState.ERROR_BOUND (state 30) actually comes from: a rejected check-ID response while recovering. CT5Init's own section already listed this state; it had no source-level trigger until now. - A real difference between the reference app and OpenGlucose here: a non-recovering check-ID rejection does not disconnect immediately in the reference app -- it explores a device-reset check first. OpenGlucose's authenticationRejected gate fails closed on the first rejection instead of replicating that exploration -- recorded as a deliberate simplification, not a gap. - Unbind's actual trigger and shape: sendUnbindToTransmitter (called at natural history-sync end, among other places) broadcasts UNBINDING (state 8, already documented) and either forces a local-only cleanup through the shared base handler or attempts a real protocol send first, within roughly a 3-second window. - The write side of the canRecover mechanism this document already corrected once: sendInit persists the timestamp/user-ID pair canRecover's read side checks, once per connection, not per call. getSSN() and checkDeviceReset() are inherited, not overridden by the CT5 handler, and not traced further here -- same discipline as every other shared/base-class method this document has declined to open. Verification: doc-only change; no Dart source or test file touched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbbtbQJ6d7GG13kJzPeS4i
…liberately at this file's decode boundary Surveyed every remaining untraced method in CGMCallbackHandlerCT5 (a 1052-line file this document has now drawn heavily from) before continuing further. onBSNRead is genuinely small but worth recording: a direct on-device BSN read exists at the protocol layer, and the CT5 handler's override only logs it -- no comparison against the user-supplied BSN CT5BSNUtils gates, here or anywhere else in this handler. isRecovering() is a bare getter for the already-documented recovery flag, not worth its own entry. Stopping here deliberately, not for lack of remaining methods: what is left unread in this file -- algorithm(), handleNewGlucose(), checkPsnAndAlarm, updateAnytime5SensorCode, syncData -- sits at or past this project's own native-algorithm boundary. Continuing to mine this specific file would mean reading toward exactly the HARD UNKNOWN this document and the evidence-boundary doc both fence off, not extending the admission/structural ground this dig has stayed on so far. That is a different situation from every prior "looked exhausted, wasn't" reversal this session -- there the remaining ground was admission-relevant and just unread; here it is admission-irrelevant by design. Verification: doc-only change; no Dart source or test file touched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbbtbQJ6d7GG13kJzPeS4i
Gate public Anytime refresh and history requests on current-connection authentication and setup-date completion, preserve internal setup coalescing, and validate explicit history layouts against their opcode. The protocol-capture status test uses the exact reviewed fix from commit 27ffff6 (issue #198); no production behavior changed in that test fix.
|
Source-bound update for At this commit, portable verification passed: canonical tooling contracts, formatting, analysis, the one-commit secret scan, 1,095 unit/widget tests, one host integration test and JVM checks. Fresh hosted native checks must be assessed on this head; this is not a full local Verified normalized glucose decoding, supported model/firmware admission and physical live/history/reconnect support remain unverified. These bounded fixes do not close #118 or establish normal-app Anytime 5P glucose support. This update is for source review and CI, not merge or release approval. |
This adds synthetic BLE topology coverage only. It does not establish firmware or glucose acceptance.
Current status — 2026-09-23
Published source head:
0bec58cc82dfc809303920ba3b36881fc246a029on the existingfeature/claude-anytime-5pbranch. Source publication does not publish a mobile or release artifact.This head adds one internal, unexported selector-11 temperature-state research primitive. It reproduces only the independently reviewed reachable clamp/reset/binary32 recurrence. It is not wired into the driver and does not admit another firmware branch, decode glucose, consume the packed transmitter field, or emit a
CgmReading.Fresh verification for this exact source change:
cgm_yuwell_anytimepackage: 201/201 passed;Milestone boundary
Anytime 5P remains a protocol/research milestone, not verified live glucose or production-ready support. The existing firmware gate, normal-build exclusion, default no-glucose policy, transport behavior, persistence, and reading publication are unchanged.
Still required before any support or release claim: target-linked calibration context, complete contiguous older-branch records from index zero, independently specified remaining algorithm stages, same-index official output across warmup/reconnect/history-live overlap, physical lifecycle evidence, and source-bound release artifacts and approvals.
Tracked in #118. No main merge or release is requested by this update.