Repository navigation
Add CBIO GS1 sensor integration - #209
shroominic wants to merge 178 commits into
Conversation
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.
Add a live Cbio/SIBIONICS GS1 session on top of the existing offline frame contract: discover the FF30 service, connect, enable FF31 notifications, and decode every received frame with parseCbioPlaintextFrame. The session stays fail-closed. The only write it can emit is the bounded `06 08 01 00 00 00 F1` read the protocol evidence supports, and only after a passive window with no notification. Activation (0x07), clock (0x03), reset, threshold, and calibration writes are never issued; calibration submission reports unsupported instead of attempting a write. Register the driver in the platform registry behind its own pure advertisement classifier so an FF30 candidate routes to the FF30 driver, and declare the package dependency plus the device-test dev dependencies the capture harness needs. Verified: packages/cgm_cbio `dart test` 24/24, `dart analyze` clean, openhealth driver registry/connection-flow tests green.
Add an integration_test entry point that drives the production FlutterBluePlusTransport through RecordingBleTransport and the unmodified CbioSensorDriver against the live sensor, without going through the Nearby sensors UI. Every phase is bounded, so a silent radio cannot hang the harness, and all evidence lands in the app-private trace sink that already backs the protocol capture lane. Discovery runs three ways: the driver's own FF30-filtered scan, an unfiltered transport scan, then a bounded raw-plugin acquisition loop that waits for an intermittent advertisement. The raw path also primes the platform's address type for the peripheral. An address recovered from advertisement evidence can be supplied at run time via --dart-define=CBIO_TARGET_DEVICE_ID so a capture is possible while the Dart scan surfacing defect is open. The harness asserts the safety contract from the trace itself: at most one write, and that write must be the allowed 0x08 read. It prints only redacted identifiers and never prints payload bytes. Pin the debug-only androidx.test espresso artifacts that the integration_test package's pinned 3.2.0 release makes ambiguous for AGP 8 namespace validation.
The platform registry now registers the Cbio driver alongside AiDEX, so the OG_DEMO selection test's expected driver set has to include it. Verified: openhealth `flutter test --exclude-tags integration` 473/473.
Android hands the app short-form UUIDs (FF30/FF31/FF32) while the driver declared the full 128-bit base form, so _locateCharacteristics never matched the FF30 service on a live link and every real session failed closed with missingNotifyCharacteristic. Canonicalise both sides onto the Bluetooth base UUID before comparing. Observed on the GS1 sensor: connect ok, service FF30 with FF31 (notify) and FF32 (write), and the previous code produced notify.enabled=[].
The first live FF31 packet is five bytes whose leading byte is the size a full frame would need, so disconnecting on the first packet could drop every later fragment. Linger for six seconds after the first packet.
FlutterBluePlusTransport.scan's stream can stay open when flutter_blue_plus stops the physical scan without emitting isScanning == false, so the discovery loop never returned and the Connect-a-sensor panel kept scanning forever. The registry now owns the scan deadline (_activeScanTimeout + _completeScanAtDeadline) so an overrun scan still ends for every consumer, bounds transport cancellation so a wedged cancel cannot hold the lifecycle open, and keeps an assert-gated, identity-free scan diagnostic for debug builds.
The single bounded 08 read on the live SIBIONICS/CBio GS1 sensor returns one five-byte FF31 notification, byte-identical across every recorded session. Add a read-only structural inspection that reports which plaintext hypotheses those bytes survive, with tests for the exact contract, orientation, constant mask, and truncation searches. The captured payload is not a complete plaintext frame: byte zero declares 36 bytes rather than 5, the additive sum is 0x56 rather than 0, byte one is not a mapped opcode, and no rotation, reversal, or constant mask exposes a valid frame. No decode is produced and none is claimed.
A connected sensor whose frames never decode parked setup on "Syncing sensor history" forever: the Cbio session enables FF31 notifications, one five-byte reply fails the offline parser with reason `length`, and the passive timer has already been cancelled, so no further state arrives. Sessions now arm a 45-second bounded wait for the first readable result while they sit in the syncing stage and have no decodable data. When the deadline elapses the host publishes a terminal error snapshot carrying `cgm.session.syncStalled`, blocks automatic reconnect, and the connect screen shows a next action instead of a spinner. Drivers that own their terminal outcome (Libre 2 phases) or an in-progress history sync are left untouched, and the Cbio driver's fail-closed write discipline is unchanged.
Add the vendor V120 read builders and a strict 0A glucose decoder, plus a transport-free sync session that polls the newest index and pages stored history under explicit budgets. Every failure path returns a status instead of a measurement, records stay raw with isUnitVerified false, and the 08 raw-data layout is refused. The device-backed harness sends only the documented read queries (03 F0 04 09, 03 F0 03 0A, 06 0A <index> 00 00 C). On the live sensor all four reads return the same five-byte frame 23 F7 6F D9 F4, which fails the plaintext contract, so the capability currently terminates in decodeFailed. No activation, clock, reset, threshold, key, or authentication write is sent.
The vendor storage, time, and glucose read queries all return the same five bytes, so the reply is not a per-command answer and the in-flight fragment reading is refuted for this path.
…tory The live sensor answers the vendor's own read queries once the link is authenticated with the vendor's per-frame stream mask, and the answer is its stored record stream: 9981 raw records, one per minute, contiguous from 2026-09-10T19:57:00Z to 2026-09-17T18:17:00Z on the last run. - cbio_crypto.dart carries the vendor stream mask and the frame transforms. The five-byte prompt the sensor sends unmasks to the valid control frame `04 00 00 00 FC`, which is what identified the encoding. - cbio_vendor_frames.dart builds the masked link frames the sensor accepts: device information, authentication, clock, and 8000 the two data reads. No activation, reset, threshold, key-registration, or firmware frame. - cbio_history_archive.dart assembles the raw `08` stream into an ordered archive with one record per index, plus explicit gap, overlap, and contiguity reporting. - The authenticated harness sends the vendor's masked sequence, records every write byte-for-byte in masked and plaintext form, and resumes the raw read at a given index so a bounded window can reach the newest record. Proven on the sensor: authentication succeeds with the 2A25 address octets as read (`result=1 status=0`); the `0A` path holds 1782 records that are all zero and stop at 2026-09-12T01:38:00Z; the `08` path streams to the present with raw `current` 39..97 and `temp` 274..460. Derived values stay marked `isUnitVerified == false` because no reference measurement confirms the scale.
Additive-only conflict resolution in packages/cgm_cbio: the barrel file, CHANGELOG, and README keep both lanes' content (union). No lane content was dropped or rewritten.
The GS1 driver now owns one authenticated session: connect, subscribe on FF31, authenticate, ingest the pushed 08 history, then poll the live edge and publish every reading as an explicitly provisional raw record. App surface changes: - the nearby-sensor list can finally complete, so a real sensor is selectable from the dashboard flow, - the hero value renders the sensor's raw field / 10 with no glucose unit, - the hero reports how many records were stored and their sensor-minute range, so the fetch is verifiable without opening a chart, - the provisional marker is on every rendered number and reads "sensor raw / 10, scale unverified". Reading positions come from the protocol's own index counter; the sensor's second counter has no epoch, so no wall-clock timestamp is invented from it. Evidence, screenshots, and the reproduction steps are in docs/testing/cbio-gs1-app-live.md.
FlutterBluePlusTransport.scan returned a stream that outlived the scan. flutter_blue_plus stops the radio at its own timeout, but the transport's cleanup awaited cancelResults, cancelScanning, awaitPendingStart, and stopScan before closeController. On a stack that leaves any of those pending, controller.close() never ran, so callers waited forever and cancelling the subscription hung for the same reason. - close the caller's stream first in closeFlutterBluePlusScanResources, - bound each cleanup step so a pending plugin future cannot hold a caller, - arm a scan deadline so a scan still closes when the plugin stops the radio without publishing isScanning = false, - cover it with a regression test that drives cleanup with futures that never complete. Measured before the fix on a Pixel 6: a 6 s window left isScanningNow false at ~7 s while the transport stream was still open at 12 s, and a 25 s timeout on the stream fired, while the plugin's raw result stream delivered 429 advertisements in the same period. Fixes #143.
The Secret Scan gate fails on this lane with one gitleaks `generic-api-key` finding in `packages/cgm_cbio/test/cbio_crypto_test.dart`. The value is a published synthetic masking vector, but it is a 50-character high-entropy string assigned to a name containing "auth", which is exactly the shape the detector is built to catch. The five masking vectors are now byte lists. The vectors and the assertions are unchanged; `dart test` still passes 60 tests, and the same package tree now reports no leaks to gitleaks 8.24.3.
`make format-check` fails on this lane in the Quality job: the new `openhealth/integration_test/cbio_glucose_query_test.dart` was committed unformatted, so the CI gate exits 2 before lint runs. This is the formatter's own output, nothing else: `dart format` reports one changed file and `make format-check` then exits 0 across every project.
…release/v0.4.0-lane-append
…release/v0.4.0-lane-append
`make test-integration` handed `test/integration/*` and `integration_test/*` to one `flutter test` call, which Flutter rejects: Integration tests and unit tests cannot be run in a single invocation. The two roots now run as separate lanes. The host lane keeps its behaviour (`test/integration/*` plus tag-marked files, and `integration_test/*` for plain Dart packages). The device lane runs `integration_test/*` in its own invocation and only against hardware named by `FLUTTER_TEST_DEVICE_ID`; otherwise it reports the deferred files and the knob that enables them, so a host without a device stays green instead of failing closed on a lane it cannot run. Adds scripts/test-workspace-integration-runner.rb, which drives the real script against a fixture repository with stubbed Flutter tooling and pins the lane split, the deferral notice, the device-id plumbing, and the existing nothing-found diagnostic. The check runs from `make tooling-check`. (cherry picked from commit d8c9acb)
Source-progress update: private full-input preservationReviewed source:
Input preservation is not complete algorithm replay or calibrated glucose. Evidence boundaryThe private phone was updated without launch to normal, non-debuggable Independent source review and canonical post-integration checks at |
Test-only follow-up: throwing BLE cleanupReviewed source: Five regression cases exercise notification-cancellation and disconnect errors, Only the existing session test file changed; production code is unchanged. Fresh canonical evidence: five focused tests and 304 package tests pass, with |
|
Source-bound update for At this commit, portable verification passed: tooling contracts, formatting, analysis, vendor-material guard, the six-commit secret scan, 1,640 unit/widget tests, one host integration test and JVM checks. Two credential-dependent tests were skipped; four physical harnesses were explicitly deferred. Fresh hosted native checks must be assessed on this head; this is not a full local Verified normalized glucose decoding, exact model/lifecycle support and physical live/history/reconnect support remain unverified. Empty normalized output is containment, not completed sensor support. This update is for source review and CI, not merge or release approval. |
Current status — 2026-09-22
Published source head:
7bc307c750fb5756a3cc9bbabeca15f8e6c65a2bon the existingfeature/cbio-gs1branch. This section supersedes current-head, verification,and readiness wording below; the historical body remains preserved for
traceability. Source publication to this PR branch does not publish a mobile or
release artifact.
The reviewed aggregate at commit
4560dc1a832629226649b7d31f87024ba0ca149drecords 369 CBIO tests passed with2 skipped, 188 N5P tests passed, 765 app host tests passed, and one bounded
host-only integration test passed: 1,323 passed and 2 skipped in total. Its
three analyzers are clean, and its normal and gated-N5P Android debug builds
pass. This is aggregate verification evidence, not a standalone GS1 app-suite,
physical-device, production-artifact, or release proof. The debug APKs are
compile evidence only.
GS1 remains not production-ready. Current identity and firmware, verified
normalized glucose decoding, complete history, lifecycle/reconnect/restart and
screen-off behavior, normal-app physical/UI and handoff evidence, physical
acceptance, and a source-bound production artifact with 8000 signing, distribution,
and release publication approval all remain open.
Status: work in progress, not release-ready
Single GS1 integration carrier for issue #208 on
feature/cbio-gs1, targetingmain. Current reviewed source:8733e96fbcbe521a8b2bee47b8504aee621b354c. No main merge, release or binary publication is requested by this update. Verified GS1 glucose decoding is still missing.The Libre2/Anytime multi-sensor base
821f9a34bfc8af12e6a31438b88b6ed75ca370eeremains an ancestor. This does not imply inclusion of all later work on other branches.Current implementation boundary
rawHistory, are normalized-only. Until a verified decoder exists, actual GS1 latest is null, history/rawHistory are empty, normalized counters are zero/unknown and decoded/raw history capabilities are false. No unverified advertisement glucose fallback is admitted.The raw integer and its historical
/10representation are not verified mg/dL or mmol/L. Empty normalized output is a safety boundary, not a completed glucose integration.Fresh local verification
Combined code at
816e8a6plus independently reviewed factual docs at8733e96:Visual source
e46ccd9is integrated as61392b1; final controller cleanup adds neutral lifecycle policy and private metadata projection without changing renderer files. Screenshots cover AiDEX/Libre2/GS1 synthetic inputs, EN/ZH, mg/dL/mmol/L, normal/empty/error/stale/warmup and details/archive. They do not establish actual GS1 decoding, phone operation or release readiness. Inherited narrow hero overflow and Chinese timeframe-segment clipping are documented shared-layout limitations, not claimed fixed.The first full pre-restoration run exposed a Yuwell test-fixture scheduling issue: a const empty-stream cancellation future crossed fake_async zones. A test-only real-zone drain fixed it with original timeout/security assertions retained; no production radio workaround was added.
Native and physical evidence remain separate
Native builds and hosted checks for this new head are pending. Earlier green CI at
ac016e3and older heads is historical evidence only, not validation of8733e96. No new local native build is claimed for this source update.The phone still has prior private normal-release build 34 and its observed
GS1-H03Bcheckpoint-time-witness mismatch. That observation does not prove a physical reset. Earlier build 32 acquired raw history but did not demonstrate reliable completed live/history operation. Shared UI restoration and private-state containment do not fix or explain the physical witness mismatch. This source has not been installed or demonstrated on the phone.A private bounded clock-witness probe is separately planned/reviewed; it is not a production feature or authorization to issue new radio commands. A separate synthetic ABI experiment failed closed without positive ABI evidence; it contributes no glucose, calibration, model or licensing claim and no vendor implementation is added here.
Release gates still open
One existing open PR is retained. No new GS1 carrier PR, main merge, force push or release is part of this update.