Skip to content

Commit

Permalink
Add error messages for OTA failure
Browse files Browse the repository at this point in the history
  • Loading branch information
Codel1417 committed Dec 2, 2024
1 parent ad4bd6c commit 0e98962
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 28 deletions.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.0.11
1.0.12
115 changes: 89 additions & 26 deletions lib/Backend/firmware_update.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -172,8 +185,10 @@ class OtaUpdater {
List<int>? 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<int>? bytes) {
Expand All @@ -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() {
Expand Down Expand Up @@ -214,14 +237,14 @@ class OtaUpdater {
}
WakelockPlus.enable();
if (firmwareFile == null) {
await downloadFirmware();
await _downloadFirmware();
}
if (otaState != OtaState.error) {
await uploadFirmware();
await _uploadFirmware();
}
}

Future<void> downloadFirmware() async {
Future<void> _downloadFirmware() async {
if (firmwareInfo == null) {
return;
}
Expand All @@ -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<void> verListener() async {
Future<void> _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<void> 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<void> _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");
Expand All @@ -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<int> chunk = firmwareFile!.skip(current).take(mtu).toList();
List<int> 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);
}
},
);
Expand All @@ -326,22 +382,29 @@ 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());
}
if (!HiveProxy.getOrDefault(settings, alwaysScanning, defaultValue: alwaysScanningDefault)) {
unawaited(stopScan());
}
}

void _cancelTimers() {
_disconnectTimer?.cancel();
_reconnectTimer?.cancel();
_finalTimer?.cancel();
}
}

@pragma('vm:entry-point')
Expand Down
29 changes: 28 additions & 1 deletion lib/Frontend/pages/ota_update.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class OtaUpdate extends ConsumerStatefulWidget {
class _OtaUpdateState extends ConsumerState<OtaUpdate> {
late OtaUpdater otaUpdater;
BaseStatefulDevice? baseStatefulDevice;
OtaError? otaError;

@override
void initState() {
Expand All @@ -36,6 +37,7 @@ class _OtaUpdateState extends ConsumerState<OtaUpdate> {
baseStatefulDevice: baseStatefulDevice!,
onProgress: (p0) => setState(() {}),
onStateChanged: (p0) => setState(() {}),
onError: (p0) => setState(() => otaError = p0),
);
unawaited(ref.read(hasOtaUpdateProvider(baseStatefulDevice!).future));
}
Expand All @@ -46,6 +48,27 @@ class _OtaUpdateState extends ConsumerState<OtaUpdate> {
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(
Expand Down Expand Up @@ -179,6 +202,10 @@ class _OtaUpdateState extends ConsumerState<OtaUpdate> {
style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center,
),
subtitle: Text(
otaError != null ? getErrorMessage(otaError!) : "",
textAlign: TextAlign.center,
),
),
),
),
Expand Down Expand Up @@ -258,7 +285,7 @@ class _OtaUpdateState extends ConsumerState<OtaUpdate> {
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}'),
Expand Down
16 changes: 16 additions & 0 deletions lib/Frontend/translation_string_definitions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit 0e98962

Please sign in to comment.