diff --git a/.editorconfig b/.editorconfig index 4c1b6f1..ef67e0b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,10 +8,14 @@ dotnet_diagnostic.CC0008.severity = error dotnet_diagnostic.CC0009.severity = error dotnet_diagnostic.CC0010.severity = error + dotnet_diagnostic.IDE0028.severity = error +dotnet_diagnostic.IDE0290.severity = none dotnet_diagnostic.IDE0305.severity = error + + +dotnet_diagnostic.CS0144.severity = error dotnet_diagnostic.CS8632.severity = error -dotnet_diagnostic.CA1822.severity = error -dotnet_diagnostic.IDE0290.severity = none \ No newline at end of file +dotnet_diagnostic.CA1822.severity = error diff --git a/Docs/Commands.md b/Docs/Commands.md index 85327c6..f1b2b7b 100644 --- a/Docs/Commands.md +++ b/Docs/Commands.md @@ -61,6 +61,7 @@ These are commands used to configure the system settings and can only be sent fr | `F14` — PinGuard Mode | `F14` or `F14:a=true` or `F14:a=false;b=false` | Read or write the persistent PinGuard mode flags stored in `SystemHeader::pinGuardFlags`. **Params:** `a=` — AllowAdvisory: when `true`, advisory (strapping/UART) pins are permitted. `b=` — Bypass: when `true`, all PinGuard checks are skipped (no validation at all). If both `a` and `b` are `true`, Bypass takes precedence and the system returns `Safe` immediately. Changes are persisted immediately via `saveHeader()` and take effect at runtime without reboot. **Response:** `a=<0\|1>` current AllowAdvisory state, `b=<0\|1>` current Bypass state. No params = read-only. | | `F15` — Pin Usage | `F15` | Returns a list of all GPIO pins currently assigned in configuration. Pins set to `0xFF` (disabled / not fitted) are omitted. **Serial response:** `v=pin1,pin2,...` (single frame). **WiFi response:** JSON object with a `pins` array. No params. | | `F16` — Pin Restrictions | `F16` | Returns the compile-time pin restriction table for the current target MCU. Pins are classified as `Hard` (always blocked, e.g. flash-reserved, PSRAM) or `Advisory` (strapping, UART0, USB, JTAG — usable only when PinGuard AllowAdvisory is enabled). **Serial response:** one line per pin as `pin - category` followed by `ACK:F16=ok`. **WiFi response:** JSON object with `"hard"` and `"advisory"` integer arrays. No params. | +| `F17` — Location Types | `F17` | Returns all `LocationType` enum values as descriptors. Each entry has `id` (uint8), `type` (`boat` or `other`), and `desc` (human-readable name). **Serial response:** one line per descriptor as `F17:id=;type=;desc=` followed by `ACK:F17=ok`. **WiFi response:** JSON object with `"success"`, `"command"`, and `"locations"` array of `{"id", "type", "description"}` objects. No params. | **OTA behaviour (F12 / F13):** @@ -103,9 +104,9 @@ Example: `GET /api/system/F2` | `C18` — RTC Pins | `C18:dat=4;clk=5;rst=6` | Set DS1302 RTC pins. Use `255` for any pin not fitted. Call `C0` to persist. | | `C19` — Network Authentication | `C19:e=1;k=MyApiKey;h=MyHmacKey` or `C19:g=1` | Configure WiFi API authentication. **Params:** `e=<0\|1>` enable/disable auth (disabled by default). `k=` set API key (max 31 chars). `h=` set HMAC-SHA256 key (max 31 chars). `g=1` auto-generate device-unique keys from the WiFi MAC address. No params returns current state as `e=<0\|1>;k=;h=`. Call `C0` to persist. When enabled, all `/api/*` endpoints require either an `X-API-Key` header matching the configured key, or valid `X-Auth-Timestamp` + `X-Auth-Signature` HMAC headers. See [Network Authentication](#network-authentication) below. | | `C20` — Timezone Offset | `C20:v=-5` | Set UTC timezone offset in hours. Valid range: −12 to +14. | -| `C21` — MMSI | `C21:123456789` | Set 9-digit Maritime Mobile Service Identity. Value directly. | -| `C22` — Call Sign | `C22:ABCD123` | Set location call sign. Value directly, truncated to max length. | -| `C23` — Home Port | `C23:Miami` | Set location home port. Value directly, truncated to max length. | +| `C21` — MMSI | `C21:v=123456789` | Set 9-digit Maritime Mobile Service Identity. Value directly. | +| `C22` — Call Sign | `C22:v=ABCD123` | Set location call sign. Value directly, truncated to max length. | +| `C23` — Home Port | `C23:v=Miami` | Set location home port. Value directly, truncated to max length. | | `C24` — LED Color | `C24:t=0;c=0;r=255;g=50;b=213` | Set LED RGB color. `t`: `0`=day, `1`=night. `c`: `0`=good, `1`=bad. RGB values 0–255. | | `C25` — LED Brightness | `C25:t=0;b=75` | Set LED brightness 0–100. `t`: `0`=day, `1`=night. | | `C26` — LED Auto Switch | `C26:v=1` | Enable/disable automatic day/night LED switching. | diff --git a/PowerControlHub/ConfigController.cpp b/PowerControlHub/ConfigController.cpp index 08bf85d..4c8f2df 100644 --- a/PowerControlHub/ConfigController.cpp +++ b/PowerControlHub/ConfigController.cpp @@ -96,10 +96,6 @@ ConfigResult ConfigController::setLocationType(const uint8_t locationlType) { if (_config == nullptr) return ConfigResult::InvalidConfig; - - if (locationlType > static_cast(LocationType::Yacht)) - return ConfigResult::InvalidParameter; - _config->location.locationType = static_cast(locationlType); updateSoundControllerConfig(); return ConfigResult::Success; diff --git a/PowerControlHub/ConfigNetworkHandler.cpp b/PowerControlHub/ConfigNetworkHandler.cpp index d3b1187..ddb09c6 100644 --- a/PowerControlHub/ConfigNetworkHandler.cpp +++ b/PowerControlHub/ConfigNetworkHandler.cpp @@ -603,7 +603,16 @@ CommandResult ConfigNetworkHandler::handleRequest(const char* method, // C21 - Set MMSI if (paramCount >= 1) { - result = _configController->setMmsi(params[0].value); + const char* mmsi = getParamValue(params, paramCount, "v"); + + if (mmsi == nullptr || strlen(mmsi) != 9 || !SystemFunctions::isAllDigits(mmsi)) + { + result = ConfigResult::InvalidParameter; + } + else + { + result = _configController->setMmsi(mmsi); + } } else { @@ -615,7 +624,16 @@ CommandResult ConfigNetworkHandler::handleRequest(const char* method, // C22 - Set call sign if (paramCount >= 1) { - result = _configController->setCallSign(params[0].value); + const char* callSign = getParamValue(params, paramCount, "v"); + + if (callSign == nullptr || strlen(callSign) > ConfigCallSignLength) + { + result = ConfigResult::InvalidParameter; + } + else + { + result = _configController->setCallSign(callSign); + } } else { @@ -627,7 +645,16 @@ CommandResult ConfigNetworkHandler::handleRequest(const char* method, // C23 - Set home port if (paramCount >= 1) { - result = _configController->setHomePort(params[0].value); + const char* homePort = getParamValue(params, paramCount, "v"); + + if (homePort == nullptr || strlen(homePort) > ConfigHomePortLength) + { + result = ConfigResult::InvalidParameter; + } + else + { + result = _configController->setHomePort(homePort); + } } else { @@ -1005,6 +1032,7 @@ CommandResult ConfigNetworkHandler::handleRequest(const char* method, if (result == ConfigResult::Success) { + formatJsonResponse(responseBuffer, bufferSize, true, ""); return CommandResult::ok(); } diff --git a/PowerControlHub/LocationConfig.h b/PowerControlHub/LocationConfig.h new file mode 100644 index 0000000..76df03f --- /dev/null +++ b/PowerControlHub/LocationConfig.h @@ -0,0 +1,33 @@ +#pragma once +#include "Config.h" + +enum class LocationTypeSubDescriptor : uint8_t +{ + Boat = 0x00, + Other = 0x01 +}; + +struct LocationTypeDescriptor +{ + uint8_t id; + LocationTypeSubDescriptor subType; + const char* description; +}; + +// Indexed by LocationType enum value +constexpr LocationTypeDescriptor LocationTypeDescriptors[] = { + [static_cast(LocationType::Power)] = { static_cast(LocationType::Power), LocationTypeSubDescriptor::Boat, "Power boat" }, + [static_cast(LocationType::Sail)] = { static_cast(LocationType::Sail), LocationTypeSubDescriptor::Boat, "Sailing boat" }, + [static_cast(LocationType::Fishing)] = { static_cast(LocationType::Fishing), LocationTypeSubDescriptor::Boat, "Fishing boat" }, + [static_cast(LocationType::Yacht)] = { static_cast(LocationType::Yacht), LocationTypeSubDescriptor::Boat, "Yacht" }, + [static_cast(LocationType::Shed)] = { static_cast(LocationType::Shed), LocationTypeSubDescriptor::Other, "Shed" }, + [static_cast(LocationType::Basement)] = { static_cast(LocationType::Basement), LocationTypeSubDescriptor::Other, "Basement" }, + [static_cast(LocationType::Workshop)] = { static_cast(LocationType::Workshop), LocationTypeSubDescriptor::Other, "Workshop" }, + [static_cast(LocationType::Garage)] = { static_cast(LocationType::Garage), LocationTypeSubDescriptor::Other, "Garage" }, + [static_cast(LocationType::Bedroom)] = { static_cast(LocationType::Bedroom), LocationTypeSubDescriptor::Other, "Bedroom" }, + [static_cast(LocationType::Office)] = { static_cast(LocationType::Office), LocationTypeSubDescriptor::Other, "Office" }, + // Note: LocationType::Other has value 0xFF and is not included in the indexed array +}; + +static_assert(std::size(LocationTypeDescriptors) == static_cast(LocationType::Office) + 1, + "LocationTypeDescriptors must cover all LocationType enum values up to Office. Update descriptors when enum changes."); diff --git a/PowerControlHub/PowerControlHub.vcxproj b/PowerControlHub/PowerControlHub.vcxproj index 68d5d60..cc96c4d 100644 --- a/PowerControlHub/PowerControlHub.vcxproj +++ b/PowerControlHub/PowerControlHub.vcxproj @@ -1,4 +1,4 @@ - + @@ -177,6 +177,7 @@ + @@ -294,6 +295,14 @@ true + + + + + + + + VisualMicroDebugger diff --git a/PowerControlHub/PowerControlHub.vcxproj.filters b/PowerControlHub/PowerControlHub.vcxproj.filters index 5ba78ec..b599f8c 100644 --- a/PowerControlHub/PowerControlHub.vcxproj.filters +++ b/PowerControlHub/PowerControlHub.vcxproj.filters @@ -717,5 +717,16 @@ Header Files\NetworkCommandHandlers + + Header Files + + + + + + + + + \ No newline at end of file diff --git a/PowerControlHub/SystemCommandHandler.cpp b/PowerControlHub/SystemCommandHandler.cpp index 4a3e219..428893b 100644 --- a/PowerControlHub/SystemCommandHandler.cpp +++ b/PowerControlHub/SystemCommandHandler.cpp @@ -20,6 +20,7 @@ #include "ConfigManager.h" #include "PinGuard.h" #include "DateTimeManager.h" +#include "LocationConfig.h" #if defined(WIFI_SUPPORT) #include "WifiController.h" #endif @@ -43,7 +44,8 @@ const char* const* SystemCommandHandler::supportedCommands(size_t& count) const SystemHeartbeatCommand, SystemInitialized, SystemFreeMemory, SystemCpuUsage, SystemBluetoothStatus, SystemWifiStatus, SystemSetDateTime, SystemGetDateTime, SystemSdCardPresent, SystemSdCardLogFileSize, SystemRtcDiagnostic, SystemUptime, - SystemCheckForUpdate, SystemOtaStatus, SystemPinGuardMode, SystemPinUsage, SystemPinRestrictions + SystemCheckForUpdate, SystemOtaStatus, SystemPinGuardMode, SystemPinUsage, + SystemPinRestrictions, SystemLocationTypes }; count = sizeof(cmds) / sizeof(cmds[0]); return cmds; @@ -79,14 +81,14 @@ bool SystemCommandHandler::handleCommand(SerialCommandManager* sender, const cha } else if (SystemFunctions::commandMatches(command, SystemFreeMemory)) { - StringKeyValue param; + StringKeyValue param{}; strncpy(param.key, ValueParamName, sizeof(param.key)); snprintf_P(param.value, sizeof(param.value), PSTR("%u"), SystemFunctions::freeMemory()); sendAckOk(sender, command, ¶m); } else if (SystemFunctions::commandMatches(command, SystemCpuUsage)) { - StringKeyValue param; + StringKeyValue param{}; strncpy(param.key, ValueParamName, sizeof(param.key)); snprintf_P(param.value, sizeof(param.value), PSTR("%u"), SystemCpuMonitor::getCpuUsage()); sendAckOk(sender, command, ¶m); @@ -157,7 +159,7 @@ bool SystemCommandHandler::handleCommand(SerialCommandManager* sender, const cha if (success) { - StringKeyValue param; + StringKeyValue param{}; strncpy(param.key, ValueParamName, sizeof(param.key)); DateTimeManager::formatDateTime(param.value, sizeof(param.value)); sendAckOk(sender, command, ¶m); @@ -174,7 +176,7 @@ bool SystemCommandHandler::handleCommand(SerialCommandManager* sender, const cha { if (DateTimeManager::isTimeSet()) { - StringKeyValue param; + StringKeyValue param{}; strncpy(param.key, ValueParamName, sizeof(param.key)); DateTimeManager::formatDateTime(param.value, sizeof(param.value)); sendAckOk(sender, command, ¶m); @@ -211,7 +213,7 @@ bool SystemCommandHandler::handleCommand(SerialCommandManager* sender, const cha } #endif - StringKeyValue param; + StringKeyValue param{}; strncpy(param.key, ValueParamName, sizeof(param.key)); snprintf_P(param.value, sizeof(param.value), PSTR("%lu"), (unsigned long)fileSize); sendAckOk(sender, command, ¶m); @@ -221,7 +223,7 @@ bool SystemCommandHandler::handleCommand(SerialCommandManager* sender, const cha char diagnosticMsg[64]; bool success = DateTimeManager::rtcDiagnostic(diagnosticMsg, sizeof(diagnosticMsg)); - StringKeyValue param; + StringKeyValue param{}; strncpy(param.key, ValueParamName, sizeof(param.key)); strncpy(param.value, diagnosticMsg, sizeof(param.value)); @@ -236,7 +238,7 @@ bool SystemCommandHandler::handleCommand(SerialCommandManager* sender, const cha } else if (SystemFunctions::commandMatches(command, SystemUptime)) { - StringKeyValue param; + StringKeyValue param{}; strncpy(param.key, ValueParamName, sizeof(param.key)); TimeParts tp = SystemFunctions::msToTimeParts(SystemFunctions::millis64()); SystemFunctions::formatTimeParts(param.value, sizeof(param.value), tp); @@ -285,7 +287,7 @@ bool SystemCommandHandler::handleCommand(SerialCommandManager* sender, const cha } constexpr uint8_t argCount = 3; - StringKeyValue respParams[argCount]; + StringKeyValue respParams[argCount]{}; strncpy(respParams[0].key, "v", sizeof(respParams[0].key)); strncpy(respParams[0].value, current, sizeof(respParams[0].value)); strncpy(respParams[1].key, "av", sizeof(respParams[1].key)); @@ -350,7 +352,7 @@ bool SystemCommandHandler::handleCommand(SerialCommandManager* sender, const cha } constexpr uint8_t argCount = 4; - StringKeyValue respParams[argCount]; + StringKeyValue respParams[argCount]{}; strncpy(respParams[0].key, "v", sizeof(respParams[0].key)); strncpy(respParams[0].value, current, sizeof(respParams[0].value)); strncpy(respParams[1].key, "av", sizeof(respParams[1].key)); @@ -402,7 +404,7 @@ bool SystemCommandHandler::handleCommand(SerialCommandManager* sender, const cha // Read back current mode SystemHeader* hdr = ConfigManager::getHeaderPtr(); constexpr uint8_t argCount = 2; - StringKeyValue respParams[argCount]; + StringKeyValue respParams[argCount]{}; strncpy(respParams[0].key, "a", sizeof(respParams[0].key)); respParams[0].value[0] = (hdr && (hdr->pinGuardFlags & PinGuardMode::AllowAdvisory)) ? '1' : '0'; respParams[0].value[1] = '\0'; @@ -416,7 +418,7 @@ bool SystemCommandHandler::handleCommand(SerialCommandManager* sender, const cha uint8_t pins[64]; uint8_t count = SystemFunctions::getUsedPins(pins, sizeof(pins)); - StringKeyValue param; + StringKeyValue param{}; strncpy(param.key, ValueParamName, sizeof(param.key)); param.value[0] = '\0'; @@ -455,6 +457,26 @@ bool SystemCommandHandler::handleCommand(SerialCommandManager* sender, const cha sendAckOk(sender, command); return true; } + else if (SystemFunctions::commandMatches(command, SystemLocationTypes)) + { + // Send location type descriptors one per line for serial clients + size_t cnt = sizeof(LocationTypeDescriptors) / sizeof(LocationTypeDescriptors[0]); + for (size_t i = 0; i < cnt; ++i) + { + const auto& d = LocationTypeDescriptors[i]; + if (d.description == nullptr) + continue; + char buffer[128]; + snprintf(buffer, sizeof(buffer), "id=%u;type=%" PRIu8 ";desc=%s", + (unsigned)d.id, + static_cast(d.subType), + d.description); + sender->sendCommand(SystemLocationTypes, buffer); + } + + sendAckOk(sender, command); + return true; + } else { sendAckErr(sender, command, F("Unknown system command")); diff --git a/PowerControlHub/SystemDefinitions.h b/PowerControlHub/SystemDefinitions.h index 0022c0e..800db04 100644 --- a/PowerControlHub/SystemDefinitions.h +++ b/PowerControlHub/SystemDefinitions.h @@ -44,6 +44,7 @@ constexpr char SystemOtaStatus[] = "F13"; constexpr char SystemPinGuardMode[] = "F14"; constexpr char SystemPinUsage[] = "F15"; constexpr char SystemPinRestrictions[] = "F16"; +constexpr char SystemLocationTypes[] = "F17"; constexpr char SystemPageDemo[] = "F150"; constexpr char RelayTurnAllOff[] = "R0"; diff --git a/PowerControlHub/SystemNetworkHandler.cpp b/PowerControlHub/SystemNetworkHandler.cpp index 2ec0a63..9f49839 100644 --- a/PowerControlHub/SystemNetworkHandler.cpp +++ b/PowerControlHub/SystemNetworkHandler.cpp @@ -23,6 +23,8 @@ #include "SystemFunctions.h" #include "FirmwareVersion.h" #include "PinGuard.h" +#include "LocationConfig.h" +#include #if defined(OTA_AUTO_UPDATE) && defined(ESP32) && defined(WIFI_SUPPORT) #include "OtaManager.h" @@ -58,22 +60,30 @@ CommandResult SystemNetworkHandler::handleRequest(const char* method, formatStatusJson(responseBuffer, bufferSize); return CommandResult::ok(); } - else if (SystemFunctions::commandMatches(command, SystemPinUsage)) + else if (SystemFunctions::commandMatches(command, SystemLocationTypes)) { - uint8_t pins[64]; - uint8_t count = SystemFunctions::getUsedPins(pins, sizeof(pins)); - size_t pos = 0; int written = snprintf(responseBuffer + pos, bufferSize - pos, - "\"success\":true,\"command\":\"%s\",\"pins\":[", command); + "\"success\":true,\"command\":\"%s\",\"locations\":[", + command); if (written > 0) pos += (size_t)written; - for (uint8_t i = 0; i < count && pos < bufferSize; ++i) + size_t cnt = sizeof(LocationTypeDescriptors) / sizeof(LocationTypeDescriptors[0]); + + for (size_t i = 0; i < cnt && pos < bufferSize; ++i) { + const auto& d = LocationTypeDescriptors[i]; + if (d.description == nullptr) + continue; + written = snprintf(responseBuffer + pos, bufferSize - pos, - "%s%u", (i > 0) ? "," : "", (unsigned)pins[i]); + "%s{\"id\":%u,\"type\":\"%u\",\"description\":\"%s\"}", + (pos > 0 && responseBuffer[pos - 1] != '[') ? "," : "", + (unsigned)d.id, + static_cast(d.subType), + d.description); if (written > 0) pos += (size_t)written; @@ -89,86 +99,60 @@ CommandResult SystemNetworkHandler::handleRequest(const char* method, return CommandResult::ok(); } - else if (SystemFunctions::commandMatches(command, SystemPinRestrictions)) + else if (SystemFunctions::commandMatches(command, SystemSetDateTime)) { - uint8_t tableSize = PinGuard::getPinTableSize(); - uint8_t hardPins[64]; - uint8_t advisoryPins[64]; - uint8_t hardCount = 0; - uint8_t advisoryCount = 0; - - for (uint8_t i = 0; i < tableSize; ++i) - { - uint8_t pin; - PinCategory category; - PinGuard::getPinTableEntry(i, pin, category); - - if (category == PinCategory::Hard) - hardPins[hardCount++] = pin; - else if (category == PinCategory::Advisory) - advisoryPins[advisoryCount++] = pin; - } - - size_t pos = 0; - - int written = snprintf(responseBuffer + pos, bufferSize - pos, - "\"success\":true,\"command\":\"%s\"", command); - - if (written > 0) - pos += (size_t)written; - - // Hard pins array - if (hardCount > 0 && pos < bufferSize) + const char* tsStr = nullptr; + for (uint8_t i = 0; i < paramCount; ++i) { - written = snprintf(responseBuffer + pos, bufferSize - pos, ",\"hard\":["); - - if (written > 0) - pos += (size_t)written; - - for (uint8_t i = 0; i < hardCount && pos < bufferSize; ++i) + if (strcmp(params[i].key, ValueParamName) == 0) { - written = snprintf(responseBuffer + pos, bufferSize - pos, - "%s%u", (i > 0) ? "," : "", (unsigned)hardPins[i]); - - if (written > 0) - pos += (size_t)written; - else - break; + tsStr = params[i].value; + break; } - - if (pos < bufferSize) - responseBuffer[pos++] = ']'; } - // Advisory pins array - if (advisoryCount > 0 && pos < bufferSize) + if (tsStr) { - written = snprintf(responseBuffer + pos, bufferSize - pos, ",\"advisory\":["); - - if (written > 0) - pos += (size_t)written; - - for (uint8_t i = 0; i < advisoryCount && pos < bufferSize; ++i) + uint64_t timestamp = static_cast(strtoull(tsStr, nullptr, 0)); + if (timestamp > 0) { - written = snprintf(responseBuffer + pos, bufferSize - pos, - "%s%u", (i > 0) ? "," : "", (unsigned)advisoryPins[i]); - - if (written > 0) - pos += (size_t)written; - else - break; + DateTimeManager::setDateTime(timestamp); + char dateTimeStr[DateTimeBufferLength]; + DateTimeManager::formatDateTime(dateTimeStr, sizeof(dateTimeStr)); + snprintf(responseBuffer, bufferSize, + "\"success\":true,\"command\":\"%s\",\"v\":\"%s\"", + command, dateTimeStr); + } + else + { + snprintf(responseBuffer, bufferSize, + "\"success\":false,\"error\":\"Invalid timestamp\""); } - - if (pos < bufferSize) - responseBuffer[pos++] = ']'; } - - if (pos < bufferSize) - responseBuffer[pos] = '\0'; - + else + { + snprintf(responseBuffer, bufferSize, + "\"success\":false,\"error\":\"Missing v parameter\""); + } + return CommandResult::ok(); + } + else if (SystemFunctions::commandMatches(command, SystemGetDateTime)) + { + char dateTimeStr[DateTimeBufferLength]; + if (DateTimeManager::formatDateTime(dateTimeStr, sizeof(dateTimeStr))) + { + snprintf(responseBuffer, bufferSize, + "\"success\":true,\"command\":\"%s\",\"v\":\"%s\"", + command, dateTimeStr); + } + else + { + snprintf(responseBuffer, bufferSize, + "\"success\":false,\"error\":\"Date/time not set\""); + } return CommandResult::ok(); } - #if defined(OTA_AUTO_UPDATE) && defined(ESP32) && defined(WIFI_SUPPORT) +#if defined(OTA_AUTO_UPDATE) && defined(ESP32) && defined(WIFI_SUPPORT) else if (SystemFunctions::commandMatches(command, SystemCheckForUpdate)) { if (!_otaManager) @@ -290,57 +274,114 @@ CommandResult SystemNetworkHandler::handleRequest(const char* method, return CommandResult::ok(); } #endif // OTA_AUTO_UPDATE - else if (SystemFunctions::commandMatches(command, SystemGetDateTime)) + else if (SystemFunctions::commandMatches(command, SystemPinUsage)) { - char dateTimeStr[DateTimeBufferLength]; - if (DateTimeManager::formatDateTime(dateTimeStr, sizeof(dateTimeStr))) + uint8_t pins[64]; + uint8_t count = SystemFunctions::getUsedPins(pins, sizeof(pins)); + + size_t pos = 0; + int written = snprintf(responseBuffer + pos, bufferSize - pos, + "\"success\":true,\"command\":\"%s\",\"pins\":[", command); + + if (written > 0) + pos += (size_t)written; + + for (uint8_t i = 0; i < count && pos < bufferSize; ++i) { - snprintf(responseBuffer, bufferSize, - "\"success\":true,\"command\":\"%s\",\"v\":\"%s\"", - command, dateTimeStr); + written = snprintf(responseBuffer + pos, bufferSize - pos, + "%s%u", (i > 0) ? "," : "", (unsigned)pins[i]); + + if (written > 0) + pos += (size_t)written; + else + break; } - else + + if (pos < bufferSize) { - snprintf(responseBuffer, bufferSize, - "\"success\":false,\"error\":\"Date/time not set\""); + responseBuffer[pos++] = ']'; + responseBuffer[pos] = '\0'; } + return CommandResult::ok(); } - else if (SystemFunctions::commandMatches(command, SystemSetDateTime)) + else if (SystemFunctions::commandMatches(command, SystemPinRestrictions)) { - const char* tsStr = nullptr; - for (uint8_t i = 0; i < paramCount; ++i) + uint8_t tableSize = PinGuard::getPinTableSize(); + uint8_t hardPins[64]{}; + uint8_t advisoryPins[64]{}; + uint8_t hardCount = 0; + uint8_t advisoryCount = 0; + + for (uint8_t i = 0; i < tableSize; ++i) { - if (strcmp(params[i].key, ValueParamName) == 0) - { - tsStr = params[i].value; - break; - } + uint8_t pin; + PinCategory category; + PinGuard::getPinTableEntry(i, pin, category); + + if (category == PinCategory::Hard) + hardPins[hardCount++] = pin; + else if (category == PinCategory::Advisory) + advisoryPins[advisoryCount++] = pin; } - if (tsStr) + size_t pos = 0; + + int written = snprintf(responseBuffer + pos, bufferSize - pos, + "\"success\":true,\"command\":\"%s\"", command); + + if (written > 0) + pos += (size_t)written; + + // Hard pins array + if (hardCount > 0 && pos < bufferSize) { - uint64_t timestamp = static_cast(strtoull(tsStr, nullptr, 0)); - if (timestamp > 0) - { - DateTimeManager::setDateTime(timestamp); - char dateTimeStr[DateTimeBufferLength]; - DateTimeManager::formatDateTime(dateTimeStr, sizeof(dateTimeStr)); - snprintf(responseBuffer, bufferSize, - "\"success\":true,\"command\":\"%s\",\"v\":\"%s\"", - command, dateTimeStr); - } - else + written = snprintf(responseBuffer + pos, bufferSize - pos, ",\"hard\":["); + + if (written > 0) + pos += (size_t)written; + + for (uint8_t i = 0; i < hardCount && pos < bufferSize; ++i) { - snprintf(responseBuffer, bufferSize, - "\"success\":false,\"error\":\"Invalid timestamp\""); + written = snprintf(responseBuffer + pos, bufferSize - pos, + "%s%u", (i > 0) ? "," : "", (unsigned)hardPins[i]); + + if (written > 0) + pos += (size_t)written; + else + break; } + + if (pos < bufferSize) + responseBuffer[pos++] = ']'; } - else + + // Advisory pins array + if (advisoryCount > 0 && pos < bufferSize) { - snprintf(responseBuffer, bufferSize, - "\"success\":false,\"error\":\"Missing v parameter\""); + written = snprintf(responseBuffer + pos, bufferSize - pos, ",\"advisory\":["); + + if (written > 0) + pos += (size_t)written; + + for (uint8_t i = 0; i < advisoryCount && pos < bufferSize; ++i) + { + written = snprintf(responseBuffer + pos, bufferSize - pos, + "%s%u", (i > 0) ? "," : "", (unsigned)advisoryPins[i]); + + if (written > 0) + pos += (size_t)written; + else + break; + } + + if (pos < bufferSize) + responseBuffer[pos++] = ']'; } + + if (pos < bufferSize) + responseBuffer[pos] = '\0'; + return CommandResult::ok(); } else @@ -408,7 +449,7 @@ void SystemNetworkHandler::formatStatusJson(char* buffer, size_t size) void SystemNetworkHandler::formatWifiStatusJson(IWifiClient* client) { - char buffer[MaximumJsonResponseBufferSize]; + char buffer[MaximumJsonResponseBufferSize]{}; buffer[0] = '\0'; formatStatusJson(buffer, sizeof(buffer)); diff --git a/PowerControlHubApp/AppShell.xaml.cs b/PowerControlHubApp/AppShell.xaml.cs index 4423f15..c94fce7 100644 --- a/PowerControlHubApp/AppShell.xaml.cs +++ b/PowerControlHubApp/AppShell.xaml.cs @@ -17,6 +17,7 @@ public AppShell() Routing.RegisterRoute(nameof(NetworkSecurityPage), typeof(NetworkSecurityPage)); Routing.RegisterRoute(nameof(NextionSettingsPage), typeof(NextionSettingsPage)); Routing.RegisterRoute(nameof(XpdzToneSettingsPage), typeof(XpdzToneSettingsPage)); + Routing.RegisterRoute(nameof(LocationSettingsPage), typeof(LocationSettingsPage)); } } } diff --git a/PowerControlHubApp/Internal/Constants.cs b/PowerControlHubApp/Internal/Constants.cs index 091f649..1e33768 100644 --- a/PowerControlHubApp/Internal/Constants.cs +++ b/PowerControlHubApp/Internal/Constants.cs @@ -198,32 +198,41 @@ internal static class Constants public const string RouteSystemPinRestrictions = "api/system/F16"; public const string RouteSystemGetDateTime = "api/system/F7"; public const string RouteSystemSetDateTime = "api/system/F6"; + public const string RouteSystemLocationTypes = "api/system/F17"; public const string RouteConfigTimezoneOffset = "api/config/C20"; + public const string RouteConfigBoatType = "api/config/C7"; + public const string RouteConfigRename = "api/config/C3"; + public const string RouteConfigMmsi = "api/config/C21"; + public const string RouteConfigCallSign = "api/config/C22"; + public const string RouteConfigHomePort = "api/config/C23"; public const string RouteConfigSdCardSpiPins = "api/config/C4"; public const string RouteConfigSdCardInitSpeed = "api/config/C31"; public const string RouteConfigSdCardCsPin = "api/config/C32"; + + public const string SaveFailed = "Save failed — device unreachable"; + // 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 RouteLocationSettingsPage = "LocationSettingsPage"; 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; + // 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}"; public const string RouteNextionSettingsPage = "NextionSettingsPage"; - public const string NextionMsgSaveFailed = "Save failed — device unreachable"; public const string NextionMsgSaved = "Nextion settings saved"; public const string NextionMsgRefreshed = "Refreshed"; public const string NextionUart1 = "UART1"; @@ -240,6 +249,7 @@ internal static class Constants public const string NextionRouteN4 = "api/config/N4"; public const string NextionRouteN5 = "api/config/N5"; public const string NextionRouteN6 = "api/config/N6"; + // Nextion JSON property names (N0 response) public const string NextionJsonN1 = "n1"; public const string NextionJsonN2 = "n2"; @@ -259,11 +269,13 @@ internal static class Constants public const string NextionJsonTx = "tx"; public const string NextionJsonBaud = "baud"; public const string NextionJsonUart = "uart"; + public const string RouteConfigAuth = "api/config/C19"; public const string ConfigAuthEnabledParam = "e"; public const string ConfigAuthApiKeyParam = "k"; public const string ConfigAuthHmacKeyParam = "h"; public const string ConfigAuthGenerateParam = "g"; + public const string RouteConfigMqttGet = "api/mqtt/{0}"; public const string RouteConfigMqttSet = "api/mqtt/{0}?v={1}"; public const string MqttConfigEnabled = "M0"; @@ -278,9 +290,7 @@ internal static class Constants public const string MqttConfigDiscoveryPrefix = "M9"; public const string MqttConnectedLabel = "Connected"; public const string MqttDisconnectedLabel = "Disconnected"; - public const string MqttMsgSaveFailed = "Save failed — device unreachable"; public const string MqttMsgSaved = "MQTT settings saved"; - public const string SdCardMsgSaveFailed = "Save failed — device unreachable"; public const string SdCardMsgSaved = "SD Card settings saved"; public const string SdCardMsgPartiallySaved = "⚠ Partially saved — SPI pins rejected (check for pin conflicts)"; public const string SdCardMsgSpiFailed = "⚠ SPI pins not saved (pin conflict or invalid pin)"; @@ -290,7 +300,6 @@ internal static class Constants public const string SdCardConfigSpiPins = "C4"; public const string SdCardConfigInitSpeed = "C31"; public const string SdCardConfigCsPin = "C32"; - public const string NetworkSecurityMsgSaveFailed = "Save failed — device unreachable"; public const string NetworkSecurityMsgSaved = "Network security settings saved"; public const string NetworkSecurityMsgKeysGenerated = "New keys generated and saved"; public const string NetworkSecurityMsgRefreshed = "Refreshed"; @@ -527,6 +536,9 @@ internal static class Constants public const string LogStartupMetaPopulated = "Startup: sensor meta cache populated."; public const string LogStartupMetaAlready = "Startup: dashboard data already available, fetching sensor meta on connection 2."; + + public const string LocationTypeBoat = "boat"; + public const string NullByte = "0x00"; public const string NibbleZero = "0x0"; } diff --git a/PowerControlHubApp/MauiProgram.cs b/PowerControlHubApp/MauiProgram.cs index 06f8f66..42b6fd4 100644 --- a/PowerControlHubApp/MauiProgram.cs +++ b/PowerControlHubApp/MauiProgram.cs @@ -106,6 +106,7 @@ public static MauiApp CreateMauiApp() builder.Services.AddSingleton(); 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.AddSingleton(); 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 b53edec..af37aa9 100644 --- a/PowerControlHubApp/Models/Json/ConfigModel.cs +++ b/PowerControlHubApp/Models/Json/ConfigModel.cs @@ -82,5 +82,8 @@ public sealed class ConfigModel [JsonPropertyName("xpdzPin")] public int XpdzTonePin { get; set; } + + [JsonPropertyName("locationType")] + public int LocationType { get; set; } } } diff --git a/PowerControlHubApp/Models/Json/SystemLocationTypesResponseModel.cs b/PowerControlHubApp/Models/Json/SystemLocationTypesResponseModel.cs new file mode 100644 index 0000000..1eda2ad --- /dev/null +++ b/PowerControlHubApp/Models/Json/SystemLocationTypesResponseModel.cs @@ -0,0 +1,30 @@ +using PowerControlHubApp.Internal; +using System.Text.Json.Serialization; + +namespace PowerControlHubApp.Models.Json; + +public class LocationTypeModel +{ + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("type")] + public int Type { get; set; } = Constants.RtcPinDisabled; + + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + public bool IsBoat => Type == 0; +} + +public class SystemLocationTypesResponseModel +{ + [JsonPropertyName("success")] + public bool Success { get; set; } + + [JsonPropertyName("command")] + public string Command { get; set; } = string.Empty; + + [JsonPropertyName("locations")] + public List Locations { get; set; } = []; +} diff --git a/PowerControlHubApp/PowerControlHubApp.csproj b/PowerControlHubApp/PowerControlHubApp.csproj index 6b7febb..2936475 100644 --- a/PowerControlHubApp/PowerControlHubApp.csproj +++ b/PowerControlHubApp/PowerControlHubApp.csproj @@ -19,6 +19,7 @@ enable disable true + true + + + + + + + + + + + + + + + + + +