feat: fw-update recovery flow
This commit is contained in:
196
lib/pages/bootloader_recovery_update_page.dart
Normal file
196
lib/pages/bootloader_recovery_update_page.dart
Normal file
@ -0,0 +1,196 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:abawo_bt_app/controller/bluetooth.dart';
|
||||
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_update_service.dart';
|
||||
import 'package:abawo_bt_app/widgets/firmware_update_fullscreen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class BootloaderRecoveryUpdateArgs {
|
||||
const BootloaderRecoveryUpdateArgs({
|
||||
required this.bootloaderDeviceId,
|
||||
required this.firmware,
|
||||
});
|
||||
|
||||
final String bootloaderDeviceId;
|
||||
final BootloaderDfuPreparedFirmware firmware;
|
||||
}
|
||||
|
||||
class BootloaderRecoveryUpdatePage extends ConsumerStatefulWidget {
|
||||
const BootloaderRecoveryUpdatePage({
|
||||
required this.args,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final BootloaderRecoveryUpdateArgs args;
|
||||
|
||||
@override
|
||||
ConsumerState<BootloaderRecoveryUpdatePage> createState() =>
|
||||
_BootloaderRecoveryUpdatePageState();
|
||||
}
|
||||
|
||||
class _BootloaderRecoveryUpdatePageState
|
||||
extends ConsumerState<BootloaderRecoveryUpdatePage> {
|
||||
FirmwareUpdateService? _firmwareUpdateService;
|
||||
StreamSubscription<DfuUpdateProgress>? _firmwareProgressSubscription;
|
||||
DfuUpdateProgress _dfuProgress = const DfuUpdateProgress(
|
||||
state: DfuUpdateState.idle,
|
||||
totalBytes: 0,
|
||||
sentBytes: 0,
|
||||
expectedOffset: 0,
|
||||
sessionId: 0,
|
||||
flags: DfuUpdateFlags(),
|
||||
);
|
||||
String? _firmwareUserMessage = 'Preparing US-DFU recovery update...';
|
||||
bool _hasStarted = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
unawaited(_startUpdate());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
unawaited(_firmwareProgressSubscription?.cancel());
|
||||
unawaited(_firmwareUpdateService?.dispose() ?? Future<void>.value());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<FirmwareUpdateService?> _ensureFirmwareUpdateService() async {
|
||||
if (_firmwareUpdateService != null) {
|
||||
return _firmwareUpdateService;
|
||||
}
|
||||
|
||||
final bluetooth = ref.read(bluetoothProvider).valueOrNull;
|
||||
if (bluetooth == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final service = FirmwareUpdateService(
|
||||
verifyAfterFinish: false,
|
||||
transport: ShifterFirmwareUpdateTransport(
|
||||
shifterService: null,
|
||||
bluetoothController: bluetooth,
|
||||
buttonDeviceId: widget.args.bootloaderDeviceId,
|
||||
),
|
||||
);
|
||||
_firmwareProgressSubscription = service.progressStream.listen((progress) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_dfuProgress = progress;
|
||||
if (progress.state == DfuUpdateState.failed &&
|
||||
progress.errorMessage != null) {
|
||||
_firmwareUserMessage = progress.errorMessage;
|
||||
} else if (progress.state == DfuUpdateState.completed) {
|
||||
_firmwareUserMessage =
|
||||
'Firmware update completed. The bootloader accepted FINISH and reset; reconnect to the device when it starts advertising again.';
|
||||
} else if (progress.state == DfuUpdateState.aborted) {
|
||||
_firmwareUserMessage = 'Firmware update canceled.';
|
||||
} else if (progress.errorMessage != null) {
|
||||
_firmwareUserMessage = progress.errorMessage;
|
||||
}
|
||||
});
|
||||
});
|
||||
_firmwareUpdateService = service;
|
||||
return service;
|
||||
}
|
||||
|
||||
Future<void> _startUpdate() async {
|
||||
if (_hasStarted) {
|
||||
return;
|
||||
}
|
||||
_hasStarted = true;
|
||||
|
||||
final updater = await _ensureFirmwareUpdateService();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
if (updater == null) {
|
||||
setState(() {
|
||||
_dfuProgress = DfuUpdateProgress(
|
||||
state: DfuUpdateState.failed,
|
||||
totalBytes: widget.args.firmware.fileBytes.length,
|
||||
sentBytes: 0,
|
||||
expectedOffset: 0,
|
||||
sessionId: widget.args.firmware.metadata.sessionId,
|
||||
flags: DfuUpdateFlags.fromRaw(widget.args.firmware.metadata.flags),
|
||||
errorMessage:
|
||||
'Firmware updater is not ready. Reconnect to US-DFU and retry.',
|
||||
);
|
||||
_firmwareUserMessage = _dfuProgress.errorMessage;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final firmware = widget.args.firmware;
|
||||
final result = await updater.startUpdate(
|
||||
imageBytes: firmware.fileBytes,
|
||||
sessionId: firmware.metadata.sessionId,
|
||||
appStart: firmware.metadata.appStart,
|
||||
imageVersion: firmware.metadata.imageVersion,
|
||||
flags: DfuUpdateFlags.fromRaw(firmware.metadata.flags),
|
||||
);
|
||||
|
||||
if (!mounted || result.isOk()) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_firmwareUserMessage = result.unwrapErr().toString();
|
||||
});
|
||||
}
|
||||
|
||||
String _dfuPhaseText(DfuUpdateState state) {
|
||||
return switch (state) {
|
||||
DfuUpdateState.idle => 'Preparing recovery update',
|
||||
DfuUpdateState.starting => 'Preparing update',
|
||||
DfuUpdateState.enteringBootloader => 'Checking bootloader mode',
|
||||
DfuUpdateState.connectingBootloader => 'Connecting to bootloader',
|
||||
DfuUpdateState.waitingForStatus => 'Waiting for bootloader status',
|
||||
DfuUpdateState.erasing => 'Starting destructive bootloader update',
|
||||
DfuUpdateState.transferring => 'Transferring firmware image',
|
||||
DfuUpdateState.finishing => 'Finalizing bootloader update',
|
||||
DfuUpdateState.rebooting => 'Waiting for bootloader reset',
|
||||
DfuUpdateState.verifying => 'Verifying updated app',
|
||||
DfuUpdateState.completed => 'Update completed',
|
||||
DfuUpdateState.aborted => 'Update canceled',
|
||||
DfuUpdateState.failed => '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';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FirmwareUpdateFullscreen(
|
||||
progress: _dfuProgress,
|
||||
selectedFirmware: widget.args.firmware,
|
||||
phaseText: _dfuPhaseText(_dfuProgress.state),
|
||||
statusText: _firmwareUserMessage,
|
||||
formattedProgressBytes:
|
||||
'${_formatBytes(_dfuProgress.sentBytes)} / ${_formatBytes(_dfuProgress.totalBytes)}',
|
||||
expectedOffsetHex:
|
||||
'0x${_dfuProgress.expectedOffset.toRadixString(16).padLeft(8, '0').toUpperCase()}',
|
||||
doneLabel: 'Back to devices',
|
||||
failedLabel: 'Back to devices',
|
||||
onDismiss: () => context.go('/devices'),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user