From 22227b7e2149e829a941cdc6741e90e7720ed434 Mon Sep 17 00:00:00 2001 From: scheianu Date: Sun, 15 Mar 2026 08:47:06 +0200 Subject: [PATCH] updated board --- board_test/board_test.ino | 310 ++++++++++++++++ board_verify.py | 252 +++++++++++++ esp32_arduino/esp32_arduino.ino | 630 ++++++++++++++++++++++++++------ 3 files changed, 1084 insertions(+), 108 deletions(-) create mode 100644 board_test/board_test.ino create mode 100644 board_verify.py diff --git a/board_test/board_test.ino b/board_test/board_test.ino new file mode 100644 index 0000000..1841e82 --- /dev/null +++ b/board_test/board_test.ino @@ -0,0 +1,310 @@ +/** + * Olimex ESP32-C6-EVB — Board Functional Test + * ================================================= + * Flash this sketch BEFORE deploying the main firmware. + * It tests every hardware subsystem and reports PASS/FAIL + * on the Serial Monitor AND on a simple web page. + * + * Board settings (Arduino IDE): + * Board : ESP32C6 Dev Module + * USB CDC On Boot : Enabled ← REQUIRED + * Flash Size : 4MB + * Upload Speed : 921600 + * + * How to read results: + * 1. Open Serial Monitor at 115200 baud after upload. + * 2. Press RESET on the board — full report prints once. + * 3. Connect a phone/PC to the same WiFi and open: + * http://192.168.0.181/test (or whatever IP prints) + * + * Relay self-test: the test pulses each relay 200 ms ON then OFF. + * You will see/hear the relays click twice each. + * + * NFC self-test: tries all baud rates (115200 / 9600 / 57600) + * on both pin orientations. Module must be wired on UEXT1. + */ + +#include +#include +#include +#include + +// ── WiFi credentials ───────────────────────────────────────────────────────── +const char* SSID = "BUON GUSTO PARTER"; +const char* PASSWORD = "arleta13"; +IPAddress STATIC_IP(192, 168, 0, 181); +IPAddress GATEWAY (192, 168, 0, 1); +IPAddress SUBNET (255, 255, 255, 0); + +// ── Pin map ─────────────────────────────────────────────────────────────────── +const int LED_PIN = 8; +const int BUT_PIN = 9; +const int RELAY_PIN[] = {10, 11, 22, 23}; // Relay 1-4 +const int INPUT_PIN[] = {1, 2, 3, 15}; // Digital Input 1-4 +const int NFC_RX = 5; // UEXT1 pin 4 +const int NFC_TX = 4; // UEXT1 pin 3 + +// ── NFC objects ─────────────────────────────────────────────────────────────── +HardwareSerial nfcSerial(1); +PN532_HSU pn532hsu(nfcSerial); +PN532 nfc(pn532hsu); + +// ── Web server ──────────────────────────────────────────────────────────────── +WebServer server(80); + +// ── Test result storage ─────────────────────────────────────────────────────── +struct TestResult { + const char* name; + bool passed; + String detail; +}; + +static const int MAX_TESTS = 20; +TestResult results[MAX_TESTS]; +int result_count = 0; +int pass_count = 0; +int fail_count = 0; + +// ── Helper: record a result ─────────────────────────────────────────────────── +void record(const char* name, bool ok, String detail = "") { + if (result_count < MAX_TESTS) { + results[result_count++] = {name, ok, detail}; + } + if (ok) pass_count++; else fail_count++; + Serial.printf(" [%s] %s%s\n", + ok ? "PASS" : "FAIL", + name, + detail.length() ? (" — " + detail).c_str() : ""); +} + +// ───────────────────────────────────────────────────────────────────────────── +// TEST FUNCTIONS +// ───────────────────────────────────────────────────────────────────────────── + +void testGPIO() { + Serial.println("\n--- GPIO Test ---"); + + // ── LED ────────────────────────────────────────────────────────────────── + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LOW); // LED on (active-low) + delay(300); + digitalWrite(LED_PIN, HIGH); // LED off + delay(100); + // We cannot read back an output pin reliably, so just record as attempted. + record("LED on/off", true, "GPIO8 — verify LED blinked"); + + // ── Button ──────────────────────────────────────────────────────────────── + pinMode(BUT_PIN, INPUT_PULLUP); + int btn = digitalRead(BUT_PIN); + // Button is pull-up; HIGH = not pressed. Either state is valid at test time. + record("Button readable", true, + String("GPIO9 = ") + (btn ? "HIGH (not pressed)" : "LOW (pressed)")); + + // ── Digital Inputs ──────────────────────────────────────────────────────── + const char* in_names[] = {"Input1 (GPIO1)", "Input2 (GPIO2)", + "Input3 (GPIO3)", "Input4 (GPIO15)"}; + for (int i = 0; i < 4; i++) { + pinMode(INPUT_PIN[i], INPUT_PULLUP); + int v = digitalRead(INPUT_PIN[i]); + record(in_names[i], true, + String("= ") + (v ? "HIGH (open)" : "LOW (active)")); + } + + // ── Relays ──────────────────────────────────────────────────────────────── + const char* rel_names[] = {"Relay1 (GPIO10)", "Relay2 (GPIO11)", + "Relay3 (GPIO22)", "Relay4 (GPIO23)"}; + for (int i = 0; i < 4; i++) { + pinMode(RELAY_PIN[i], OUTPUT); + digitalWrite(RELAY_PIN[i], LOW); + } + delay(100); + + for (int i = 0; i < 4; i++) { + // Pulse ON for 200 ms — you should hear/see relay click + digitalWrite(RELAY_PIN[i], HIGH); + delay(200); + digitalWrite(RELAY_PIN[i], LOW); + delay(100); + record(rel_names[i], true, "pulsed 200 ms — listen for click"); + } +} + +// ── WiFi ───────────────────────────────────────────────────────────────────── +void testWiFi() { + Serial.println("\n--- WiFi Test ---"); + WiFi.disconnect(true); + delay(200); + WiFi.mode(WIFI_STA); + WiFi.config(STATIC_IP, GATEWAY, SUBNET); + + bool connected = false; + for (int pass = 1; pass <= 3 && !connected; pass++) { + Serial.printf(" Connecting (attempt %d/3)...", pass); + WiFi.begin(SSID, PASSWORD); + for (int t = 0; t < 40 && WiFi.status() != WL_CONNECTED; t++) { + delay(500); + Serial.print("."); + } + Serial.println(); + connected = (WiFi.status() == WL_CONNECTED); + if (!connected && pass < 3) { WiFi.disconnect(true); delay(500); WiFi.mode(WIFI_STA); WiFi.config(STATIC_IP, GATEWAY, SUBNET); } + } + + if (connected) { + record("WiFi connect", true, + WiFi.localIP().toString() + " RSSI=" + String(WiFi.RSSI()) + " dBm"); + } else { + record("WiFi connect", false, + "status=" + String(WiFi.status()) + " — check SSID/password"); + } +} + +// ── NFC ────────────────────────────────────────────────────────────────────── +void testNFC() { + Serial.println("\n--- NFC (PN532 HSU) Test ---"); + + const long BAUDS[] = {115200, 9600, 57600, 38400}; + const int NPINS[2][2]= {{NFC_RX, NFC_TX}, {NFC_TX, NFC_RX}}; + uint32_t ver = 0; + long found_baud = 0; + int found_rx = NFC_RX, found_tx = NFC_TX; + + for (int pi = 0; pi < 2 && !ver; pi++) { + for (int bi = 0; bi < 4 && !ver; bi++) { + int rx = NPINS[pi][0], tx = NPINS[pi][1]; + Serial.printf(" baud=%-7ld RX=GPIO%d TX=GPIO%d ... ", BAUDS[bi], rx, tx); + nfcSerial.begin(BAUDS[bi], SERIAL_8N1, rx, tx); + delay(500); + nfc.begin(); + ver = nfc.getFirmwareVersion(); + if (ver) { + found_baud = BAUDS[bi]; found_rx = rx; found_tx = tx; + Serial.println("FOUND"); + } else { + Serial.println("no response"); + delay(100); + } + } + } + + if (ver) { + nfc.SAMConfig(); + char detail[80]; + snprintf(detail, sizeof(detail), + "PN5%02X FW=%d.%d baud=%ld RX=GPIO%d TX=GPIO%d", + (ver >> 24) & 0xFF, + (ver >> 16) & 0xFF, (ver >> 8) & 0xFF, + found_baud, found_rx, found_tx); + record("NFC PN532 init", true, detail); + } else { + record("NFC PN532 init", false, + "not detected — check DIP switches (both=0) & UEXT1 wiring"); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// WEB PAGE GET /test +// ───────────────────────────────────────────────────────────────────────────── + +void handleTestPage() { + String h = "Board Test"; + h += ""; + h += ""; + h += "

Olimex ESP32-C6-EVB — Functional Test

"; + h += "
MAC: " + WiFi.macAddress() + "   IP: " + + WiFi.localIP().toString() + "   Uptime: " + + String(millis() / 1000) + "s
"; + + h += ""; + for (int i = 0; i < result_count; i++) { + bool ok = results[i].passed; + h += ""; + h += ""; + h += ""; + h += ""; + } + h += "
#TestResultDetail
" + String(i + 1) + "" + String(results[i].name) + "" + + String(ok ? "PASS" : "FAIL") + "" + results[i].detail + "
"; + + bool all_ok = (fail_count == 0); + h += "
"; + h += String(pass_count) + " PASSED   /   " + + String(fail_count) + " FAILED   out of " + + String(result_count) + " tests"; + if (all_ok) h += "   ✓ Board OK"; + else h += "   ✗ Check failures above"; + h += "
"; + server.send(200, "text/html", h); +} + +void handleTestJSON() { + String j = "{\"pass\":" + String(pass_count) + + ",\"fail\":" + String(fail_count) + + ",\"total\":" + String(result_count) + + ",\"board_ok\":" + String(fail_count == 0 ? "true" : "false") + + ",\"mac\":\"" + WiFi.macAddress() + "\"" + + ",\"ip\":\"" + WiFi.localIP().toString() + "\"" + + ",\"uptime_s\":" + String(millis() / 1000) + + ",\"tests\":["; + for (int i = 0; i < result_count; i++) { + if (i) j += ","; + j += "{\"name\":\"" + String(results[i].name) + "\"" + + ",\"pass\":" + String(results[i].passed ? "true" : "false") + + ",\"detail\":\"" + results[i].detail + "\"}"; + } + j += "]}"; + server.send(200, "application/json", j); +} + +// ───────────────────────────────────────────────────────────────────────────── +// setup / loop +// ───────────────────────────────────────────────────────────────────────────── + +void setup() { + Serial.begin(115200); + delay(2000); + for (int i = 0; i < 10 && !Serial; i++) delay(500); + + Serial.println("\n\n╔══════════════════════════════════════╗"); + Serial.println("║ Olimex ESP32-C6-EVB Board Test ║"); + Serial.println("╚══════════════════════════════════════╝"); + + testGPIO(); + testWiFi(); + testNFC(); + + // ── Start web server ────────────────────────────────────────────────────── + server.on("/", HTTP_GET, handleTestPage); + server.on("/test", HTTP_GET, handleTestPage); + server.on("/test.json", HTTP_GET, handleTestJSON); + server.onNotFound([](){ server.send(404, "text/plain", "use /test or /test.json"); }); + server.begin(); + + // ── Final summary on serial ─────────────────────────────────────────────── + Serial.println("\n╔══════════════════════════════════════╗"); + Serial.printf( "║ PASSED: %2d FAILED: %2d TOTAL: %2d ║\n", + pass_count, fail_count, result_count); + Serial.println(fail_count == 0 + ? "║ ✓ ALL TESTS PASSED — board is OK ║" + : "║ ✗ FAILURES DETECTED — see above ║"); + Serial.println("╚══════════════════════════════════════╝"); + if (WiFi.status() == WL_CONNECTED) { + Serial.printf("\nOpen browser: http://%s/test\n", WiFi.localIP().toString().c_str()); + Serial.printf("Or fetch JSON: http://%s/test.json\n\n", WiFi.localIP().toString().c_str()); + } +} + +void loop() { + server.handleClient(); +} diff --git a/board_verify.py b/board_verify.py new file mode 100644 index 0000000..1e709d3 --- /dev/null +++ b/board_verify.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 +""" +Olimex ESP32-C6-EVB — Remote Board Verification Script +========================================================= +Queries the board's REST API and verifies every subsystem. + +Usage: + python3 board_verify.py # uses default IP 192.168.0.181 + python3 board_verify.py 192.168.0.200 # custom IP + python3 board_verify.py --json # machine-readable output + +Requirements: pip install requests (already in location_managemet requirements) + +What it tests: + 1. Board reachability (GET /api/status) + 2. All 4 relays (POST /relay/on, GET /relay/status, POST /relay/off) + 3. All 4 digital inputs (GET /input/status) + 4. LED (POST /led/on + /led/off) + 5. NFC reader (GET /nfc/status) + 6. NFC config API (GET /nfc/config) + +NOTE: Relay tests cycle each relay ON→verify→OFF→verify. + You should hear/see the relay click during the test. +""" + +import sys +import json +import time +import argparse +import requests + +TIMEOUT = 5 # seconds per HTTP request +RELAY_DLY = 0.4 # seconds to wait between relay on/status/off + +# ───────────────────────────────────────────────────────────────────────────── +# Result tracking +# ───────────────────────────────────────────────────────────────────────────── + +results = [] + +def record(name: str, ok: bool, detail: str = "") -> bool: + results.append({"name": name, "pass": ok, "detail": detail}) + icon = "\033[32m[PASS]\033[0m" if ok else "\033[31m[FAIL]\033[0m" + print(f" {icon} {name}" + (f" — {detail}" if detail else "")) + return ok + + +def _get(url: str): + try: + r = requests.get(url, timeout=TIMEOUT) + r.raise_for_status() + return r.json() + except requests.exceptions.ConnectionError: + return None + except Exception as e: + return {"_error": str(e)} + + +def _post(url: str): + try: + r = requests.post(url, timeout=TIMEOUT) + r.raise_for_status() + return r.json() + except requests.exceptions.ConnectionError: + return None + except Exception as e: + return {"_error": str(e)} + + +# ───────────────────────────────────────────────────────────────────────────── +# Tests +# ───────────────────────────────────────────────────────────────────────────── + +def test_reachability(base: str) -> bool: + print("\n── Connectivity ──────────────────────────────") + data = _get(f"{base}/api/status") + if data is None: + record("Board reachable", False, f"no response from {base}") + return False + if "_error" in data: + record("Board reachable", False, data["_error"]) + return False + + record("Board reachable", True, + f"IP {base.split('//')[1]} " + f"nfc_init={data.get('nfc_initialized','?')} " + f"nfc_uid={data.get('nfc_last_uid') or '(none)'}") + return True + + +def test_relays(base: str): + print("\n── Relay Tests ───────────────────────────────") + for relay in range(1, 5): + # Turn ON + r_on = _post(f"{base}/relay/on?relay={relay}") + if r_on is None: + record(f"Relay {relay} ON", False, "no response"); continue + time.sleep(RELAY_DLY) + + # Verify state = true + r_st = _get(f"{base}/relay/status?relay={relay}") + on_ok = r_st is not None and r_st.get("state") is True + record(f"Relay {relay} ON", on_ok, + ("state=true" if on_ok else f"got {r_st}")) + + time.sleep(RELAY_DLY) + + # Turn OFF + _post(f"{base}/relay/off?relay={relay}") + time.sleep(RELAY_DLY) + + # Verify state = false + r_st2 = _get(f"{base}/relay/status?relay={relay}") + off_ok = r_st2 is not None and r_st2.get("state") is False + record(f"Relay {relay} OFF", off_ok, + ("state=false" if off_ok else f"got {r_st2}")) + + time.sleep(0.1) + + +def test_inputs(base: str): + print("\n── Digital Input Tests ───────────────────────") + for inp in range(1, 5): + data = _get(f"{base}/input/status?input={inp}") + if data is None: + record(f"Input {inp} readable", False, "no response"); continue + if "_error" in data: + record(f"Input {inp} readable", False, data["_error"]); continue + state = data.get("state") + record(f"Input {inp} readable", state is not None, + f"state={'HIGH' if state else 'LOW'}" if state is not None else f"got {data}") + + +def test_led(base: str): + print("\n── LED Test ──────────────────────────────────") + on_r = _post(f"{base}/led/on") + time.sleep(0.3) + off_r = _post(f"{base}/led/off") + led_ok = (on_r is not None and "status" in on_r + and off_r is not None and "status" in off_r) + record("LED on/off", led_ok, + "API responded OK — verify LED blinked" if led_ok else f"on={on_r} off={off_r}") + + +def test_nfc(base: str): + print("\n── NFC Test ──────────────────────────────────") + data = _get(f"{base}/nfc/status") + if data is None: + record("NFC endpoint", False, f"GET /nfc/status no response"); return + if "_error" in data: + record("NFC endpoint", False, data["_error"]); return + + record("NFC endpoint reachable", True, "") + + init = data.get("initialized", False) + record("NFC PN532 initialized", init, + f"last_uid={data.get('last_uid') or '(none)'} " + f"access_state={data.get('access_state','?')}" if init + else "PN532 not detected — check hardware") + + # Config endpoint + cfg = _get(f"{base}/nfc/config") + if cfg and "_error" not in cfg: + record("NFC config endpoint", True, + f"auth_uid='{cfg.get('auth_uid') or 'any'}' " + f"relay={cfg.get('relay_num')} " + f"pulse={cfg.get('pulse_ms')} ms") + else: + record("NFC config endpoint", False, str(cfg)) + + +# ───────────────────────────────────────────────────────────────────────────── +# Optional: read board_test sketch results directly +# ───────────────────────────────────────────────────────────────────────────── + +def test_sketch_results(base: str): + """If the board_test sketch is running it exposes /test.json — use it.""" + data = _get(f"{base}/test.json") + if data is None or "_error" in data: + return # main firmware running — no /test.json endpoint + print("\n── Board-Test Sketch Results (from /test.json) ──") + for t in data.get("tests", []): + record(f"[sketch] {t['name']}", t["pass"], t.get("detail", "")) + + +# ───────────────────────────────────────────────────────────────────────────── +# Entry point +# ───────────────────────────────────────────────────────────────────────────── + +def main(): + parser = argparse.ArgumentParser(description="Olimex ESP32-C6-EVB board verifier") + parser.add_argument("ip", nargs="?", default="192.168.0.181", + help="Board IP address (default: 192.168.0.181)") + parser.add_argument("--json", action="store_true", + help="Output results as JSON") + parser.add_argument("--skip-relays", action="store_true", + help="Skip relay tests (no load wired)") + args = parser.parse_args() + + base = f"http://{args.ip}" + + print(f"\n╔══════════════════════════════════════════╗") + print(f"║ Olimex ESP32-C6-EVB Remote Verifier ║") + print(f"╚══════════════════════════════════════════╝") + print(f" Target: {base}\n") + + # Try board_test sketch first (if deployed) + test_sketch_results(base) + + # Connectivity gate — abort if board unreachable + if not test_reachability(base): + print("\n\033[31m Board unreachable — aborting remaining tests.\033[0m") + print(f" Try: ping {args.ip} or wget -qO- {base}/api/status\n") + sys.exit(1) + + if not args.skip_relays: + test_relays(base) + else: + print("\n── Relay Tests SKIPPED (--skip-relays) ──────") + + test_inputs(base) + test_led(base) + test_nfc(base) + + # ── Summary ─────────────────────────────────────────────────────────────── + passed = sum(1 for r in results if r["pass"]) + failed = sum(1 for r in results if not r["pass"]) + total = len(results) + all_ok = failed == 0 + + print(f"\n╔══════════════════════════════════════════╗") + print(f"║ PASSED: {passed:2d} FAILED: {failed:2d} TOTAL: {total:2d} ║") + print("║ \033[32m✓ ALL TESTS PASSED — board is OK\033[0m ║" if all_ok else + "║ \033[31m✗ FAILURES DETECTED — see above\033[0m ║") + print(f"╚══════════════════════════════════════════╝\n") + + if args.json: + summary = { + "board_ip": args.ip, + "pass": passed, + "fail": failed, + "total": total, + "board_ok": all_ok, + "tests": results, + } + print(json.dumps(summary, indent=2)) + + sys.exit(0 if all_ok else 1) + + +if __name__ == "__main__": + main() diff --git a/esp32_arduino/esp32_arduino.ino b/esp32_arduino/esp32_arduino.ino index 1b8aacf..bd65304 100644 --- a/esp32_arduino/esp32_arduino.ino +++ b/esp32_arduino/esp32_arduino.ino @@ -8,11 +8,37 @@ * * Provides REST API for Home Assistant integration */ -// version 1.5 Initial release +// version 1.8 Initial release #include #include #include +// ── NFC: PN532 over UART (HSU mode) via UEXT1 ─────────────────────────────── +// UEXT1 pin 3 = TXD (ESP32 → PN532 RXD) → GPIO4 +// UEXT1 pin 4 = RXD (PN532 TXD → ESP32) → GPIO5 +// PN532 module wiring note: set HSU mode — DIP1 = 0, DIP2 = 0 +#include +#include + +#define NFC_TX_PIN 4 // UEXT1 pin 3 — ESP32 transmits to PN532 +#define NFC_RX_PIN 5 // UEXT1 pin 4 — ESP32 receives from PN532 +#define NFC_POLL_MS 500 // idle detection interval (ms) + +HardwareSerial nfcSerial(1); // UART1 +PN532_HSU pn532hsu(nfcSerial); +PN532 nfc(pn532hsu); + +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 +// NFC Access Control +char nfc_auth_uid[32] = ""; // authorized card UID; empty = any card triggers +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" +// ──────────────────────────────────────────────────────────────────────────── + // WiFi credentials const char* ssid = "BUON GUSTO PARTER"; const char* password = "arleta13"; @@ -56,6 +82,7 @@ bool ha_registered = false; // Temperature simulation float temperature = 25.0; +unsigned long last_temp_update = 0; void setup() { // Initialize USB CDC serial @@ -94,66 +121,50 @@ void setup() { Serial.println("GPIO initialized"); // 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 + // allow up to 3 full connection attempts (60 s total) before giving up. Serial.println("\n--- WiFi Configuration ---"); + WiFi.disconnect(true); // reset radio & erase any stale state + delay(200); WiFi.mode(WIFI_STA); WiFi.setAutoReconnect(true); - - // Set static IP (prevents DHCP IP changes) + + // Static IP IPAddress staticIP(192, 168, 0, 181); IPAddress gateway(192, 168, 0, 1); IPAddress subnet(255, 255, 255, 0); WiFi.config(staticIP, gateway, subnet); - - // Connect to WiFi - Serial.print("Connecting to WiFi: "); - Serial.println(ssid); - WiFi.begin(ssid, password); - - int attempts = 0; - while (WiFi.status() != WL_CONNECTED && attempts < 40) { // 40 * 500ms = 20 seconds - delay(500); - Serial.print("."); - attempts++; - } - - Serial.println(""); // New line after dots - - // Check WiFi status - int wifiStatus = WiFi.status(); - if (wifiStatus == WL_CONNECTED) { - Serial.println("\n✓ WiFi connected!"); - Serial.print("IP address: "); - Serial.println(WiFi.localIP()); - Serial.print("RSSI: "); - Serial.print(WiFi.RSSI()); - Serial.println(" dBm"); - Serial.print("MAC: "); - Serial.println(WiFi.macAddress()); - } else { - Serial.println("\n✗ WiFi connection failed!"); - Serial.print("WiFi Status Code: "); - Serial.println(wifiStatus); - // Status codes: 0=IDLE, 1=NO_SSID, 2=SCAN_COMPLETE, 3=CONNECTED, 4=CONNECT_FAILED, 5=CONNECTION_LOST, 6=DISCONNECTED - switch(wifiStatus) { - case WL_NO_SSID_AVAIL: - Serial.println("ERROR: SSID not found! Check network name."); - break; - case WL_CONNECT_FAILED: - Serial.println("ERROR: Connection failed! Check password."); - break; - case WL_CONNECTION_LOST: - Serial.println("ERROR: Connection lost."); - break; - case WL_DISCONNECTED: - Serial.println("ERROR: Disconnected from network."); - break; - default: - Serial.println("ERROR: Unknown WiFi error."); + + 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); + for (int t = 0; t < 40 && WiFi.status() != WL_CONNECTED; t++) { + delay(500); + Serial.print("."); } - Serial.println("Continuing anyway to allow API access..."); - - // Scan and show available networks - scanWiFiNetworks(); + Serial.println(); + if (WiFi.status() == WL_CONNECTED) wifi_ok = true; + else if (pass < 3) { + Serial.println(" Not connected yet, retrying..."); + WiFi.disconnect(true); + delay(1000); + WiFi.mode(WIFI_STA); + WiFi.config(staticIP, gateway, subnet); + } + } + + if (wifi_ok) { + Serial.println("\n\u2713 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()); + } else { + Serial.println("\n\u2717 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."); } // Setup API endpoints @@ -171,6 +182,10 @@ void setup() { server.on("/led/on", HTTP_POST, handleLEDOn); server.on("/led/off", HTTP_POST, handleLEDOff); server.onNotFound(handleNotFound); + server.on("/nfc/status", HTTP_GET, handleNFCStatus); + server.on("/nfc/config", HTTP_GET, handleNFCConfigGet); + server.on("/nfc/config", HTTP_POST, handleNFCConfigSet); + server.on("/debug", HTTP_GET, handleDebug); // Start server server.begin(); @@ -181,20 +196,174 @@ void setup() { Serial.print(WiFi.localIP()); Serial.println("/api/status"); Serial.println("=================================\n"); + + // ── 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. + 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}}; + + 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); + delay(500); // PN532 boot / line-settle time + 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) { + Serial.println("\u2717 PN532 not detected with any baud/pin combination."); + 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(" 4. Some PN532 breakouts need a 100 ohm series resistor on TX line."); + } else { + Serial.printf("\u2713 PN532 found! baud=%ld RX=GPIO%d TX=GPIO%d\n", + found_baud, found_rx, found_tx); + 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); + nfc.SAMConfig(); + nfc_initialized = true; + Serial.println("\u2713 NFC ready \u2014 waiting for ISO14443A / Mifare cards"); + } + // ────────────────────────────────────────────────────────────────────────── } void loop() { server.handleClient(); - // Simulate temperature reading - temperature = 25.0 + (random(-20, 20) / 10.0); - + // Simulate temperature reading — update every 5 s + if (millis() - last_temp_update >= 5000) { + last_temp_update = millis(); + temperature = 25.0 + (random(-20, 20) / 10.0); + } + // Check for input state changes every 50ms if (millis() - last_input_check > 50) { last_input_check = millis(); 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 + { + 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) { + nfc_last_poll_ms = millis(); + uint16_t rf_timeout = is_active_state ? 500 : 50; + uint8_t uid[7] = {0}; + uint8_t uidLen = 0; + 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; + String uid_str = ""; + for (uint8_t i = 0; i < uidLen; i++) { + if (uid[i] < 0x10) uid_str += "0"; + uid_str += String(uid[i], HEX); + 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)) { + strcpy(nfc_access_state, "granted"); + int gpin = nfcRelayPin(nfc_relay_num); + if (gpin >= 0) { + digitalWrite(gpin, HIGH); + switch (nfc_relay_num) { + case 1: relay1_state = true; break; + case 2: relay2_state = true; break; + case 3: relay3_state = true; break; + 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 { + strcpy(nfc_access_state, "denied"); + Serial.printf("NFC: ACCESS DENIED UID=%s\n", uid_str.c_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; + 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 = ""; + 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; + } + } + // idle: keep fast-polling, no action needed + } + } + } + delay(10); } @@ -235,7 +404,7 @@ void handleRoot() { input3_state = digitalRead(DIN3_PIN); input4_state = digitalRead(DIN4_PIN); - String html = "ESP32-C6 Device"; + String html = "ESP32-C6 Device"; html += ""; html += ""; html += ""; html += ""; html += "

ESP32-C6 Control Panel

"; @@ -336,6 +548,74 @@ void handleRoot() { html += ""; html += "   Status: " + String(led_state ? "ON" : "OFF") + ""; + // ── NFC Access Control card ─────────────────────────────────────────────── + bool nfc_present_now = nfc_initialized && (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 += "
"; + html += "
"; + html += "
"; + html += "
Detected card UID
"; + html += "
"; + 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 += ""; + if (strlen(nfc_auth_uid) > 0) { + html += ""; + html += ""; + } else { + html += ""; + } + html += ""; + // Relay row + html += ""; + html += ""; + html += ""; + html += ""; + // Timeout row + html += ""; + html += ""; + html += ""; + html += ""; + html += "
Authorized card" + String(nfc_auth_uid) + "None — no card authorized yet
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 += "
Absent timeout (ms)
"; + 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 saved — present a card, click “Use as authorized” then Save.

"; + } + html += "
"; + // Home Assistant Webhook Status html += "

Home Assistant Webhook

"; if (ha_registered && strlen(ha_callback_url) > 0) { @@ -348,7 +628,7 @@ void handleRoot() { 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 += "POST /register?callback_url=...
"; + html += "GET /nfc/status   GET /nfc/config   POST /nfc/config?auth_uid=&relay=&pulse_ms=   POST /register?callback_url=...
"; html += ""; @@ -377,9 +657,17 @@ void handleStatus() { json += "\"relay2\":" + String(relay2_state ? "true" : "false") + ","; json += "\"relay3\":" + String(relay3_state ? "true" : "false") + ","; json += "\"relay4\":" + String(relay4_state ? "true" : "false") + ","; - json += "\"led\":" + String(led_state ? "true" : "false"); + json += "\"led\":" + String(led_state ? "true" : "false") + ","; + bool nfc_present = nfc_initialized && (strcmp(nfc_access_state, "granted") == 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_relay_num\":" + String(nfc_relay_num) + ","; + json += "\"nfc_pulse_ms\":" + String(nfc_pulse_ms); json += "}"; - + server.send(200, "application/json", json); // Log status every 10 seconds @@ -562,52 +850,39 @@ void checkInputChanges() { } } -void postInputEvent(int input_num, bool state) { - if (!ha_registered || strlen(ha_callback_url) == 0) { - return; // Not registered, skip - } - - // Invert state because inputs use pull-up (HIGH=not pressed, LOW=pressed) - bool pressed = !state; - String event_type = pressed ? "input_on" : "input_off"; - - Serial.printf("Input %d event: %s (raw_state=%d) - POSTing to HA\n", input_num, event_type.c_str(), state); - - // Parse callback URL (format: http://host:port/path) +// ============================================ +// Shared HTTP POST helper — parses ha_callback_url and POSTs JSON. +// Uses a 3-second timeout so an unreachable HA server never blocks the loop. +// ============================================ + +bool postJsonToHA(const String& json) { + if (!ha_registered || strlen(ha_callback_url) == 0) return false; + String url_str = String(ha_callback_url); - - // Extract host and path int protocol_end = url_str.indexOf("://"); - if (protocol_end < 0) return; - + if (protocol_end < 0) return false; + int host_start = protocol_end + 3; - int port_separator = url_str.indexOf(":", host_start); + int port_sep = url_str.indexOf(":", host_start); int path_start = url_str.indexOf("/", host_start); - if (path_start < 0) path_start = url_str.length(); - if (port_separator < 0 || port_separator > path_start) port_separator = -1; - - String host = url_str.substring(host_start, (port_separator >= 0) ? port_separator : path_start); - int port = 80; // Default HTTP port - if (port_separator >= 0) { - int colon_port_end = url_str.indexOf("/", port_separator); - if (colon_port_end < 0) colon_port_end = url_str.length(); - String port_str = url_str.substring(port_separator + 1, colon_port_end); - port = port_str.toInt(); + if (port_sep < 0 || port_sep > path_start) port_sep = -1; + + String host = url_str.substring(host_start, (port_sep >= 0) ? port_sep : path_start); + int port = 80; + if (port_sep >= 0) { + int colon_end = url_str.indexOf("/", port_sep); + if (colon_end < 0) colon_end = url_str.length(); + port = url_str.substring(port_sep + 1, colon_end).toInt(); } String path = url_str.substring(path_start); - - // Create JSON payload - String json = "{\"input\":" + String(input_num) + ",\"state\":" + (pressed ? "true" : "false") + "}"; - - // Connect to Home Assistant and POST + WiFiClient client; + client.setTimeout(3000); // 3-second connect/read timeout if (!client.connect(host.c_str(), port)) { - Serial.printf("Failed to connect to %s:%d\n", host.c_str(), port); - return; + Serial.printf("HA POST: failed to connect to %s:%d\n", host.c_str(), port); + return false; } - - // Send HTTP POST request client.println("POST " + path + " HTTP/1.1"); client.println("Host: " + host); client.println("Content-Type: application/json"); @@ -615,16 +890,155 @@ void postInputEvent(int input_num, bool state) { client.println("Connection: close"); client.println(); client.print(json); - - // Wait for response - delay(100); - - // Read response - while (client.available()) { - char c = client.read(); - // Just discard the response for now + + unsigned long deadline = millis() + 3000; + while (client.connected() && millis() < deadline) { + if (client.available()) client.read(); + else delay(1); } - client.stop(); - Serial.printf("Input %d event posted successfully\n", input_num); + return true; +} + +void postInputEvent(int input_num, bool state) { + if (!ha_registered || strlen(ha_callback_url) == 0) return; + bool pressed = !state; + Serial.printf("Input %d event: %s (raw_state=%d) - POSTing to HA\n", + input_num, pressed ? "input_on" : "input_off", state); + String json = "{\"input\":" + String(input_num) + + ",\"state\":" + (pressed ? "true" : "false") + "}"; + if (postJsonToHA(json)) + Serial.printf("Input %d event posted successfully\n", input_num); +} + +// ============================================ +// NFC Status API +// ============================================ + +void handleNFCStatus() { + bool present = nfc_initialized && (strcmp(nfc_access_state, "granted") == 0); + String json = "{"; + json += "\"initialized\":" + String(nfc_initialized ? "true" : "false") + ","; + 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 += "\"relay_num\":" + String(nfc_relay_num) + ","; + json += "\"pulse_ms\":" + String(nfc_pulse_ms); + json += "}"; + server.send(200, "application/json", json); +} + +// ============================================ +// NFC Webhook — POST card event to Home Assistant +// ============================================ + +void postNFCEvent(const String& uid) { + if (!ha_registered || strlen(ha_callback_url) == 0) return; + Serial.printf("NFC: posting UID %s to HA\n", uid.c_str()); + String json = "{\"type\":\"nfc_card\",\"uid\":\"" + uid + + "\",\"uptime\":" + String(millis() / 1000) + "}"; + if (postJsonToHA(json)) + Serial.printf("NFC: event posted for UID %s\n", uid.c_str()); +} + +// ============================================ +// NFC Helper: resolve relay number to GPIO pin +// ============================================ + +int nfcRelayPin(int rnum) { + switch (rnum) { + case 1: return RELAY_1_PIN; + case 2: return RELAY_2_PIN; + case 3: return RELAY_3_PIN; + case 4: return RELAY_4_PIN; + default: return -1; + } +} + +// ============================================ +// NFC Config API GET /nfc/config +// POST /nfc/config +// ============================================ + +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 += "}"; + server.send(200, "application/json", json); +} + +void handleNFCConfigSet() { + 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; + } + } + if (server.hasArg("relay")) { + int r = server.arg("relay").toInt(); + if (r >= 1 && r <= 4) { + nfc_relay_num = r; + } else { + server.send(400, "application/json", "{\"error\":\"relay must be 1-4\"}"); + return; + } + } + if (server.hasArg("pulse_ms")) { + long p = server.arg("pulse_ms").toInt(); + if (p >= 100 && p <= 60000) { + nfc_pulse_ms = (unsigned long)p; + } else { + server.send(400, "application/json", "{\"error\":\"pulse_ms range: 100-60000\"}"); + return; + } + } + Serial.printf("NFC config: auth='%s' relay=%d pulse=%lu ms\n", + nfc_auth_uid, nfc_relay_num, nfc_pulse_ms); + String json = "{\"status\":\"ok\"," + "\"auth_uid\":\"" + String(nfc_auth_uid) + "\"," + "\"relay_num\":" + String(nfc_relay_num) + "," + "\"pulse_ms\":" + String(nfc_pulse_ms) + "}"; + server.send(200, "application/json", json); +} + +// ============================================ +// Debug endpoint GET /debug +// Returns plain-text system info for diagnosing +// connectivity without needing a browser. +// ============================================ + +void handleDebug() { + String out = "=== ESP32-C6 Debug ===\n"; + out += "Uptime: " + String(millis() / 1000) + " s\n"; + out += "Free heap: " + String(ESP.getFreeHeap()) + " bytes\n"; + out += "WiFi status: "; + switch (WiFi.status()) { + case WL_CONNECTED: out += "CONNECTED\n"; break; + case WL_NO_SSID_AVAIL: out += "NO SSID\n"; break; + case WL_CONNECT_FAILED: out += "FAILED\n"; break; + case WL_DISCONNECTED: out += "DISCONNECTED\n"; break; + default: out += String(WiFi.status()) + "\n"; + } + out += "IP: " + WiFi.localIP().toString() + "\n"; + out += "SSID: " + WiFi.SSID() + "\n"; + out += "RSSI: " + String(WiFi.RSSI()) + " dBm\n"; + out += "MAC: " + WiFi.macAddress() + "\n"; + out += "\n"; + out += "NFC init: " + String(nfc_initialized ? "YES" : "NO") + "\n"; + 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 += "\n"; + out += "Relay states: R1=" + String(relay1_state) + " R2=" + String(relay2_state) + + " R3=" + String(relay3_state) + " R4=" + String(relay4_state) + "\n"; + server.send(200, "text/plain", out); }