From e316e1d22fc789068617ae40904654f443f1795c Mon Sep 17 00:00:00 2001 From: k3ldar Date: Sat, 18 Jul 2026 16:05:33 +0200 Subject: [PATCH] Add OTA updates via app --- PowerControlHub/PowerControlHubApp.cpp | 1 + PowerControlHub/SystemNetworkHandler.cpp | 126 ++++++++++++++++++ PowerControlHub/SystemNetworkHandler.h | 14 ++ PowerControlHubApp/Internal/Constants.cs | 5 + .../ViewModels/DashboardViewModel.cs | 12 +- .../ViewModels/SettingsViewModel.cs | 114 ++++++++++++++-- PowerControlHubApp/Views/DashboardPage.xaml | 9 +- PowerControlHubApp/Views/SettingsPage.xaml | 65 +++++++++ PowerControlHubApp/Views/SettingsPage.xaml.cs | 6 + 9 files changed, 332 insertions(+), 20 deletions(-) diff --git a/PowerControlHub/PowerControlHubApp.cpp b/PowerControlHub/PowerControlHubApp.cpp index 3ef5f91..a72357a 100644 --- a/PowerControlHub/PowerControlHubApp.cpp +++ b/PowerControlHub/PowerControlHubApp.cpp @@ -337,6 +337,7 @@ void PowerControlHubApp::setup(RemoteSensor** remoteSensors, uint8_t remoteSenso #if defined(OTA_AUTO_UPDATE) && defined(ESP32) && defined(WIFI_SUPPORT) _systemCommandHandler.setOtaManager(&_otaManager); + _systemNetworkHandler.setOtaManager(&_otaManager); _otaManager.begin(); #endif diff --git a/PowerControlHub/SystemNetworkHandler.cpp b/PowerControlHub/SystemNetworkHandler.cpp index 541c378..e5a863d 100644 --- a/PowerControlHub/SystemNetworkHandler.cpp +++ b/PowerControlHub/SystemNetworkHandler.cpp @@ -23,6 +23,10 @@ #include "SystemFunctions.h" #include "FirmwareVersion.h" +#if defined(OTA_AUTO_UPDATE) && defined(ESP32) && defined(WIFI_SUPPORT) +#include "OtaManager.h" +#endif + #if defined(SD_CARD_SUPPORT) #include "MicroSdDriver.h" #endif @@ -84,6 +88,128 @@ CommandResult SystemNetworkHandler::handleRequest(const char* method, return CommandResult::ok(); } + #if defined(OTA_AUTO_UPDATE) && defined(ESP32) && defined(WIFI_SUPPORT) + else if (SystemFunctions::commandMatches(command, SystemCheckForUpdate)) + { + if (!_otaManager) + { + snprintf(responseBuffer, bufferSize, "\"success\":false,\"error\":\"OTA not available\""); + return CommandResult::ok(); + } + + const char* applyStr = nullptr; + + for (uint8_t i = 0; i < paramCount; ++i) + { + if (strcmp(params[i].key, "apply") == 0) + { + applyStr = params[i].value; + break; + } + } + + bool applyNow = applyStr && (applyStr[0] == '1'); + _otaManager->triggerCheck(applyNow); + + const char* stateStr = "idle"; + + switch (_otaManager->getState()) + { + case OtaState::Idle: + stateStr = "triggered"; + break; + + case OtaState::Checking: + stateStr = "checking"; + break; + + case OtaState::UpdateAvailable: + stateStr = "available"; + break; + + case OtaState::Downloading: + stateStr = "downloading"; + break; + + case OtaState::Rebooting: + stateStr = "rebooting"; + break; + + case OtaState::Failed: + stateStr = "failed"; + break; + + case OtaState::UpToDate: + stateStr = "uptodate"; + break; + } + + snprintf(responseBuffer, bufferSize, + "\"success\":true,\"command\":\"%s\",\"v\":\"v%u.%u.%u.%u\",\"av\":\"%s\",\"s\":\"%s\"", + command, + FirmwareMajor, FirmwareMinor, FirmwarePatch, FirmwareBuild, + _otaManager->getAvailableVersion(), + stateStr); + + return CommandResult::ok(); + } + else if (SystemFunctions::commandMatches(command, SystemOtaStatus)) + { + const char* stateStr = "disabled"; + const char* avVersion = ""; + char autoApply = '0'; + + if (_otaManager) + { + switch (_otaManager->getState()) + { + case OtaState::Idle: + stateStr = "idle"; + break; + + case OtaState::Checking: + stateStr = "checking"; + break; + + case OtaState::UpdateAvailable: + stateStr = "available"; + break; + + case OtaState::Downloading: + stateStr = "downloading"; + break; + + case OtaState::Rebooting: + stateStr = "rebooting"; + break; + + case OtaState::Failed: + stateStr = "failed"; + break; + + case OtaState::UpToDate: + stateStr = "uptodate"; + break; + } + + avVersion = _otaManager->getAvailableVersion(); + + SystemHeader* hdr = ConfigManager::getHeaderPtr(); + if (hdr && (hdr->reserved[0] & OtaFlagAutoApply)) + autoApply = '1'; + } + + snprintf(responseBuffer, bufferSize, + "\"success\":true,\"command\":\"%s\",\"v\":\"v%u.%u.%u.%u\",\"av\":\"%s\",\"s\":\"%s\",\"auto\":\"%c\"", + command, + FirmwareMajor, FirmwareMinor, FirmwarePatch, FirmwareBuild, + avVersion, + stateStr, + autoApply); + + return CommandResult::ok(); + } +#endif // OTA_AUTO_UPDATE else { return CommandResult::error(InvalidCommandParameters); diff --git a/PowerControlHub/SystemNetworkHandler.h b/PowerControlHub/SystemNetworkHandler.h index ccf309a..6c27209 100644 --- a/PowerControlHub/SystemNetworkHandler.h +++ b/PowerControlHub/SystemNetworkHandler.h @@ -25,6 +25,9 @@ #include "SdCardLogger.h" #endif +#if defined(OTA_AUTO_UPDATE) && defined(ESP32) && defined(WIFI_SUPPORT) +class OtaManager; +#endif class SystemNetworkHandler : public INetworkCommandHandler { @@ -35,6 +38,10 @@ class SystemNetworkHandler : public INetworkCommandHandler SdCardLogger* _sdCardLogger; #endif +#if defined(OTA_AUTO_UPDATE) && defined(ESP32) && defined(WIFI_SUPPORT) + OtaManager* _otaManager{ nullptr }; +#endif + public: explicit SystemNetworkHandler(WifiController* wifiController); @@ -45,6 +52,13 @@ class SystemNetworkHandler : public INetworkCommandHandler } #endif +#if defined(OTA_AUTO_UPDATE) && defined(ESP32) && defined(WIFI_SUPPORT) + void setOtaManager(OtaManager* otaManager) + { + _otaManager = otaManager; + } +#endif + const char* getRoute() const override { return "/api/system"; } void formatWifiStatusJson(IWifiClient* client) override; diff --git a/PowerControlHubApp/Internal/Constants.cs b/PowerControlHubApp/Internal/Constants.cs index 859d20a..e8c78c8 100644 --- a/PowerControlHubApp/Internal/Constants.cs +++ b/PowerControlHubApp/Internal/Constants.cs @@ -33,6 +33,10 @@ internal static class Constants public const string MessageDeviceUnreachable = "Device unreachable"; public const string MessageToggleFailed = "Toggle failed — see log"; public const string LogOtaTrigger = "OTA: triggering firmware install…"; + public const string OtaTriggerFailed = "OTA trigger failed"; + public const string OtaDialogTitle = "Install Firmware"; + public const string OtaDialogMessage = "An OTA update to {0} is available. Download and install now?"; + public const string OtaDialogAccept = "Install"; public const int RelayCount = 8; public const int UnconfiguredPin = 255; // Dashboard poller log messages @@ -195,6 +199,7 @@ internal static class Constants public const string UserAgentValue = "PowerControlHub/1.0"; public const string PowerHubNotConfigured = "PowerHubService is not configured. Call Configure() first."; public const string ErrorKey = "error"; + public const char QuoteChar = '"'; public const string PreferenceKey = "app_theme"; public const string ThemeLight = "Light"; public const string ThemeDark = "Dark"; diff --git a/PowerControlHubApp/ViewModels/DashboardViewModel.cs b/PowerControlHubApp/ViewModels/DashboardViewModel.cs index 17130b0..27b6d9e 100644 --- a/PowerControlHubApp/ViewModels/DashboardViewModel.cs +++ b/PowerControlHubApp/ViewModels/DashboardViewModel.cs @@ -1,6 +1,7 @@ using PowerControlHubApp.Models; using PowerControlHubApp.Models.Json; using PowerControlHubApp.Services; +using Microsoft.Maui.Controls; using System.Collections.ObjectModel; using System.ComponentModel; using System.Runtime.CompilerServices; @@ -36,6 +37,7 @@ public class DashboardViewModel : INotifyPropertyChanged public ICommand RefreshCommand { get; } public ICommand ToggleRelayCommand { get; } public ICommand InstallUpdateCommand { get; } + public ICommand FirmwareLabelTappedCommand { get; } /// True when the firmware reported an update is available. public bool UpdateAvailable => _otaStatus?.UpdateAvailable == true; @@ -90,7 +92,6 @@ private set { _systemTime = value; OnPropertyChanged(); - OnPropertyChanged(nameof(SystemFirmwareAndTime)); } } @@ -102,7 +103,6 @@ private set { _systemFirmware = value; OnPropertyChanged(); - OnPropertyChanged(nameof(SystemFirmwareAndTime)); } } @@ -116,14 +116,13 @@ private set { _systemUptime = value; OnPropertyChanged(); - OnPropertyChanged(nameof(SystemFirmwareAndTime)); } } /// - /// Combined firmware and time display used by the status bar. + /// Colour for the firmware label in the status bar. Changes when update available. /// - public string SystemFirmwareAndTime => $"FW: {SystemFirmware} • {SystemUptime}"; + public Color SystemFirmwareColor => UpdateAvailable ? Color.FromArgb(ColorWarning) : Color.FromArgb(ColorAsHex1); /// Label shown in the update banner. public string OtaBannerLabel => _otaStatus?.BannerLabel ?? string.Empty; @@ -212,6 +211,7 @@ public DashboardViewModel(PowerHubService service, LogService log, RelayStore re RefreshCommand = new Command(async () => await RefreshAsync()); ToggleRelayCommand = new Command(async relay => await ToggleRelayAsync(relay)); InstallUpdateCommand = new Command(async () => await InstallUpdateAsync(), () => CanInstallUpdate); + FirmwareLabelTappedCommand = new Command(async () => await Shell.Current.GoToAsync(RouteSettingsPage)); _relayStore.Relays.CollectionChanged += (_, _) => { @@ -393,6 +393,7 @@ private void NotifyOtaProperties() OnPropertyChanged(nameof(OtaBannerLabel)); OnPropertyChanged(nameof(ShowOtaBanner)); OnPropertyChanged(nameof(OtaBannerColor)); + OnPropertyChanged(nameof(SystemFirmwareColor)); OnPropertyChanged(nameof(CanInstallUpdate)); ((Command)InstallUpdateCommand).ChangeCanExecute(); } @@ -404,6 +405,7 @@ private void UpdateSystem(SystemModel system) // Populate firmware and uptime so the status bar can show them SystemFirmware = string.IsNullOrEmpty(system.Fw) ? DoubleDash : system.Fw; SystemUptime = string.IsNullOrEmpty(system.Uptime) ? DoubleDash : system.Uptime; + OnPropertyChanged(nameof(SystemFirmwareColor)); } private void UpdateRelays(IReadOnlyList incoming) diff --git a/PowerControlHubApp/ViewModels/SettingsViewModel.cs b/PowerControlHubApp/ViewModels/SettingsViewModel.cs index 5c58828..f274ff2 100644 --- a/PowerControlHubApp/ViewModels/SettingsViewModel.cs +++ b/PowerControlHubApp/ViewModels/SettingsViewModel.cs @@ -1,7 +1,8 @@ +using PowerControlHubApp.Models.Json; +using PowerControlHubApp.Services; using System.ComponentModel; using System.Runtime.CompilerServices; using System.Windows.Input; -using PowerControlHubApp.Services; using static PowerControlHubApp.Internal.Constants; namespace PowerControlHubApp.ViewModels; @@ -17,14 +18,17 @@ public class SettingsViewModel : INotifyPropertyChanged private string _pinsInUseText = string.Empty; private bool _isPinsRefreshing; private int _pinsCount; + private OtaStatusModel _otaStatus; + private bool _isCheckingOta; public string IpAddress { get => _ipAddress; - set - { _ipAddress = value; - OnPropertyChanged(); + set + { + _ipAddress = value; + OnPropertyChanged(); } } @@ -32,10 +36,10 @@ public string Port { get => _port; - set - { - _port = value; - OnPropertyChanged(); + set + { + _port = value; + OnPropertyChanged(); } } @@ -43,11 +47,11 @@ public string StatusMessage { get => _statusMessage; - set - { - _statusMessage = value; - OnPropertyChanged(); - OnPropertyChanged(nameof(HasStatusMessage)); + set + { + _statusMessage = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(HasStatusMessage)); } } @@ -109,6 +113,8 @@ public bool IsPinsRefreshing public ICommand SaveCommand { get; } public ICommand RefreshPinsCommand { get; } + public ICommand CheckForUpdateCommand { get; } + public ICommand InstallFirmwareCommand { get; } public SettingsViewModel(PowerHubService service, ThemeService themeService) { @@ -116,12 +122,94 @@ public SettingsViewModel(PowerHubService service, ThemeService themeService) _themeService = themeService; SaveCommand = new Command(Save); RefreshPinsCommand = new Command(async () => await RefreshPinsAsync()); + CheckForUpdateCommand = new Command(async () => await CheckForUpdateAsync()); + InstallFirmwareCommand = new Command(async () => await InstallFirmwareAsync(), () => CanInstallFirmware); IpAddress = Preferences.Get(KeyDeviceIpAddress, string.Empty); Port = Preferences.Get(KeyDeviceIpPort, DefaultDeviceIpPort); _selectedTheme = ThemeService.Current; } + public string CurrentFirmwareVersion => _otaStatus?.CurrentVersion ?? DoubleDash; + + public string AvailableFirmwareVersion => string.IsNullOrEmpty(_otaStatus?.AvailableVersion) ? DoubleDash : _otaStatus.AvailableVersion; + + public bool IsUpdateAvailable => _otaStatus?.UpdateAvailable == true; + + public bool IsOtaBusy => (_otaStatus?.IsBusy == true) || _isCheckingOta; + + public bool CanInstallFirmware => _otaStatus?.UpdateAvailable == true && !_isCheckingOta; + + public async Task CheckForUpdateAsync() + { + if (!_service.IsConfigured || _isCheckingOta) + return; + + _isCheckingOta = true; + try + { + var ota = await _service.GetOtaStatusAsync(); + _otaStatus = ota; + NotifyOtaProperties(); + } + catch + { + StatusMessage = MessageDeviceUnreachable; + } + finally + { + _isCheckingOta = false; + OnPropertyChanged(nameof(IsOtaBusy)); + ((Command)InstallFirmwareCommand).ChangeCanExecute(); + } + } + + public async Task InstallFirmwareAsync() + { + if (!CanInstallFirmware) + return; + + string message = string.Format(OtaDialogMessage, AvailableFirmwareVersion); + bool confirmed = await Application.Current!.MainPage!.DisplayAlertAsync( + OtaDialogTitle, + message, + OtaDialogAccept, + MsgCancel); + + if (!confirmed) + return; + + try + { + var ok = await _service.TriggerOtaInstallAsync(); + + if (ok) + { + // give the device a moment then refresh status + await Task.Delay(TimeSpan.FromSeconds(SecondsTwo)); + await CheckForUpdateAsync(); + } + else + { + StatusMessage = OtaTriggerFailed; + } + } + catch + { + StatusMessage = OtaTriggerFailed; + } + } + + private void NotifyOtaProperties() + { + OnPropertyChanged(nameof(CurrentFirmwareVersion)); + OnPropertyChanged(nameof(AvailableFirmwareVersion)); + OnPropertyChanged(nameof(IsUpdateAvailable)); + OnPropertyChanged(nameof(IsOtaBusy)); + OnPropertyChanged(nameof(CanInstallFirmware)); + ((Command)InstallFirmwareCommand).ChangeCanExecute(); + } + public async Task RefreshPinsAsync() { if (!_service.IsConfigured || _isPinsRefreshing) diff --git a/PowerControlHubApp/Views/DashboardPage.xaml b/PowerControlHubApp/Views/DashboardPage.xaml index 184ec07..22cd8a8 100644 --- a/PowerControlHubApp/Views/DashboardPage.xaml +++ b/PowerControlHubApp/Views/DashboardPage.xaml @@ -247,13 +247,18 @@ - + diff --git a/PowerControlHubApp/Views/SettingsPage.xaml b/PowerControlHubApp/Views/SettingsPage.xaml index f035e12..c875a12 100644 --- a/PowerControlHubApp/Views/SettingsPage.xaml +++ b/PowerControlHubApp/Views/SettingsPage.xaml @@ -35,6 +35,7 @@ +