From 0e989629c866f987916307d88b5a96edf385a6d5 Mon Sep 17 00:00:00 2001 From: Codel1417 Date: Mon, 2 Dec 2024 18:31:38 -0500 Subject: [PATCH] Add error messages for OTA failure --- VERSION | 2 +- lib/Backend/firmware_update.dart | 115 ++++++++++++++---- lib/Frontend/pages/ota_update.dart | 29 ++++- .../translation_string_definitions.dart | 16 +++ 4 files changed, 134 insertions(+), 28 deletions(-) diff --git a/VERSION b/VERSION index 59e9e604..bb83058e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.11 +1.0.12 diff --git a/lib/Backend/firmware_update.dart b/lib/Backend/firmware_update.dart index febbc094..cd975bf4 100644 --- a/lib/Backend/firmware_update.dart +++ b/lib/Backend/firmware_update.dart @@ -139,9 +139,22 @@ enum OtaState { rebooting, } +enum OtaError { + md5Mismatch, + downloadFailed, + gearVersionMismatch, + gearReturnedError, + uploadFailed, + gearReconnectTimeout, + gearDisconnectTimeout, + gearOtaFinalTimeout, +} + class OtaUpdater { Function(double)? onProgress; Function(OtaState)? onStateChanged; + Function(OtaError)? onError; + BaseStatefulDevice baseStatefulDevice; OtaState _otaState = OtaState.standby; @@ -172,8 +185,10 @@ class OtaUpdater { List? firmwareFile; String? downloadedMD5; bool _wakelockEnabledBeforehand = false; - int current = 0; - Timer? _timer; + int currentFirmwareUploadPosition = 0; + Timer? _disconnectTimer; + Timer? _reconnectTimer; + Timer? _finalTimer; final Logger _otaLogger = Logger('otaLogger'); void setManualOtaFile(List? bytes) { @@ -187,6 +202,14 @@ class OtaUpdater { downloadProgress = 1; } + void _onError(OtaError error) { + otaState = OtaState.error; + if (onError != null) { + onError!(error); + } + _cancelTimers(); + } + double _previousProgress = 0; void _updateProgress() { @@ -214,14 +237,14 @@ class OtaUpdater { } WakelockPlus.enable(); if (firmwareFile == null) { - await downloadFirmware(); + await _downloadFirmware(); } if (otaState != OtaState.error) { - await uploadFirmware(); + await _uploadFirmware(); } } - Future downloadFirmware() async { + Future _downloadFirmware() async { if (firmwareInfo == null) { return; } @@ -244,33 +267,55 @@ class OtaUpdater { if (digest.toString() == firmwareInfo!.md5sum) { firmwareFile = rs.data; } else { - otaState = OtaState.error; + _onError(OtaError.md5Mismatch); } } } catch (e) { - otaState = OtaState.error; + _onError(OtaError.downloadFailed); } } - Future verListener() async { + Future _verListener() async { Version version = baseStatefulDevice.fwVersion.value; FWInfo? fwInfo = firmwareInfo; if (fwInfo != null && version.compareTo(const Version()) > 0 && otaState == OtaState.rebooting) { bool updated = version.compareTo(getVersionSemVer(fwInfo.version)) >= 0; - otaState = updated ? OtaState.completed : OtaState.error; + if (!updated) { + _onError(OtaError.gearVersionMismatch); + } else { + otaState = OtaState.completed; + } } } - void fwInfoListener() { + void _fwInfoListener() { firmwareInfo = baseStatefulDevice.fwInfo.value; } - Future uploadFirmware() async { + void _connectivityStateListener() { + ConnectivityState connectivityState = baseStatefulDevice.deviceConnectionState.value; + if (OtaState.rebooting == otaState) { + if (connectivityState == ConnectivityState.disconnected) { + _disconnectTimer?.cancel(); + _reconnectTimer = Timer( + const Duration(seconds: 30), + () { + _otaLogger.warning("Gear did not reconnect"); + _onError(OtaError.gearReconnectTimeout); + }, + ); + } else if (connectivityState == ConnectivityState.connected) { + _reconnectTimer?.cancel(); + } + } + } + + Future _uploadFirmware() async { otaState = OtaState.upload; uploadProgress = 0; if (firmwareFile != null) { int mtu = baseStatefulDevice.mtu.value - 10; - current = 0; + currentFirmwareUploadPosition = 0; baseStatefulDevice.gearReturnedError.value = false; _otaLogger.info("Holding the command queue"); @@ -281,42 +326,53 @@ class OtaUpdater { while (uploadProgress < 1 && otaState != OtaState.error) { baseStatefulDevice.deviceState.value = DeviceState.busy; // hold the command queue if (baseStatefulDevice.gearReturnedError.value) { - otaState = OtaState.error; + _onError(OtaError.gearReturnedError); break; } - List chunk = firmwareFile!.skip(current).take(mtu).toList(); + List chunk = firmwareFile!.skip(currentFirmwareUploadPosition).take(mtu).toList(); if (chunk.isNotEmpty) { try { await sendMessage(baseStatefulDevice, chunk, withoutResponse: true); } catch (e, s) { _otaLogger.severe("Exception during ota upload:$e", e, s); - if ((current + chunk.length) / firmwareFile!.length < 0.99) { - otaState = OtaState.error; + if ((currentFirmwareUploadPosition + chunk.length) / firmwareFile!.length < 0.99) { + _onError(OtaError.uploadFailed); + return; } } - current = current + chunk.length; + currentFirmwareUploadPosition = currentFirmwareUploadPosition + chunk.length; } else { - current = firmwareFile!.length; + currentFirmwareUploadPosition = firmwareFile!.length; } - uploadProgress = current / firmwareFile!.length; + uploadProgress = currentFirmwareUploadPosition / firmwareFile!.length; _updateProgress(); } if (uploadProgress == 1) { _otaLogger.info("File Uploaded"); otaState = OtaState.rebooting; + //TODO: check if gear disconnects beginScan( scanReason: ScanReason.manual, timeout: const Duration(seconds: 60), - ); // start scanning for the gear to reconnect - _timer = Timer( + ); + + _disconnectTimer = Timer( + const Duration(seconds: 30), + () { + _otaLogger.warning("Gear did not disconnect"); + _onError(OtaError.gearDisconnectTimeout); + }, + ); + // start scanning for the gear to reconnect + _finalTimer = Timer( const Duration(seconds: 60), () { if (otaState != OtaState.completed) { _otaLogger.warning("Gear did not return correct version after reboot"); - otaState = OtaState.error; + _onError(OtaError.gearOtaFinalTimeout); } }, ); @@ -326,15 +382,16 @@ class OtaUpdater { } } - OtaUpdater({this.onProgress, this.onStateChanged, required this.baseStatefulDevice}) { + OtaUpdater({this.onProgress, this.onStateChanged, required this.baseStatefulDevice, this.onError}) { firmwareInfo ??= baseStatefulDevice.fwInfo.value; WakelockPlus.enabled.then((value) => _wakelockEnabledBeforehand = value); - baseStatefulDevice.fwVersion.addListener(verListener); - baseStatefulDevice.fwInfo.addListener(fwInfoListener); + baseStatefulDevice.fwVersion.addListener(_verListener); + baseStatefulDevice.fwInfo.addListener(_fwInfoListener); + baseStatefulDevice.deviceConnectionState.addListener(_connectivityStateListener); } void dispose() { - _timer?.cancel(); + _cancelTimers(); if (!_wakelockEnabledBeforehand) { unawaited(WakelockPlus.disable()); } @@ -342,6 +399,12 @@ class OtaUpdater { unawaited(stopScan()); } } + + void _cancelTimers() { + _disconnectTimer?.cancel(); + _reconnectTimer?.cancel(); + _finalTimer?.cancel(); + } } @pragma('vm:entry-point') diff --git a/lib/Frontend/pages/ota_update.dart b/lib/Frontend/pages/ota_update.dart index 080dba4d..e3508beb 100644 --- a/lib/Frontend/pages/ota_update.dart +++ b/lib/Frontend/pages/ota_update.dart @@ -27,6 +27,7 @@ class OtaUpdate extends ConsumerStatefulWidget { class _OtaUpdateState extends ConsumerState { late OtaUpdater otaUpdater; BaseStatefulDevice? baseStatefulDevice; + OtaError? otaError; @override void initState() { @@ -36,6 +37,7 @@ class _OtaUpdateState extends ConsumerState { baseStatefulDevice: baseStatefulDevice!, onProgress: (p0) => setState(() {}), onStateChanged: (p0) => setState(() {}), + onError: (p0) => setState(() => otaError = p0), ); unawaited(ref.read(hasOtaUpdateProvider(baseStatefulDevice!).future)); } @@ -46,6 +48,27 @@ class _OtaUpdateState extends ConsumerState { otaUpdater.dispose(); } + String getErrorMessage(OtaError otaError) { + switch (otaError) { + case OtaError.md5Mismatch: + return otaFailedReasonMD5Mismatch(); + case OtaError.downloadFailed: + return otaFailedReasonDownloadFailed(); + case OtaError.gearVersionMismatch: + return otaFailedReasonGearVersionMismatch(); + case OtaError.gearReturnedError: + return otaFailedReasonGearReturnedError(); + case OtaError.uploadFailed: + return otaFailedReasonUploadFailed(); + case OtaError.gearReconnectTimeout: + return otaFailedReasonGearReconnectTimeout(); + case OtaError.gearDisconnectTimeout: + return otaFailedReasonGearDisconnectTimeout(); + case OtaError.gearOtaFinalTimeout: + return otaFailedReasonGearOtaFinalTimeout(); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -179,6 +202,10 @@ class _OtaUpdateState extends ConsumerState { style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center, ), + subtitle: Text( + otaError != null ? getErrorMessage(otaError!) : "", + textAlign: TextAlign.center, + ), ), ), ), @@ -258,7 +285,7 @@ class _OtaUpdateState extends ConsumerState { subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Upload Progress: ${otaUpdater.current} / ${otaUpdater.firmwareFile?.length} = ${otaUpdater.uploadProgress.toStringAsPrecision(3)}'), + Text('Upload Progress: ${otaUpdater.currentFirmwareUploadPosition} / ${otaUpdater.firmwareFile?.length} = ${otaUpdater.uploadProgress.toStringAsPrecision(3)}'), Text('MTU: ${baseStatefulDevice!.mtu.value}'), Text('OtaState: ${otaUpdater.otaState.name}'), Text('DeviceState: ${baseStatefulDevice!.deviceState.value}'), diff --git a/lib/Frontend/translation_string_definitions.dart b/lib/Frontend/translation_string_definitions.dart index 5c578866..8583c2a5 100644 --- a/lib/Frontend/translation_string_definitions.dart +++ b/lib/Frontend/translation_string_definitions.dart @@ -276,6 +276,22 @@ String otaCompletedTitle() => Intl.message("Update Completed", name: 'otaComplet String otaFailedTitle() => Intl.message("Update Failed. Please restart your gear and try again", name: 'otaFailedTitle', desc: 'Title for the text that appears when an OTA update has failed'); +String otaFailedReasonMD5Mismatch() => Intl.message("The downloaded file failed verification", name: 'otaFailedReasonMD5Mismatch', desc: 'md5Mismatch error message for uploading firmware'); + +String otaFailedReasonDownloadFailed() => Intl.message("Failed to download the firmware file", name: 'otaFailedReasonDownloadFailed', desc: 'downloadFailed error message for uploading firmware'); + +String otaFailedReasonGearVersionMismatch() => Intl.message("The firmware version on your gear does not match the uploaded firmware version", name: 'otaFailedReasonGearVersionMismatch', desc: 'gearVersionMismatch error message for uploading firmware'); + +String otaFailedReasonGearReturnedError() => Intl.message("The gear returned an unknown error", name: 'otaFailedReasonGearReturnedError', desc: 'gearReturnedError error message for uploading firmware'); + +String otaFailedReasonUploadFailed() => Intl.message("Failed to upload firmware to gear", name: 'otaFailedReasonUploadFailed', desc: 'uploadFailed error message for uploading firmware'); + +String otaFailedReasonGearReconnectTimeout() => Intl.message("The gear did not reconnect in time", name: 'otaFailedReasonGearReconnectTimeout', desc: 'gearReconnectTimeout error message for uploading firmware'); + +String otaFailedReasonGearDisconnectTimeout() => Intl.message("The gear did not disconnect in time and likely didn't reboot", name: 'otaFailedReasonGearDisconnectTimeout', desc: 'gearDisconnectTimeout error message for uploading firmware'); + +String otaFailedReasonGearOtaFinalTimeout() => Intl.message("Timed out waiting for gear to return its new firmware version", name: 'otaFailedReasonGearOtaFinalTimeout', desc: 'gearOtaFinalTimeout error message for uploading firmware'); + String otaLowBattery() => Intl.message("Low Battery. Please charge your gear to at least 50%", name: 'otaLowBattery', desc: 'Title for the text that appears when an OTA update was blocked due to low battery'); String triggerInfoDescription() => Intl.message(