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
46 changes: 26 additions & 20 deletions PowerControlHub/Dht11SensorHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;

Expand All @@ -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;
}
Expand All @@ -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;
}

Expand Down
100 changes: 51 additions & 49 deletions PowerControlHub/SensorConfig.h
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
#include "SystemDefinitions.h"
#include "PinGuard.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;
PinUse pinUse; // intended use for GPIO slots (PinUse::Sensor for non-GPIO slots)
};

struct SensorTypeDescriptor {
Expand All @@ -20,99 +22,99 @@ 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 },
{ "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<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 },
{ "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<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 },
{ "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<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 },
{ "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<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 },
{ "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<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 },
{ "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<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 },
{ "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 },
},
},
};
Expand Down
27 changes: 25 additions & 2 deletions PowerControlHub/SensorConfigNetworkHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<uint8_t>(config->sensors.sensors[idx].sensorType);
bool isGpioSlot = false;
PinUse intendedUse = PinUse::Sensor;

if (sensorType < static_cast<uint8_t>(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))
Expand Down
23 changes: 23 additions & 0 deletions PowerControlHub/SensorNetworkHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "SystemFunctions.h"
#include "ConfigManager.h"
#include "SensorConfig.h"
#include "PinGuard.h"


SensorNetworkHandler::SensorNetworkHandler(SensorController* sensorController)
Expand Down Expand Up @@ -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<uint8_t>(config->sensors.sensors[idx].sensorType);
bool isGpioSlot = false;
PinUse intendedUse = PinUse::Sensor;

if (sensorType < static_cast<uint8_t>(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();
Expand Down
7 changes: 7 additions & 0 deletions PowerControlHubApp/Internal/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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)";
Expand Down
Loading
Loading