diff --git a/PowerControlHub/ConfigController.cpp b/PowerControlHub/ConfigController.cpp index 7c1f755..08bf85d 100644 --- a/PowerControlHub/ConfigController.cpp +++ b/PowerControlHub/ConfigController.cpp @@ -532,10 +532,27 @@ ConfigResult ConfigController::setRtcPins(const uint8_t dataPin, const uint8_t c if (_config == nullptr) return ConfigResult::InvalidConfig; - if (PinGuard::isBlocked(PinGuard::validate(dataPin, PinUse::Output)) || - PinGuard::isBlocked(PinGuard::validate(clockPin, PinUse::Output)) || - PinGuard::isBlocked(PinGuard::validate(resetPin, PinUse::Output))) + // Temporarily clear current RTC pin assignments so they don't + // conflict with themselves during validation (getUsedPins collects + // the current rtc.dataPin/clockPin/resetPin values). + const uint8_t oldData = _config->rtc.dataPin; + const uint8_t oldClock = _config->rtc.clockPin; + const uint8_t oldReset = _config->rtc.resetPin; + + _config->rtc.dataPin = PinDisabled; + _config->rtc.clockPin = PinDisabled; + _config->rtc.resetPin = PinDisabled; + + const bool blocked = PinGuard::isBlocked(PinGuard::validate(dataPin, PinUse::Output)) + || PinGuard::isBlocked(PinGuard::validate(clockPin, PinUse::Output)) + || PinGuard::isBlocked(PinGuard::validate(resetPin, PinUse::Output)); + + if (blocked) { + // Restore old values on failure + _config->rtc.dataPin = oldData; + _config->rtc.clockPin = oldClock; + _config->rtc.resetPin = oldReset; return ConfigResult::InvalidPin; } diff --git a/PowerControlHub/ConfigNetworkHandler.cpp b/PowerControlHub/ConfigNetworkHandler.cpp index 12921c9..bda1882 100644 --- a/PowerControlHub/ConfigNetworkHandler.cpp +++ b/PowerControlHub/ConfigNetworkHandler.cpp @@ -1216,6 +1216,16 @@ void ConfigNetworkHandler::formatStatusJson(IWifiClient* client) client->print(config->sdCard.csPin); client->print(","); + // C18 RTC DS1302 pins + client->print("\"rtcPins\":{"); + client->print("\"dat\":"); + client->print(config->rtc.dataPin); + client->print(",\"clk\":"); + client->print(config->rtc.clockPin); + client->print(",\"rst\":"); + client->print(config->rtc.resetPin); + client->print("},"); + // C19 Network auth client->print("\"auth\":{"); client->print("\"enabled\":"); diff --git a/PowerControlHubApp/AppShell.xaml.cs b/PowerControlHubApp/AppShell.xaml.cs index 81ef5f6..b60eeee 100644 --- a/PowerControlHubApp/AppShell.xaml.cs +++ b/PowerControlHubApp/AppShell.xaml.cs @@ -13,6 +13,7 @@ public AppShell() Routing.RegisterRoute(nameof(TimeSettingsPage), typeof(TimeSettingsPage)); Routing.RegisterRoute(nameof(MqttSettingsPage), typeof(MqttSettingsPage)); Routing.RegisterRoute(nameof(SdCardSettingsPage), typeof(SdCardSettingsPage)); + Routing.RegisterRoute(nameof(RtcSettingsPage), typeof(RtcSettingsPage)); Routing.RegisterRoute(nameof(NetworkSecurityPage), typeof(NetworkSecurityPage)); Routing.RegisterRoute(nameof(NextionSettingsPage), typeof(NextionSettingsPage)); } diff --git a/PowerControlHubApp/Internal/Constants.cs b/PowerControlHubApp/Internal/Constants.cs index f54b97d..bc3679a 100644 --- a/PowerControlHubApp/Internal/Constants.cs +++ b/PowerControlHubApp/Internal/Constants.cs @@ -202,6 +202,16 @@ internal static class Constants public const string RouteConfigSdCardSpiPins = "api/config/C4"; public const string RouteConfigSdCardInitSpeed = "api/config/C31"; public const string RouteConfigSdCardCsPin = "api/config/C32"; + // RTC config + public const string RouteConfigRtc = "api/config/C18"; + public const string RouteRtcSettingsPage = "RtcSettingsPage"; + public const string RtcMsgSaveFailed = "Save failed — device unreachable"; + public const string RtcMsgSaved = "RTC settings saved"; + public const string RtcMsgRefreshed = "Refreshed"; + public const string JsonRtcDataPin = "dat"; + public const string JsonRtcClockPin = "clk"; + public const string JsonRtcResetPin = "rst"; + public const int RtcPinDisabled = 255; // Nextion display config public const string RouteConfigNextionGet = "api/config/N0"; public const string RouteConfigNextionSetFormat = "api/config/N{0}?v={1}"; diff --git a/PowerControlHubApp/MauiProgram.cs b/PowerControlHubApp/MauiProgram.cs index e050d3b..908f25c 100644 --- a/PowerControlHubApp/MauiProgram.cs +++ b/PowerControlHubApp/MauiProgram.cs @@ -108,6 +108,7 @@ public static MauiApp CreateMauiApp() builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); @@ -124,6 +125,7 @@ public static MauiApp CreateMauiApp() builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); diff --git a/PowerControlHubApp/Models/Json/ConfigModel.cs b/PowerControlHubApp/Models/Json/ConfigModel.cs index 23b6d32..db62f37 100644 --- a/PowerControlHubApp/Models/Json/ConfigModel.cs +++ b/PowerControlHubApp/Models/Json/ConfigModel.cs @@ -76,5 +76,8 @@ public sealed class ConfigModel [JsonPropertyName("sdCardCsPin")] public int SdCardCsPin { get; set; } + + [JsonPropertyName("rtcPins")] + public RtcConfigModel Rtc { get; set; } } } diff --git a/PowerControlHubApp/Models/RtcConfigModel.cs b/PowerControlHubApp/Models/RtcConfigModel.cs new file mode 100644 index 0000000..4b1c5c7 --- /dev/null +++ b/PowerControlHubApp/Models/RtcConfigModel.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace PowerControlHubApp.Models +{ + public sealed class RtcConfigModel + { + [JsonPropertyName("dat")] + public int Dat { get; set; } + + [JsonPropertyName("clk")] + public int Clk { get; set; } + + [JsonPropertyName("rst")] + public int Rst { get; set; } + } +} diff --git a/PowerControlHubApp/PowerControlHubApp.csproj b/PowerControlHubApp/PowerControlHubApp.csproj index 700fc1c..45f5813 100644 --- a/PowerControlHubApp/PowerControlHubApp.csproj +++ b/PowerControlHubApp/PowerControlHubApp.csproj @@ -118,6 +118,9 @@ MSBuild:Compile + + MSBuild:Compile + MSBuild:Compile diff --git a/PowerControlHubApp/Services/ConfigConnection.cs b/PowerControlHubApp/Services/ConfigConnection.cs index ce55e5f..ea4d0b7 100644 --- a/PowerControlHubApp/Services/ConfigConnection.cs +++ b/PowerControlHubApp/Services/ConfigConnection.cs @@ -632,6 +632,55 @@ public async Task SetSdCardCsPinAsync(int pin, CancellationToken ct = defa return await SetConfigValueAsync(RouteConfigSdCardCsPin, pin, ct); } + public async Task<(int DataPin, int ClockPin, int ResetPin)?> GetRtcPinsAsync(CancellationToken ct = default) + { + try + { + string json = await _client.GetStringAsync(RouteConfigRtc, ct); + using JsonDocument doc = JsonDocument.Parse(json); + + if (doc.RootElement.TryGetProperty(ResultSuccess, out var success) && + success.GetBoolean()) + { + int? data = null; + int? clock = null; + int? reset = null; + + if (doc.RootElement.TryGetProperty(JsonRtcDataPin, out var d) && d.ValueKind == JsonValueKind.Number && d.TryGetInt32(out int dv)) + data = dv; + + if (doc.RootElement.TryGetProperty(JsonRtcClockPin, out var c) && c.ValueKind == JsonValueKind.Number && c.TryGetInt32(out int cv)) + clock = cv; + + if (doc.RootElement.TryGetProperty(JsonRtcResetPin, out var r) && r.ValueKind == JsonValueKind.Number && r.TryGetInt32(out int rv)) + reset = rv; + + if (data.HasValue && clock.HasValue && reset.HasValue) + return (data.Value, clock.Value, reset.Value); + } + } + catch + { + // fall through + } + + return null; + } + + public async Task SetRtcPinsAsync(int dataPin, int clockPin, int resetPin, CancellationToken ct = default) + { + try + { + string url = $"{RouteConfigRtc}?{JsonRtcDataPin}={dataPin}&{JsonRtcClockPin}={clockPin}&{JsonRtcResetPin}={resetPin}"; + HttpResponseMessage response = await _client.PostAsync(url, null, ct); + return response.IsSuccessStatusCode; + } + catch + { + return false; + } + } + public async Task GetAuthConfigAsync(CancellationToken ct = default) { try diff --git a/PowerControlHubApp/Services/ConfigPoller.cs b/PowerControlHubApp/Services/ConfigPoller.cs index eb5e1ad..dbdb8b7 100644 --- a/PowerControlHubApp/Services/ConfigPoller.cs +++ b/PowerControlHubApp/Services/ConfigPoller.cs @@ -225,6 +225,10 @@ private async Task RunLoopAsync(CancellationToken stoppingToken) public Task SetSdCardCsPinAsync(int pin, CancellationToken ct = default) => _connection.SetSdCardCsPinAsync(pin, ct); + public Task<(int DataPin, int ClockPin, int ResetPin)?> GetRtcPinsAsync(CancellationToken ct = default) => _connection.GetRtcPinsAsync(ct); + + public Task SetRtcPinsAsync(int dataPin, int clockPin, int resetPin, CancellationToken ct = default) => _connection.SetRtcPinsAsync(dataPin, clockPin, resetPin, ct); + public Task GetAuthConfigAsync(CancellationToken ct = default) => _connection.GetAuthConfigAsync(ct); public Task SetAuthEnabledAsync(bool enabled, CancellationToken ct = default) => _connection.SetAuthEnabledAsync(enabled, ct); diff --git a/PowerControlHubApp/Services/IConfigConnection.cs b/PowerControlHubApp/Services/IConfigConnection.cs index cadd038..e66e0c4 100644 --- a/PowerControlHubApp/Services/IConfigConnection.cs +++ b/PowerControlHubApp/Services/IConfigConnection.cs @@ -58,6 +58,8 @@ public interface IConfigConnection Task SetSdCardInitSpeedAsync(int speed, CancellationToken ct = default); Task GetSdCardCsPinAsync(CancellationToken ct = default); Task SetSdCardCsPinAsync(int pin, CancellationToken ct = default); + Task<(int DataPin, int ClockPin, int ResetPin)?> GetRtcPinsAsync(CancellationToken ct = default); + Task SetRtcPinsAsync(int dataPin, int clockPin, int resetPin, CancellationToken ct = default); Task GetAuthConfigAsync(CancellationToken ct = default); Task SetAuthEnabledAsync(bool enabled, CancellationToken ct = default); Task SetAuthApiKeyAsync(string apiKey, CancellationToken ct = default); diff --git a/PowerControlHubApp/Services/PowerHubService.cs b/PowerControlHubApp/Services/PowerHubService.cs index ef0ceee..d49cb7e 100644 --- a/PowerControlHubApp/Services/PowerHubService.cs +++ b/PowerControlHubApp/Services/PowerHubService.cs @@ -333,6 +333,16 @@ public Task GetAuthConfigAsync(CancellationToken ct = default) return _configConnection.GetAuthConfigAsync(ct); } + public Task<(int DataPin, int ClockPin, int ResetPin)?> GetRtcPinsAsync(CancellationToken ct = default) + { + return _configConnection.GetRtcPinsAsync(ct); + } + + public Task SetRtcPinsAsync(int dataPin, int clockPin, int resetPin, CancellationToken ct = default) + { + return _configConnection.SetRtcPinsAsync(dataPin, clockPin, resetPin, ct); + } + public Task SetAuthEnabledAsync(bool enabled, CancellationToken ct = default) { return _configConnection.SetAuthEnabledAsync(enabled, ct); diff --git a/PowerControlHubApp/ViewModels/RtcSettingsViewModel.cs b/PowerControlHubApp/ViewModels/RtcSettingsViewModel.cs new file mode 100644 index 0000000..598324e --- /dev/null +++ b/PowerControlHubApp/ViewModels/RtcSettingsViewModel.cs @@ -0,0 +1,180 @@ +using PowerControlHubApp.Models.Json; +using PowerControlHubApp.Services; +using System.Windows.Input; +using static PowerControlHubApp.Internal.Constants; + +namespace PowerControlHubApp.ViewModels; + +public sealed class RtcSettingsViewModel : BaseViewModel +{ + private string _dataPin = string.Empty; + private string _clockPin = string.Empty; + private string _resetPin = string.Empty; + private bool _isRefreshing; + private bool _isSaving; + + public RtcSettingsViewModel(PowerHubService service, LogService log) + : base(service, log) + { + RefreshCommand = new Command(async () => await RefreshAsync()); + SaveAllCommand = new Command(async () => await SaveAllAsync()); + } + + public ICommand SaveAllCommand { get; } + + public string DataPin + { + get => _dataPin; + + set + { + _dataPin = value; + OnPropertyChanged(); + } + } + + public string ClockPin + { + get => _clockPin; + + set + { + _clockPin = value; + OnPropertyChanged(); + } + } + + public string ResetPin + { + get => _resetPin; + + set + { + _resetPin = value; + OnPropertyChanged(); + } + } + + public bool IsRefreshing + { + get => _isRefreshing; + + set + { + _isRefreshing = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(IsNotRefreshing)); + } + } + + public bool IsNotRefreshing => !_isRefreshing; + + public bool IsSaving + { + get => _isSaving; + + set + { + _isSaving = value; + OnPropertyChanged(); + } + } + + public bool HasStatusMessage => !string.IsNullOrEmpty(StatusMessage); + + public async Task RefreshAsync() + { + if (!Service.IsConfigured || _isRefreshing) + return; + + IsRefreshing = true; + + try + { + var index = await Service.GetDashboardDataAsync(); + + MainThread.BeginInvokeOnMainThread(() => + { + if (index?.Config?.Rtc != null) + { + DataPin = index.Config.Rtc.Dat == RtcPinDisabled ? string.Empty : index.Config.Rtc.Dat.ToString(); + ClockPin = index.Config.Rtc.Clk == RtcPinDisabled ? string.Empty : index.Config.Rtc.Clk.ToString(); + ResetPin = index.Config.Rtc.Rst == RtcPinDisabled ? string.Empty : index.Config.Rtc.Rst.ToString(); + } + + IsConnected = true; + StatusMessage = $"{RtcMsgRefreshed} {DateTime.Now:HH:mm:ss}"; + OnPropertyChanged(nameof(HasStatusMessage)); + }); + } + catch + { + MainThread.BeginInvokeOnMainThread(() => + { + IsConnected = false; + StatusMessage = MessageDeviceUnreachable; + OnPropertyChanged(nameof(HasStatusMessage)); + }); + } + finally + { + IsRefreshing = false; + } + } + + public async Task SaveAllAsync() + { + if (!Service.IsConfigured || _isSaving) + return; + + IsSaving = true; + + try + { + bool pinsFailed = false; + + if (int.TryParse(DataPin, out int data) && int.TryParse(ClockPin, out int clock) && int.TryParse(ResetPin, out int reset)) + { + var ok = await Service.SetRtcPinsAsync(data, clock, reset); + pinsFailed = !ok; + } + + await Service.SaveSettingsAsync(); + + MainThread.BeginInvokeOnMainThread(() => + { + if (pinsFailed) + { + StatusMessage = RtcMsgSaveFailed; + } + else + { + StatusMessage = RtcMsgSaved; + } + + OnPropertyChanged(nameof(HasStatusMessage)); + }); + } + catch + { + MainThread.BeginInvokeOnMainThread(() => + { + StatusMessage = RtcMsgSaveFailed; + OnPropertyChanged(nameof(HasStatusMessage)); + }); + } + finally + { + IsSaving = false; + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822", Justification = "Called polymorphically from OnDisappearing")] + public void Cleanup() + { + } + + protected override void OnDataFetched(IndexModel index) + { + } +} diff --git a/PowerControlHubApp/ViewModels/SystemViewModel.cs b/PowerControlHubApp/ViewModels/SystemViewModel.cs index 0972315..4d503e3 100644 --- a/PowerControlHubApp/ViewModels/SystemViewModel.cs +++ b/PowerControlHubApp/ViewModels/SystemViewModel.cs @@ -109,6 +109,8 @@ public bool HasPinRestrictions public ICommand NavigateToSdCardSettingsCommand { get; } + public ICommand NavigateToRtcSettingsCommand { get; } + public ICommand NavigateToNetworkSecurityCommand { get; } public ICommand NavigateToNextionSettingsCommand { get; } @@ -122,6 +124,7 @@ public SystemViewModel(PowerHubService service, LogService log) NavigateToTimeSettingsCommand = new Command(async () => await Shell.Current.GoToAsync(RouteTimeSettingsPage)); NavigateToMqttSettingsCommand = new Command(async () => await Shell.Current.GoToAsync(RouteMqttSettingsPage)); NavigateToSdCardSettingsCommand = new Command(async () => await Shell.Current.GoToAsync(RouteSdCardSettingsPage)); + NavigateToRtcSettingsCommand = new Command(async () => await Shell.Current.GoToAsync(RouteRtcSettingsPage)); NavigateToNetworkSecurityCommand = new Command(async () => await Shell.Current.GoToAsync(RouteNetworkSecurityPage)); NavigateToNextionSettingsCommand = new Command(async () => await Shell.Current.GoToAsync(RouteNextionSettingsPage)); } diff --git a/PowerControlHubApp/Views/RtcSettingsPage.xaml b/PowerControlHubApp/Views/RtcSettingsPage.xaml new file mode 100644 index 0000000..9098c01 --- /dev/null +++ b/PowerControlHubApp/Views/RtcSettingsPage.xaml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + +