diff --git a/PowerControlHub/Dht11SensorHandler.h b/PowerControlHub/Dht11SensorHandler.h index 781655a..2aba9b5 100644 --- a/PowerControlHub/Dht11SensorHandler.h +++ b/PowerControlHub/Dht11SensorHandler.h @@ -61,46 +61,46 @@ class Dht11SensorHandler : public BaseSensor, public BroadcastLoggerSupport protected: // ── Return codes ───────────────────────────────────────────────────────── - static constexpr int Dht11Ok = 0; - static constexpr int Dht11Timeout = -1; + static constexpr int Dht11Ok = 0; + static constexpr int Dht11Timeout = -1; static constexpr int Dht11Checksum = -2; // ── DHT11 one-wire protocol constants ──────────────────────────────────── // Spec: host pulls bus LOW for a minimum of 18 ms to trigger the sensor. - static constexpr uint8_t Dht11StartLowMs = 18; + static constexpr uint8_t Dht11StartLowMs = 18; // Spec: host drives HIGH for 20–40 µs before releasing to INPUT so the // sensor can detect the rising edge. 40 µs satisfies the upper bound. - static constexpr uint8_t Dht11StartHighUs = 40; + static constexpr uint8_t Dht11StartHighUs = 40; // Spec: 40 bits (5 bytes) per reading, transmitted MSB-first within each byte. - static constexpr uint8_t Dht11BitCount = 40; - static constexpr uint8_t Dht11ByteCount = 5; - static constexpr uint8_t Dht11BitsPerByte = 8; + static constexpr uint8_t Dht11BitCount = 40; + static constexpr uint8_t Dht11ByteCount = 5; + static constexpr uint8_t Dht11BitsPerByte = 8; // Spec: each bit is preceded by a 50 µs LOW separator, then a HIGH pulse: // bit '0' → 26–28 µs HIGH // bit '1' → 70 µs HIGH // 40 µs sits unambiguously between the two ranges and is the standard threshold. - static constexpr uint8_t Dht11BitOneThresholdUs = 40; + static constexpr uint8_t Dht11BitOneThresholdUs = 40; // Spec: byte layout of the 5 transmitted data bytes. - static constexpr uint8_t Dht11HumIntIdx = 0; // humidity integer part - static constexpr uint8_t Dht11HumDecIdx = 1; // humidity decimal part (always 0 on DHT11) - static constexpr uint8_t Dht11TmpIntIdx = 2; // temperature integer part - static constexpr uint8_t Dht11TmpDecIdx = 3; // temperature decimal part (always 0 on DHT11) - static constexpr uint8_t Dht11ChecksumIdx = 4; // checksum = (byte0+byte1+byte2+byte3) & 0xFF + static constexpr uint8_t Dht11HumIntIdx = 0; // humidity integer part + static constexpr uint8_t Dht11HumDecIdx = 1; // humidity decimal part (always 0 on DHT11) + static constexpr uint8_t Dht11TmpIntIdx = 2; // temperature integer part + static constexpr uint8_t Dht11TmpDecIdx = 3; // temperature decimal part (always 0 on DHT11) + static constexpr uint8_t Dht11ChecksumIdx = 4; // checksum = (byte0+byte1+byte2+byte3) & 0xFF // Spec: decimal byte encodes tenths (e.g. decimal byte 5 → +0.5 °C / +0.5 %). - static constexpr float Dht11DecimalScale = 0.1f; + static constexpr float Dht11DecimalScale = 0.1f; // Iteration cap for every bounded spin-wait in readDht11(). // digitalRead() on ESP32 at 240 MHz takes ~0.5–1 µs, so 10 000 iterations // gives a ~5–10 ms hard ceiling per transition — well above any legitimate // DHT11 signal period (max 80 µs response, 70 µs bit-HIGH) yet fast enough // to recover from a fault without stalling the main loop. - static constexpr unsigned int Dht11MaxLoopCount = 10000; + static constexpr unsigned int Dht11MaxLoopCount = 10000; /** * @brief Read temperature and humidity from a DHT11 sensor. @@ -140,18 +140,23 @@ class Dht11SensorHandler : public BaseSensor, public BroadcastLoggerSupport loopCnt = Dht11MaxLoopCount; while (digitalRead(pin) == HIGH) - if (loopCnt-- == 0) return Dht11Timeout; + { + if (loopCnt-- == 0) + return Dht11Timeout; + } // Read Dht11BitCount bits; each bit = 50 µs LOW separator + variable-width HIGH for (int i = 0; i < Dht11BitCount; i++) { loopCnt = Dht11MaxLoopCount; + while (digitalRead(pin) == LOW) if (loopCnt-- == 0) return Dht11Timeout; unsigned long t = micros(); loopCnt = Dht11MaxLoopCount; + while (digitalRead(pin) == HIGH) if (loopCnt-- == 0) return Dht11Timeout; @@ -168,14 +173,14 @@ class Dht11SensorHandler : public BaseSensor, public BroadcastLoggerSupport } // Verify checksum: low byte of the sum of the four data bytes - uint8_t sum = (bits[Dht11HumIntIdx] + bits[Dht11HumDecIdx] - + bits[Dht11TmpIntIdx] + bits[Dht11TmpDecIdx]) & 0xFF; + uint8_t sum = (bits[Dht11HumIntIdx] + bits[Dht11HumDecIdx] + + bits[Dht11TmpIntIdx] + bits[Dht11TmpDecIdx]) & 0xFF; if (bits[Dht11ChecksumIdx] != sum) return Dht11Checksum; - outHumidity = bits[Dht11HumIntIdx] + (bits[Dht11HumDecIdx] * Dht11DecimalScale); - outTemperature = bits[Dht11TmpIntIdx] + (bits[Dht11TmpDecIdx] * Dht11DecimalScale); + outHumidity = bits[Dht11HumIntIdx] + (bits[Dht11HumDecIdx] * Dht11DecimalScale); + outTemperature = bits[Dht11TmpIntIdx] + (bits[Dht11TmpDecIdx] * Dht11DecimalScale); return Dht11Ok; } @@ -200,6 +205,7 @@ class Dht11SensorHandler : public BaseSensor, public BroadcastLoggerSupport snprintf(buffer, sizeof(buffer), "DHT11 read error: %s", errStr); sendError(buffer, "DHT11 Error"); } + return TempHumidityCheckMs; } diff --git a/PowerControlHub/SensorConfig.h b/PowerControlHub/SensorConfig.h index 2391b05..b588c84 100644 --- a/PowerControlHub/SensorConfig.h +++ b/PowerControlHub/SensorConfig.h @@ -1,4 +1,5 @@ #include "SystemDefinitions.h" +#include "PinGuard.h" struct SensorFieldDescriptor { const char label[20]; // e.g. "Analogue Pin", "Threshold" @@ -6,6 +7,7 @@ struct SensorFieldDescriptor { int16_t minVal; int16_t maxVal; int16_t defaultValue; + PinUse pinUse; // intended use for GPIO slots (PinUse::Sensor for non-GPIO slots) }; struct SensorTypeDescriptor { @@ -20,99 +22,99 @@ constexpr SensorTypeDescriptor SensorDescriptors[] = { [static_cast(SensorIdList::WaterSensor)] = { .name = "Water Sensor", .pins = { - { "Data Pin", "gpio", 0, 39, 255 }, - { "Power Pin", "gpio", 0, 39, 255 }, - { "Unused", "none", 0, 0, 0 }, - { "Unused", "none", 0, 0, 0 }, + { "Data Pin", "gpio", 0, 39, 255, PinUse::Sensor }, + { "Power Pin", "gpio", 0, 39, 255, PinUse::Output }, + { "Unused", "none", 0, 0, 0, PinUse::Sensor }, + { "Unused", "none", 0, 0, 0, PinUse::Sensor }, }, - .options1 = { { "Unused", "none", 0, 0, 0 }, { "Unused", "none", 0, 0, 0 } }, - .options2 = { { "Unused", "none", 0, 0, 0 }, { "Unused", "none", 0, 0, 0 } }, + .options1 = { { "Unused", "none", 0, 0, 0, PinUse::Sensor }, { "Unused", "none", 0, 0, 0, PinUse::Sensor } }, + .options2 = { { "Unused", "none", 0, 0, 0, PinUse::Sensor }, { "Unused", "none", 0, 0, 0, PinUse::Sensor } }, }, [static_cast(SensorIdList::Dht11Sensor)] = { .name = "DHT11", .pins = { - { "Data Pin", "gpio", 0, 39, 255 }, - { "Unused", "none", 0, 0, 0 }, - { "Unused", "none", 0, 0, 0 }, - { "Unused", "none", 0, 0, 0 }, + { "Data Pin", "gpio", 0, 39, 255, PinUse::Sensor }, + { "Unused", "none", 0, 0, 0, PinUse::Sensor }, + { "Unused", "none", 0, 0, 0, PinUse::Sensor }, + { "Unused", "none", 0, 0, 0, PinUse::Sensor }, }, - .options1 = { { "Unused", "none", 0, 0, 0 }, { "Unused", "none", 0, 0, 0 } }, - .options2 = { { "Unused", "none", 0, 0, 0 }, { "Unused", "none", 0, 0, 0 } }, + .options1 = { { "Unused", "none", 0, 0, 0, PinUse::Sensor }, { "Unused", "none", 0, 0, 0, PinUse::Sensor } }, + .options2 = { { "Unused", "none", 0, 0, 0, PinUse::Sensor }, { "Unused", "none", 0, 0, 0, PinUse::Sensor } }, }, [static_cast(SensorIdList::LightSensor)] = { .name = "Light Sensor", .pins = { - { "Analogue Pin", "gpio", 0, 39, 255 }, - { "Unused", "none", 0, 0, 0 }, - { "Unused", "none", 0, 0, 0 }, - { "Unused", "none", 0, 0, 0 }, + { "Analogue Pin", "gpio", 0, 39, 255, PinUse::Sensor }, + { "Unused", "none", 0, 0, 0, PinUse::Sensor }, + { "Unused", "none", 0, 0, 0, PinUse::Sensor }, + { "Unused", "none", 0, 0, 0, PinUse::Sensor }, }, .options1 = { - { "Mode", "int8", 0, 1, 0 }, - { "Unused", "none", 0, 0, 0 }, + { "Mode", "int8", 0, 1, 0, PinUse::Sensor }, + { "Unused", "none", 0, 0, 0, PinUse::Sensor }, }, .options2 = { - { "Threshold", "int16", 0, 4095, 512 }, - { "Unused", "none", 0, 0, 0 }, + { "Threshold", "int16", 0, 4095, 512, PinUse::Sensor }, + { "Unused", "none", 0, 0, 0, PinUse::Sensor }, }, }, [static_cast(SensorIdList::GpsSensor)] = { .name = "GPS", .pins = { - { "RX Pin", "gpio", 0, 39, 255 }, - { "TX Pin", "gpio", 0, 39, 255 }, - { "Unused", "none", 0, 0, 0 }, - { "Unused", "none", 0, 0, 0 }, + { "RX Pin", "gpio", 0, 39, 255, PinUse::Sensor }, + { "TX Pin", "gpio", 0, 39, 255, PinUse::Output }, + { "Unused", "none", 0, 0, 0, PinUse::Sensor }, + { "Unused", "none", 0, 0, 0, PinUse::Sensor }, }, .options1 = { - { "UART Num", "int8", 1, 2, 2 }, - { "Unused", "none", 0, 0, 0 }, + { "UART Num", "int8", 1, 2, 2, PinUse::Sensor }, + { "Unused", "none", 0, 0, 0, PinUse::Sensor }, }, - .options2 = { { "Unused", "none", 0, 0, 0 }, { "Unused", "none", 0, 0, 0 } }, + .options2 = { { "Unused", "none", 0, 0, 0, PinUse::Sensor }, { "Unused", "none", 0, 0, 0, PinUse::Sensor } }, }, [static_cast(SensorIdList::SystemSensor)] = { .name = "System Sensor", .pins = { - { "Unused", "none", 0, 0, 0 }, - { "Unused", "none", 0, 0, 0 }, - { "Unused", "none", 0, 0, 0 }, - { "Unused", "none", 0, 0, 0 }, + { "Unused", "none", 0, 0, 0, PinUse::Sensor }, + { "Unused", "none", 0, 0, 0, PinUse::Sensor }, + { "Unused", "none", 0, 0, 0, PinUse::Sensor }, + { "Unused", "none", 0, 0, 0, PinUse::Sensor }, }, - .options1 = { { "Unused", "none", 0, 0, 0 }, { "Unused", "none", 0, 0, 0 } }, - .options2 = { { "Unused", "none", 0, 0, 0 }, { "Unused", "none", 0, 0, 0 } }, + .options1 = { { "Unused", "none", 0, 0, 0, PinUse::Sensor }, { "Unused", "none", 0, 0, 0, PinUse::Sensor } }, + .options2 = { { "Unused", "none", 0, 0, 0, PinUse::Sensor }, { "Unused", "none", 0, 0, 0, PinUse::Sensor } }, }, [static_cast(SensorIdList::BinaryPresenceSensor)] = { .name = "Binary Presence", .pins = { - { "Sensor Pin", "gpio", 0, 39, 255 }, - { "onDetected", "int8", 0, 255, 0 }, - { "onClear", "int8", 0, 255, 0 }, - { "Unused", "none", 0, 0, 0 }, + { "Sensor Pin", "gpio", 0, 39, 255, PinUse::Sensor }, + { "onDetected", "int8", 0, 255, 0, PinUse::Sensor }, + { "onClear", "int8", 0, 255, 0, PinUse::Sensor }, + { "Unused", "none", 0, 0, 0, PinUse::Sensor }, }, .options1 = { - { "Active State", "int8", 0, 1, 1 }, - { "Detect Action", "int8", 0, 255, 0 }, + { "Active State", "int8", 0, 1, 1, PinUse::Sensor }, + { "Detect Action", "int8", 0, 255, 0, PinUse::Sensor }, }, .options2 = { - { "Unused", "none", 0, 0, 0 }, - { "Clear Action", "int16", 0, 255, 0 }, + { "Unused", "none", 0, 0, 0, PinUse::Sensor }, + { "Clear Action", "int16", 0, 255, 0, PinUse::Sensor }, }, }, [static_cast(SensorIdList::VoltageSensor)] = { .name = "Voltage Sensor", .pins = { - { "Analogue Pin", "gpio", 0, 39, 255 }, - { "Unused", "none", 0, 0, 0 }, - { "Unused", "none", 0, 0, 0 }, - { "Unused", "none", 0, 0, 0 }, + { "Analogue Pin", "gpio", 0, 39, 255, PinUse::Sensor }, + { "Unused", "none", 0, 0, 0, PinUse::Sensor }, + { "Unused", "none", 0, 0, 0, PinUse::Sensor }, + { "Unused", "none", 0, 0, 0, PinUse::Sensor }, }, .options1 = { - { "ADC Vref", "int8", 0, 50, 50 }, - { "R2 (tenths)", "int8", 0, 255, 75 }, + { "ADC Vref", "int8", 0, 50, 50, PinUse::Sensor }, + { "R2 (tenths)", "int8", 0, 255, 75, PinUse::Sensor }, }, .options2 = { - { "R1 (kOhm)", "int16", 0, 1000, 30 }, - { "Low Warn (Vx10)", "int16", 0, 500, 0 }, + { "R1 (kOhm)", "int16", 0, 1000, 30, PinUse::Sensor }, + { "Low Warn (Vx10)", "int16", 0, 500, 0, PinUse::Sensor }, }, }, }; diff --git a/PowerControlHub/SensorConfigNetworkHandler.cpp b/PowerControlHub/SensorConfigNetworkHandler.cpp index d512c2c..e893d95 100644 --- a/PowerControlHub/SensorConfigNetworkHandler.cpp +++ b/PowerControlHub/SensorConfigNetworkHandler.cpp @@ -22,6 +22,8 @@ #include "ConfigController.h" #include "SystemDefinitions.h" #include "SystemFunctions.h" +#include "SensorConfig.h" +#include "PinGuard.h" CommandResult SensorConfigNetworkHandler::handleRequest(const char* method, const char* command, @@ -170,8 +172,29 @@ CommandResult SensorConfigNetworkHandler::handleRequest(const char* method, } else { - config->sensors.sensors[idx].pins[slot] = pin; - result = ConfigResult::Success; + // Validate pin through PinGuard — only GPIO slots represent actual hardware pins + const uint8_t sensorType = static_cast(config->sensors.sensors[idx].sensorType); + bool isGpioSlot = false; + PinUse intendedUse = PinUse::Sensor; + + if (sensorType < static_cast(SensorIdList::Count)) + { + const SensorFieldDescriptor& field = SensorDescriptors[sensorType].pins[slot]; + isGpioSlot = (strcmp(field.type, "gpio") == 0); + intendedUse = field.pinUse; + } + + // Always validate GPIO slots through PinGuard (covers hard-blocks, advisory, and InUse). + // Non-GPIO slots store payload bytes (relay indices, action types) — skip PinGuard. + if (isGpioSlot && PinGuard::isBlocked(PinGuard::validate(pin, intendedUse))) + { + result = ConfigResult::InvalidPin; + } + else + { + config->sensors.sensors[idx].pins[slot] = pin; + result = ConfigResult::Success; + } } } else if (SystemFunctions::commandMatches(command, SensorConfigSetEnabled)) diff --git a/PowerControlHub/SensorNetworkHandler.cpp b/PowerControlHub/SensorNetworkHandler.cpp index 4788b9b..5a2dd5b 100644 --- a/PowerControlHub/SensorNetworkHandler.cpp +++ b/PowerControlHub/SensorNetworkHandler.cpp @@ -21,6 +21,7 @@ #include "SystemFunctions.h" #include "ConfigManager.h" #include "SensorConfig.h" +#include "PinGuard.h" SensorNetworkHandler::SensorNetworkHandler(SensorController* sensorController) @@ -313,6 +314,28 @@ CommandResult SensorNetworkHandler::handleRequest(const char* method, return CommandResult::error(InvalidCommandParameters); } + // Validate pin through PinGuard — only GPIO slots represent actual hardware pins + { + const uint8_t sensorType = static_cast(config->sensors.sensors[idx].sensorType); + bool isGpioSlot = false; + PinUse intendedUse = PinUse::Sensor; + + if (sensorType < static_cast(SensorIdList::Count)) + { + const SensorFieldDescriptor& field = SensorDescriptors[sensorType].pins[slot]; + isGpioSlot = (strcmp(field.type, "gpio") == 0); + intendedUse = field.pinUse; + } + + // Always validate GPIO slots through PinGuard (covers hard-blocks, advisory, and InUse). + // Non-GPIO slots store payload bytes (relay indices, action types) — skip PinGuard. + if (isGpioSlot && PinGuard::isBlocked(PinGuard::validate(pin, intendedUse))) + { + formatJsonResponse(responseBuffer, bufferSize, false, "Pin blocked by PinGuard"); + return CommandResult::error(InvalidCommandParameters); + } + } + config->sensors.sensors[idx].pins[slot] = pin; formatJsonResponse(responseBuffer, bufferSize, true); return CommandResult::ok(); diff --git a/PowerControlHubApp/Internal/Constants.cs b/PowerControlHubApp/Internal/Constants.cs index 93ad27f..859d20a 100644 --- a/PowerControlHubApp/Internal/Constants.cs +++ b/PowerControlHubApp/Internal/Constants.cs @@ -21,6 +21,7 @@ internal static class Constants public const string ColorLogError = "#ff4444"; public const string ColorLogDefault = "#888888"; public const string DoubleDash = "--"; + public const string CommaSpace = ", "; public const string FontOpenSansRegular = "OpenSans-Regular.ttf"; public const string FontSansSemiBold = "OpenSans-Semibold.ttf"; public const string FontSansSemiBoldName = "OpenSansSemibold"; @@ -177,6 +178,7 @@ internal static class Constants public const string RouteSaveConfig = "api/config/C0"; public const string RouteOtaUpdate = "api/system/F13"; public const string RouteUpdateOta = "api/system/F12?apply=1"; + public const string RouteSystemPins = "api/system/F15"; public const string ForwardSlash = "/"; public const string ResultSuccess = "success"; public const string RouteDashboardPage = "//DashboardPage"; @@ -250,6 +252,11 @@ internal static class Constants public const string SensorTypeVoltage = "Voltage"; public const string SensorTypeUnknown = "Unknown"; public const string SensorTypeMetaDataUnavailable = "Sensor type metadata unavailable."; + public const string FailAddUpdateSensor = "add/update sensor entry"; + public const string FailEnableDisableSensor = "change enabled state"; + public const string FailSaveSettings = "save settings to disk"; + public const string FailSeparator = "; "; + public const string FailPrefix = "⚠ Failed to: "; // Sensor type display names with enum values for picker public const string SensorTypeWaterPicker = "Water (0)"; diff --git a/PowerControlHubApp/Models/Json/SystemPinsResponseModel.cs b/PowerControlHubApp/Models/Json/SystemPinsResponseModel.cs new file mode 100644 index 0000000..7c7049e --- /dev/null +++ b/PowerControlHubApp/Models/Json/SystemPinsResponseModel.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; + +namespace PowerControlHubApp.Models.Json; + +/// +/// Response model for GET /api/system/F15 — returns the list of physical GPIO pins +/// currently assigned to any relay, sensor, or peripheral on the device. +/// +public class SystemPinsResponseModel +{ + [JsonPropertyName("success")] + public bool Success { get; set; } + + [JsonPropertyName("command")] + public string Command { get; set; } + + [JsonPropertyName("pins")] + public List Pins { get; set; } = []; +} diff --git a/PowerControlHubApp/Services/DashboardConnection.cs b/PowerControlHubApp/Services/DashboardConnection.cs index b23b1cf..aad65c6 100644 --- a/PowerControlHubApp/Services/DashboardConnection.cs +++ b/PowerControlHubApp/Services/DashboardConnection.cs @@ -38,6 +38,12 @@ public void Configure(string ipAddress, int port) public bool IsConfigured => !string.IsNullOrEmpty(_baseUrl); + public async Task GetSystemPinsAsync(CancellationToken ct = default) + { + string json = await _client.GetStringAsync(RouteSystemPins, ct); + return JsonSerializer.Deserialize(json, JsonOptions); + } + public async Task GetDashboardDataAsync(CancellationToken ct = default) { string json = await _client.GetStringAsync(RouteApiIndex, ct); diff --git a/PowerControlHubApp/Services/IDashboardConnection.cs b/PowerControlHubApp/Services/IDashboardConnection.cs index d4d59d0..7f2bb3d 100644 --- a/PowerControlHubApp/Services/IDashboardConnection.cs +++ b/PowerControlHubApp/Services/IDashboardConnection.cs @@ -5,5 +5,6 @@ public interface IDashboardConnection { bool IsConfigured { get; } Task GetDashboardDataAsync(CancellationToken ct = default); + Task GetSystemPinsAsync(CancellationToken ct = default); } } diff --git a/PowerControlHubApp/Services/PowerHubService.cs b/PowerControlHubApp/Services/PowerHubService.cs index a2bd139..38685f5 100644 --- a/PowerControlHubApp/Services/PowerHubService.cs +++ b/PowerControlHubApp/Services/PowerHubService.cs @@ -44,6 +44,11 @@ public void Configure(string ipAddress, int port) cc.Configure(ipAddress, port); } + public Task GetSystemPinsAsync(CancellationToken ct = default) + { + return _dashboardConnection.GetSystemPinsAsync(ct); + } + public Task GetDashboardDataAsync(CancellationToken ct = default) { return _dashboardConnection.GetDashboardDataAsync(ct); diff --git a/PowerControlHubApp/ViewModels/LocalSensorDetailViewModel.cs b/PowerControlHubApp/ViewModels/LocalSensorDetailViewModel.cs index 55f94fe..eced233 100644 --- a/PowerControlHubApp/ViewModels/LocalSensorDetailViewModel.cs +++ b/PowerControlHubApp/ViewModels/LocalSensorDetailViewModel.cs @@ -752,41 +752,80 @@ private async Task SaveAsync() return; } - // Add/update the sensor entry (S1) - bool ok = await _service.AddUpdateLocalSensorAsync(_sensorIndex, SensorType, (sbyte)Opt1_0, (sbyte)Opt1_1); + // Collect failures with descriptive labels + List failures = []; + + // Add/update the sensor entry (S1) — label only if it fails + if (!await _service.AddUpdateLocalSensorAsync(_sensorIndex, SensorType, (sbyte)Opt1_0, (sbyte)Opt1_1)) + failures.Add(FailAddUpdateSensor); // Rename (S3) if changed if (_original == null || _original.Name != Name) - ok &= await _service.RenameLocalSensorAsync(_sensorIndex, Name ?? string.Empty); + { + if (!await _service.RenameLocalSensorAsync(_sensorIndex, Name ?? string.Empty)) + failures.Add($"rename to \"{Name}\""); + } // Set pins (S4) if (_original == null || _original.Pin0 != Pin0) - ok &= await _service.SetLocalSensorPinAsync(_sensorIndex, 0, (byte)Pin0); + { + if (!await _service.SetLocalSensorPinAsync(_sensorIndex, 0, (byte)Pin0)) + failures.Add($"set pin 0 to {Pin0Display}"); + } if (_original == null || _original.Pin1 != Pin1) - ok &= await _service.SetLocalSensorPinAsync(_sensorIndex, 1, (byte)Pin1); + { + if (!await _service.SetLocalSensorPinAsync(_sensorIndex, 1, (byte)Pin1)) + failures.Add($"set pin 1 to {Pin1Display}"); + } // Set options (S6) if (_original == null || _original.Opt1_0 != Opt1_0) - ok &= await _service.SetLocalSensorOptionAsync(_sensorIndex, 0, 0, Opt1_0); + { + if (!await _service.SetLocalSensorOptionAsync(_sensorIndex, 0, 0, Opt1_0)) + failures.Add($"set option {Opt1_0Label} to {Opt1_0}"); + } if (_original == null || _original.Opt1_1 != Opt1_1) - ok &= await _service.SetLocalSensorOptionAsync(_sensorIndex, 1, 0, Opt1_1); + { + if (!await _service.SetLocalSensorOptionAsync(_sensorIndex, 1, 0, Opt1_1)) + failures.Add($"set option {Opt1_1Label} to {Opt1_1}"); + } if (_original == null || _original.Opt2_0 != Opt2_0) - ok &= await _service.SetLocalSensorOptionAsync(_sensorIndex, 0, 1, Opt2_0); + { + if (!await _service.SetLocalSensorOptionAsync(_sensorIndex, 0, 1, Opt2_0)) + failures.Add($"set option {Opt2_0Label} to {Opt2_0}"); + } if (_original == null || _original.Opt2_1 != Opt2_1) - ok &= await _service.SetLocalSensorOptionAsync(_sensorIndex, 1, 1, Opt2_1); + { + if (!await _service.SetLocalSensorOptionAsync(_sensorIndex, 1, 1, Opt2_1)) + failures.Add($"set option {Opt2_1Label} to {Opt2_1}"); + } // Set enabled (S5) if (_original == null || _original.Enabled != IsEnabled) - ok &= await _service.SetLocalSensorEnabledAsync(_sensorIndex, IsEnabled); + { + if (!await _service.SetLocalSensorEnabledAsync(_sensorIndex, IsEnabled)) + failures.Add(FailEnableDisableSensor); + } - if (ok) - ok &= await _service.SaveSettingsAsync(); + // Save changes to disk + if (failures.Count == 0) + { + if (!await _service.SaveSettingsAsync()) + failures.Add(FailSaveSettings); + } + else + { + // Still attempt to save even if some commands failed + await _service.SaveSettingsAsync(); + } - StatusMessage = ok ? SavedOk : SavedFailed; + StatusMessage = failures.Count == 0 + ? SavedOk + : $"{FailPrefix}{string.Join(FailSeparator, failures)}"; } catch (Exception ex) { diff --git a/PowerControlHubApp/ViewModels/SettingsViewModel.cs b/PowerControlHubApp/ViewModels/SettingsViewModel.cs index 9436564..5c58828 100644 --- a/PowerControlHubApp/ViewModels/SettingsViewModel.cs +++ b/PowerControlHubApp/ViewModels/SettingsViewModel.cs @@ -14,6 +14,9 @@ public class SettingsViewModel : INotifyPropertyChanged private string _port = DefaultDeviceIpPort; private string _statusMessage = string.Empty; private string _selectedTheme; + private string _pinsInUseText = string.Empty; + private bool _isPinsRefreshing; + private int _pinsCount; public string IpAddress { @@ -69,17 +72,87 @@ public string SelectedTheme } } + public string PinsInUseText + { + get => _pinsInUseText; + set + { + _pinsInUseText = value; + OnPropertyChanged(); + } + } + + public int PinsCount + { + get => _pinsCount; + set + { + _pinsCount = value; + OnPropertyChanged(); + } + } + + public bool IsPinsRefreshing + { + get => _isPinsRefreshing; + set + { + _isPinsRefreshing = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(IsNotPinsRefreshing)); + } + } + + public bool IsNotPinsRefreshing => !_isPinsRefreshing; + + public bool HasPins => PinsCount > 0; + public ICommand SaveCommand { get; } + public ICommand RefreshPinsCommand { get; } public SettingsViewModel(PowerHubService service, ThemeService themeService) { _service = service; _themeService = themeService; SaveCommand = new Command(Save); + RefreshPinsCommand = new Command(async () => await RefreshPinsAsync()); IpAddress = Preferences.Get(KeyDeviceIpAddress, string.Empty); Port = Preferences.Get(KeyDeviceIpPort, DefaultDeviceIpPort); - _selectedTheme = ThemeService.Current; + _selectedTheme = ThemeService.Current; + } + + public async Task RefreshPinsAsync() + { + if (!_service.IsConfigured || _isPinsRefreshing) + return; + + IsPinsRefreshing = true; + + try + { + var result = await _service.GetSystemPinsAsync(); + + if (result?.Success == true && result.Pins?.Count > 0) + { + PinsCount = result.Pins.Count; + PinsInUseText = string.Join(CommaSpace, result.Pins); + } + else + { + PinsCount = 0; + PinsInUseText = DoubleDash; + } + } + catch + { + PinsCount = 0; + PinsInUseText = MessageDeviceUnreachable; + } + finally + { + IsPinsRefreshing = false; + } } private void Save() diff --git a/PowerControlHubApp/Views/SettingsPage.xaml b/PowerControlHubApp/Views/SettingsPage.xaml index 368bed6..f035e12 100644 --- a/PowerControlHubApp/Views/SettingsPage.xaml +++ b/PowerControlHubApp/Views/SettingsPage.xaml @@ -115,6 +115,54 @@ + + + + +