diff --git a/esp32_arduino/esp32_arduino.ino b/esp32_arduino/esp32_arduino.ino index 0c60064..ac33d42 100644 --- a/esp32_arduino/esp32_arduino.ino +++ b/esp32_arduino/esp32_arduino.ino @@ -8,14 +8,17 @@ * * Provides REST API for Home Assistant integration */ -// version 2.1 Initial release +// version 2.2.1 Persistent NFC config via NVS #include #include #include +#include + +Preferences prefs; // ── NFC: PN532 over UART (HSU mode) via UEXT1 ─────────────────────────────── // UEXT1 pin 3 = TXD (ESP32 → PN532 RXD) → GPIO4 -// UEXT1 pin 4 = RXD (PN532 TXD → ESP32) → GPIO5 +// UEXT1 pin 4 = RXD (PN532 TXD → ESP32) → GPO5 // PN532 module wiring note: set HSU mode — DIP1 = 0, DIP2 = 0 #include #include @@ -44,6 +47,7 @@ char nfc_auth_uid[32] = ""; // authorized card UID; empty = any int nfc_relay_num = 1; // relay to open on match (1-4) unsigned long nfc_pulse_ms = 5000; // absence timeout: relay closes after this many ms of no card char nfc_access_state[8] = "idle"; // "idle" | "granted" | "denied" +bool nfc_enabled = false; // access control module on/off; off by default // ──────────────────────────────────────────────────────────────────────────── // WiFi credentials @@ -91,6 +95,33 @@ bool ha_registered = false; float temperature = 25.0; unsigned long last_temp_update = 0; +// ── Forward declarations ────────────────────────────────────────────────────── +// Required because setup()/loop() reference handlers defined later in the file. +void handleRoot(); +void handleStatus(); +void handleRelayOn(); +void handleRelayOff(); +void handleRelayStatus(); +void handleInputStatus(); +void handleRegister(); +void handleLEDOn(); +void handleLEDOff(); +void handleNotFound(); +void handleNFCStatus(); +void handleNFCConfigGet(); +void handleNFCConfigSet(); +void handleNFCEnable(); +void handleDebug(); +void postNFCEvent(const String& uid); +void postInputEvent(int input_num, bool state); +int nfcRelayPin(int rnum); +bool postJsonToHA(const String& json); +bool verifyAPIRequest(); +bool requireAuth(); +void checkInputChanges(); +void scanWiFiNetworks(); +// ───────────────────────────────────────────────────────────────────────────── + void setup() { // Initialize USB CDC serial Serial.begin(115200); @@ -126,7 +157,36 @@ void setup() { digitalWrite(RELAY_4_PIN, LOW); Serial.println("GPIO initialized"); - + + // ── Load persistent NFC config from NVS ────────────────────────────────── + prefs.begin("nfc_cfg", true); // read-only namespace + String saved_uid = prefs.getString("auth_uid", ""); + if (saved_uid.length() > 0 && saved_uid.length() < sizeof(nfc_auth_uid)) { + saved_uid.toCharArray(nfc_auth_uid, sizeof(nfc_auth_uid)); + } + nfc_relay_num = prefs.getInt("relay_num", 1); + nfc_pulse_ms = prefs.getULong("pulse_ms", 5000); + nfc_enabled = prefs.getBool("enabled", false); + prefs.end(); + Serial.printf("NFC config loaded: auth='%s' relay=%d pulse=%lu ms\n", + nfc_auth_uid, nfc_relay_num, nfc_pulse_ms); + // ───────────────────────────────────────────────────────────────────────── + + // ── Load persistent HA registration from NVS ───────────────────────────── + prefs.begin("ha_cfg", true); // read-only namespace + String saved_url = prefs.getString("callback_url", ""); + if (saved_url.length() > 0 && saved_url.length() < sizeof(ha_callback_url)) { + saved_url.toCharArray(ha_callback_url, sizeof(ha_callback_url)); + ha_registered = true; + } + prefs.end(); + if (ha_registered) { + Serial.printf("HA registration loaded: %s\n", ha_callback_url); + } else { + Serial.println("HA registration: not registered"); + } + // ───────────────────────────────────────────────────────────────────────── + // Configure WiFi // ESP32-C6 note: the radio sometimes needs a full reset cycle before it can // associate. We do WiFi.disconnect(true) → mode() → config() → begin() and @@ -138,7 +198,7 @@ void setup() { WiFi.setAutoReconnect(true); // Static IP - IPAddress staticIP(192, 168, 0, 181); + IPAddress staticIP(192, 168, 0, 240); IPAddress gateway(192, 168, 0, 1); IPAddress subnet(255, 255, 255, 0); WiFi.config(staticIP, gateway, subnet); @@ -214,6 +274,7 @@ void setup() { server.on("/nfc/status", HTTP_GET, handleNFCStatus); server.on("/nfc/config", HTTP_GET, handleNFCConfigGet); server.on("/nfc/config", HTTP_POST, handleNFCConfigSet); + server.on("/nfc/enable", HTTP_POST, handleNFCEnable); server.on("/debug", HTTP_GET, handleDebug); // ── Collect HMAC auth headers for verifyAPIRequest() ───────────────────────── @@ -255,11 +316,13 @@ void setup() { long baud = NFC_BAUDS[bi]; Serial.printf(" Trying baud=%-7ld RX=GPIO%d TX=GPIO%d ...\n", baud, rx, tx); nfcSerial.begin(baud, SERIAL_8N1, rx, tx); - delay(500); // PN532 boot / line-settle time + // Serve HTTP and feed watchdog during probe delay instead of blocking + { unsigned long _t = millis(); while (millis() - _t < 500) { server.handleClient(); yield(); } } nfc.begin(); // PN532_HSU::begin() is a no-op — pins already set versiondata = nfc.getFirmwareVersion(); - if (!versiondata) delay(200); - else { found_baud = baud; found_rx = rx; found_tx = tx; } + if (!versiondata) { + unsigned long _t = millis(); while (millis() - _t < 200) { server.handleClient(); yield(); } + } else { found_baud = baud; found_rx = rx; found_tx = tx; } } } @@ -311,7 +374,7 @@ void loop() { bool is_active_state = (strcmp(nfc_access_state, "granted") == 0 || strcmp(nfc_access_state, "denied") == 0); unsigned long nfc_interval = is_active_state ? nfc_pulse_ms : (unsigned long)NFC_POLL_MS; - if (nfc_initialized && millis() - nfc_last_poll_ms >= nfc_interval) { + if (nfc_enabled && nfc_initialized && millis() - nfc_last_poll_ms >= nfc_interval) { nfc_last_poll_ms = millis(); uint16_t rf_timeout = is_active_state ? 500 : 50; uint8_t uid[7] = {0}; @@ -439,69 +502,155 @@ void handleRoot() { input3_state = digitalRead(DIN3_PIN); input4_state = digitalRead(DIN4_PIN); - String html = "ESP32-C6 Device"; - html += ""; + String html = ""; + html += "ESP32-C6 Device"; + html += ""; html += ""; + // ── JavaScript ───────────────────────────────────────────────────────── html += ""; - html += ""; - html += "

ESP32-C6 Control Panel

"; + html += "window.addEventListener('load',function(){updateStatus();setInterval(updateStatus,2000);});"; + html += ""; + // ── Body ─────────────────────────────────────────────────────────────── + html += ""; + // Topbar + html += "
"; + html += "

🔌 ESP32-C6

"; + html += "
"; + html += ""; + html += "
"; + html += "
"; // Device Info - html += "

Device Info

"; - html += "IP: " + WiFi.localIP().toString() + "   "; - html += "RSSI: " + String(WiFi.RSSI()) + " dBm   "; - html += "Temp: " + String(temperature, 1) + "°C   "; - html += "Uptime: " + String(millis() / 1000) + "s
"; + html += "
"; + html += "
"; + html += "IP " + WiFi.localIP().toString() + ""; + html += "RSSI " + String(WiFi.RSSI()) + " dBm"; + html += "Temp " + String(temperature, 1) + " °C"; + html += "Uptime " + String(millis() / 1000) + " s"; + html += "
"; - // Inputs + Relays side by side in a 2-column row + // Inputs + Relays side by side html += "
"; - - // Inputs — 2-column inner grid, round LED indicator + // Inputs html += "

Inputs

"; bool inputStates[5] = {false, input1_state, input2_state, input3_state, input4_state}; for (int i = 1; i <= 4; i++) { - bool pressed = !inputStates[i]; // pull-up: LOW=pressed + bool pressed = !inputStates[i]; html += "
"; html += "
"; - html += "
Input " + String(i) + "
"; - html += "
" + String(pressed ? "PRESSED" : "NOT PRESSED") + "
"; + html += "
IN " + String(i) + "
"; + html += "
" + String(pressed ? "PRESSED" : "OPEN") + "
"; html += "
"; } - html += "
"; // close Inputs card - - // Relays — 2-column inner grid, toggle buttons + html += "
"; + // Relays html += "

Relays

"; bool relayStates[5] = {false, relay1_state, relay2_state, relay3_state, relay4_state}; for (int i = 1; i <= 4; i++) { bool on = relayStates[i]; html += ""; + html += "onclick='toggleRelay(" + String(i) + ")'>Relay " + String(i) + ": " + String(on ? "ON" : "OFF") + ""; } - html += "
"; // close Relays card - html += ""; // close .card-row + html += ""; + html += ""; // .card-row // LED Control html += "

LED Control

"; html += ""; html += ""; - html += "   Status: " + String(led_state ? "ON" : "OFF") + "
"; + html += "   Status: " + String(led_state ? "ON" : "OFF") + ""; - // ── NFC Access Control card ─────────────────────────────────────────────── - bool nfc_present_now = nfc_initialized && (strcmp(nfc_access_state, "granted") == 0); + // NFC Access Control + bool nfc_present_now = nfc_initialized && nfc_enabled && (strcmp(nfc_access_state, "granted") == 0); String ac_class = String(nfc_access_state) == "granted" ? "nfc-granted" : String(nfc_access_state) == "denied" ? "nfc-denied" : "nfc-idle"; String ac_text = String(nfc_access_state) == "granted" ? "ACCESS GRANTED" : String(nfc_access_state) == "denied" ? "ACCESS DENIED" : "Waiting for card"; html += "

NFC Access Control (PN532 — UEXT1)

"; - - // ── Live status row ──────────────────────────────────────────────────── - html += "
"; + // Module enable row + html += "
"; + html += "Module: " + String(nfc_enabled ? "Active" : "Disabled") + ""; + html += ""; + html += "
"; + // Live UID row + html += "
"; html += "
"; - html += "
"; - html += "
Detected card UID
"; - html += "
"; - html += (nfc_last_uid.length() > 0 ? nfc_last_uid : "No card inserted"); - html += "
"; - html += ""; + html += "
"; + html += "
Detected card UID
"; + html += "
" + (nfc_last_uid.length() > 0 ? nfc_last_uid : "No card inserted") + "
"; + html += "
"; + html += ""; html += "
"; html += "
" + ac_text + "
"; - - // ── Current Settings (read-only) ─────────────────────────────────────── - html += "

Current Settings

"; - html += ""; - // Authorized card row - html += ""; - html += ""; + // Current settings table + html += "

Current Settings

"; + html += "
Authorized card
"; + html += ""; if (strlen(nfc_auth_uid) > 0) { - html += ""; - html += ""; + html += ""; + html += ""; } else { - html += ""; + html += ""; } html += ""; - // Relay row - html += ""; - html += ""; - html += ""; - html += ""; - // Timeout row - html += ""; - html += ""; - html += ""; - html += ""; + html += ""; + html += ""; html += "
Authorized card" + String(nfc_auth_uid) + "" + String(nfc_auth_uid) + "None — no card authorized yetNone — no card authorized yet
Trigger relayRelay " + String(nfc_relay_num) + "
Absent timeout" + String(nfc_pulse_ms) + " ms
Trigger relayRelay " + String(nfc_relay_num) + "
Absent timeout" + String(nfc_pulse_ms) + " ms
"; - - // ── Edit Settings form ───────────────────────────────────────────────── - html += "

Edit Settings

"; - html += "
"; - html += "
Authorized UID
"; - html += "
"; - html += "
Trigger relay
"; - html += "
"; + html += "
Trigger relay
"; + html += "
"; - html += "
Absent timeout (ms)
"; - html += "
"; - html += ""; + html += "
Absent timeout (ms)
"; + html += "
"; + html += ""; html += "
"; if (!nfc_initialized) { - html += "

✗ PN532 not detected — check UEXT1 wiring (TX=GPIO4, RX=GPIO5)

"; + html += "

✗ PN532 not detected — check UEXT1 wiring (TX=GPIO4, RX=GPIO5)

"; } if (nfc_initialized && strlen(nfc_auth_uid) == 0) { - html += "

⚠ No authorized UID saved — present a card, click “Use as authorized” then Save.

"; + html += "

⚠ No authorized UID — present a card, click “Use as authorized” then Save.

"; } - html += "
"; + html += "
"; // close NFC card - // Home Assistant Webhook Status + // HA Webhook html += "

Home Assistant Webhook

"; if (ha_registered && strlen(ha_callback_url) > 0) { html += "
✓ Connected — " + String(ha_callback_url) + "
"; @@ -660,12 +819,15 @@ void handleRoot() { } html += "
"; - html += "

API Endpoints

"; - html += "GET /api/status   POST /relay/on?relay=1-4   POST /relay/off?relay=1-4
"; - html += "GET /input/status?input=1-4   POST /led/on   POST /led/off
"; - html += "GET /nfc/status   GET /nfc/config   POST /nfc/config?auth_uid=&relay=&pulse_ms=   POST /register?callback_url=...
"; + // API reference + html += "

API Endpoints

"; + html += "GET /api/status  •  POST /relay/on?relay=1-4  •  POST /relay/off?relay=1-4
"; + html += "GET /input/status?input=1-4  •  POST /led/on  •  POST /led/off
"; + html += "GET /nfc/status  •  GET /nfc/config  •  POST /nfc/config?auth_uid=&relay=&pulse_ms=
"; + html += "POST /nfc/enable?state=0|1  •  POST /register?callback_url=..."; + html += "
"; - html += ""; + html += ""; // .page server.send(200, "text/html", html); } @@ -673,7 +835,7 @@ void handleRoot() { // ── Web UI authentication helper ──────────────────────────────────────── bool requireAuth() { if (!server.authenticate(WEB_USER, WEB_PASSWORD)) { - server.requestAuthentication(DIGEST_AUTH, "ESP32-C6 Control Panel", + server.requestAuthentication(BASIC_AUTH, "ESP32-C6 Control Panel", "Login required"); return false; } @@ -782,7 +944,8 @@ void handleStatus() { json += "\"nfc_access_state\":\"" + String(nfc_access_state) + "\","; json += "\"nfc_auth_uid\":\"" + String(nfc_auth_uid) + "\","; json += "\"nfc_relay_num\":" + String(nfc_relay_num) + ","; - json += "\"nfc_pulse_ms\":" + String(nfc_pulse_ms); + json += "\"nfc_pulse_ms\":" + String(nfc_pulse_ms) + ","; + json += "\"nfc_enabled\":" + String(nfc_enabled ? "true" : "false"); json += "}"; server.send(200, "application/json", json); @@ -943,7 +1106,13 @@ void handleRegister() { url.toCharArray(ha_callback_url, 256); ha_registered = true; - + + // ── Persist HA registration to NVS ─────────────────────────────────────── + prefs.begin("ha_cfg", false); // read-write namespace + prefs.putString("callback_url", ha_callback_url); + prefs.end(); + // ───────────────────────────────────────────────────────────────────────── + Serial.printf("Home Assistant webhook registered: %s\n", ha_callback_url); server.send(200, "application/json", "{\"status\":\"ok\",\"message\":\"Webhook registered\"}"); } @@ -1041,11 +1210,49 @@ void postInputEvent(int input_num, bool state) { // NFC Status API // ============================================ +// ============================================ +// NFC Enable/Disable POST /nfc/enable?state=0|1 +// ============================================ + +void handleNFCEnable() { + if (!verifyAPIRequest()) return; + if (!server.hasArg("state")) { + server.send(400, "application/json", "{\"error\":\"Missing state (0 or 1)\"}"); + return; + } + nfc_enabled = (server.arg("state").toInt() != 0); + // When disabling while a card is granted, close the relay and reset state + if (!nfc_enabled) { + if (strcmp(nfc_access_state, "granted") == 0) { + int pin = nfcRelayPin(nfc_relay_num); + if (pin >= 0) { + digitalWrite(pin, LOW); + switch (nfc_relay_num) { + case 1: relay1_state = false; break; + case 2: relay2_state = false; break; + case 3: relay3_state = false; break; + case 4: relay4_state = false; break; + } + } + } + strcpy(nfc_access_state, "idle"); + nfc_last_uid = ""; + nfc_miss_count = 0; + } + prefs.begin("nfc_cfg", false); + prefs.putBool("enabled", nfc_enabled); + prefs.end(); + Serial.printf("NFC access control: %s\n", nfc_enabled ? "ENABLED" : "DISABLED"); + server.send(200, "application/json", + String("{\"status\":\"ok\",\"nfc_enabled\":") + (nfc_enabled ? "true" : "false") + "}"); +} + void handleNFCStatus() { if (!verifyAPIRequest()) return; - bool present = nfc_initialized && (strcmp(nfc_access_state, "granted") == 0); + bool present = nfc_initialized && nfc_enabled && (strcmp(nfc_access_state, "granted") == 0); String json = "{"; json += "\"initialized\":" + String(nfc_initialized ? "true" : "false") + ","; + json += "\"nfc_enabled\":" + String(nfc_enabled ? "true" : "false") + ","; json += "\"card_present\":" + String(present ? "true" : "false") + ","; json += "\"last_uid\":\"" + nfc_last_uid + "\","; json += "\"access_state\":\"" + String(nfc_access_state) + "\","; @@ -1093,7 +1300,8 @@ void handleNFCConfigGet() { String json = "{"; json += "\"auth_uid\":\"" + String(nfc_auth_uid) + "\","; json += "\"relay_num\":" + String(nfc_relay_num) + ","; - json += "\"pulse_ms\":" + String(nfc_pulse_ms); + json += "\"pulse_ms\":" + String(nfc_pulse_ms) + ","; + json += "\"nfc_enabled\":" + String(nfc_enabled ? "true" : "false"); json += "}"; server.send(200, "application/json", json); } @@ -1131,6 +1339,13 @@ void handleNFCConfigSet() { } Serial.printf("NFC config: auth='%s' relay=%d pulse=%lu ms\n", nfc_auth_uid, nfc_relay_num, nfc_pulse_ms); + // ── Persist to NVS so settings survive power cycles ────────────────────── + prefs.begin("nfc_cfg", false); // read-write namespace + prefs.putString("auth_uid", nfc_auth_uid); + prefs.putInt("relay_num", nfc_relay_num); + prefs.putULong("pulse_ms", nfc_pulse_ms); + prefs.end(); + // ───────────────────────────────────────────────────────────────────────── String json = "{\"status\":\"ok\"," "\"auth_uid\":\"" + String(nfc_auth_uid) + "\"," "\"relay_num\":" + String(nfc_relay_num) + ","