Compare commits

...

2 Commits

Author SHA1 Message Date
2ac68e09ab bd: backup 2026-03-04 17:07 2026-03-04 18:07:18 +01:00
1dbbf191e6 feat(dfu): add firmware update controls to device page 2026-03-04 18:07:12 +01:00
4 changed files with 463 additions and 23 deletions

View File

@ -1,10 +1,10 @@
{
"last_dolt_commit": "sj669kgsnkm472b9vv8ejj7tuniiibjr",
"last_dolt_commit": "mf3is3p9ve79on2q1achdjp8v50rq6s3",
"last_event_id": 0,
"timestamp": "2026-03-03T16:11:51.022190652Z",
"timestamp": "2026-03-04T17:07:18.317614374Z",
"counts": {
"issues": 10,
"events": 21,
"events": 24,
"comments": 0,
"dependencies": 21,
"labels": 0,

View File

@ -19,3 +19,6 @@
{"actor":"Yandrik","comment":null,"created_at":"2026-03-03T17:01:01Z","event_type":"claimed","id":22,"issue_id":"abawo_bt_app-20q.4","new_value":"{\"assignee\":\"Yandrik\",\"status\":\"in_progress\"}","old_value":"{\"id\":\"abawo_bt_app-20q.4\",\"title\":\"Add firmware file selection and binary validation flow\",\"description\":\"Implement local firmware artifact input for v1.\\n\\nWork:\\n- Integrate file picker for local firmware .bin\\n- Read bytes safely and validate non-empty payload\\n- Guard against malformed selections and unsupported files\\n- Compute total_len and crc32 from selected bytes\\n- Generate per-session session_id and set flags=0x00 for v1\",\"acceptance_criteria\":\"- User can select .bin and app obtains byte payload\\n- Validation errors are explicit and user-facing\\n- Metadata (size/crc/session) is available to transfer engine\",\"status\":\"open\",\"priority\":2,\"issue_type\":\"task\",\"owner\":\"me@yandrik.dev\",\"created_at\":\"2026-03-03T15:39:10Z\",\"created_by\":\"Yandrik\",\"updated_at\":\"2026-03-03T15:39:10Z\"}"}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-03T17:04:51Z","event_type":"closed","id":23,"issue_id":"abawo_bt_app-20q.4","new_value":"Completed","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-03T17:07:21Z","event_type":"claimed","id":24,"issue_id":"abawo_bt_app-20q.9","new_value":"{\"assignee\":\"Yandrik\",\"status\":\"in_progress\"}","old_value":"{\"id\":\"abawo_bt_app-20q.9\",\"title\":\"Handle post-FINISH disconnect/reboot and reconnect verification\",\"description\":\"Implement robust completion handling around expected device reset.\\n\\nWork:\\n- Treat disconnect after successful FINISH as expected behavior\\n- Reconnect with update-specific timeout strategy\\n- Verify device is reachable/readable after reconnect\\n- Surface success only after reconnect verification path\\n- Document limitation: no strict firmware version compare until firmware exposes version characteristic\",\"acceptance_criteria\":\"- Expected reset does not appear as generic failure\\n- Reconnect path is attempted and result is surfaced\\n- Completion criteria are consistent with v1 definition\",\"status\":\"open\",\"priority\":1,\"issue_type\":\"task\",\"owner\":\"me@yandrik.dev\",\"created_at\":\"2026-03-03T15:39:28Z\",\"created_by\":\"Yandrik\",\"updated_at\":\"2026-03-03T15:39:28Z\"}"}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-03T17:12:02Z","event_type":"closed","id":25,"issue_id":"abawo_bt_app-20q.9","new_value":"Added post-FINISH reset disconnect, reconnect, and reachability verification before marking update complete","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-03T17:12:17Z","event_type":"claimed","id":26,"issue_id":"abawo_bt_app-20q.3","new_value":"{\"assignee\":\"Yandrik\",\"status\":\"in_progress\"}","old_value":"{\"id\":\"abawo_bt_app-20q.3\",\"title\":\"Integrate firmware update UI into device details page\",\"description\":\"Add user-facing update controls and status presentation.\\n\\nWork:\\n- Add update card with Select Firmware, Start Update, Cancel\\n- Show phase text, progress %, bytes sent/acked, and retry status\\n- Disable conflicting actions (gear writes / connect button-to-bike) during DFU\\n- Show explicit reboot expectation after FINISH\\n- Persist/clear transient state correctly on page lifecycle changes\",\"acceptance_criteria\":\"- UI can run full update flow start-to-finish\\n- Progress/state transitions are visible and consistent\\n- Conflicting controls are disabled during active transfer\\n- Failures and cancellations are clearly shown\",\"status\":\"open\",\"priority\":1,\"issue_type\":\"feature\",\"owner\":\"me@yandrik.dev\",\"created_at\":\"2026-03-03T15:39:10Z\",\"created_by\":\"Yandrik\",\"updated_at\":\"2026-03-03T15:39:10Z\"}"}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-04T18:07:17Z","event_type":"closed","id":27,"issue_id":"abawo_bt_app-20q.3","new_value":"Integrated firmware file selection, update controls, progress display, and DFU state handling into device details page","old_value":""}

View File

@ -1,10 +1,10 @@
{"acceptance_criteria":"- User can select a firmware .bin and complete upload end-to-end\n- Upload follows protocol and handles packet loss via retransmit\n- App shows clear progress and failure states\n- Device reconnects after reboot and is reachable","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"c3bd8fa9b5b9d51b04cde5e11c2d8cdedaca29e049a64e8521172a69093b4ba3","created_at":"2026-03-03T15:37:50Z","created_by":"Yandrik","crystallizes":0,"defer_until":null,"description":"Implement the firmware update flow defined in universal-shifters/update-process.md for the Flutter app.\n\nScope:\n- Manual local .bin selection and upload over BLE GATT\n- START/DATA/FINISH/ABORT protocol support\n- Cumulative ACK handling with retransmit\n- Expected reboot/disconnect handling and reconnect check\n\nOut of scope for v1:\n- Hosted firmware distribution/backend\n- Cryptographic signature verification in app\n- Encrypted payload transport mode","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"abawo_bt_app-20q","is_template":0,"issue_type":"epic","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@yandrik.dev","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Implement Universal Shifters BLE DFU v1 in app (manual .bin upload)","updated_at":"2026-03-03T15:37:50Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"- All protocol constants match spec exactly\n- No duplicated literal DFU UUID/opcode values across services/UI\n- Domain models compile and are ready for transfer engine integration","actor":"","agent_state":"","assignee":"Yandrik","await_id":"","await_type":"","close_reason":"Implemented DFU constants, flags, opcodes, and typed progress models in shifter_types.dart","closed_at":"2026-03-03T15:45:20Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"d471157a1af6199f0ac0a1565b05b28d1a477e66d9166a3865edb42ea1e8c2ae","created_at":"2026-03-03T15:38:34Z","created_by":"Yandrik","crystallizes":0,"defer_until":null,"description":"Add protocol surface in app code for DFU support.\n\nWork:\n- Add UUID constants for dfu_control (...40008), dfu_data (...40009), dfu_ack (...4000a)\n- Add opcode/frame constants (START=0x01, FINISH=0x02, ABORT=0x03, frame size 64, payload size 63)\n- Add flags constants and typed update-state/progress models used by service/UI\n- Remove future magic numbers by centralizing constants","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"abawo_bt_app-20q.1","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@yandrik.dev","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add DFU protocol constants and domain models","updated_at":"2026-03-03T15:45:20Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"- CRC test vector passes: \"123456789\" =\u003e 0xCBF43926\n- START/FINISH/ABORT encoders produce exact lengths/byte layout\n- Frame segmentation handles final partial payload correctly\n- Seq wrap and ack+1 rewind helpers covered by tests","actor":"","agent_state":"","assignee":"Yandrik","await_id":"","await_type":"","close_reason":"Added DFU protocol codec/CRC utilities with unit tests for payloads, frames, and sequence helpers","closed_at":"2026-03-03T15:48:57Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"9ba609e63c9e2f8a4e95de6795dace11c610aa9c13908b5dab7aa6cb83ab58d4","created_at":"2026-03-03T15:39:10Z","created_by":"Yandrik","crystallizes":0,"defer_until":null,"description":"Implement pure protocol helpers.\n\nWork:\n- Build START payload (11 bytes, LE fields)\n- Build FINISH/ABORT payloads\n- Build/segment DATA frames (seq + 63-byte payload)\n- Implement ACK/sequence helpers including wrapping behavior\n- Implement CRC32 (ISO-HDLC reflected polynomial 0xEDB88320, init/final xor FFFFFFFF)","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"abawo_bt_app-20q.2","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@yandrik.dev","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Implement DFU packet codec and CRC32 utilities with tests","updated_at":"2026-03-03T15:48:57Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"- UI can run full update flow start-to-finish\n- Progress/state transitions are visible and consistent\n- Conflicting controls are disabled during active transfer\n- Failures and cancellations are clearly shown","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"e8d2f905536263a05de5e7e86bf8f02fa0491d7129e30f36f2c4dd09acab1882","created_at":"2026-03-03T15:39:10Z","created_by":"Yandrik","crystallizes":0,"defer_until":null,"description":"Add user-facing update controls and status presentation.\n\nWork:\n- Add update card with Select Firmware, Start Update, Cancel\n- Show phase text, progress %, bytes sent/acked, and retry status\n- Disable conflicting actions (gear writes / connect button-to-bike) during DFU\n- Show explicit reboot expectation after FINISH\n- Persist/clear transient state correctly on page lifecycle changes","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"abawo_bt_app-20q.3","is_template":0,"issue_type":"feature","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@yandrik.dev","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Integrate firmware update UI into device details page","updated_at":"2026-03-03T15:39:10Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"- UI can run full update flow start-to-finish\n- Progress/state transitions are visible and consistent\n- Conflicting controls are disabled during active transfer\n- Failures and cancellations are clearly shown","actor":"","agent_state":"","assignee":"Yandrik","await_id":"","await_type":"","close_reason":"Integrated firmware file selection, update controls, progress display, and DFU state handling into device details page","closed_at":"2026-03-04T17:07:18Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"e8d2f905536263a05de5e7e86bf8f02fa0491d7129e30f36f2c4dd09acab1882","created_at":"2026-03-03T15:39:10Z","created_by":"Yandrik","crystallizes":0,"defer_until":null,"description":"Add user-facing update controls and status presentation.\n\nWork:\n- Add update card with Select Firmware, Start Update, Cancel\n- Show phase text, progress %, bytes sent/acked, and retry status\n- Disable conflicting actions (gear writes / connect button-to-bike) during DFU\n- Show explicit reboot expectation after FINISH\n- Persist/clear transient state correctly on page lifecycle changes","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"abawo_bt_app-20q.3","is_template":0,"issue_type":"feature","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@yandrik.dev","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Integrate firmware update UI into device details page","updated_at":"2026-03-04T17:07:18Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"- User can select .bin and app obtains byte payload\n- Validation errors are explicit and user-facing\n- Metadata (size/crc/session) is available to transfer engine","actor":"","agent_state":"","assignee":"Yandrik","await_id":"","await_type":"","close_reason":"Completed","closed_at":"2026-03-03T16:04:51Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"be8d8646593e4fc6c269383efb41808c40ffdfb3103e58a90acc0142e32f4711","created_at":"2026-03-03T15:39:10Z","created_by":"Yandrik","crystallizes":0,"defer_until":null,"description":"Implement local firmware artifact input for v1.\n\nWork:\n- Integrate file picker for local firmware .bin\n- Read bytes safely and validate non-empty payload\n- Guard against malformed selections and unsupported files\n- Compute total_len and crc32 from selected bytes\n- Generate per-session session_id and set flags=0x00 for v1","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"abawo_bt_app-20q.4","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@yandrik.dev","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add firmware file selection and binary validation flow","updated_at":"2026-03-03T16:04:51Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"- Tests cover success and critical failure/retry paths\n- Wrap-around and ack rewind behavior is validated\n- Regressions in sequencing/CRC are caught automatically","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"f784095261bfc41eed5544a567f667bb2ba4816433f1d5d6ae21ea4616c7109a","created_at":"2026-03-03T15:39:10Z","created_by":"Yandrik","crystallizes":0,"defer_until":null,"description":"Add targeted tests for protocol and engine behavior.\n\nWork:\n- Unit tests for codec + CRC + sequence helpers\n- Engine tests with mocked BLE ack stream for:\n - happy path\n - dropped frame / stalled ACK and rewind\n - timeout and bounded retry fail\n - cancel/abort cleanup\n- Ensure deterministic tests for wrap-around sequence scenarios","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"abawo_bt_app-20q.5","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@yandrik.dev","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Add DFU test suite for happy path, loss, stalls, and cancel","updated_at":"2026-03-03T15:39:10Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"- Team can execute update and triage failures from docs\n- v1 limitations are explicit and not ambiguous\n- QA checklist is actionable and complete","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"3a5dd3fd4d901ae53a80b3c7b488b41db5e09f7cc50da0e6c185c075b8ad7e51","created_at":"2026-03-03T15:39:10Z","created_by":"Yandrik","crystallizes":0,"defer_until":null,"description":"Document how to use and support the new updater.\n\nWork:\n- Add app-side DFU flow docs (select/start/progress/reboot/reconnect)\n- Add troubleshooting matrix for common failures (MTU, stalled ACK, reconnect timeout, CRC mismatch)\n- Record explicit v1 limitations and future security/version-verification roadmap\n- Add manual QA checklist for release validation","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"abawo_bt_app-20q.6","is_template":0,"issue_type":"chore","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@yandrik.dev","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Document DFU v1 operator flow, troubleshooting, and constraints","updated_at":"2026-03-03T15:39:10Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"- Happy path reaches done with full ACKed transfer\n- Loss/stall path retransmits and recovers correctly\n- Cancel triggers ABORT and returns to idle cleanly\n- Engine surfaces explicit error reasons for UI","actor":"","agent_state":"","assignee":"Yandrik","await_id":"","await_type":"","close_reason":"Implemented firmware transfer engine with preflight, cumulative ACK handling, retries, cancel ABORT, and unit tests","closed_at":"2026-03-03T16:00:45Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"042d39f373b05c758d2d2724c757a7e790de522c2def68f8f12e9f2fbbb70dc6","created_at":"2026-03-03T15:39:18Z","created_by":"Yandrik","crystallizes":0,"defer_until":null,"description":"Build the runtime transfer engine used by UI.\n\nWork:\n- Subscribe to dfu_ack indications before START\n- Send START and require initial ACK 0xFF\n- Stream dfu_data using write without response in windows (configurable, default 8)\n- Track cumulative ACK; on stall/timeout rewind to ack+1 (wrapping aware)\n- Handle invalid/no-progress scenarios with bounded retries\n- Send FINISH after full acked upload\n- Support ABORT for cancellation and terminal error cleanup\n- Emit state/progress stream for UI","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"abawo_bt_app-20q.7","is_template":0,"issue_type":"feature","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@yandrik.dev","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Implement BLE DFU transfer engine with cumulative ACK retransmit","updated_at":"2026-03-03T16:00:45Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"- Upload start is blocked when MTU/connection preconditions fail\n- Error messages explain what failed and next step\n- Preflight result is exposed for transfer start path","actor":"","agent_state":"","assignee":"Yandrik","await_id":"","await_type":"","close_reason":"Implemented DFU preflight checks for connection state and negotiated MTU with typed results and tests","closed_at":"2026-03-03T15:55:11Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"558fa59b42c42b122eefc43d3483d1a61bdd995464dbc36d166f763f42366e87","created_at":"2026-03-03T15:39:24Z","created_by":"Yandrik","crystallizes":0,"defer_until":null,"description":"Add runtime guards required for protocol correctness.\n\nWork:\n- Ensure active connection to target button before DFU start\n- Request elevated MTU (e.g. 128/247) before upload\n- Validate negotiated MTU supports 64-byte data writes (ATT payload requirement)\n- Fail early with actionable message when transport preconditions are not met","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"abawo_bt_app-20q.8","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@yandrik.dev","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add BLE DFU preflight checks (MTU and connection readiness)","updated_at":"2026-03-03T15:55:11Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"- Expected reset does not appear as generic failure\n- Reconnect path is attempted and result is surfaced\n- Completion criteria are consistent with v1 definition","actor":"","agent_state":"","assignee":"Yandrik","await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"024e8aa245b93098013fbd66102a621c9ecd58a1dda9fc276c74cd278bdd3512","created_at":"2026-03-03T15:39:28Z","created_by":"Yandrik","crystallizes":0,"defer_until":null,"description":"Implement robust completion handling around expected device reset.\n\nWork:\n- Treat disconnect after successful FINISH as expected behavior\n- Reconnect with update-specific timeout strategy\n- Verify device is reachable/readable after reconnect\n- Surface success only after reconnect verification path\n- Document limitation: no strict firmware version compare until firmware exposes version characteristic","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"abawo_bt_app-20q.9","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@yandrik.dev","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"in_progress","target":"","timeout_ns":0,"title":"Handle post-FINISH disconnect/reboot and reconnect verification","updated_at":"2026-03-03T16:07:21Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"- Expected reset does not appear as generic failure\n- Reconnect path is attempted and result is surfaced\n- Completion criteria are consistent with v1 definition","actor":"","agent_state":"","assignee":"Yandrik","await_id":"","await_type":"","close_reason":"Added post-FINISH reset disconnect, reconnect, and reachability verification before marking update complete","closed_at":"2026-03-03T16:12:02Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"024e8aa245b93098013fbd66102a621c9ecd58a1dda9fc276c74cd278bdd3512","created_at":"2026-03-03T15:39:28Z","created_by":"Yandrik","crystallizes":0,"defer_until":null,"description":"Implement robust completion handling around expected device reset.\n\nWork:\n- Treat disconnect after successful FINISH as expected behavior\n- Reconnect with update-specific timeout strategy\n- Verify device is reachable/readable after reconnect\n- Surface success only after reconnect verification path\n- Document limitation: no strict firmware version compare until firmware exposes version characteristic","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"abawo_bt_app-20q.9","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@yandrik.dev","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Handle post-FINISH disconnect/reboot and reconnect verification","updated_at":"2026-03-03T16:12:02Z","waiters":"","wisp_type":"","work_type":""}

View File

@ -1,6 +1,9 @@
import 'dart:async';
import 'package:abawo_bt_app/model/firmware_file_selection.dart';
import 'package:abawo_bt_app/model/shifter_types.dart';
import 'package:abawo_bt_app/service/firmware_file_selection_service.dart';
import 'package:abawo_bt_app/service/firmware_update_service.dart';
import 'package:abawo_bt_app/service/shifter_service.dart';
import 'package:abawo_bt_app/widgets/bike_scan_dialog.dart';
import 'package:abawo_bt_app/widgets/gear_ratio_editor_card.dart';
@ -64,9 +67,46 @@ class _DeviceDetailsPageState extends ConsumerState<DeviceDetailsPage> {
List<double> _gearRatios = const [];
int _defaultGearIndex = 0;
late final FirmwareFileSelectionService _firmwareFileSelectionService;
FirmwareUpdateService? _firmwareUpdateService;
StreamSubscription<DfuUpdateProgress>? _firmwareProgressSubscription;
DfuV1PreparedFirmware? _selectedFirmware;
DfuUpdateProgress _dfuProgress = const DfuUpdateProgress(
state: DfuUpdateState.idle,
totalBytes: 0,
sentBytes: 0,
lastAckedSequence: 0xFF,
sessionId: 0,
flags: DfuUpdateFlags(),
);
bool _isSelectingFirmware = false;
bool _isStartingFirmwareUpdate = false;
String? _firmwareUserMessage;
bool get _isFirmwareUpdateBusy {
if (_isStartingFirmwareUpdate) {
return true;
}
switch (_dfuProgress.state) {
case DfuUpdateState.starting:
case DfuUpdateState.waitingForAck:
case DfuUpdateState.transferring:
case DfuUpdateState.finishing:
return true;
case DfuUpdateState.idle:
case DfuUpdateState.completed:
case DfuUpdateState.aborted:
case DfuUpdateState.failed:
return false;
}
}
@override
void initState() {
super.initState();
_firmwareFileSelectionService = FirmwareFileSelectionService(
filePicker: LocalFirmwareFilePicker(),
);
_connectionStatusSubscription =
ref.listenManual<AsyncValue<(ConnectionStatus, String?)>>(
connectionStatusProvider,
@ -88,6 +128,8 @@ class _DeviceDetailsPageState extends ConsumerState<DeviceDetailsPage> {
_connectionStatusSubscription?.close();
_statusSubscription?.cancel();
_shifterService?.dispose();
_firmwareProgressSubscription?.cancel();
unawaited(_firmwareUpdateService?.dispose() ?? Future<void>.value());
super.dispose();
}
@ -100,6 +142,9 @@ class _DeviceDetailsPageState extends ConsumerState<DeviceDetailsPage> {
_isExitingPage = true;
_reconnectTimeoutTimer?.cancel();
await _firmwareUpdateService?.cancelUpdate();
await _disposeFirmwareUpdateService();
final bluetooth = ref.read(bluetoothProvider).value;
await bluetooth?.disconnect();
await _stopStatusStreaming();
@ -127,11 +172,13 @@ class _DeviceDetailsPageState extends ConsumerState<DeviceDetailsPage> {
if (_wasConnectedToCurrentDevice &&
!_isReconnecting &&
status == ConnectionStatus.disconnected) {
status == ConnectionStatus.disconnected &&
!_isFirmwareUpdateBusy) {
_startReconnect();
}
if (!isCurrentDevice || status == ConnectionStatus.disconnected) {
if ((!isCurrentDevice || status == ConnectionStatus.disconnected) &&
!_isFirmwareUpdateBusy) {
_stopStatusStreaming();
}
}
@ -161,6 +208,13 @@ class _DeviceDetailsPageState extends ConsumerState<DeviceDetailsPage> {
Future<void> _startStatusStreamingIfNeeded() async {
if (_shifterService != null) {
_statusSubscription ??= _shifterService!.statusStream.listen((status) {
if (!mounted) {
return;
}
_recordStatus(status);
});
_shifterService!.startStatusNotifications();
if (!_hasLoadedGearRatios && !_isGearRatiosLoading) {
unawaited(_loadGearRatios());
}
@ -216,13 +270,26 @@ class _DeviceDetailsPageState extends ConsumerState<DeviceDetailsPage> {
Future<void> _stopStatusStreaming() async {
await _statusSubscription?.cancel();
_statusSubscription = null;
if (_isFirmwareUpdateBusy) {
return;
}
await _disposeFirmwareUpdateService();
await _shifterService?.dispose();
_shifterService = null;
}
Future<void> _disposeFirmwareUpdateService() async {
await _firmwareProgressSubscription?.cancel();
_firmwareProgressSubscription = null;
await _firmwareUpdateService?.dispose();
_firmwareUpdateService = null;
}
Future<void> _loadGearRatios() async {
final shifter = _shifterService;
if (shifter == null || _isGearRatiosLoading) {
if (shifter == null || _isGearRatiosLoading || _isFirmwareUpdateBusy) {
return;
}
@ -257,6 +324,10 @@ class _DeviceDetailsPageState extends ConsumerState<DeviceDetailsPage> {
Future<String?> _saveGearRatios(
List<double> ratios, int defaultGearIndex) async {
if (_isFirmwareUpdateBusy) {
return 'Gear ratio changes are disabled during firmware update.';
}
final shifter = _shifterService;
if (shifter == null) {
return 'Status channel is not ready yet.';
@ -289,6 +360,15 @@ class _DeviceDetailsPageState extends ConsumerState<DeviceDetailsPage> {
}
Future<void> _connectButtonToBike() async {
if (_isFirmwareUpdateBusy) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Connect to bike is disabled during firmware updates.'),
),
);
return;
}
final selectedBike = await BikeScanDialog.show(
context,
excludedDeviceId: widget.deviceAddress,
@ -328,6 +408,177 @@ class _DeviceDetailsPageState extends ConsumerState<DeviceDetailsPage> {
);
}
Future<FirmwareUpdateService?> _ensureFirmwareUpdateService() async {
final shifter = _shifterService;
if (shifter == null) {
return null;
}
if (_firmwareUpdateService != null) {
return _firmwareUpdateService;
}
final asyncBluetooth = ref.read(bluetoothProvider);
final bluetooth = asyncBluetooth.valueOrNull;
if (bluetooth == null) {
return null;
}
final service = FirmwareUpdateService(
transport: ShifterFirmwareUpdateTransport(
shifterService: shifter,
bluetoothController: bluetooth,
buttonDeviceId: widget.deviceAddress,
),
);
_firmwareProgressSubscription = service.progressStream.listen((progress) {
if (!mounted) {
return;
}
setState(() {
_dfuProgress = progress;
if (progress.state == DfuUpdateState.failed &&
progress.errorMessage != null) {
_firmwareUserMessage = progress.errorMessage;
}
if (progress.state == DfuUpdateState.completed) {
_firmwareUserMessage =
'Firmware update completed. The button rebooted and reconnected.';
}
if (progress.state == DfuUpdateState.aborted) {
_firmwareUserMessage = 'Firmware update canceled.';
}
});
});
_firmwareUpdateService = service;
return service;
}
Future<void> _selectFirmwareFile() async {
if (_isFirmwareUpdateBusy || _isSelectingFirmware) {
return;
}
setState(() {
_isSelectingFirmware = true;
_firmwareUserMessage = null;
});
final result = await _firmwareFileSelectionService.selectAndPrepareDfuV1();
if (!mounted) {
return;
}
setState(() {
_isSelectingFirmware = false;
if (result.isSuccess) {
_selectedFirmware = result.firmware;
_firmwareUserMessage =
'Selected ${result.firmware!.fileName}. Ready to start update.';
} else if (!result.isCanceled) {
_firmwareUserMessage = result.failure?.message;
}
});
}
Future<void> _startFirmwareUpdate() async {
if (_isFirmwareUpdateBusy || _isSelectingFirmware) {
return;
}
final firmware = _selectedFirmware;
if (firmware == null) {
setState(() {
_firmwareUserMessage =
'Select a firmware .bin file before starting the update.';
});
return;
}
await _startStatusStreamingIfNeeded();
final updater = await _ensureFirmwareUpdateService();
if (!mounted) {
return;
}
if (updater == null) {
setState(() {
_firmwareUserMessage =
'Firmware updater is not ready. Ensure the button is connected.';
});
return;
}
setState(() {
_isStartingFirmwareUpdate = true;
_firmwareUserMessage =
'Starting update. Keep this screen open and stay near the button.';
});
final result = await updater.startUpdate(
imageBytes: firmware.fileBytes,
sessionId: firmware.metadata.sessionId,
flags: DfuUpdateFlags.fromRaw(firmware.metadata.flags),
);
if (!mounted) {
return;
}
setState(() {
_isStartingFirmwareUpdate = false;
if (result.isErr()) {
_firmwareUserMessage = result.unwrapErr().toString();
}
});
}
Future<void> _cancelFirmwareUpdate() async {
final updater = _firmwareUpdateService;
if (updater == null || !_isFirmwareUpdateBusy) {
return;
}
setState(() {
_firmwareUserMessage = 'Canceling firmware update...';
});
await updater.cancelUpdate();
}
String _dfuPhaseText(DfuUpdateState state) {
switch (state) {
case DfuUpdateState.idle:
return 'Idle';
case DfuUpdateState.starting:
return 'Sending START command';
case DfuUpdateState.waitingForAck:
return 'Waiting for ACK from button';
case DfuUpdateState.transferring:
return 'Transferring firmware frames';
case DfuUpdateState.finishing:
return 'Finalizing update and waiting for reboot/reconnect';
case DfuUpdateState.completed:
return 'Update completed';
case DfuUpdateState.aborted:
return 'Update canceled';
case DfuUpdateState.failed:
return 'Update failed';
}
}
String _formatBytes(int bytes) {
if (bytes < 1024) {
return '$bytes B';
}
if (bytes < 1024 * 1024) {
return '${(bytes / 1024).toStringAsFixed(1)} KB';
}
return '${(bytes / (1024 * 1024)).toStringAsFixed(2)} MB';
}
String _hexByte(int value) {
return '0x${(value & 0xFF).toRadixString(16).padLeft(2, '0').toUpperCase()}';
}
Future<void> _terminateConnectionAndGoHome(String toastMessage) async {
await _disconnectOnClose();
@ -465,6 +716,13 @@ class _DeviceDetailsPageState extends ConsumerState<DeviceDetailsPage> {
final isCurrentConnected = connectionData != null &&
connectionData.$1 == ConnectionStatus.connected &&
connectionData.$2 == widget.deviceAddress;
final canSelectFirmware =
isCurrentConnected && !_isSelectingFirmware && !_isFirmwareUpdateBusy;
final canStartFirmware = isCurrentConnected &&
!_isSelectingFirmware &&
!_isFirmwareUpdateBusy &&
_selectedFirmware != null;
final canCancelFirmware = _isFirmwareUpdateBusy;
return PopScope(
canPop: false,
@ -508,27 +766,53 @@ class _DeviceDetailsPageState extends ConsumerState<DeviceDetailsPage> {
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: _connectButtonToBike,
onPressed:
_isFirmwareUpdateBusy ? null : _connectButtonToBike,
icon: const Icon(Icons.link),
label: const Text('Connect Button to Bike'),
),
),
const SizedBox(height: 16),
GearRatioEditorCard(
ratios: _gearRatios,
defaultGearIndex: _defaultGearIndex,
isLoading: _isGearRatiosLoading,
errorText: _gearRatiosError,
onRetry: _loadGearRatios,
onSave: _saveGearRatios,
presets: const [
GearRatioPreset(
name: 'KeAnt Classic',
description:
'17-step baseline from KeAnt cross app gearing.',
ratios: _keAntRatios,
_FirmwareUpdateCard(
selectedFirmware: _selectedFirmware,
progress: _dfuProgress,
isSelecting: _isSelectingFirmware,
isStarting: _isStartingFirmwareUpdate,
canSelect: canSelectFirmware,
canStart: canStartFirmware,
canCancel: canCancelFirmware,
phaseText: _dfuPhaseText(_dfuProgress.state),
statusText: _firmwareUserMessage,
formattedProgressBytes:
'${_formatBytes(_dfuProgress.sentBytes)} / ${_formatBytes(_dfuProgress.totalBytes)}',
onSelectFirmware: _selectFirmwareFile,
onStartUpdate: _startFirmwareUpdate,
onCancelUpdate: _cancelFirmwareUpdate,
ackSequenceHex: _hexByte(_dfuProgress.lastAckedSequence),
),
const SizedBox(height: 16),
Opacity(
opacity: _isFirmwareUpdateBusy ? 0.6 : 1,
child: AbsorbPointer(
absorbing: _isFirmwareUpdateBusy,
child: GearRatioEditorCard(
ratios: _gearRatios,
defaultGearIndex: _defaultGearIndex,
isLoading: _isGearRatiosLoading,
errorText: _gearRatiosError,
onRetry:
_isFirmwareUpdateBusy ? null : _loadGearRatios,
onSave: _saveGearRatios,
presets: const [
GearRatioPreset(
name: 'KeAnt Classic',
description:
'17-step baseline from KeAnt cross app gearing.',
ratios: _keAntRatios,
),
],
),
],
),
),
],
],
@ -583,6 +867,159 @@ class _StatusHistoryEntry {
final CentralStatus status;
}
class _FirmwareUpdateCard extends StatelessWidget {
const _FirmwareUpdateCard({
required this.selectedFirmware,
required this.progress,
required this.isSelecting,
required this.isStarting,
required this.canSelect,
required this.canStart,
required this.canCancel,
required this.phaseText,
required this.statusText,
required this.formattedProgressBytes,
required this.ackSequenceHex,
required this.onSelectFirmware,
required this.onStartUpdate,
required this.onCancelUpdate,
});
final DfuV1PreparedFirmware? selectedFirmware;
final DfuUpdateProgress progress;
final bool isSelecting;
final bool isStarting;
final bool canSelect;
final bool canStart;
final bool canCancel;
final String phaseText;
final String? statusText;
final String formattedProgressBytes;
final String ackSequenceHex;
final Future<void> Function() onSelectFirmware;
final Future<void> Function() onStartUpdate;
final Future<void> Function() onCancelUpdate;
bool get _showProgress {
return progress.totalBytes > 0 ||
progress.sentBytes > 0 ||
progress.state != DfuUpdateState.idle;
}
bool get _showRebootExpectation {
return progress.state == DfuUpdateState.finishing ||
progress.state == DfuUpdateState.completed;
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.55),
border: Border.all(
color: colorScheme.outlineVariant.withValues(alpha: 0.6),
),
),
padding: const EdgeInsets.fromLTRB(14, 12, 14, 14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Firmware Update',
style: TextStyle(fontSize: 17, fontWeight: FontWeight.w700),
),
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
OutlinedButton.icon(
onPressed: canSelect ? onSelectFirmware : null,
icon: isSelecting
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.upload_file),
label: const Text('Select Firmware'),
),
FilledButton.icon(
onPressed: canStart ? onStartUpdate : null,
icon: isStarting
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.system_update_alt),
label: const Text('Start Update'),
),
TextButton.icon(
onPressed: canCancel ? onCancelUpdate : null,
icon: const Icon(Icons.stop_circle_outlined),
label: const Text('Cancel Update'),
),
],
),
const SizedBox(height: 10),
Text(
selectedFirmware == null
? 'Selected file: none'
: 'Selected file: ${selectedFirmware!.fileName}',
style: theme.textTheme.bodyMedium,
),
if (selectedFirmware != null) ...[
const SizedBox(height: 4),
Text(
'Size: ${selectedFirmware!.fileBytes.length} bytes | Session: ${selectedFirmware!.metadata.sessionId} | CRC32: 0x${selectedFirmware!.metadata.crc32.toRadixString(16).padLeft(8, '0').toUpperCase()}',
style: theme.textTheme.bodySmall,
),
],
const SizedBox(height: 10),
Text('Phase: $phaseText', style: theme.textTheme.bodyMedium),
if (_showProgress) ...[
const SizedBox(height: 8),
LinearProgressIndicator(
value: progress.totalBytes > 0 ? progress.fractionComplete : 0,
),
const SizedBox(height: 6),
Text(
'${progress.percentComplete}% • $formattedProgressBytes • Last ACK $ackSequenceHex',
style: theme.textTheme.bodySmall,
),
],
if (_showRebootExpectation) ...[
const SizedBox(height: 8),
Text(
'Expected behavior: the button reboots after FINISH, disconnects briefly, then reconnects.',
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
],
if (statusText != null && statusText!.trim().isNotEmpty) ...[
const SizedBox(height: 8),
Text(
statusText!,
style: theme.textTheme.bodySmall?.copyWith(
color: progress.state == DfuUpdateState.failed
? colorScheme.error
: theme.textTheme.bodySmall?.color,
),
),
],
],
),
);
}
}
class _StatusBanner extends StatelessWidget {
const _StatusBanner({
required this.status,