From 512c31d35618782f9e811b9cd01244ca0362f8f3 Mon Sep 17 00:00:00 2001 From: Yandrik Date: Tue, 5 May 2026 20:19:43 +0200 Subject: [PATCH] feat: snackbar for flash err + disconnect on dfurecovery end --- lib/model/shifter_types.dart | 3 ++ .../bootloader_recovery_update_page.dart | 53 +++++++++++++++++-- lib/pages/device_details_page.dart | 13 +++++ lib/service/firmware_update_service.dart | 3 ++ .../service/firmware_update_service_test.dart | 33 +++++++++++- 5 files changed, 101 insertions(+), 4 deletions(-) diff --git a/lib/model/shifter_types.dart b/lib/model/shifter_types.dart index e15576e..fc3f25a 100644 --- a/lib/model/shifter_types.dart +++ b/lib/model/shifter_types.dart @@ -57,6 +57,9 @@ const int universalShifterDfuFlagEncrypted = 0x01; const int universalShifterDfuFlagSigned = 0x02; const int universalShifterDfuFlagNone = 0x00; +const String universalShifterBootMetadataWarningMessage = + 'WARNING: The device failed writing to its internal storage. This might be a temporary problem. If it continues happening, the internal storage may be broken. If so, please contact abawo support'; + const int errorSequence = 1; const int errorFtmsMissing = 2; const int errorPairingAuth = 3; diff --git a/lib/pages/bootloader_recovery_update_page.dart b/lib/pages/bootloader_recovery_update_page.dart index 566df8c..f537ad1 100644 --- a/lib/pages/bootloader_recovery_update_page.dart +++ b/lib/pages/bootloader_recovery_update_page.dart @@ -59,11 +59,43 @@ class _BootloaderRecoveryUpdatePageState @override void dispose() { + final service = _firmwareUpdateService; + final bluetooth = ref.read(bluetoothProvider).valueOrNull; + _firmwareUpdateService = null; unawaited(_firmwareProgressSubscription?.cancel()); - unawaited(_firmwareUpdateService?.dispose() ?? Future.value()); + unawaited(() async { + await service?.dispose(); + await _disconnectBootloaderIfStillConnected(bluetooth: bluetooth); + }()); super.dispose(); } + Future _disconnectBootloaderIfStillConnected({ + BluetoothController? bluetooth, + }) async { + bluetooth ??= ref.read(bluetoothProvider).valueOrNull; + if (bluetooth == null) { + return; + } + + final currentState = bluetooth.currentConnectionState; + if (currentState.$2 != widget.args.bootloaderDeviceId || + (currentState.$1 != ConnectionStatus.connected && + currentState.$1 != ConnectionStatus.connecting)) { + return; + } + + await bluetooth.disconnect(); + } + + Future _dismissToDevices() async { + await _disconnectBootloaderIfStillConnected(); + if (!mounted) { + return; + } + context.go('/devices'); + } + Future _ensureFirmwareUpdateService() async { if (_firmwareUpdateService != null) { return _firmwareUpdateService; @@ -144,9 +176,24 @@ class _BootloaderRecoveryUpdatePageState if (!mounted || result.isOk()) { return; } + await _disconnectBootloaderIfStillConnected(); + if (!mounted) { + return; + } + final errorMessage = result.unwrapErr().toString(); setState(() { - _firmwareUserMessage = result.unwrapErr().toString(); + _firmwareUserMessage = errorMessage; }); + + if (errorMessage.startsWith(universalShifterBootMetadataWarningMessage)) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'WARNING: The device failed writing to its internal storage. This might be a temporary problem. If it continues happening, the internal storage may be broken. If so, please contact abawo support', + ), + ), + ); + } } String _dfuPhaseText(DfuUpdateState state) { @@ -190,7 +237,7 @@ class _BootloaderRecoveryUpdatePageState '0x${_dfuProgress.expectedOffset.toRadixString(16).padLeft(8, '0').toUpperCase()}', doneLabel: 'Back to devices', failedLabel: 'Back to devices', - onDismiss: () => context.go('/devices'), + onDismiss: () => unawaited(_dismissToDevices()), ); } } diff --git a/lib/pages/device_details_page.dart b/lib/pages/device_details_page.dart index 55d2efa..abfde59 100644 --- a/lib/pages/device_details_page.dart +++ b/lib/pages/device_details_page.dart @@ -651,6 +651,19 @@ class _DeviceDetailsPageState extends ConsumerState { } }); + if (result.isErr() && + result.unwrapErr().toString().startsWith( + universalShifterBootMetadataWarningMessage, + )) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'WARNING: The device failed writing to its internal storage. This might be a temporary problem. If it continues happening, the internal storage may be broken. If so, please contact abawo support', + ), + ), + ); + } + if (result.isOk()) { _hasLoadedDeviceTelemetry = false; unawaited(_loadDeviceTelemetry(force: true)); diff --git a/lib/service/firmware_update_service.dart b/lib/service/firmware_update_service.dart index 26d6398..2e96597 100644 --- a/lib/service/firmware_update_service.dart +++ b/lib/service/firmware_update_service.dart @@ -353,6 +353,9 @@ class FirmwareUpdateService { ); } _handleStatusPayload(statusResult.unwrap()); + if (_latestStatus?.code == DfuBootloaderStatusCode.bootMetadataError) { + throw _DfuFailure(universalShifterBootMetadataWarningMessage); + } _log.info('Initial bootloader DFU status read succeeded'); } diff --git a/test/service/firmware_update_service_test.dart b/test/service/firmware_update_service_test.dart index 9a14cc9..4baebdc 100644 --- a/test/service/firmware_update_service_test.dart +++ b/test/service/firmware_update_service_test.dart @@ -333,6 +333,35 @@ void main() { await transport.dispose(); }); + test('fails early on boot metadata error before START', () async { + final image = _validImage(40); + final transport = _FakeFirmwareUpdateTransport( + totalBytes: image.length, + initialStatusCode: DfuBootloaderStatusCode.bootMetadataError, + ); + final service = FirmwareUpdateService( + transport: transport, + defaultStatusTimeout: const Duration(milliseconds: 100), + ); + + final result = await service.startUpdate( + imageBytes: image, + sessionId: 18, + ); + + expect(result.isErr(), isTrue); + expect(result.unwrapErr().toString(), + startsWith(universalShifterBootMetadataWarningMessage)); + expect( + transport.controlWrites + .where((write) => write.first == universalShifterDfuOpcodeStart), + isEmpty); + expect(service.currentProgress.state, DfuUpdateState.failed); + + await service.dispose(); + await transport.dispose(); + }); + test('cancel after START sends session-scoped ABORT', () async { final image = _validImage(80); final firstFrameSent = Completer(); @@ -373,6 +402,7 @@ void main() { class _FakeFirmwareUpdateTransport implements FirmwareUpdateTransport { _FakeFirmwareUpdateTransport({ required this.totalBytes, + this.initialStatusCode = DfuBootloaderStatusCode.ok, this.startStatusCode = DfuBootloaderStatusCode.ok, this.alreadyInBootloader = false, this.failEnterBootloader = false, @@ -387,6 +417,7 @@ class _FakeFirmwareUpdateTransport implements FirmwareUpdateTransport { }); final int totalBytes; + final DfuBootloaderStatusCode initialStatusCode; final DfuBootloaderStatusCode startStatusCode; final bool alreadyInBootloader; final bool failEnterBootloader; @@ -461,7 +492,7 @@ class _FakeFirmwareUpdateTransport implements FirmwareUpdateTransport { @override Future>> readStatus() async { steps.add('readStatus'); - return Ok(_status(DfuBootloaderStatusCode.ok, 0, 0)); + return Ok(_status(initialStatusCode, 0, 0)); } @override