Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Docs/Commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,8 @@ WiFi commands return JSON (`{"success":true}` or `{"success":false,"message":"
| `R5` — Get All Relay Config | `R5` | Returns full relay config as a series of `R6`–`R11` responses. No params. |
| `R6` — Relay Rename | `R6:2=Bilge` or `R6:2=Bilge\|Bilge Pump` | Rename relay `idx`. Param: `<idx>=<shortName>` or `<idx>=<short\|long>`. Short ≤5 chars (home page), long ≤20 chars (buttons page). |
| `R7` — Set Button Color | `R7:2=4` or `R7:2=255` (clear) | Set active (on) button colour for relay `idx`. `0`=Blue, `1`=Green, `2`=Orange, `3`=Purple, `4`=Red, `5`=Yellow, `255`=clear. |

| `R7` — Set Button Color | `R7:2=4` or `R7:2=255` (clear) | Set active (on) button colour for relay `idx`. Accepts Nextion picture IDs (as stored on the device): `2`=Blue, `3`=Green, `4`=Grey, `5`=Orange, `6`=Red, `7`=Yellow, `255`=clear. |
| `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. |
Expand Down Expand Up @@ -336,7 +338,7 @@ Route: `/api/config/{command}` (shares the config route)

| Command | Example | Purpose |
|---|---|---|
| `S0` — Get All Sensor Config | `S0` | Returns one `S0` frame per configured sensor: `i=<idx>;t=<type>;n=<name>;p0=<pin>;p1=<pin>;u0=<opt1_0>;u1=<opt1_1>;o0=<opt2_0>;o1=<opt2_1>;en=<0\|1>`. The labels in the response are read-only aliases: `p0`/`p1` = `pins[0]`/`pins[1]`; `u0`/`u1` = `options1[0]`/`options1[1]` (signed byte); `o0`/`o1` = `options2[0]`/`options2[1]` (signed 16-bit). No params. |
| `S0` — Get All Sensor Config | `S0` or `S0?meta=1` | Returns one `S0` frame per configured sensor: `i=<idx>;t=<type>;n=<name>;p0=<pin>;p1=<pin>;u0=<opt1_0>;u1=<opt1_1>;o0=<opt2_0>;o1=<opt2_1>;en=<0\|1>`. The labels in the response are read-only aliases: `p0`/`p1` = `pins[0]`/`pins[1]`; `u0`/`u1` = `options1[0]`/`options1[1]` (signed byte); `o0`/`o1` = `options2[0]`/`options2[1]` (signed 16-bit). **WiFi only:** Append `?meta=1` to include a `"meta"` block with sensor type descriptors (field labels, types, ranges, defaults) for self-describing dynamic UI. No serial params. |
| `S1` — Add / Update Sensor | `S1:i=<idx>;t=<type>;o0=<val>;o1=<val>` | Add or update sensor at index `i`. **Only four params are accepted:** `i` (sensor index), `t` (type — `SensorIdList` enum value), `o0` (writes `options1[0]`), `o1` (writes `options1[1]`). Pins and all other options must be set separately via `S4` and `S6` **after** `S1`. All four params are required. ACK carries `reboot=1`. |
| `S2` — Remove Sensor | `S2:v=0` | Remove (clear) sensor at index. Param: `v=<idx>`. ACK carries `reboot=1`. |
| `S3` — Rename Sensor | `S3:0=Bilge Pump` | Rename sensor at index. Param: `<idx>=<name>`. ACK carries `reboot=1`. |
Expand Down
5 changes: 5 additions & 0 deletions Docs/PinGuard.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ Selected when `CONFIG_IDF_TARGET_ESP32S3` is defined by the toolchain.
| GPIO | Reason |
|---|---|
| 26, 27, 28, 29, 30, 31, 32 | Connected to internal Octal flash/PSRAM — any access causes immediate crash |
| 35, 36, 37 | Octal PSRAM control pins (CS, CK, DQS) — only blocked when `BOARD_HAS_PSRAM` or `CONFIG_SPIRAM_SUPPORT` is defined (i.e. PSRAM is fitted) |

#### Hard-blocked for output uses only (safe for input)

Expand All @@ -166,6 +167,10 @@ Selected when `CONFIG_IDF_TARGET_ESP32S3` is defined by the toolchain.
| 3 | UART0 RX |
| 19 | USB D− |
| 20 | USB D+ |
| 39 | JTAG TMS — usable as GPIO if JTAG disabled in eFuse |
| 40 | JTAG TCK — usable as GPIO if JTAG disabled in eFuse |
| 41 | JTAG TDI — usable as GPIO if JTAG disabled in eFuse |
| 42 | JTAG TDO — usable as GPIO if JTAG disabled in eFuse |
| 45 | Boot-mode strapping |
| 46 | Boot-mode strapping |

Expand Down
14 changes: 14 additions & 0 deletions Docs/Pins.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,20 @@ GPIO34, 35, 36 (SVP), and 39 (SVN) are **input-only** — they have no output dr
| **1, 3** | UART0 TX/RX — used by `Serial` (the USB-UART debug port). Do not reassign unless `Serial` is not in use. |
| **34–39** | Input-only — cannot drive outputs. Safe for analog/digital sensor reads only. |

> **ESP32-S3 note:** GPIO 34–38 are output-capable on the S3. However, if PSRAM is fitted (N8 variant), GPIO **35, 36, 37** are consumed by the Octal PSRAM interface and must not be used for any purpose — see the S3-specific rows below.

#### ESP32-S3 only — PSRAM (conditionally blocked)

| GPIO(s) | Reason |
|---|---|
| **35, 36, 37** | ⛔ Octal PSRAM control pins (CS, CK, DQS). Only blocked when PSRAM is enabled at compile time (`BOARD_HAS_PSRAM` / `CONFIG_SPIRAM_SUPPORT`). On non-PSRAM S3 boards these pins are safe. |

#### ESP32-S3 only — JTAG (advisory)

| GPIO(s) | Reason |
|---|---|
| **39, 40, 41, 42** | ⚠️ USB JTAG (TMS, TCK, TDI, TDO). Can be used as GPIO if JTAG is disabled in eFuse, but risky otherwise. |

---

## 5. Porting Caveats
Expand Down
2 changes: 0 additions & 2 deletions PowerControlHub/ConfigNetworkHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,6 @@ CommandResult ConfigNetworkHandler::handleRequest(const char* method,
}
else
{
if (color < DefaultValue)
color += 2;
result = (_relayController->setButtonColor(relayIndex, color) == RelayResult::Success)
? ConfigResult::Success : ConfigResult::Failed;
}
Expand Down
15 changes: 15 additions & 0 deletions PowerControlHub/PinGuard.h
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,13 @@ struct PinTableEntry
// ── ESP32-S3 Dev Module ───────────────────────────────────────────────────────
// Hard blocks
// GPIO 26–32 : connected to internal Octal flash/PSRAM — hard for ALL uses
// GPIO 35–37 : Octal PSRAM control pins (CS, CK, DQS) — hard for ALL uses
// only when PSRAM is fitted (BOARD_HAS_PSRAM / CONFIG_SPIRAM_SUPPORT)
// GPIO 45–46 : input-only on S3 silicon (promoted to Hard for output uses)
// Advisory
// GPIO 0,3,19,20,45,46 : strapping / USB / UART0
// GPIO 39–42 : USB JTAG (TMS, TCK, TDI, TDO) — usable as GPIO if JTAG is
// disabled in eFuse, but risky otherwise
static const PinTableEntry _pinTable[] PROGMEM = {
// Flash/PSRAM-reserved — hard for ALL uses
{ 26, PinCategory::Hard },
Expand All @@ -134,6 +138,12 @@ static const PinTableEntry _pinTable[] PROGMEM = {
{ 30, PinCategory::Hard },
{ 31, PinCategory::Hard },
{ 32, PinCategory::Hard },
// Octal PSRAM control pins (CS, CK, DQS) — hard for ALL uses when PSRAM fitted
#if defined(BOARD_HAS_PSRAM) || defined(CONFIG_SPIRAM_SUPPORT)
{ 35, PinCategory::Hard },
{ 36, PinCategory::Hard },
{ 37, PinCategory::Hard },
#endif
// Input-only on S3 — Advisory here; validate() promotes to Hard for output
{ 45, PinCategory::Advisory },
{ 46, PinCategory::Advisory },
Expand All @@ -142,6 +152,11 @@ static const PinTableEntry _pinTable[] PROGMEM = {
{ 3, PinCategory::Advisory }, // UART0 RX
{ 19, PinCategory::Advisory }, // USB D-
{ 20, PinCategory::Advisory }, // USB D+
// USB JTAG — advisory for all uses (can be repurposed if JTAG fused out)
{ 39, PinCategory::Advisory }, // JTAG TMS
{ 40, PinCategory::Advisory }, // JTAG TCK
{ 41, PinCategory::Advisory }, // JTAG TDI
{ 42, PinCategory::Advisory }, // JTAG TDO
};
static constexpr uint8_t _pinTableSize = sizeof(_pinTable) / sizeof(_pinTable[0]);

Expand Down
9 changes: 6 additions & 3 deletions PowerControlHub/PowerControlHub.vcxproj

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions PowerControlHub/PowerControlHub.vcxproj.filters
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,9 @@
<Filter>Header Files\Components</Filter>
</ClCompile>
<ClCompile Include="PowerControlHub.ino" />
<ClCompile Include="SensorConfigNetworkHandler.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="SoundCommandHandler.h">
Expand Down Expand Up @@ -708,5 +711,11 @@
<ClInclude Include="ChunkedWifiClient.h">
<Filter>Header Files\Wifi</Filter>
</ClInclude>
<ClInclude Include="SensorConfigNetworkHandler.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="SensorConfig.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
</Project>
3 changes: 2 additions & 1 deletion PowerControlHub/PowerControlHubApp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ void PowerControlHubApp::configureWifiSupport(Config* config)
// network command handlers
INetworkCommandHandler* networkHandlers[] = { &_relayNetworkHandler, &_soundNetworkHandler, &_warningNetworkHandler,
&_systemNetworkHandler, _sensorNetworkHandler, &_configNetworkHandler, &_schedulerNetworkHandler,
&_externalSensorNetworkHandler, _webIndexNetworkHandler,
&_externalSensorNetworkHandler, &_sensorConfigNetworkHandler, _webIndexNetworkHandler,
&_wifiCommandBridge
};
size_t networkHandlerCount = sizeof(networkHandlers) / sizeof(networkHandlers[0]);
Expand All @@ -486,6 +486,7 @@ void PowerControlHubApp::configureWifiSupport(Config* config)
_sensorNetworkHandler,
&_schedulerNetworkHandler,
&_externalSensorNetworkHandler,
&_sensorConfigNetworkHandler,
};
uint8_t jsonVisitorCount = sizeof(jsonVisitors) / sizeof(jsonVisitors[0]);
_wifiController.registerJsonVisitors(jsonVisitors, jsonVisitorCount);
Expand Down
2 changes: 2 additions & 0 deletions PowerControlHub/PowerControlHubApp.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
#include "NextionConfigCommandHandler.h"
#include "ExternalSensorConfigCommandHandler.h"
#include "ExternalSensorNetworkHandler.h"
#include "SensorConfigNetworkHandler.h"
#include "BluetoothRadioBridge.h"
#include "IBluetoothRadio.h"
#include "WifiController.h"
Expand Down Expand Up @@ -156,6 +157,7 @@ class PowerControlHubApp
SchedulerCommandHandler _schedulerCommandHandler;
SchedulerNetworkHandler _schedulerNetworkHandler;
ExternalSensorNetworkHandler _externalSensorNetworkHandler;
SensorConfigNetworkHandler _sensorConfigNetworkHandler;
WifiCommandBridge _wifiCommandBridge;

// Persistent storage for the serial handler array so WifiCommandBridge
Expand Down
2 changes: 0 additions & 2 deletions PowerControlHub/RelayNetworkHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,6 @@ CommandResult RelayNetworkHandler::handleRequest(const char* method,
formatJsonResponse(responseBuffer, bufferSize, false, "Invalid relay index");
return CommandResult::error(InvalidCommandParameters);
}
if (color < DefaultValue)
color += 2;
_relayController->setButtonColor(relayIndex, color);
formatJsonResponse(responseBuffer, bufferSize, true);
return CommandResult::ok();
Expand Down
122 changes: 122 additions & 0 deletions PowerControlHub/SensorConfig.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#include "SystemDefinitions.h"

struct SensorFieldDescriptor {
const char label[20]; // e.g. "Analogue Pin", "Threshold"
const char type[20]; // "gpio", "int8", "int16", "none"
int16_t minVal;
int16_t maxVal;
int16_t defaultValue;
};

struct SensorTypeDescriptor {
const char* name;
SensorFieldDescriptor pins[ConfigMaxSensorPins];
SensorFieldDescriptor options1[2];
SensorFieldDescriptor options2[2];
};

// Indexed by SensorIdList value
constexpr SensorTypeDescriptor SensorDescriptors[] = {
[static_cast<size_t>(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 },
},
.options1 = { { "Unused", "none", 0, 0, 0 }, { "Unused", "none", 0, 0, 0 } },
.options2 = { { "Unused", "none", 0, 0, 0 }, { "Unused", "none", 0, 0, 0 } },
},
[static_cast<size_t>(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 },
},
.options1 = { { "Unused", "none", 0, 0, 0 }, { "Unused", "none", 0, 0, 0 } },
.options2 = { { "Unused", "none", 0, 0, 0 }, { "Unused", "none", 0, 0, 0 } },
},
[static_cast<size_t>(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 },
},
.options1 = {
{ "Mode", "int8", 0, 1, 0 },
{ "Unused", "none", 0, 0, 0 },
},
.options2 = {
{ "Threshold", "int16", 0, 4095, 512 },
{ "Unused", "none", 0, 0, 0 },
},
},
[static_cast<size_t>(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 },
},
.options1 = {
{ "UART Num", "int8", 1, 2, 2 },
{ "Unused", "none", 0, 0, 0 },
},
.options2 = { { "Unused", "none", 0, 0, 0 }, { "Unused", "none", 0, 0, 0 } },
},
[static_cast<size_t>(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 },
},
.options1 = { { "Unused", "none", 0, 0, 0 }, { "Unused", "none", 0, 0, 0 } },
.options2 = { { "Unused", "none", 0, 0, 0 }, { "Unused", "none", 0, 0, 0 } },
},
[static_cast<size_t>(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 },
},
.options1 = {
{ "Active State", "int8", 0, 1, 1 },
{ "Detect Action", "int8", 0, 255, 0 },
},
.options2 = {
{ "Unused", "none", 0, 0, 0 },
{ "Clear Action", "int16", 0, 255, 0 },
},
},
[static_cast<size_t>(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 },
},
.options1 = {
{ "ADC Vref", "int8", 0, 50, 50 },
{ "R2 (tenths)", "int8", 0, 255, 75 },
},
.options2 = {
{ "R1 (kOhm)", "int16", 0, 1000, 30 },
{ "Low Warn (Vx10)", "int16", 0, 500, 0 },
},
},
};

static_assert(
std::size(SensorDescriptors) == static_cast<size_t>(SensorIdList::Count),
"SensorDescriptors missing an entry for a SensorIdList value!");
Loading
Loading