diff --git a/Docs/Commands.md b/Docs/Commands.md index 060a032..85327c6 100644 --- a/Docs/Commands.md +++ b/Docs/Commands.md @@ -246,7 +246,7 @@ WiFi commands return JSON (`{"success":true}` or `{"success":false,"message":" | `R8` — Set Default State | `R8:3=1` | Power-on default state for relay `idx`: `0`=off, `1`=on. | | `R9` — Link Relays | `R9:3=4` (link) — `R9:3=255` (unlink) | Link two relays so toggling one toggles the other. `255` to unlink. | | `R10` — Set Action Type | `R10:2=0` / `R10:2=1` / `R10:2=2` | Set relay action type: `0`=Default, `1`=Horn (activates sound system), `2`=NightRelay (activates at night per light sensor). Only one relay per type at a time. | -| `R11` — Set Pin | `R11:2=15` | Set GPIO pin for relay `idx`. Must be a valid non-zero pin number. | +| `R11` — Set Pin | `R11:2=15` | Set GPIO pin for relay `idx`. Must be a valid non-zero pin number or 255 to disable | ### WiFi relay commands Route: `/api/relay/{command}` diff --git a/PowerControlHub/ConfigNetworkHandler.cpp b/PowerControlHub/ConfigNetworkHandler.cpp index bda1882..d3b1187 100644 --- a/PowerControlHub/ConfigNetworkHandler.cpp +++ b/PowerControlHub/ConfigNetworkHandler.cpp @@ -1027,11 +1027,12 @@ void ConfigNetworkHandler::formatStatusJson(IWifiClient* client) client->print("\"config\":{"); - // Name + // C3 Name client->print("\"name\":\""); client->print(config->location.name); client->print("\","); + // C4 Spi Pins client->print("\"spiPins\":{"); client->print("\"sck\":"); client->print(config->spiPins.sckPin); @@ -1043,22 +1044,22 @@ void ConfigNetworkHandler::formatStatusJson(IWifiClient* client) client->print(config->spiPins.misoPin); client->print("},"); - // Vessel type - client->print("\"vesselType\":"); - client->print(static_cast(config->location.locationType)); + // C6 Xpdz Tone + client->print("\"xpdzPin\":"); + client->print(static_cast(config->xpdzTone.pin)); client->print(","); - // Sound relay ID - client->print("\"hornRelayIndex\":"); - client->print(static_cast(config->sound.hornRelayIndex)); + // C7 Location type + client->print("\"locationType\":"); + client->print(static_cast(config->location.locationType)); client->print(","); - // Sound start delay + // C9 Sound start delay client->print("\"soundStartDelayMs\":"); client->print(static_cast(config->sound.startDelayMs)); client->print(","); - // Bluetooth, WiFi, SSID, Password, Port, AccessMode + // C10 - C17 Bluetooth, WiFi, SSID, Password, Port, AccessMode client->print("\"bluetoothEnabled\":"); client->print(config->network.bluetoothEnabled ? "true" : "false"); client->print(","); @@ -1110,6 +1111,26 @@ void ConfigNetworkHandler::formatStatusJson(IWifiClient* client) 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\":"); + client->print(config->auth.enabled ? "true" : "false"); + client->print(",\"apiKey\":\""); + client->print(config->auth.apiKey); + client->print("\",\"hmacKey\":\""); + client->print(config->auth.hmacKey); + client->print("\"},"); + // C20 Timezone offset client->print("\"timezoneOffset\":"); client->print(static_cast(config->system.timezoneOffset)); @@ -1206,6 +1227,8 @@ void ConfigNetworkHandler::formatStatusJson(IWifiClient* client) client->print(config->sound.badRepeatMs); client->print("},"); + // C29, C30 Serial only + // C31 SD Card Initialize Speed client->print("\"sdCardInitializeSpeed\":"); client->print(config->sdCard.initializeSpeed); @@ -1214,27 +1237,6 @@ void ConfigNetworkHandler::formatStatusJson(IWifiClient* client) // C32 SD Card CS pin client->print("\"sdCardCsPin\":"); 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\":"); - client->print(config->auth.enabled ? "true" : "false"); - client->print(",\"apiKey\":\""); - client->print(config->auth.apiKey); - client->print("\",\"hmacKey\":\""); - client->print(config->auth.hmacKey); - client->print("\"}"); client->print("}"); } diff --git a/PowerControlHub/RelayNetworkHandler.cpp b/PowerControlHub/RelayNetworkHandler.cpp index a65b51b..4ec9c8c 100644 --- a/PowerControlHub/RelayNetworkHandler.cpp +++ b/PowerControlHub/RelayNetworkHandler.cpp @@ -414,19 +414,23 @@ void RelayNetworkHandler::formatWifiStatusJson(IWifiClient* client) if (config != nullptr) { client->print(",\"homeMap\":["); + for (uint8_t i = 0; i < ConfigHomeButtons; i++) { if (i > 0) client->print(","); client->print(config->relay.homePageMapping[i]); } + client->print("]"); client->print(",\"linked\":["); + for (uint8_t i = 0; i < ConfigMaxLinkedRelays; i++) { if (i > 0) client->print(","); + client->print("["); client->print(config->relay.linkedRelays[i][0]); client->print(","); diff --git a/PowerControlHubApp/AppShell.xaml.cs b/PowerControlHubApp/AppShell.xaml.cs index b60eeee..4423f15 100644 --- a/PowerControlHubApp/AppShell.xaml.cs +++ b/PowerControlHubApp/AppShell.xaml.cs @@ -16,6 +16,7 @@ public AppShell() Routing.RegisterRoute(nameof(RtcSettingsPage), typeof(RtcSettingsPage)); Routing.RegisterRoute(nameof(NetworkSecurityPage), typeof(NetworkSecurityPage)); Routing.RegisterRoute(nameof(NextionSettingsPage), typeof(NextionSettingsPage)); + Routing.RegisterRoute(nameof(XpdzToneSettingsPage), typeof(XpdzToneSettingsPage)); } } } diff --git a/PowerControlHubApp/Internal/Constants.cs b/PowerControlHubApp/Internal/Constants.cs index bc3679a..091f649 100644 --- a/PowerControlHubApp/Internal/Constants.cs +++ b/PowerControlHubApp/Internal/Constants.cs @@ -212,6 +212,13 @@ internal static class Constants public const string JsonRtcClockPin = "clk"; public const string JsonRtcResetPin = "rst"; public const int RtcPinDisabled = 255; + // XpdzTone / Buzzer config + public const string RouteConfigXpdzTone = "api/config/C6"; + public const string RouteXpdzToneSettingsPage = "XpdzToneSettingsPage"; + public const string XpdzToneMsgSaveFailed = "Save failed — device unreachable"; + public const string XpdzToneMsgSaved = "Buzzer settings saved"; + public const string XpdzToneMsgRefreshed = "Refreshed"; + public const string JsonXpdzPin = "xpdzPin"; // 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 908f25c..06f8f66 100644 --- a/PowerControlHubApp/MauiProgram.cs +++ b/PowerControlHubApp/MauiProgram.cs @@ -111,6 +111,7 @@ public static MauiApp CreateMauiApp() builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); // Pages builder.Services.AddSingleton(); @@ -128,6 +129,7 @@ public static MauiApp CreateMauiApp() builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); #if DEBUG builder.Logging.AddDebug(); diff --git a/PowerControlHubApp/Models/Json/ConfigModel.cs b/PowerControlHubApp/Models/Json/ConfigModel.cs index db62f37..b53edec 100644 --- a/PowerControlHubApp/Models/Json/ConfigModel.cs +++ b/PowerControlHubApp/Models/Json/ConfigModel.cs @@ -79,5 +79,8 @@ public sealed class ConfigModel [JsonPropertyName("rtcPins")] public RtcConfigModel Rtc { get; set; } + + [JsonPropertyName("xpdzPin")] + public int XpdzTonePin { get; set; } } } diff --git a/PowerControlHubApp/PowerControlHubApp.csproj b/PowerControlHubApp/PowerControlHubApp.csproj index 45f5813..6b7febb 100644 --- a/PowerControlHubApp/PowerControlHubApp.csproj +++ b/PowerControlHubApp/PowerControlHubApp.csproj @@ -163,6 +163,9 @@ MSBuild:Compile + + MSBuild:Compile + diff --git a/PowerControlHubApp/Services/ConfigConnection.cs b/PowerControlHubApp/Services/ConfigConnection.cs index ea4d0b7..fc3ce46 100644 --- a/PowerControlHubApp/Services/ConfigConnection.cs +++ b/PowerControlHubApp/Services/ConfigConnection.cs @@ -681,6 +681,20 @@ public async Task SetRtcPinsAsync(int dataPin, int clockPin, int resetPin, } } + public async Task SetXpdzTonePinAsync(int pin, CancellationToken ct = default) + { + try + { + string url = $"{RouteConfigXpdzTone}?v={pin}"; + 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 dbdb8b7..08c25d7 100644 --- a/PowerControlHubApp/Services/ConfigPoller.cs +++ b/PowerControlHubApp/Services/ConfigPoller.cs @@ -229,6 +229,8 @@ private async Task RunLoopAsync(CancellationToken stoppingToken) public Task SetRtcPinsAsync(int dataPin, int clockPin, int resetPin, CancellationToken ct = default) => _connection.SetRtcPinsAsync(dataPin, clockPin, resetPin, ct); + public Task SetXpdzTonePinAsync(int pin, CancellationToken ct = default) => _connection.SetXpdzTonePinAsync(pin, ct); + public Task GetAuthConfigAsync(CancellationToken ct = default) => _connection.GetAuthConfigAsync(ct); public Task SetAuthEnabledAsync(bool enabled, CancellationToken ct = default) => _connection.SetAuthEnabledAsync(enabled, ct); @@ -240,11 +242,17 @@ private async Task RunLoopAsync(CancellationToken stoppingToken) public Task GenerateAuthKeysAsync(CancellationToken ct = default) => _connection.GenerateAuthKeysAsync(ct); public Task GetNextionConfigAsync(CancellationToken ct = default) => _connection.GetNextionConfigAsync(ct); + public Task SetNextionEnabledAsync(bool enabled, CancellationToken ct = default) => _connection.SetNextionEnabledAsync(enabled, ct); + public Task SetNextionHardwareSerialAsync(bool hardwareSerial, CancellationToken ct = default) => _connection.SetNextionHardwareSerialAsync(hardwareSerial, ct); + public Task SetNextionRxPinAsync(int pin, CancellationToken ct = default) => _connection.SetNextionRxPinAsync(pin, ct); + public Task SetNextionTxPinAsync(int pin, CancellationToken ct = default) => _connection.SetNextionTxPinAsync(pin, ct); + public Task SetNextionBaudRateAsync(int baudRate, CancellationToken ct = default) => _connection.SetNextionBaudRateAsync(baudRate, ct); + public Task SetNextionUartNumberAsync(int uartNumber, CancellationToken ct = default) => _connection.SetNextionUartNumberAsync(uartNumber, ct); public void Dispose() diff --git a/PowerControlHubApp/Services/IConfigConnection.cs b/PowerControlHubApp/Services/IConfigConnection.cs index e66e0c4..88d82b5 100644 --- a/PowerControlHubApp/Services/IConfigConnection.cs +++ b/PowerControlHubApp/Services/IConfigConnection.cs @@ -60,6 +60,7 @@ public interface IConfigConnection 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 SetXpdzTonePinAsync(int pin, 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 d49cb7e..e97a6db 100644 --- a/PowerControlHubApp/Services/PowerHubService.cs +++ b/PowerControlHubApp/Services/PowerHubService.cs @@ -343,6 +343,11 @@ public Task SetRtcPinsAsync(int dataPin, int clockPin, int resetPin, Cance return _configConnection.SetRtcPinsAsync(dataPin, clockPin, resetPin, ct); } + public Task SetXpdzTonePinAsync(int pin, CancellationToken ct = default) + { + return _configConnection.SetXpdzTonePinAsync(pin, ct); + } + public Task SetAuthEnabledAsync(bool enabled, CancellationToken ct = default) { return _configConnection.SetAuthEnabledAsync(enabled, ct); diff --git a/PowerControlHubApp/ViewModels/SystemViewModel.cs b/PowerControlHubApp/ViewModels/SystemViewModel.cs index 4d503e3..8972797 100644 --- a/PowerControlHubApp/ViewModels/SystemViewModel.cs +++ b/PowerControlHubApp/ViewModels/SystemViewModel.cs @@ -115,6 +115,8 @@ public bool HasPinRestrictions public ICommand NavigateToNextionSettingsCommand { get; } + public ICommand NavigateToXpdzToneSettingsCommand { get; } + public SystemViewModel(PowerHubService service, LogService log) : base(service, log) { @@ -127,6 +129,7 @@ public SystemViewModel(PowerHubService service, LogService log) 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)); + NavigateToXpdzToneSettingsCommand = new Command(async () => await Shell.Current.GoToAsync(RouteXpdzToneSettingsPage)); } protected override void OnDataFetched(IndexModel index) diff --git a/PowerControlHubApp/ViewModels/XpdzToneSettingsViewModel.cs b/PowerControlHubApp/ViewModels/XpdzToneSettingsViewModel.cs new file mode 100644 index 0000000..2b0ba2a --- /dev/null +++ b/PowerControlHubApp/ViewModels/XpdzToneSettingsViewModel.cs @@ -0,0 +1,154 @@ +using PowerControlHubApp.Models.Json; +using PowerControlHubApp.Services; +using System.Windows.Input; +using static PowerControlHubApp.Internal.Constants; + +namespace PowerControlHubApp.ViewModels; + +public sealed class XpdzToneSettingsViewModel : BaseViewModel +{ + private string _pin = string.Empty; + private bool _isRefreshing; + private bool _isSaving; + + public XpdzToneSettingsViewModel(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 Pin + { + get => _pin; + + set + { + _pin = 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 != null) + { + Pin = index.Config.XpdzTonePin == UnconfiguredPin ? string.Empty : index.Config.XpdzTonePin.ToString(); + } + + IsConnected = true; + StatusMessage = $"{XpdzToneMsgRefreshed} {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 pinFailed = false; + + if (int.TryParse(Pin, out int pin)) + { + var ok = await Service.SetXpdzTonePinAsync(pin); + pinFailed = !ok; + } + + await Service.SaveSettingsAsync(); + + MainThread.BeginInvokeOnMainThread(() => + { + if (pinFailed) + { + StatusMessage = XpdzToneMsgSaveFailed; + } + else + { + StatusMessage = XpdzToneMsgSaved; + } + + OnPropertyChanged(nameof(HasStatusMessage)); + }); + } + catch + { + MainThread.BeginInvokeOnMainThread(() => + { + StatusMessage = XpdzToneMsgSaveFailed; + 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/Views/SystemPage.xaml b/PowerControlHubApp/Views/SystemPage.xaml index bd538f9..1a6ef0e 100644 --- a/PowerControlHubApp/Views/SystemPage.xaml +++ b/PowerControlHubApp/Views/SystemPage.xaml @@ -75,6 +75,15 @@ Padding="4" BorderColor="Blue" BorderWidth="1"/> +