197 lines
6.3 KiB
Dart
197 lines
6.3 KiB
Dart
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'),
|
|
);
|
|
}
|
|
}
|