feat: snackbar for flash err + disconnect on dfurecovery end

This commit is contained in:
2026-05-05 20:19:43 +02:00
parent f1491749d5
commit 512c31d356
5 changed files with 101 additions and 4 deletions

View File

@ -57,6 +57,9 @@ const int universalShifterDfuFlagEncrypted = 0x01;
const int universalShifterDfuFlagSigned = 0x02; const int universalShifterDfuFlagSigned = 0x02;
const int universalShifterDfuFlagNone = 0x00; 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 errorSequence = 1;
const int errorFtmsMissing = 2; const int errorFtmsMissing = 2;
const int errorPairingAuth = 3; const int errorPairingAuth = 3;

View File

@ -59,11 +59,43 @@ class _BootloaderRecoveryUpdatePageState
@override @override
void dispose() { void dispose() {
final service = _firmwareUpdateService;
final bluetooth = ref.read(bluetoothProvider).valueOrNull;
_firmwareUpdateService = null;
unawaited(_firmwareProgressSubscription?.cancel()); unawaited(_firmwareProgressSubscription?.cancel());
unawaited(_firmwareUpdateService?.dispose() ?? Future<void>.value()); unawaited(() async {
await service?.dispose();
await _disconnectBootloaderIfStillConnected(bluetooth: bluetooth);
}());
super.dispose(); super.dispose();
} }
Future<void> _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<void> _dismissToDevices() async {
await _disconnectBootloaderIfStillConnected();
if (!mounted) {
return;
}
context.go('/devices');
}
Future<FirmwareUpdateService?> _ensureFirmwareUpdateService() async { Future<FirmwareUpdateService?> _ensureFirmwareUpdateService() async {
if (_firmwareUpdateService != null) { if (_firmwareUpdateService != null) {
return _firmwareUpdateService; return _firmwareUpdateService;
@ -144,9 +176,24 @@ class _BootloaderRecoveryUpdatePageState
if (!mounted || result.isOk()) { if (!mounted || result.isOk()) {
return; return;
} }
await _disconnectBootloaderIfStillConnected();
if (!mounted) {
return;
}
final errorMessage = result.unwrapErr().toString();
setState(() { 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) { String _dfuPhaseText(DfuUpdateState state) {
@ -190,7 +237,7 @@ class _BootloaderRecoveryUpdatePageState
'0x${_dfuProgress.expectedOffset.toRadixString(16).padLeft(8, '0').toUpperCase()}', '0x${_dfuProgress.expectedOffset.toRadixString(16).padLeft(8, '0').toUpperCase()}',
doneLabel: 'Back to devices', doneLabel: 'Back to devices',
failedLabel: 'Back to devices', failedLabel: 'Back to devices',
onDismiss: () => context.go('/devices'), onDismiss: () => unawaited(_dismissToDevices()),
); );
} }
} }

View File

@ -651,6 +651,19 @@ class _DeviceDetailsPageState extends ConsumerState<DeviceDetailsPage> {
} }
}); });
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()) { if (result.isOk()) {
_hasLoadedDeviceTelemetry = false; _hasLoadedDeviceTelemetry = false;
unawaited(_loadDeviceTelemetry(force: true)); unawaited(_loadDeviceTelemetry(force: true));

View File

@ -353,6 +353,9 @@ class FirmwareUpdateService {
); );
} }
_handleStatusPayload(statusResult.unwrap()); _handleStatusPayload(statusResult.unwrap());
if (_latestStatus?.code == DfuBootloaderStatusCode.bootMetadataError) {
throw _DfuFailure(universalShifterBootMetadataWarningMessage);
}
_log.info('Initial bootloader DFU status read succeeded'); _log.info('Initial bootloader DFU status read succeeded');
} }

View File

@ -333,6 +333,35 @@ void main() {
await transport.dispose(); 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 { test('cancel after START sends session-scoped ABORT', () async {
final image = _validImage(80); final image = _validImage(80);
final firstFrameSent = Completer<void>(); final firstFrameSent = Completer<void>();
@ -373,6 +402,7 @@ void main() {
class _FakeFirmwareUpdateTransport implements FirmwareUpdateTransport { class _FakeFirmwareUpdateTransport implements FirmwareUpdateTransport {
_FakeFirmwareUpdateTransport({ _FakeFirmwareUpdateTransport({
required this.totalBytes, required this.totalBytes,
this.initialStatusCode = DfuBootloaderStatusCode.ok,
this.startStatusCode = DfuBootloaderStatusCode.ok, this.startStatusCode = DfuBootloaderStatusCode.ok,
this.alreadyInBootloader = false, this.alreadyInBootloader = false,
this.failEnterBootloader = false, this.failEnterBootloader = false,
@ -387,6 +417,7 @@ class _FakeFirmwareUpdateTransport implements FirmwareUpdateTransport {
}); });
final int totalBytes; final int totalBytes;
final DfuBootloaderStatusCode initialStatusCode;
final DfuBootloaderStatusCode startStatusCode; final DfuBootloaderStatusCode startStatusCode;
final bool alreadyInBootloader; final bool alreadyInBootloader;
final bool failEnterBootloader; final bool failEnterBootloader;
@ -461,7 +492,7 @@ class _FakeFirmwareUpdateTransport implements FirmwareUpdateTransport {
@override @override
Future<Result<List<int>>> readStatus() async { Future<Result<List<int>>> readStatus() async {
steps.add('readStatus'); steps.add('readStatus');
return Ok(_status(DfuBootloaderStatusCode.ok, 0, 0)); return Ok(_status(initialStatusCode, 0, 0));
} }
@override @override