Repository navigation
fix(cbio): keep the authenticated harness from stranding its GATT client - #204
shroominic wants to merge 1 commit into
Conversation
Two consecutive device runs on 2026-09-18 showed `targetAcquired: true` with
`notifications: 0` and `writes: {}`, then a third added `disconnect_failed` and
`gatt_not_released`. The failure was after acquisition, and it cascaded.
The harness wrapped every FlutterBluePlus call in an outer Dart timeout.
`connect`, `disconnect`, `discoverServices`, `setNotifyValue`, and a
with-response `write` each hold `flutter_blue_plus`'s global Bluetooth mutex for
as long as their plugin future is pending, so an outer timeout did not cancel
the operation: it abandoned the future that owned the mutex. The following
`disconnect()` then blocked on that mutex until its own outer timeout fired, so
the link was reported unreleased and left up, and the next run connected into a
client the sensor still had.
The plugin owns those timeouts. The harness now passes them through, stops the
scanner before connecting, retries one Android status-133 connect the way the
app transport does, tolerates a refused ATT MTU negotiation the way the app
session does, and reports `gattReleased` only after the disconnect finished with
the device actually disconnected.
Refs #200
|
Attribution note from the A/B I ran while landing this, so the record is accurate. With the app's BLE permissions granted, the unmodified harness ( What this PR does is remove the failure modes that turned a bad device state into a wedged lane: the stranded GATT client and the unowned plugin futures. Those are the exact shapes recorded in |
|
Not absorbed by the GS1 consolidation (#209, feature/cbio-gs1). Left open on purpose: the GATT lifecycle work (the attempt bookkeeping and the bounded wait around the authenticated harness) is not in the consolidated |
|
Carried into #209 (head The harness no longer wraps plugin calls in a Dart Closing as carried. Carrying PR: #209. |
Closes #200. Base: #202.
Two consecutive device runs recorded
targetAcquired: truewithnotifications: 0andwrites: {}, and the third addeddisconnect_failedplus a failedgatt_not_releasedinvariant. The failure was after acquisition and it cascaded.The harness wrapped every FlutterBluePlus call in an outer Dart timeout. In
flutter_blue_plus2.2.1connect,disconnect, anddiscoverServiceseach hold_MutexFactory.getMutexForKey("global")for the life of their plugin future (writeandsetNotifyValuedo too), and they own their own timeouts. An outer timeout therefore does not cancel the operation — it abandons the future that holds the mutex. The nextdisconnect()blocked onmtx.take()until its own outer timeout fired,gattReleasedstayed false, and the GATT client was left registered for the following run.This is the same class as #143, and the app transport already avoids it (
disconnectWithPluginTimeout,writeWithPluginTimeout,setNotifyWithPluginTimeout,connectWithScanStoppedRetry). The harness now uses that proven shape:mtu: nullon connect,requestMtu(247)after discovery, and a refused negotiation is logged asmtu_failedinstead of ending the session — matching the app session;gattReleasedis reported only after the disconnect completed with the device actually disconnected, and a link that never came up reportsdisconnect-skip not-connectedrather than a false success or a false failure.No assertion is relaxed: the allowed-write set, the closed error taxonomy, and every invariant in
CbioSessionEvidenceare unchanged.Device evidence, from a clean install with #202 in place: