Skip to content

Instantly share code, notes, and snippets.

@pral2a
Created February 4, 2026 08:27
Show Gist options
  • Select an option

  • Save pral2a/2919ad436d2d370d3428dcb2200f9f77 to your computer and use it in GitHub Desktop.

Select an option

Save pral2a/2919ad436d2d370d3428dcb2200f9f77 to your computer and use it in GitHub Desktop.
/*
LLUM 2026 # Underlight Light Controller
ESP32-WROOM + HC-SR04 (x6) + Relays (x6) + Web UI + Persistent config + Debug logger
*/
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <HCSR04.h>
#include <Preferences.h>
struct SensorFilter;
// ============================================================
// Debug Logger
// ============================================================
class DebugLog {
public:
enum Level : uint8_t { ERROR=0, WARN=1, INFO=2, DEBUG=3, VERBOSE=4 };
void begin(HardwareSerial &s, Level lvl) { serial=&s; level=lvl; }
void setLevel(Level lvl) { level=lvl; }
Level getLevel() const { return level; }
void error(const String &m){ log(ERROR,"ERR",m); }
void warn(const String &m){ log(WARN,"WRN",m); }
void info(const String &m){ log(INFO,"INF",m); }
void debug(const String &m){ log(DEBUG,"DBG",m); }
void verbose(const String &m){ log(VERBOSE,"VRB",m); }
private:
HardwareSerial *serial=nullptr;
Level level=INFO;
void log(Level l,const char* tag,const String &msg){
if(!serial) return;
if(l>level) return;
serial->print("["); serial->print(millis()); serial->print("] ");
serial->print(tag); serial->print(": ");
serial->println(msg);
}
};
DebugLog LOG;
// ============================================================
// Wi-Fi config (your provided values)
// ============================================================
static const char* WIFI_SSID = "underlight";
static const char* WIFI_PASS = "stationiris";
static IPAddress LOCAL_IP(192, 168, 1, 50);
static IPAddress GATEWAY(192, 168, 1, 1);
static IPAddress SUBNET(255, 255, 255, 0);
static IPAddress DNS1(1, 1, 1, 1);
static IPAddress DNS2(8, 8, 8, 8);
// ============================================================
// System sizing
// ============================================================
static const uint8_t MAX_CHANNELS = 6;
static const uint8_t NUM_SENSORS = 6; // set 1..6
static const uint8_t NUM_RELAYS = 6; // set 1..6 (can be <= NUM_SENSORS)
// ============================================================
// HC-SR04 pins (your provided values)
// ============================================================
static const int TRIG_PIN = 13;
static int ECHO_PINS[MAX_CHANNELS] = { 14, 27, 26, 25, 33, 32 };
// ============================================================
// Relay pins (your provided values)
// ============================================================
static const bool RELAY_ACTIVE_HIGH = true;
static const int RELAY_PINS[MAX_CHANNELS] = { 15, 2, 4, 16, 17, 5 };
// ============================================================
// Animation pattern (10 steps => 1s total @100ms)
// ============================================================
static const uint16_t ANIM_STEP_MS = 100;
static const bool ANIM_PATTERN[10] = { 1,0,1,0,1,1,0,1,1,1 };
// ============================================================
// Timing
// ============================================================
static const uint16_t SENSOR_CYCLE_DEFAULT_MS = 500; // >=60ms recommended
static const uint16_t WIFI_CHECK_MS = 2000;
// ============================================================
// Persistence (EEPROM-like) via Preferences/NVS
// ============================================================
Preferences prefs;
static const char* PREFS_NS = "barrier";
static const uint32_t CFG_MAGIC = 0xBADC0DE1;
struct Config {
uint32_t magic;
// Sensing window
float minCm;
float maxCm;
float hystCm;
// Filtering
uint8_t avgWindow; // 1..12 (we cap)
uint8_t confirmSamples; // 1..10 (we cap)
// Behaviour
bool autoMode;
bool animationsEnabled;
// Sampling cadence
uint16_t sensorCycleMs; // 60..300 typically
// Debug
uint8_t logLevel; // 0..4
};
Config cfg;
static void setDefaultConfig() {
cfg.magic = CFG_MAGIC;
cfg.minCm = 1.0f;
cfg.maxCm = 16.0f;
cfg.hystCm = 3.0f;
cfg.avgWindow = 8;
cfg.confirmSamples = 3;
cfg.autoMode = true;
cfg.animationsEnabled = true;
cfg.sensorCycleMs = SENSOR_CYCLE_DEFAULT_MS;
cfg.logLevel = DebugLog::INFO;
}
static void clampConfig() {
// Keep values sane for a robust installation
if (cfg.minCm < 1.0f) cfg.minCm = 1.0f;
if (cfg.maxCm < cfg.minCm + 1.0f) cfg.maxCm = cfg.minCm + 1.0f;
if (cfg.hystCm < 0.0f) cfg.hystCm = 0.0f;
if (cfg.hystCm > 30.0f) cfg.hystCm = 30.0f;
if (cfg.avgWindow < 1) cfg.avgWindow = 1;
if (cfg.avgWindow > 12) cfg.avgWindow = 12;
if (cfg.confirmSamples < 1) cfg.confirmSamples = 1;
if (cfg.confirmSamples > 10) cfg.confirmSamples = 10;
if (cfg.sensorCycleMs < 60) cfg.sensorCycleMs = 60;
if (cfg.sensorCycleMs > 300) cfg.sensorCycleMs = 300;
if (cfg.logLevel > DebugLog::VERBOSE) cfg.logLevel = DebugLog::INFO;
}
static bool loadConfig() {
prefs.begin(PREFS_NS, true);
size_t len = prefs.getBytesLength("cfg");
if (len != sizeof(Config)) {
prefs.end();
return false;
}
Config tmp;
size_t got = prefs.getBytes("cfg", &tmp, sizeof(Config));
prefs.end();
if (got != sizeof(Config)) return false;
if (tmp.magic != CFG_MAGIC) return false;
cfg = tmp;
clampConfig();
return true;
}
static bool saveConfig() {
clampConfig();
prefs.begin(PREFS_NS, false);
size_t put = prefs.putBytes("cfg", &cfg, sizeof(Config));
prefs.end();
return (put == sizeof(Config));
}
static void factoryResetConfig() {
prefs.begin(PREFS_NS, false);
prefs.clear();
prefs.end();
}
// ============================================================
// Globals
// ============================================================
WebServer server(80);
HCSR04 hc(TRIG_PIN, ECHO_PINS, NUM_SENSORS);
static bool relayTarget[MAX_CHANNELS] = {0};
static bool relayActual[MAX_CHANNELS] = {0};
struct RelayAnim {
bool animating=false;
uint8_t step=0;
int8_t dir=1;
uint32_t nextTickMs=0;
};
static RelayAnim rAnim[MAX_CHANNELS];
static uint32_t lastSensorSampleMs = 0;
static uint32_t lastWifiCheckMs = 0;
static wl_status_t lastWifiStatus = WL_IDLE_STATUS;
// ============================================================
// Sensor filtering state (rolling average implemented with fixed max buffer)
// We store up to 12 samples per sensor; cfg.avgWindow chooses how many are used.
// ============================================================
static const uint8_t AVG_WINDOW_MAX = 12;
struct SensorFilter {
float buf[AVG_WINDOW_MAX];
uint8_t idx=0;
uint8_t count=0;
float sum=0.0f;
float avgCm=0.0f;
bool state=false;
uint8_t confirm=0;
};
static SensorFilter sFilt[MAX_CHANNELS];
// ============================================================
// Helpers
// ============================================================
static inline bool isValidReading(float cm) { return (cm > 0.0f); }
static float updateRollingAverage(SensorFilter &sf, float sample) {
uint8_t W = cfg.avgWindow;
if (W < 1) W = 1;
if (W > AVG_WINDOW_MAX) W = AVG_WINDOW_MAX;
// If buffer isn't full yet, we grow until count==W
if (sf.count < W) {
sf.buf[sf.idx] = sample;
sf.sum += sample;
sf.count++;
} else {
// Full window: subtract the element we overwrite
sf.sum -= sf.buf[sf.idx];
sf.buf[sf.idx] = sample;
sf.sum += sample;
}
sf.idx = (sf.idx + 1) % W;
sf.avgCm = sf.sum / (float)sf.count;
return sf.avgCm;
}
static bool desiredStateFromAvg(bool currentState, float avgCm) {
// Hysteresis around [min..max]
if (!currentState) {
return (avgCm >= (cfg.minCm + cfg.hystCm) && avgCm <= (cfg.maxCm - cfg.hystCm));
} else {
return (avgCm >= (cfg.minCm - cfg.hystCm) && avgCm <= (cfg.maxCm + cfg.hystCm));
}
}
static void updateStableSensorState(struct SensorFilter &sf, bool desired) {
if (desired == sf.state) {
sf.confirm = 0;
return;
}
sf.confirm++;
if (sf.confirm >= cfg.confirmSamples) {
sf.state = desired;
sf.confirm = 0;
}
}
static void applyRelayPin(uint8_t i, bool logicalOn) {
bool pinLevel = RELAY_ACTIVE_HIGH ? logicalOn : !logicalOn;
digitalWrite(RELAY_PINS[i], pinLevel ? LOW : HIGH);
if (relayActual[i] != logicalOn) {
relayActual[i] = logicalOn;
LOG.info(String("Relay ") + String(i + 1) + (logicalOn ? " ON" : " OFF"));
}
}
static void startRelayAnimation(uint8_t i, bool fromState, bool toState) {
if (fromState == toState) return;
rAnim[i].animating = true;
rAnim[i].step = toState ? 0 : 9;
rAnim[i].dir = toState ? +1 : -1;
rAnim[i].nextTickMs = millis();
LOG.debug(String("Relay ") + String(i + 1) + " animation start");
}
static void updateRelayAnimations() {
uint32_t now = millis();
for (uint8_t i = 0; i < NUM_RELAYS; i++) {
if (!rAnim[i].animating) continue;
if ((int32_t)(now - rAnim[i].nextTickMs) < 0) continue;
applyRelayPin(i, ANIM_PATTERN[rAnim[i].step]);
int next = (int)rAnim[i].step + (int)rAnim[i].dir;
if (next < 0 || next > 9) {
rAnim[i].animating = false;
applyRelayPin(i, relayTarget[i]);
LOG.debug(String("Relay ") + String(i + 1) + " animation end");
} else {
rAnim[i].step = (uint8_t)next;
rAnim[i].nextTickMs = now + ANIM_STEP_MS;
}
}
}
static void updateRelaysNonBlocking() {
for (uint8_t i = 0; i < NUM_RELAYS; i++) {
if (rAnim[i].animating) continue;
if (relayActual[i] != relayTarget[i]) {
if (cfg.animationsEnabled) startRelayAnimation(i, relayActual[i], relayTarget[i]);
else applyRelayPin(i, relayTarget[i]);
}
}
}
static void sampleSensors() {
for (uint8_t i = 0; i < NUM_SENSORS; i++) {
float cm = hc.dist(i);
delay(cfg.sensorCycleMs/NUM_SENSORS);
if (isValidReading(cm)) {
float avg = updateRollingAverage(sFilt[i], cm);
bool desired = desiredStateFromAvg(sFilt[i].state, avg);
bool before = sFilt[i].state;
updateStableSensorState(sFilt[i], desired);
if (before != sFilt[i].state) {
LOG.debug(String("Sensor ") + String(i + 1) +
" -> " + (sFilt[i].state ? "ON" : "OFF") +
" (avg " + String(sFilt[i].avgCm, 1) + "cm)");
}
} else {
LOG.verbose(String("Sensor ") + String(i + 1) + " invalid");
}
}
}
static void mapSensorsToRelaysIfAuto() {
if (!cfg.autoMode) return;
for (uint8_t i = 0; i < NUM_RELAYS; i++) {
relayTarget[i] = (i < NUM_SENSORS) ? sFilt[i].state : false;
}
}
// ============================================================
// Wi-Fi robustness + logging
// ============================================================
static const char* wifiStatusStr(wl_status_t st) {
switch (st) {
case WL_CONNECTED: return "CONNECTED";
case WL_DISCONNECTED: return "DISCONNECTED";
case WL_CONNECT_FAILED: return "CONNECT_FAILED";
case WL_CONNECTION_LOST: return "CONNECTION_LOST";
case WL_NO_SSID_AVAIL: return "NO_SSID";
default: return "OTHER";
}
}
static void ensureWifi() {
uint32_t now = millis();
if (now - lastWifiCheckMs < WIFI_CHECK_MS) return;
lastWifiCheckMs = now;
wl_status_t st = WiFi.status();
if (st != lastWifiStatus) {
lastWifiStatus = st;
String msg = String("WiFi status: ") + wifiStatusStr(st);
if (st == WL_CONNECTED) {
msg += " | IP " + WiFi.localIP().toString();
msg += " | RSSI " + String(WiFi.RSSI());
}
LOG.info(msg);
}
if (st == WL_CONNECTED) return;
LOG.warn("WiFi reconnect...");
WiFi.disconnect(false);
WiFi.begin(WIFI_SSID, WIFI_PASS);
}
// ============================================================
// Web UI helpers
// ============================================================
static String checked(bool v) { return v ? "checked" : ""; }
static String selected(bool v) { return v ? "selected" : ""; }
static String renderPage() {
String page;
page.reserve(9000);
page += F("<!doctype html><html><head><meta charset='utf-8'>");
page += F("<meta name='viewport' content='width=device-width,initial-scale=1'>");
page += F("<title>LLUM26 # Underlight Light Control</title>");
page += F("<style>");
page += F("body{font-family:monospace;margin:16px;max-width:980px}");
page += F("table{border-collapse:collapse;width:100%;margin:10px 0}");
page += F("td,th{border:1px solid #ccc;padding:6px;text-align:center}");
page += F(".muted{color:#666}");
page += F("input[type=number]{width:90px}");
page += F("fieldset{border:1px solid #ccc;padding:10px;margin:10px 0}");
page += F("legend{padding:0 6px}");
page += F("</style></head><body>");
page += F("<h3>LLUM26 # Underlight Light Control</h3>");
page += F("<div class='muted'>IP: ");
page += WiFi.localIP().toString();
page += F(" | Wi-Fi: ");
page += (WiFi.status() == WL_CONNECTED) ? F("CONNECTED") : F("DISCONNECTED");
page += F(" | Mode: ");
page += cfg.autoMode ? F("AUTO") : F("MANUAL");
page += F(" | Anim: ");
page += cfg.animationsEnabled ? F("ON") : F("OFF");
page += F("</div>");
// Inputs table (auto-refreshed via JS)
page += F("<h4>Inputs (Sensors)</h4>");
page += F("<table id='sTable'><tr><th>#</th>");
for (uint8_t i=0; i<NUM_SENSORS; i++) { page += "<th>" + String(i+1) + "</th>"; }
page += F("</tr><tr><th>Status</th>");
for (uint8_t i=0; i<NUM_SENSORS; i++) { page += "<td id='ss" + String(i) + "'>...</td>"; }
page += F("</tr><tr><th>Avg cm</th>");
for (uint8_t i=0; i<NUM_SENSORS; i++) { page += "<td id='sc" + String(i) + "'>...</td>"; }
page += F("</tr></table>");
// Outputs: manual toggles (still useful for testing)
page += F("<h4>Outputs (Relays)</h4>");
page += F("<form method='GET' action='/set'>");
page += F("<fieldset><legend>Mode</legend>");
page += F("<label><input type='checkbox' name='auto' value='1' ");
page += checked(cfg.autoMode);
page += F("> Automatic (unchecked = Manual)</label><br>");
page += F("<label><input type='checkbox' name='anim' value='1' ");
page += checked(cfg.animationsEnabled);
page += F("> Enable transitions</label>");
page += F("</fieldset>");
page += F("<table><tr><th>#</th>");
for (uint8_t i=0; i<NUM_RELAYS; i++) page += "<th>" + String(i+1) + "</th>";
page += F("</tr><tr><th>Target</th>");
for (uint8_t i=0; i<NUM_RELAYS; i++) {
page += F("<td><input type='checkbox' name='r");
page += String(i+1);
page += F("' value='1' ");
if (relayTarget[i]) page += F("checked ");
if (cfg.autoMode) page += F("disabled ");
page += F("></td>");
}
page += F("</tr><tr><th>Actual</th>");
for (uint8_t i=0; i<NUM_RELAYS; i++) {
page += F("<td>");
page += relayActual[i] ? F("ON") : F("OFF");
page += F("</td>");
}
page += F("</tr></table>");
page += F("<button type='submit'>Apply</button>");
page += F("</form>");
// Config form (persisted)
page += F("<h4>Configuration</h4>");
page += F("<form method='POST' action='/config'>");
page += F("<fieldset><legend>Window (cm)</legend>");
page += F("MIN <input type='number' step='0.1' name='min' value='"); page += String(cfg.minCm,1); page += F("'> ");
page += F("MAX <input type='number' step='0.1' name='max' value='"); page += String(cfg.maxCm,1); page += F("'> ");
page += F("HYST <input type='number' step='0.1' name='hyst' value='"); page += String(cfg.hystCm,1); page += F("'> ");
page += F("</fieldset>");
page += F("<fieldset><legend>Filtering</legend>");
page += F("Avg window <input type='number' name='avg' min='1' max='12' value='"); page += String(cfg.avgWindow); page += F("'> ");
page += F("Confirm samples <input type='number' name='conf' min='1' max='10' value='"); page += String(cfg.confirmSamples); page += F("'> ");
page += F("</fieldset>");
page += F("<fieldset><legend>Timing</legend>");
page += F("Sensor cycle ms <input type='number' name='cycle' min='60' max='300' value='"); page += String(cfg.sensorCycleMs); page += F("'> ");
page += F("</fieldset>");
page += F("<fieldset><legend>Debug</legend>");
page += F("Log level <select name='log'>");
page += "<option value='0' " + String(cfg.logLevel==0?"selected":"") + ">ERROR</option>";
page += "<option value='1' " + String(cfg.logLevel==1?"selected":"") + ">WARN</option>";
page += "<option value='2' " + String(cfg.logLevel==2?"selected":"") + ">INFO</option>";
page += "<option value='3' " + String(cfg.logLevel==3?"selected":"") + ">DEBUG</option>";
page += "<option value='4' " + String(cfg.logLevel==4?"selected":"") + ">VERBOSE</option>";
page += F("</select>");
page += F("</fieldset>");
page += F("<button type='submit'>Save configuration</button>");
page += F("</form>");
// Factory reset
page += F("<h4>Factory reset</h4>");
page += F("<form method='POST' action='/reset' onsubmit=\"return confirm('Factory reset will clear saved settings and reboot. Continue?');\">");
page += F("<button type='submit'>Factory reset</button>");
page += F("</form>");
// Tiny JS refresh: fetch /status every 0.5s and update cells
page += F("<script>");
page += F("async function tick(){");
page += F(" try{");
page += F(" const r=await fetch('/status',{cache:'no-store'});");
page += F(" if(!r.ok) return;");
page += F(" const j=await r.json();");
page += F(" for(let i=0;i<j.n;i++){");
page += F(" const s=document.getElementById('ss'+i);");
page += F(" const c=document.getElementById('sc'+i);");
page += F(" if(s) s.textContent=j.s[i]?'ON':'OFF';");
page += F(" if(c) c.textContent=j.c[i].toFixed(1);");
page += F(" }");
page += F(" }catch(e){}");
page += F("}");
page += F("setInterval(tick,500); tick();");
page += F("</script>");
page += F("</body></html>");
return page;
}
// ============================================================
// HTTP handlers
// ============================================================
static void handleRoot() {
LOG.verbose("HTTP GET /");
server.send(200, "text/html; charset=utf-8", renderPage());
}
static void handleStatus() {
// Minimal JSON: {n:6, s:[0/1..], c:[avg..]}
String json;
json.reserve(200);
json += F("{\"n\":");
json += String(NUM_SENSORS);
json += F(",\"s\":[");
for (uint8_t i=0; i<NUM_SENSORS; i++) {
json += sFilt[i].state ? "1" : "0";
if (i < NUM_SENSORS-1) json += ",";
}
json += F("],\"c\":[");
for (uint8_t i=0; i<NUM_SENSORS; i++) {
json += String(sFilt[i].avgCm, 1);
if (i < NUM_SENSORS-1) json += ",";
}
json += F("]}");
server.send(200, "application/json; charset=utf-8", json);
}
static void handleSet() {
LOG.verbose("HTTP GET /set");
bool newAuto = server.hasArg("auto");
bool newAnim = server.hasArg("anim");
if (newAuto != cfg.autoMode) {
cfg.autoMode = newAuto;
LOG.info(String("Mode -> ") + (cfg.autoMode ? "AUTO" : "MANUAL"));
} else {
cfg.autoMode = newAuto;
}
if (newAnim != cfg.animationsEnabled) {
cfg.animationsEnabled = newAnim;
LOG.info(String("Animations -> ") + (cfg.animationsEnabled ? "ON" : "OFF"));
} else {
cfg.animationsEnabled = newAnim;
}
// Manual: read relay targets from checkboxes
if (!cfg.autoMode) {
for (uint8_t i=0; i<NUM_RELAYS; i++) {
String key = "r" + String(i+1);
bool t = server.hasArg(key);
if (relayTarget[i] != t) {
relayTarget[i] = t;
LOG.debug(String("Manual target relay ") + String(i+1) + " -> " + (t?"ON":"OFF"));
}
}
}
// Persist these behaviour toggles too (so power cycles keep the mode)
saveConfig();
server.sendHeader("Location", "/");
server.send(302, "text/plain", "OK");
}
static float argToFloat(const String &s, float fallback) {
if (s.length() == 0) return fallback;
return s.toFloat();
}
static int argToInt(const String &s, int fallback) {
if (s.length() == 0) return fallback;
return s.toInt();
}
static void handleConfig() {
// POST /config
LOG.info("HTTP POST /config");
if (server.hasArg("min")) cfg.minCm = argToFloat(server.arg("min"), cfg.minCm);
if (server.hasArg("max")) cfg.maxCm = argToFloat(server.arg("max"), cfg.maxCm);
if (server.hasArg("hyst")) cfg.hystCm = argToFloat(server.arg("hyst"), cfg.hystCm);
if (server.hasArg("avg")) cfg.avgWindow = (uint8_t) argToInt(server.arg("avg"), cfg.avgWindow);
if (server.hasArg("conf")) cfg.confirmSamples = (uint8_t) argToInt(server.arg("conf"), cfg.confirmSamples);
if (server.hasArg("cycle")) cfg.sensorCycleMs = (uint16_t) argToInt(server.arg("cycle"), cfg.sensorCycleMs);
if (server.hasArg("log")) {
cfg.logLevel = (uint8_t) argToInt(server.arg("log"), cfg.logLevel);
}
clampConfig();
bool ok = saveConfig();
LOG.info(String("Config saved: ") + (ok ? "OK" : "FAIL"));
LOG.info("Window " + String(cfg.minCm,1) + ".." + String(cfg.maxCm,1) + " hyst " + String(cfg.hystCm,1));
LOG.info("Filter avg=" + String(cfg.avgWindow) + " confirm=" + String(cfg.confirmSamples) + " cycle=" + String(cfg.sensorCycleMs));
LOG.info("LogLevel=" + String(cfg.logLevel));
// Apply log level immediately
LOG.setLevel((DebugLog::Level)cfg.logLevel);
// Redirect back
server.sendHeader("Location", "/");
server.send(302, "text/plain", ok ? "OK" : "FAIL");
}
static void handleReset() {
// POST /reset
LOG.warn("HTTP POST /reset -> factory reset");
factoryResetConfig();
delay(50);
server.send(200, "text/plain; charset=utf-8", "Factory reset OK. Rebooting...");
delay(200);
ESP.restart();
}
// ============================================================
// Setup / Loop
// ============================================================
void setup() {
Serial.begin(115200);
delay(200);
// Load persisted config or defaults
if (!loadConfig()) {
setDefaultConfig();
saveConfig();
}
clampConfig();
LOG.begin(Serial, (DebugLog::Level)cfg.logLevel);
LOG.info("Boot");
LOG.info(String("Loaded config: window ") + String(cfg.minCm,1) + ".." + String(cfg.maxCm,1) +
" hyst " + String(cfg.hystCm,1) + " avg " + String(cfg.avgWindow) +
" conf " + String(cfg.confirmSamples) + " cycle " + String(cfg.sensorCycleMs));
LOG.info(String("Mode=") + (cfg.autoMode?"AUTO":"MANUAL") + " Anim=" + (cfg.animationsEnabled?"ON":"OFF"));
// Relay init
for (uint8_t i=0; i<NUM_RELAYS; i++) {
pinMode(RELAY_PINS[i], OUTPUT);
relayTarget[i] = false;
relayActual[i] = false;
applyRelayPin(i, false);
}
// Wi-Fi init
WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(true);
WiFi.persistent(false);
if (!WiFi.config(LOCAL_IP, GATEWAY, SUBNET, DNS1, DNS2)) {
LOG.warn("WiFi.config failed (static IP), falling back to DHCP");
} else {
LOG.info(String("Static IP set: ") + LOCAL_IP.toString());
}
WiFi.begin(WIFI_SSID, WIFI_PASS);
LOG.info(String("WiFi begin: SSID=") + WIFI_SSID);
lastWifiStatus = WiFi.status();
LOG.info(String("WiFi status: ") + wifiStatusStr(lastWifiStatus));
// Web routes
server.on("/", HTTP_GET, handleRoot);
server.on("/status", HTTP_GET, handleStatus);
server.on("/set", HTTP_GET, handleSet);
server.on("/config", HTTP_POST, handleConfig);
server.on("/reset", HTTP_POST, handleReset);
server.begin();
LOG.info("HTTP server started on port 80");
}
void loop() {
server.handleClient();
ensureWifi();
// Sensor sampling cycle (blocking, acceptable; everything else non-blocking)
uint32_t now = millis();
if (now - lastSensorSampleMs >= cfg.sensorCycleMs) {
lastSensorSampleMs = now;
sampleSensors();
mapSensorsToRelaysIfAuto();
}
// Relay updates
updateRelayAnimations();
updateRelaysNonBlocking();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment