8000 Prepare OpenGlucose 0.4.0: GS1 and Anytime 5P integration by shroominic · Pull Request #139 · shroominic/OpenGlucose · GitHub
[go: up one dir, main page]

Skip to content

Prepare OpenGlucose 0.4.0: GS1 and Anytime 5P integration - #139

Open
shroominic wants to merge 240 commits into
mainfrom
release/v0.4.0
Open

shroominic wants to merge 240 commits into
mainfrom
release/v0.4.0

Conversation

@shroominic
@shroominic shroominic commented Sep 17, 2026 •
Copy link
Copy Markdown
Owner

Current status — 2026-09-23

Published aggregate source head: 37ec296 on release/v0.4.0. It contains the exact reviewed N5P source commit 0bec58cc82dfc809303920ba3b36881fc246a029, the existing GS1 and Anytime integration history, the independently reviewed Android Health activity compatibility fix a7a5d81, and the separate Android location-manifest lint fix 37ec296. Source publication does not publish a mobile or release artifact.

The N5P change is 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.

The Android Health compatibility commit changes only MainActivity from FlutterActivity to FlutterFragmentActivity, as required by the pinned health 13.3.1 plugin's ComponentActivity permission-launcher contract. The follow-up manifest commit adds ACCESS_COARSE_LOCATION alongside the existing unrestricted ACCESS_FINE_LOCATION; it changes no runtime or UI flow.

Android verification for this exact published sequence:

  • ordinary .debug APK build from default lib/main.dart: GREEN;
  • compiled MainActivity ancestry: FlutterFragmentActivity -> FragmentActivity -> ComponentActivity;
  • packaged HealthPlugin cast and registerForActivityResult call: verified in APK DEX;
  • packaged manifest contains both ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION;
  • fresh :app:lintDebug --rerun-tasks: GREEN, 0 errors and 19 existing warnings, 383/383 tasks executed;
  • Flutter analyzer: no issues;
  • independent source and artifact reviews for both commits: GO, no P0-P3 findings;
  • physical install, cold launch, and Health plugin registration: PENDING because the phone is currently offline from Tailscale and unreachable by ping, ADB, and SSH. No phone action was attempted from this lane.

Preserved ordinary debug artifacts:

  • Health-only predecessor: SHA-256 d313550154d2ef5234df0e2adfa86a39a0ade8ecec1af0bd39b34dc3d484b41e;
  • Health plus location-manifest fix: SHA-256 36be268f5fb6483c677d70bcd595b1410cc7e04040a5dd28f922c8c5bc7ee11f.

These host/artifact results are not physical runtime acceptance and are not release acceptance.

Fresh aggregate checks for the N5P integration:

  • full cgm_yuwell_anytime package: 201/201 passed;
  • Dart format: 33 files checked, 0 changed;
  • Dart analyze with fatal infos: no issues;
  • vendor-material guard: canary rejected and tree clean;
  • independent source review: GO, no P0-P3 findings.

Release boundary

This remains an integration candidate, not release-ready. GS1 and Anytime physical identity, normalized live/history decoding, lifecycle/reconnect/restart and screen-off behavior, normal-app physical/UI handoff evidence, source-bound production artifacts, signing, distribution, and explicit release approval remain open. The N5P temperature primitive does not reduce those gates or establish physical firmware compatibility. Android Health runtime verification also remains open until the phone is reachable.

Existing sensor feature PRs and issues remain separate: GS1 #208/#209 and Anytime #118/#106. No main merge, tag, or release is requested by this update.

shroominic and others added 30 commits September 9, 2026 01:39
Snapshot the local libre-protocol-capture worktree so contest cloud agents
can branch from a shared remote: cgm_driver_registry, cgm_libre2,
cgm_libre2_glucose, cgm_yuwell_anytime, debug protocol-capture harness,
and Anytime 5P passive-capture runbook. No raw captures or health data.
Do not await the in-flight connect before the promotion gate releases, and
type ControlledSession.disconnectError as Exception? for only_throw_errors.
…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 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 catch 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
This adds synthetic BLE topology coverage only. It does not establish firmware or glucose acceptance.

This branch has not been deployed

No deployments
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.

1 participant

0