From 494f91e0016c6603d6443cc2e3e6fe69da460952 Mon Sep 17 00:00:00 2001 From: ske087 Date: Sat, 13 Jun 2026 10:31:40 +0300 Subject: [PATCH] updated card reader to multiple cards added on nfc --- .../esp32_arduino/esp32_arduino.ino | 645 +++++++++++++----- .../esp32_arduino/secrets.h.example | 14 + 2 files changed, 486 insertions(+), 173 deletions(-) diff --git a/olimex_ESP32-C6-EVB/esp32_arduino/esp32_arduino.ino b/olimex_ESP32-C6-EVB/esp32_arduino/esp32_arduino.ino index ac33d42..d23dca3 100644 --- a/olimex_ESP32-C6-EVB/esp32_arduino/esp32_arduino.ino +++ b/olimex_ESP32-C6-EVB/esp32_arduino/esp32_arduino.ino @@ -1,14 +1,14 @@ /** * ESP32-C6 Home Assistant Integration * Arduino IDE Project - * + * v18 * Board: ESP32C6 Dev Module * Flash Size: 4MB * USB CDC On Boot: Enabled (REQUIRED for serial output!) * * Provides REST API for Home Assistant integration */ -// version 2.2.1 Persistent NFC config via NVS +// version 2.3.0 C5-parity: secrets.h WiFi/IP, relay toggle, NFC probe, improved NFC polling, WiFi LED #include #include #include @@ -42,18 +42,17 @@ bool nfc_initialized = false; String nfc_last_uid = ""; unsigned long nfc_last_poll_ms = 0; int nfc_miss_count = 0; // consecutive polls with no card detected +unsigned long nfc_absent_since = 0; // millis() when card first went absent (GRANTED→absent) // NFC Access Control -char nfc_auth_uid[32] = ""; // authorized card UID; empty = any card triggers +#define NFC_MAX_CARDS 10 // maximum authorized cards +char nfc_auth_uids[NFC_MAX_CARDS][32] = {}; // authorized UIDs (empty string = unused slot) +int nfc_auth_count = 0; // number of active cards 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 +unsigned long nfc_pulse_ms = 0; // relay release delay after card removed (ms); 0 = off after ~1 s debounce char nfc_access_state[8] = "idle"; // "idle" | "granted" | "denied" bool nfc_enabled = false; // access control module on/off; off by default // ──────────────────────────────────────────────────────────────────────────── -// WiFi credentials -const char* ssid = "BUON GUSTO PARTER"; -const char* password = "arleta13"; - // Web server on port 80 WebServer server(80); @@ -102,6 +101,7 @@ void handleStatus(); void handleRelayOn(); void handleRelayOff(); void handleRelayStatus(); +void handleRelayToggle(); void handleInputStatus(); void handleRegister(); void handleLEDOn(); @@ -111,6 +111,7 @@ void handleNFCStatus(); void handleNFCConfigGet(); void handleNFCConfigSet(); void handleNFCEnable(); +void handleNFCProbe(); void handleDebug(); void postNFCEvent(const String& uid); void postInputEvent(int input_num, bool state); @@ -123,15 +124,21 @@ void scanWiFiNetworks(); // ───────────────────────────────────────────────────────────────────────────── void setup() { - // Initialize USB CDC serial + // ── USB CDC serial init ─────────────────────────────────────────────────── + // IMPORTANT: In Arduino IDE set Tools → USB CDC On Boot → Enabled + // Without that, Serial maps to UART and you'll see nothing on the USB port. Serial.begin(115200); - delay(2000); // Give time for USB CDC to initialize - - // Wait for serial port to be ready (up to 5 seconds) - for (int i = 0; i < 10 && !Serial; i++) { - delay(500); + + // After a flash-and-reset cycle the USB CDC port re-enumerates. We wait up + // to 5 s for the host to open the port, then continue regardless so the + // board doesn't hang when running headless (no monitor attached). + { + unsigned long _t = millis(); + while (!Serial && millis() - _t < 5000) { delay(100); } } - + // Brief additional settle time for the CDC driver on the host side + delay(200); + Serial.println("\n\n================================="); Serial.println("ESP32-C6 Home Assistant Device"); Serial.println("Arduino Framework"); @@ -149,27 +156,34 @@ void setup() { pinMode(DIN2_PIN, INPUT_PULLUP); pinMode(DIN3_PIN, INPUT_PULLUP); pinMode(DIN4_PIN, INPUT_PULLUP); - // Set all outputs to LOW - digitalWrite(LED_PIN, LOW); + // Set all outputs to initial safe state + digitalWrite(LED_PIN, HIGH); // HIGH = LED OFF (active LOW) — will turn ON when WiFi connects digitalWrite(RELAY_1_PIN, LOW); digitalWrite(RELAY_2_PIN, LOW); digitalWrite(RELAY_3_PIN, LOW); digitalWrite(RELAY_4_PIN, LOW); - Serial.println("GPIO initialized"); + Serial.println("GPIO initialized - LED OFF (will turn ON when WiFi connects)"); // ── 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_pulse_ms = prefs.getULong("pulse_ms", 0); nfc_enabled = prefs.getBool("enabled", false); + nfc_auth_count = 0; + for (int i = 0; i < NFC_MAX_CARDS; i++) { + String key = "uid" + String(i); + String saved = prefs.getString(key.c_str(), ""); + if (saved.length() > 0 && saved.length() < 32) { + saved.toCharArray(nfc_auth_uids[i], 32); + nfc_auth_count++; + } else { + nfc_auth_uids[i][0] = '\0'; + } + } prefs.end(); - Serial.printf("NFC config loaded: auth='%s' relay=%d pulse=%lu ms\n", - nfc_auth_uid, nfc_relay_num, nfc_pulse_ms); + Serial.printf("NFC config loaded: %d authorized card(s), relay=%d, pulse=%lu ms\n", + nfc_auth_count, nfc_relay_num, nfc_pulse_ms); // ───────────────────────────────────────────────────────────────────────── // ── Load persistent HA registration from NVS ───────────────────────────── @@ -197,16 +211,36 @@ void setup() { WiFi.mode(WIFI_STA); WiFi.setAutoReconnect(true); - // Static IP - IPAddress staticIP(192, 168, 0, 240); - IPAddress gateway(192, 168, 0, 1); - IPAddress subnet(255, 255, 255, 0); - WiFi.config(staticIP, gateway, subnet); + // Parse static IP configuration from secrets.h + // Note: WiFi.config() must be called right before WiFi.begin() on ESP32-C6; + // calling it before disconnect(true)/mode() causes it to be silently cleared. + IPAddress _staticIP, _gateway, _subnet, _dns1, _dns2; + bool _have_static = false; + if (USE_STATIC_IP) { + if (_staticIP.fromString(STATIC_IP_ADDR) && + _gateway.fromString(STATIC_GATEWAY) && + _subnet.fromString(STATIC_SUBNET) && + _dns1.fromString(STATIC_DNS1) && + _dns2.fromString(STATIC_DNS2)) { + _have_static = true; + Serial.printf("Static IP will be applied: %s\n", STATIC_IP_ADDR); + } else { + Serial.println("⚠ Invalid static IP configuration — falling back to DHCP"); + } + } else { + Serial.println("Using DHCP for IP configuration"); + } bool wifi_ok = false; for (int pass = 1; pass <= 3 && !wifi_ok; pass++) { - Serial.printf("Connecting to WiFi: %s (attempt %d/3)\n", ssid, pass); - WiFi.begin(ssid, password); + Serial.printf("Connecting to WiFi: %s (attempt %d/3)\n", WIFI_SSID, pass); + // Apply static IP immediately before begin() so it is never cleared by a + // preceding disconnect(true) call. + if (_have_static) { + WiFi.config(_staticIP, _gateway, _subnet, _dns1, _dns2); + Serial.printf("Static IP configured: %s\n", STATIC_IP_ADDR); + } + WiFi.begin(WIFI_SSID, WIFI_PASSWORD); for (int t = 0; t < 40 && WiFi.status() != WL_CONNECTED; t++) { delay(500); Serial.print("."); @@ -218,20 +252,23 @@ void setup() { WiFi.disconnect(true); delay(1000); WiFi.mode(WIFI_STA); - WiFi.config(staticIP, gateway, subnet); } } if (wifi_ok) { - Serial.println("\n\u2713 WiFi connected!"); + Serial.println("\n✓ WiFi connected!"); Serial.print(" IP : "); Serial.println(WiFi.localIP()); Serial.print(" RSSI : "); Serial.print(WiFi.RSSI()); Serial.println(" dBm"); Serial.print(" MAC : "); Serial.println(WiFi.macAddress()); + digitalWrite(LED_PIN, LOW); // LED steady ON = WiFi connected (active LOW) + led_state = true; } else { - Serial.println("\n\u2717 WiFi connection failed after 3 attempts."); + Serial.println("\n✗ WiFi connection failed after 3 attempts."); Serial.print(" Status code: "); Serial.println(WiFi.status()); - Serial.println(" Check: correct SSID/password, 2.4 GHz band, board in range."); - Serial.println(" HTTP server will start anyway \u2014 accessible once WiFi reconnects."); + Serial.println(" Check: correct WIFI_SSID/WIFI_PASSWORD, 2.4 GHz band, board in range."); + Serial.println(" HTTP server will start anyway — accessible once WiFi reconnects."); + digitalWrite(LED_PIN, HIGH); // LED OFF = no WiFi + led_state = false; } // ── NTP time sync ──────────────────────────────────────────────────────────── @@ -260,9 +297,10 @@ void setup() { server.on("/", handleRoot); server.on("/api/status", HTTP_GET, handleStatus); // Relay endpoints - server.on("/relay/on", HTTP_POST, handleRelayOn); - server.on("/relay/off", HTTP_POST, handleRelayOff); - server.on("/relay/status", HTTP_GET, handleRelayStatus); + server.on("/relay/on", HTTP_POST, handleRelayOn); + server.on("/relay/off", HTTP_POST, handleRelayOff); + server.on("/relay/toggle", HTTP_POST, handleRelayToggle); + server.on("/relay/status", HTTP_GET, handleRelayStatus); // Input endpoints server.on("/input/status", HTTP_GET, handleInputStatus); // Home Assistant webhook registration @@ -275,6 +313,7 @@ void setup() { server.on("/nfc/config", HTTP_GET, handleNFCConfigGet); server.on("/nfc/config", HTTP_POST, handleNFCConfigSet); server.on("/nfc/enable", HTTP_POST, handleNFCEnable); + server.on("/nfc/probe", HTTP_POST, handleNFCProbe); server.on("/debug", HTTP_GET, handleDebug); // ── Collect HMAC auth headers for verifyAPIRequest() ───────────────────────── @@ -293,57 +332,60 @@ void setup() { // ── NFC (PN532 HSU) initialisation — multi-baud auto-detect ───────────── // The PN532 default HSU baud is 115200, but some modules ship at 9600. - // We try both, plus RX/TX swapped, so the board will find the module - // regardless of those two variables. + // Only the correct pin pair (NFC_RX_PIN / NFC_TX_PIN) is tried — the board + // has a fixed UEXT1 wiring so pin-swap probing is unnecessary and causes + // hangs when no module is connected (the PN532 library uses its own blocking + // read loop that does not respect HardwareSerial::setTimeout()). Serial.println("--- NFC (PN532 HSU) Initialization ---"); // Baud rates to try, in order - const long NFC_BAUDS[] = {115200, 9600, 57600, 38400}; - const int NFC_NBAUDS = 4; - // Pin pairs to try: {RX, TX}. Second pair = swapped. - const int NFC_PINS[2][2] = {{NFC_RX_PIN, NFC_TX_PIN}, - {NFC_TX_PIN, NFC_RX_PIN}}; + const long NFC_BAUDS[] = {115200, 9600, 57600, 38400}; + const int NFC_NBAUDS = 4; uint32_t versiondata = 0; long found_baud = 0; - int found_rx = NFC_RX_PIN; - int found_tx = NFC_TX_PIN; - for (int pi = 0; pi < 2 && !versiondata; pi++) { - int rx = NFC_PINS[pi][0]; - int tx = NFC_PINS[pi][1]; - for (int bi = 0; bi < NFC_NBAUDS && !versiondata; bi++) { - 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); - // 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) { - unsigned long _t = millis(); while (millis() - _t < 200) { server.handleClient(); yield(); } - } else { found_baud = baud; found_rx = rx; found_tx = tx; } + for (int bi = 0; bi < NFC_NBAUDS && !versiondata; bi++) { + long baud = NFC_BAUDS[bi]; + Serial.printf(" Trying baud=%-7ld RX=GPIO%d TX=GPIO%d ...\n", baud, NFC_RX_PIN, NFC_TX_PIN); + nfcSerial.begin(baud, SERIAL_8N1, NFC_RX_PIN, NFC_TX_PIN); + nfcSerial.setTimeout(800); + // Drain any garbage from a floating / unconnected RX line + while (nfcSerial.available()) nfcSerial.read(); + // Let the bus settle; keep HTTP alive during wait + { unsigned long _t = millis(); while (millis() - _t < 300) { server.handleClient(); yield(); } } + nfc.begin(); + versiondata = nfc.getFirmwareVersion(); + if (!versiondata) { + while (nfcSerial.available()) nfcSerial.read(); + unsigned long _t = millis(); while (millis() - _t < 100) { server.handleClient(); yield(); } + } else { + found_baud = baud; } } if (!versiondata) { Serial.println("\u2717 PN532 not detected with any baud/pin combination."); + Serial.println(" NFC capabilities are NOT available \u2014 all other features remain active."); + Serial.println(" (Connect a PN532 via UEXT1 and reboot to enable NFC.)"); Serial.println(" Hardware checklist:"); Serial.println(" 1. DIP/solder-jumpers on PN532 board: BOTH = 0 (HSU mode)"); Serial.println(" Some boards label them SEL0/SEL1 or I0/I1 — both must be LOW."); Serial.println(" 2. Power: UEXT1 pin 1 = 3V3, pin 2 = GND. Measure with multimeter."); - Serial.println(" 3. Wiring: UEXT1 pin 3 (GPIO4) ↔ PN532 RXD"); - Serial.println(" UEXT1 pin 4 (GPIO5) ↔ PN532 TXD"); + Serial.println(" 3. Wiring: UEXT1 pin 3 (GPIO4) \u2194 PN532 RXD"); + Serial.println(" UEXT1 pin 4 (GPIO5) \u2194 PN532 TXD"); Serial.println(" 4. Some PN532 breakouts need a 100 ohm series resistor on TX line."); + nfcSerial.end(); // release UART — don't hold pins in an indeterminate state + nfc_initialized = false; } else { Serial.printf("\u2713 PN532 found! baud=%ld RX=GPIO%d TX=GPIO%d\n", - found_baud, found_rx, found_tx); + found_baud, NFC_RX_PIN, NFC_TX_PIN); Serial.print(" Chip: PN5"); Serial.println((versiondata >> 24) & 0xFF, HEX); Serial.printf(" Firmware: %d.%d\n", (versiondata >> 16) & 0xFF, (versiondata >> 8) & 0xFF); // Re-init serial with confirmed settings - nfcSerial.begin(found_baud, SERIAL_8N1, found_rx, found_tx); + nfcSerial.begin(found_baud, SERIAL_8N1, NFC_RX_PIN, NFC_TX_PIN); nfc.SAMConfig(); nfc_initialized = true; Serial.println("\u2713 NFC ready \u2014 waiting for ISO14443A / Mifare cards"); @@ -366,23 +408,64 @@ void loop() { checkInputChanges(); } - // ── NFC: two-phase polling ──────────────────────────────────────────────── - // IDLE : fast poll every NFC_POLL_MS (500 ms), 50 ms RF timeout - // GRANTED/DENIED: slow presence-check every nfc_pulse_ms, 500 ms RF timeout - // 2 consecutive misses required to confirm card is gone + // ── WiFi LED status: steady ON = connected, OFF = disconnected ─────────── + static bool last_wifi_state = false; + bool wifi_now = (WiFi.status() == WL_CONNECTED); + if (wifi_now != last_wifi_state) { + last_wifi_state = wifi_now; + if (wifi_now) { + digitalWrite(LED_PIN, LOW); // LED ON — WiFi reconnected (active LOW) + led_state = true; + Serial.printf("\u2713 WiFi reconnected: %s\n", WiFi.localIP().toString().c_str()); + } else { + digitalWrite(LED_PIN, HIGH); // LED OFF — WiFi lost + led_state = false; + Serial.println("\u26a0 WiFi connection lost"); + } + } + // ───────────────────────────────────────────────────────────────────────── + + // ── NFC polling ────────────────────────────────────────────────────────── + // State machine with a "card present" flag: + // + // IDLE → poll every 1 s with 100 ms RF timeout (quick new-card scan) + // card found + authorized → relay ON, state = GRANTED + // card found + denied → state = DENIED + // + // GRANTED → poll every 1 s with 500 ms RF timeout (card needs time to + // power back up after inRelease on the previous poll) + // card still present → reset absence timer, keep relay ON + // card absent → start absence timer + // absent > limit → relay OFF, state = IDLE + // limit = nfc_pulse_ms if > 0, otherwise default 5 000 ms + // + // DENIED → poll every 1 s; card gone → state = IDLE { - 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_enabled && nfc_initialized && millis() - nfc_last_poll_ms >= nfc_interval) { + const unsigned long NFC_POLL_INTERVAL = 1000UL; + + bool is_granted = (strcmp(nfc_access_state, "granted") == 0); + bool is_denied = (strcmp(nfc_access_state, "denied") == 0); + + // Absent limit: configurable via nfc_pulse_ms (web UI), default 5 s + unsigned long absent_limit = (nfc_pulse_ms > 0) ? nfc_pulse_ms : 5000UL; + + if (nfc_enabled && nfc_initialized && millis() - nfc_last_poll_ms >= NFC_POLL_INTERVAL) { nfc_last_poll_ms = millis(); - uint16_t rf_timeout = is_active_state ? 500 : 50; + + // GRANTED: use a longer RF timeout — after inRelease the card returns to + // its IDLE state and needs 300-500 ms to be powered up and respond. + // Using 100 ms here is why presence checks were failing every poll. + uint16_t rf_timeout = is_granted ? 500 : 100; + uint8_t uid[7] = {0}; uint8_t uidLen = 0; + while (nfcSerial.available()) nfcSerial.read(); // flush stale UART bytes bool found = nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLen, rf_timeout); + if (found && uidLen > 0) { - nfc.inRelease(1); // deselect → card returns to ISO14443A IDLE state for next poll - nfc_miss_count = 0; + nfc.inRelease(1); // deselect — card returns to ISO14443A IDLE for next poll + + // Build UID string String uid_str = ""; for (uint8_t i = 0; i < uidLen; i++) { if (uid[i] < 0x10) uid_str += "0"; @@ -390,26 +473,28 @@ void loop() { if (i < uidLen - 1) uid_str += ":"; } uid_str.toUpperCase(); - if (strcmp(nfc_access_state, "granted") == 0) { - // Presence check passed — card still on reader - Serial.printf("NFC: card present UID=%s\n", uid_str.c_str()); - } else if (strcmp(nfc_access_state, "denied") == 0 && uid_str == nfc_last_uid) { - // Same card that was already denied is still on reader — do nothing - Serial.printf("NFC: denied card still present UID=%s\n", uid_str.c_str()); - } else { - // New card — authenticate - nfc_last_uid = uid_str; - Serial.printf("NFC: card UID=%s\n", uid_str.c_str()); - postNFCEvent(uid_str); - // Require an explicit authorized UID — empty = no card is authorized yet - if (strlen(nfc_auth_uid) == 0) { - strcpy(nfc_access_state, "denied"); - Serial.printf("NFC: ACCESS DENIED — no authorized UID configured. Set one in the web UI.\n"); - } else if (uid_str == String(nfc_auth_uid)) { + + // Card is physically present — reset absence timer regardless of state + nfc_absent_since = 0; + + bool is_authorized = false; + if (nfc_auth_count > 0) { + for (int i = 0; i < NFC_MAX_CARDS; i++) { + if (strlen(nfc_auth_uids[i]) > 0 && uid_str == String(nfc_auth_uids[i])) { + is_authorized = true; + break; + } + } + } + + if (is_authorized) { + if (!is_granted) { + // First detection of this authorized card: open relay strcpy(nfc_access_state, "granted"); - int gpin = nfcRelayPin(nfc_relay_num); - if (gpin >= 0) { - digitalWrite(gpin, HIGH); + nfc_last_uid = uid_str; + int pin = nfcRelayPin(nfc_relay_num); + if (pin >= 0) { + digitalWrite(pin, HIGH); switch (nfc_relay_num) { case 1: relay1_state = true; break; case 2: relay2_state = true; break; @@ -417,19 +502,34 @@ void loop() { case 4: relay4_state = true; break; } } - Serial.printf("NFC: ACCESS GRANTED relay=%d (presence-check every %lums)\n", - nfc_relay_num, nfc_pulse_ms); - } else { + Serial.printf("NFC: ACCESS GRANTED UID=%s — relay %d ON\n", + uid_str.c_str(), nfc_relay_num); + postNFCEvent(uid_str); + } + // Already GRANTED and card still present — absence timer already + // reset above; relay stays ON, no further action needed. + + } else { + // Card present but not in the authorized list + if (!is_denied || uid_str != nfc_last_uid) { strcpy(nfc_access_state, "denied"); + nfc_last_uid = uid_str; Serial.printf("NFC: ACCESS DENIED UID=%s\n", uid_str.c_str()); + postNFCEvent(uid_str); } } + } else { - // No card this poll - if (strcmp(nfc_access_state, "granted") == 0) { - nfc_miss_count++; - if (nfc_miss_count >= 2) { // 2 consecutive presence-check failures = card gone - nfc_miss_count = 0; + // No card detected this poll + if (is_granted) { + // Start the absence timer on first missed poll + if (nfc_absent_since == 0) { + nfc_absent_since = millis(); + Serial.printf("NFC: card not detected — relay OFF if absent > %lu ms\n", + absent_limit); + } + if (millis() - nfc_absent_since >= absent_limit) { + // Card has been absent long enough — close relay int pin = nfcRelayPin(nfc_relay_num); if (pin >= 0) { digitalWrite(pin, LOW); @@ -440,27 +540,23 @@ void loop() { case 4: relay4_state = false; break; } } + nfc_absent_since = 0; + nfc_last_uid = ""; strcpy(nfc_access_state, "idle"); - nfc_last_uid = ""; - Serial.printf("NFC: card removed — relay %d closed\n", nfc_relay_num); - } else { - Serial.printf("NFC: presence miss %d/2 — retrying\n", nfc_miss_count); - } - } else if (strcmp(nfc_access_state, "denied") == 0) { - nfc_miss_count++; - if (nfc_miss_count >= 2) { - Serial.printf("NFC: denied card removed\n"); - strcpy(nfc_access_state, "idle"); - nfc_last_uid = ""; - nfc_miss_count = 0; + Serial.printf("NFC: card absent %lu ms — relay %d OFF\n", + absent_limit, nfc_relay_num); } + } else { + // DENIED or IDLE — card gone, just return to idle + nfc_absent_since = 0; + nfc_last_uid = ""; + strcpy(nfc_access_state, "idle"); } - // idle: keep fast-polling, no action needed } } } - - delay(10); + // ────────────────────────────────────────────────────────────────────────── + yield(); } // ============================================ @@ -617,6 +713,8 @@ void handleRoot() { html += "table.settings tr:last-child{border-bottom:none}"; html += "table.settings .val{font-weight:700;color:var(--text)}"; html += "table.settings .mono{font-family:monospace;color:#1b5e20}"; + html += ".uid-tag{display:inline-flex;align-items:center;gap:4px;background:var(--card2,#e8f5e9);color:#1b5e20;border-radius:4px;padding:3px 8px;margin:2px 4px 2px 0;font-family:monospace;font-size:13px}"; + html += ".uid-tag-del{cursor:default}"; html += ".api-list{font-size:12px;color:var(--text2);line-height:2}"; html += ""; // ── JavaScript ───────────────────────────────────────────────────────── @@ -647,7 +745,7 @@ void handleRoot() { html += "b.className='relay-btn '+(on?'relay-on':'relay-off');"; html += "b.textContent='Relay '+i+': '+(on?'ON':'OFF');}"; html += "var nled=document.getElementById('nfc-led');"; - html += "if(nled)nled.className='led '+(d.nfc_card_present?'led-on':'led-off');"; + html += "if(nled)nled.className='led '+(d.nfc_last_uid?'led-on':'led-off');"; html += "var nuid=document.getElementById('nfc-uid');"; html += "if(nuid)nuid.textContent=d.nfc_last_uid||'No card inserted';"; html += "var ncb=document.getElementById('nfc-copy-btn');if(ncb)ncb.disabled=!d.nfc_card_present;"; @@ -656,12 +754,13 @@ void handleRoot() { html += "nac.className='nfc-state nfc-'+as;"; html += "nac.textContent=as==='granted'?'ACCESS GRANTED':as==='denied'?'ACCESS DENIED':'Waiting for card';}"; html += "var ef=document.getElementById('nfc-auth-field');"; - html += "if(ef&&document.activeElement!==ef)ef.value=d.nfc_auth_uid||'';"; + html += "if(ef&&document.activeElement!==ef)ef.value=d.nfc_last_uid||'';"; html += "var ad=document.getElementById('nfc-auth-display');"; - html += "if(ad){if(d.nfc_auth_uid){ad.style.color='#1b5e20';ad.textContent=d.nfc_auth_uid;}"; - html += "else{ad.style.color='var(--wh-err-c)';ad.textContent='None \u2014 no card authorized yet';}}"; + html += "if(ad){var uids=d.nfc_auth_uids||[];"; + html += "if(uids.length>0){ad.style.color='#1b5e20';ad.innerHTML=uids.map(function(u,i){return ''+u+'';}).join('');}"; + html += "else{ad.style.color='var(--wh-err-c)';ad.innerHTML='None \u2014 no card authorized yet';}}"; html += "var rs=document.getElementById('nfc-relay-sel');if(rs&&document.activeElement!==rs)rs.value=d.nfc_relay_num||1;"; - html += "var pf=document.getElementById('nfc-pulse-field');if(pf&&document.activeElement!==pf)pf.value=d.nfc_pulse_ms||3000;"; + html += "var pf=document.getElementById('nfc-pulse-field');if(pf&&document.activeElement!==pf)pf.value=d.nfc_pulse_ms!=null?d.nfc_pulse_ms:0;"; html += "var nmb=document.getElementById('nfc-module-btn');"; html += "if(nmb){var ne=!!d.nfc_enabled;nmb.dataset.enabled=ne?'1':'0';"; html += "nmb.className='btn '+(ne?'nfc-module-on':'nfc-module-off');"; @@ -681,18 +780,33 @@ void handleRoot() { html += "if(ns){ns.textContent=ne?'Active':'Disabled';ns.style.color=ne?'#4CAF50':'#f44336';}}"; html += "else{alert('Error: '+d.error);}}).catch(function(e){alert('Network error');});}"; // NFC helpers - html += "function clearNFCAuth(){if(!confirm('Remove the authorized card?'))return;"; - html += "fetch('/nfc/config?auth_uid=&relay='+document.getElementById('nfc-relay-sel').value+'&pulse_ms='+document.getElementById('nfc-pulse-field').value,{method:'POST'})"; + html += "function getCurrentUIDs(){"; + html += "var tags=document.querySelectorAll('#nfc-uid-list .uid-tag-del');"; + html += "return Array.from(tags).map(function(t){return t.dataset.uid;}).filter(function(u){return u&&u.length>0;});}"; + html += "function renderUIDList(uids){"; + html += "var list=document.getElementById('nfc-uid-list');if(!list)return;"; + html += "list.innerHTML='';"; + html += "uids.forEach(function(u){"; + html += "var s=document.createElement('span');s.className='uid-tag uid-tag-del';s.dataset.uid=u;"; + html += "s.innerHTML=u+' ';"; + html += "list.appendChild(s);});"; + html += "if(uids.length===0)list.innerHTML='None \u2014 no card authorized yet';}"; + html += "function removeCard(uid){"; + html += "var uids=getCurrentUIDs().filter(function(u){return u!==uid;});"; + html += "fetch('/nfc/config?auth_uids='+encodeURIComponent(uids.join(','))+'&relay='+document.getElementById('nfc-relay-sel').value+'&pulse_ms='+document.getElementById('nfc-pulse-field').value,{method:'POST'})"; html += ".then(function(r){return r.json();})"; - html += ".then(function(d){if(d.status==='ok'){alert('Authorized card cleared.');}else{alert('Error: '+d.error);}})"; + html += ".then(function(d){if(d.status==='ok'){renderUIDList(d.auth_uids||[]);}else{alert('Error: '+d.error);}})"; html += ".catch(function(e){alert('Network error');});}"; html += "function saveNFCConfig(){"; - html += "var uid=document.getElementById('nfc-auth-field').value.trim().toUpperCase();"; + html += "var newuid=document.getElementById('nfc-auth-field').value.trim().toUpperCase();"; + html += "var uids=getCurrentUIDs();"; + html += "if(newuid.length>0){if(uids.indexOf(newuid)===-1){if(uids.length>=10){alert('Maximum 10 authorized cards reached. Remove one first.');return;}uids.push(newuid);}"; + html += "document.getElementById('nfc-auth-field').value='';}"; html += "var relay=document.getElementById('nfc-relay-sel').value;"; html += "var pulse=document.getElementById('nfc-pulse-field').value;"; - html += "fetch('/nfc/config?auth_uid='+encodeURIComponent(uid)+'&relay='+relay+'&pulse_ms='+pulse,{method:'POST'})"; + html += "fetch('/nfc/config?auth_uids='+encodeURIComponent(uids.join(','))+'&relay='+relay+'&pulse_ms='+pulse,{method:'POST'})"; html += ".then(function(r){return r.json();})"; - html += ".then(function(d){if(d.status==='ok'){alert('Saved!\\nUID: '+(d.auth_uid||'(none)')+'\\nRelay: '+d.relay_num+'\\nTimeout: '+d.pulse_ms+'ms');}else{alert('Error: '+d.error);}})"; + html += ".then(function(d){if(d.status==='ok'){renderUIDList(d.auth_uids||[]);alert('Saved! '+d.auth_uids.length+' card(s) authorized.');}else{alert('Error: '+d.error);}})"; html += ".catch(function(e){alert('Network error');});}"; html += "function copyUID(){var u=document.getElementById('nfc-uid').textContent;"; html += "if(u&&u!=='No card inserted'){var f=document.getElementById('nfc-auth-field');f.value=u;"; @@ -776,37 +890,44 @@ void handleRoot() { // Current settings table html += "

Current Settings

"; html += ""; - html += ""; - if (strlen(nfc_auth_uid) > 0) { - html += ""; - html += ""; + html += ""; + html += ""; + for (int i = 0; i < NFC_MAX_CARDS; i++) { + if (strlen(nfc_auth_uids[i]) > 0) { + html += ""; + html += String(nfc_auth_uids[i]); + html += " "; + html += ""; + } + } } - html += ""; + html += ""; html += ""; - html += ""; + html += ""; html += "
Authorized card" + String(nfc_auth_uid) + "
Authorized cards"; + if (nfc_auth_count == 0) { + html += "None — no card authorized yet"; } else { - html += "None — no card authorized yet
Trigger relayRelay " + String(nfc_relay_num) + "
Absent timeout" + String(nfc_pulse_ms) + " ms
Absent timeout" + String(nfc_pulse_ms) + " ms (0 = 5 s default)
"; // Edit settings form html += "

Edit Settings

"; html += "
"; - html += "
Authorized UID
"; - html += "
"; + html += "
Add authorized UID (max 10 cards)
"; + html += "
"; html += "
Trigger relay
"; html += "
"; - html += "
Absent timeout (ms)
"; - html += "
"; - html += ""; + html += "
Absent timeout (ms, 0 = 5 s default)
"; + html += "
"; + html += ""; html += "
"; if (!nfc_initialized) { html += "

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

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

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

"; + if (nfc_initialized && nfc_auth_count == 0) { + html += "

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

"; } html += ""; // close NFC card @@ -821,7 +942,7 @@ void handleRoot() { // API reference html += "

API Endpoints

"; - html += "GET /api/status  •  POST /relay/on?relay=1-4  •  POST /relay/off?relay=1-4
"; + html += "GET /api/status  •  POST /relay/on?relay=1-4  •  POST /relay/off?relay=1-4  •  POST /relay/toggle?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=..."; @@ -937,12 +1058,16 @@ void handleStatus() { json += "\"relay3\":" + String(relay3_state ? "true" : "false") + ","; json += "\"relay4\":" + String(relay4_state ? "true" : "false") + ","; json += "\"led\":" + String(led_state ? "true" : "false") + ","; - bool nfc_present = nfc_initialized && (strcmp(nfc_access_state, "granted") == 0); + bool nfc_present = nfc_initialized && nfc_last_uid.length() > 0; json += "\"nfc_initialized\":" + String(nfc_initialized ? "true" : "false") + ","; json += "\"nfc_card_present\":" + String(nfc_present ? "true" : "false") + ","; json += "\"nfc_last_uid\":\"" + nfc_last_uid + "\","; json += "\"nfc_access_state\":\"" + String(nfc_access_state) + "\","; - json += "\"nfc_auth_uid\":\"" + String(nfc_auth_uid) + "\","; + json += "\"nfc_auth_uid\":\"" + (nfc_auth_count > 0 ? String(nfc_auth_uids[0]) : "") + "\","; + // Full list for multi-card support + json += "\"nfc_auth_uids\":["; + { bool f=true; for(int i=0;i0){if(!f)json+=",";json+="\""+String(nfc_auth_uids[i])+"\"";f=false;}} } + json += "],"; json += "\"nfc_relay_num\":" + String(nfc_relay_num) + ","; json += "\"nfc_pulse_ms\":" + String(nfc_pulse_ms) + ","; json += "\"nfc_enabled\":" + String(nfc_enabled ? "true" : "false"); @@ -1012,6 +1137,36 @@ void handleRelayOff() { Serial.printf("Relay %d OFF\n", relay_num); } +void handleRelayToggle() { + if (!verifyAPIRequest()) return; + if (!server.hasArg("relay")) { + server.send(400, "application/json", "{\"error\":\"Missing relay parameter\"}"); + return; + } + + int relay_num = server.arg("relay").toInt(); + int pin = -1; + bool *state_ptr = nullptr; + + switch (relay_num) { + case 1: pin = RELAY_1_PIN; state_ptr = &relay1_state; break; + case 2: pin = RELAY_2_PIN; state_ptr = &relay2_state; break; + case 3: pin = RELAY_3_PIN; state_ptr = &relay3_state; break; + case 4: pin = RELAY_4_PIN; state_ptr = &relay4_state; break; + default: + server.send(400, "application/json", "{\"error\":\"Invalid relay number (1-4)\"}"); + return; + } + + // Flip current state (active HIGH: HIGH = ON, LOW = OFF) + bool new_state = !(*state_ptr); + digitalWrite(pin, new_state ? HIGH : LOW); + *state_ptr = new_state; + String json = "{\"status\":\"ok\",\"state\":" + String(new_state ? "true" : "false") + "}"; + server.send(200, "application/json", json); + Serial.printf("Relay %d TOGGLE → %s\n", relay_num, new_state ? "ON" : "OFF"); +} + void handleRelayStatus() { if (!verifyAPIRequest()) return; if (!server.hasArg("relay")) { @@ -1247,6 +1402,84 @@ void handleNFCEnable() { String("{\"status\":\"ok\",\"nfc_enabled\":") + (nfc_enabled ? "true" : "false") + "}"); } +void handleNFCProbe() { + // Manual NFC detection at runtime with timeout protection + if (!requireAuth()) return; + + server.setContentLength(CONTENT_LENGTH_UNKNOWN); + server.send(200, "text/plain"); + server.sendContent("Starting NFC probe (with 300ms timeout per attempt)...\n\n"); + + const long NFC_BAUDS[] = {115200, 9600, 57600, 38400}; + const int NFC_NBAUDS = 4; + const int NFC_PINS[2][2] = {{NFC_RX_PIN, NFC_TX_PIN}, + {NFC_TX_PIN, NFC_RX_PIN}}; + + uint32_t versiondata = 0; + long found_baud = 0; + int found_rx = NFC_RX_PIN; + int found_tx = NFC_TX_PIN; + + for (int pi = 0; pi < 2 && !versiondata; pi++) { + int rx = NFC_PINS[pi][0]; + int tx = NFC_PINS[pi][1]; + for (int bi = 0; bi < NFC_NBAUDS && !versiondata; bi++) { + long baud = NFC_BAUDS[bi]; + char msg[128]; + snprintf(msg, sizeof(msg), " Trying baud=%-7ld RX=GPIO%d TX=GPIO%d ... ", baud, rx, tx); + server.sendContent(msg); + + nfcSerial.begin(baud, SERIAL_8N1, rx, tx); + delay(100); + while (nfcSerial.available()) nfcSerial.read(); + nfc.begin(); + + unsigned long probe_start = millis(); + versiondata = nfc.getFirmwareVersion(); + unsigned long probe_time = millis() - probe_start; + + if (versiondata) { + found_baud = baud; found_rx = rx; found_tx = tx; + snprintf(msg, sizeof(msg), "✓ FOUND in %lu ms\n", probe_time); + server.sendContent(msg); + } else { + server.sendContent("✗\n"); + } + server.handleClient(); + yield(); + } + } + + server.sendContent("\n"); + if (!versiondata) { + server.sendContent("✗ PN532 not detected with any baud/pin combination.\n\n"); + server.sendContent("Hardware checklist:\n"); + server.sendContent("1. DIP/solder-jumpers on PN532: BOTH = 0 for HSU mode\n"); + server.sendContent(" (Some boards label them SEL0/SEL1 or I0/I1 — both must be LOW)\n"); + server.sendContent("2. Power: UEXT1 pin 1 = 3.3V, pin 2 = GND (measure with multimeter)\n"); + server.sendContent("3. Wiring: UEXT1 pin 3 (GPIO4) ↔ PN532 RXD\n"); + server.sendContent(" UEXT1 pin 4 (GPIO5) ↔ PN532 TXD\n"); + server.sendContent("4. Some PN532 boards need 100Ω series resistor on TX line\n"); + server.sendContent("5. Power off the module, power on again, then run probe\n"); + nfc_initialized = false; + } else { + char msg[256]; + snprintf(msg, sizeof(msg), + "✓ PN532 found! baud=%ld RX=GPIO%d TX=GPIO%d\n" + " Chip: PN5%02X\n" + " Firmware: %d.%d\n\n", + found_baud, found_rx, found_tx, + (versiondata >> 24) & 0xFF, + (versiondata >> 16) & 0xFF, (versiondata >> 8) & 0xFF); + server.sendContent(msg); + nfcSerial.begin(found_baud, SERIAL_8N1, found_rx, found_tx); + nfc.SAMConfig(); + nfc_initialized = true; + server.sendContent("✓ NFC initialized successfully — ready for card detection\n"); + } + server.sendContent(""); // Finish response +} + void handleNFCStatus() { if (!verifyAPIRequest()) return; bool present = nfc_initialized && nfc_enabled && (strcmp(nfc_access_state, "granted") == 0); @@ -1256,7 +1489,10 @@ void handleNFCStatus() { json += "\"card_present\":" + String(present ? "true" : "false") + ","; json += "\"last_uid\":\"" + nfc_last_uid + "\","; json += "\"access_state\":\"" + String(nfc_access_state) + "\","; - json += "\"auth_uid\":\"" + String(nfc_auth_uid) + "\","; + json += "\"auth_uid\":\"" + String(nfc_auth_count > 0 ? nfc_auth_uids[0] : "") + "\","; + json += "\"auth_uids\":["; + { bool f=true; for(int i=0;i0){if(!f)json+=",";json+="\""+String(nfc_auth_uids[i])+"\"";f=false;}} } + json += "],"; json += "\"relay_num\":" + String(nfc_relay_num) + ","; json += "\"pulse_ms\":" + String(nfc_pulse_ms); json += "}"; @@ -1298,7 +1534,16 @@ int nfcRelayPin(int rnum) { void handleNFCConfigGet() { if (!verifyAPIRequest()) return; String json = "{"; - json += "\"auth_uid\":\"" + String(nfc_auth_uid) + "\","; + json += "\"auth_uids\":["; + bool first = true; + for (int i = 0; i < NFC_MAX_CARDS; i++) { + if (strlen(nfc_auth_uids[i]) > 0) { + if (!first) json += ","; + json += "\"" + String(nfc_auth_uids[i]) + "\""; + first = false; + } + } + json += "],"; json += "\"relay_num\":" + String(nfc_relay_num) + ","; json += "\"pulse_ms\":" + String(nfc_pulse_ms) + ","; json += "\"nfc_enabled\":" + String(nfc_enabled ? "true" : "false"); @@ -1308,17 +1553,34 @@ void handleNFCConfigGet() { void handleNFCConfigSet() { if (!verifyAPIRequest()) return; - if (server.hasArg("auth_uid")) { - String u = server.arg("auth_uid"); - u.trim(); - u.toUpperCase(); - if (u.length() < sizeof(nfc_auth_uid)) { - u.toCharArray(nfc_auth_uid, sizeof(nfc_auth_uid)); - } else { - server.send(400, "application/json", "{\"error\":\"auth_uid too long (max 31 chars)\"}"); - return; + + // auth_uids: comma-separated list of UIDs, e.g. "04:AB:CD:EF,04:11:22:33" + // Sending auth_uids= (empty) clears all authorized cards. + if (server.hasArg("auth_uids")) { + String raw = server.arg("auth_uids"); + raw.trim(); + raw.toUpperCase(); + // Clear all slots first + for (int i = 0; i < NFC_MAX_CARDS; i++) nfc_auth_uids[i][0] = '\0'; + nfc_auth_count = 0; + if (raw.length() > 0) { + int start = 0; + int slot = 0; + while (slot < NFC_MAX_CARDS) { + int comma = raw.indexOf(',', start); + String uid = (comma == -1) ? raw.substring(start) : raw.substring(start, comma); + uid.trim(); + if (uid.length() > 0 && uid.length() < 32) { + uid.toCharArray(nfc_auth_uids[slot], 32); + slot++; + nfc_auth_count++; + } + if (comma == -1) break; + start = comma + 1; + } } } + if (server.hasArg("relay")) { int r = server.arg("relay").toInt(); if (r >= 1 && r <= 4) { @@ -1330,26 +1592,58 @@ void handleNFCConfigSet() { } if (server.hasArg("pulse_ms")) { long p = server.arg("pulse_ms").toInt(); - if (p >= 100 && p <= 60000) { + if (p >= 0 && p <= 60000) { nfc_pulse_ms = (unsigned long)p; } else { - server.send(400, "application/json", "{\"error\":\"pulse_ms range: 100-60000\"}"); + server.send(400, "application/json", "{\"error\":\"pulse_ms range: 0-60000\"}"); return; } } - 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); + + Serial.printf("NFC config: %d card(s) authorized, relay=%d, pulse=%lu ms\n", + nfc_auth_count, nfc_relay_num, nfc_pulse_ms); + + // If relay was ON from a previous grant, turn it off before resetting state + 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; + nfc_absent_since = 0; + + // ── Persist to NVS ──────────────────────────────────────────────────────── + prefs.begin("nfc_cfg", false); + prefs.putInt("relay_num", nfc_relay_num); + prefs.putULong("pulse_ms", nfc_pulse_ms); + for (int i = 0; i < NFC_MAX_CARDS; i++) { + String key = "uid" + String(i); + prefs.putString(key.c_str(), nfc_auth_uids[i]); + } prefs.end(); // ───────────────────────────────────────────────────────────────────────── - String json = "{\"status\":\"ok\"," - "\"auth_uid\":\"" + String(nfc_auth_uid) + "\"," - "\"relay_num\":" + String(nfc_relay_num) + "," - "\"pulse_ms\":" + String(nfc_pulse_ms) + "}"; + + // Build response with current uid list + String json = "{\"status\":\"ok\",\"auth_uids\":["; + bool first = true; + for (int i = 0; i < NFC_MAX_CARDS; i++) { + if (strlen(nfc_auth_uids[i]) > 0) { + if (!first) json += ","; + json += "\"" + String(nfc_auth_uids[i]) + "\""; + first = false; + } + } + json += "],\"relay_num\":" + String(nfc_relay_num) + + ",\"pulse_ms\":" + String(nfc_pulse_ms) + "}"; server.send(200, "application/json", json); } @@ -1382,7 +1676,12 @@ void handleDebug() { out += "NFC last UID:" + String(nfc_last_uid.length() > 0 ? nfc_last_uid : "(none)") + "\n"; out += "NFC state: " + String(nfc_access_state) + "\n"; out += "NFC relay: " + String(nfc_relay_num) + "\n"; - out += "NFC auth: " + String(strlen(nfc_auth_uid) > 0 ? nfc_auth_uid : "(any)") + "\n"; + out += "NFC auth: " + String(nfc_auth_count) + " card(s)\n"; + for (int i = 0; i < NFC_MAX_CARDS; i++) { + if (strlen(nfc_auth_uids[i]) > 0) { + out += " [" + String(i) + "] " + String(nfc_auth_uids[i]) + "\n"; + } + } out += "\n"; out += "Relay states: R1=" + String(relay1_state) + " R2=" + String(relay2_state) + " R3=" + String(relay3_state) + " R4=" + String(relay4_state) + "\n"; diff --git a/olimex_ESP32-C6-EVB/esp32_arduino/secrets.h.example b/olimex_ESP32-C6-EVB/esp32_arduino/secrets.h.example index c4b3a90..20ecc79 100644 --- a/olimex_ESP32-C6-EVB/esp32_arduino/secrets.h.example +++ b/olimex_ESP32-C6-EVB/esp32_arduino/secrets.h.example @@ -10,6 +10,20 @@ // WEB_USER / WEB_PASSWORD — credentials for the browser control panel. // ───────────────────────────────────────────────────────────────────────────── +// ─ WiFi Credentials ─────────────────────────────────────────────────────── +#define WIFI_SSID "your_wifi_network" +#define WIFI_PASSWORD "your_wifi_password" + +// ─ Static IP Configuration ──────────────────────────────────────────────── +// Set USE_STATIC_IP to true for a fixed IP; false = use DHCP +#define USE_STATIC_IP true +#define STATIC_IP_ADDR "192.168.0.240" // C6-EVB board IP +#define STATIC_GATEWAY "192.168.0.1" // Local network gateway +#define STATIC_SUBNET "255.255.255.0" // Subnet mask +#define STATIC_DNS1 "8.8.8.8" // Google DNS primary +#define STATIC_DNS2 "8.8.4.4" // Google DNS secondary + +// ─ Web Server Credentials ───────────────────────────────────────────────── #define API_SECRET "REPLACE_WITH_OUTPUT_OF_python3_-c_import_secrets_token_hex_32" #define WEB_USER "your_username" #define WEB_PASSWORD "your_password"