docs: document bootloader OTA flow
This commit is contained in:
@ -531,7 +531,7 @@ class _DeviceDetailsPageState extends ConsumerState<DeviceDetailsPage> {
|
||||
}
|
||||
if (progress.state == DfuUpdateState.completed) {
|
||||
_firmwareUserMessage =
|
||||
'Firmware update completed. The button rebooted and reconnected.';
|
||||
'Firmware update completed. The bootloader rebooted into the updated app and verification passed.';
|
||||
}
|
||||
if (progress.state == DfuUpdateState.aborted) {
|
||||
_firmwareUserMessage = 'Firmware update canceled.';
|
||||
@ -564,7 +564,7 @@ class _DeviceDetailsPageState extends ConsumerState<DeviceDetailsPage> {
|
||||
if (result.isSuccess) {
|
||||
_selectedFirmware = result.firmware;
|
||||
_firmwareUserMessage =
|
||||
'Selected ${result.firmware!.fileName}. Ready to start update.';
|
||||
'Validated ${result.firmware!.fileName}. Ready for bootloader update.';
|
||||
} else if (!result.isCanceled) {
|
||||
_firmwareUserMessage = result.failure?.message;
|
||||
}
|
||||
@ -601,12 +601,14 @@ class _DeviceDetailsPageState extends ConsumerState<DeviceDetailsPage> {
|
||||
setState(() {
|
||||
_isStartingFirmwareUpdate = true;
|
||||
_firmwareUserMessage =
|
||||
'Starting update. Keep this screen open and stay near the button.';
|
||||
'Requesting bootloader mode. Keep this screen open and stay near the button.';
|
||||
});
|
||||
|
||||
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),
|
||||
);
|
||||
|
||||
@ -1021,7 +1023,7 @@ class _DeviceDetailsPageState extends ConsumerState<DeviceDetailsPage> {
|
||||
'${_formatBytes(_dfuProgress.sentBytes)} / ${_formatBytes(_dfuProgress.totalBytes)}',
|
||||
onSelectFirmware: _selectFirmwareFile,
|
||||
onStartUpdate: _startFirmwareUpdate,
|
||||
ackSequenceHex:
|
||||
expectedOffsetHex:
|
||||
'0x${_dfuProgress.expectedOffset.toRadixString(16).padLeft(8, '0').toUpperCase()}',
|
||||
),
|
||||
] else if (isCurrentConnected) ...[
|
||||
@ -1069,7 +1071,7 @@ class _FirmwareUpdateCard extends StatelessWidget {
|
||||
required this.phaseText,
|
||||
required this.statusText,
|
||||
required this.formattedProgressBytes,
|
||||
required this.ackSequenceHex,
|
||||
required this.expectedOffsetHex,
|
||||
required this.onSelectFirmware,
|
||||
required this.onStartUpdate,
|
||||
});
|
||||
@ -1083,7 +1085,7 @@ class _FirmwareUpdateCard extends StatelessWidget {
|
||||
final String phaseText;
|
||||
final String? statusText;
|
||||
final String formattedProgressBytes;
|
||||
final String ackSequenceHex;
|
||||
final String expectedOffsetHex;
|
||||
final Future<void> Function() onSelectFirmware;
|
||||
final Future<void> Function() onStartUpdate;
|
||||
|
||||
@ -1095,9 +1097,33 @@ class _FirmwareUpdateCard extends StatelessWidget {
|
||||
|
||||
bool get _showRebootExpectation {
|
||||
return progress.state == DfuUpdateState.finishing ||
|
||||
progress.state == DfuUpdateState.rebooting ||
|
||||
progress.state == DfuUpdateState.verifying ||
|
||||
progress.state == DfuUpdateState.completed;
|
||||
}
|
||||
|
||||
String? get _bootloaderStatusText {
|
||||
final status = progress.bootloaderStatus;
|
||||
if (status == null) {
|
||||
return null;
|
||||
}
|
||||
final codeLabel = switch (status.code) {
|
||||
DfuBootloaderStatusCode.ok => 'OK',
|
||||
DfuBootloaderStatusCode.parseError => 'parse error',
|
||||
DfuBootloaderStatusCode.stateError => 'state error',
|
||||
DfuBootloaderStatusCode.boundsError => 'bounds error',
|
||||
DfuBootloaderStatusCode.crcError => 'CRC error',
|
||||
DfuBootloaderStatusCode.flashError => 'flash error',
|
||||
DfuBootloaderStatusCode.unsupportedError => 'unsupported flags',
|
||||
DfuBootloaderStatusCode.vectorError => 'vector table error',
|
||||
DfuBootloaderStatusCode.queueFull => 'queue full',
|
||||
DfuBootloaderStatusCode.bootMetadataError => 'boot metadata error',
|
||||
DfuBootloaderStatusCode.unknown =>
|
||||
'unknown 0x${status.rawCode.toRadixString(16).padLeft(2, '0').toUpperCase()}',
|
||||
};
|
||||
return 'Bootloader status: $codeLabel, session ${status.sessionId}, expected offset 0x${status.expectedOffset.toRadixString(16).padLeft(8, '0').toUpperCase()}';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
@ -1122,7 +1148,7 @@ class _FirmwareUpdateCard extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Select a firmware image, review the transfer state, and start the update when ready.',
|
||||
'Select a raw app image for the single-slot bootloader. Once START is accepted, the active app slot is erased.',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.68),
|
||||
),
|
||||
@ -1181,6 +1207,11 @@ class _FirmwareUpdateCard extends StatelessWidget {
|
||||
'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: 2),
|
||||
Text(
|
||||
'App start: 0x${selectedFirmware!.metadata.appStart.toRadixString(16).padLeft(8, '0').toUpperCase()} | Image version: ${selectedFirmware!.metadata.imageVersion} | Reset: 0x${selectedFirmware!.metadata.vectorReset.toRadixString(16).padLeft(8, '0').toUpperCase()}',
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
@ -1196,14 +1227,21 @@ class _FirmwareUpdateCard extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'${progress.percentComplete}% • $formattedProgressBytes • Last ACK $ackSequenceHex',
|
||||
'${progress.percentComplete}% • $formattedProgressBytes • Expected offset $expectedOffsetHex',
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
if (_bootloaderStatusText != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_bootloaderStatusText!,
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
],
|
||||
if (_showRebootExpectation) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Expected behavior: the button reboots after FINISH, disconnects briefly, then reconnects.',
|
||||
'Expected behavior: after FINISH, the bootloader verifies the image, resets, and the updated app confirms itself before reconnecting.',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
|
||||
Reference in New Issue
Block a user