updated board
This commit is contained in:
310
board_test/board_test.ino
Normal file
310
board_test/board_test.ino
Normal file
@@ -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 <WiFi.h>
|
||||
#include <WebServer.h>
|
||||
#include <PN532_HSU.h>
|
||||
#include <PN532.h>
|
||||
|
||||
// ── 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 = "<!DOCTYPE html><html><head><title>Board Test</title>";
|
||||
h += "<meta name='viewport' content='width=device-width,initial-scale=1'>";
|
||||
h += "<style>";
|
||||
h += "body{font-family:monospace;margin:24px;background:#1a1a1a;color:#eee}";
|
||||
h += "h1{font-size:20px;color:#fff;margin-bottom:4px}";
|
||||
h += ".sub{color:#888;font-size:13px;margin-bottom:20px}";
|
||||
h += "table{border-collapse:collapse;width:100%;max-width:700px}";
|
||||
h += "th{background:#333;padding:8px 12px;text-align:left;font-size:13px;color:#aaa}";
|
||||
h += "td{padding:8px 12px;border-bottom:1px solid #333;font-size:13px}";
|
||||
h += ".pass{color:#4CAF50;font-weight:bold}.fail{color:#f44336;font-weight:bold}";
|
||||
h += ".detail{color:#aaa;font-size:12px}";
|
||||
h += ".summary{margin-top:16px;padding:12px;border-radius:6px;font-size:15px}";
|
||||
h += ".ok{background:#1b5e20;color:#a5d6a7}.bad{background:#b71c1c;color:#ffcdd2}";
|
||||
h += "</style></head><body>";
|
||||
h += "<h1>Olimex ESP32-C6-EVB — Functional Test</h1>";
|
||||
h += "<div class='sub'>MAC: " + WiFi.macAddress() + " IP: "
|
||||
+ WiFi.localIP().toString() + " Uptime: "
|
||||
+ String(millis() / 1000) + "s</div>";
|
||||
|
||||
h += "<table><tr><th>#</th><th>Test</th><th>Result</th><th>Detail</th></tr>";
|
||||
for (int i = 0; i < result_count; i++) {
|
||||
bool ok = results[i].passed;
|
||||
h += "<tr><td>" + String(i + 1) + "</td>";
|
||||
h += "<td>" + String(results[i].name) + "</td>";
|
||||
h += "<td class='" + String(ok ? "pass" : "fail") + "'>"
|
||||
+ String(ok ? "PASS" : "FAIL") + "</td>";
|
||||
h += "<td class='detail'>" + results[i].detail + "</td></tr>";
|
||||
}
|
||||
h += "</table>";
|
||||
|
||||
bool all_ok = (fail_count == 0);
|
||||
h += "<div class='summary " + String(all_ok ? "ok" : "bad") + "'>";
|
||||
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 += "</div></body></html>";
|
||||
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();
|
||||
}
|
||||
252
board_verify.py
Normal file
252
board_verify.py
Normal file
@@ -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()
|
||||
@@ -8,11 +8,37 @@
|
||||
*
|
||||
* Provides REST API for Home Assistant integration
|
||||
*/
|
||||
// version 1.5 Initial release
|
||||
// version 1.8 Initial release
|
||||
#include <WiFi.h>
|
||||
#include <WebServer.h>
|
||||
#include <WiFiClient.h>
|
||||
|
||||
// ── 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 <PN532_HSU.h>
|
||||
#include <PN532.h>
|
||||
|
||||
#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 = "<html><head><title>ESP32-C6 Device</title>";
|
||||
String html = "<html><head><meta charset='UTF-8'><title>ESP32-C6 Device</title>";
|
||||
html += "<meta name='viewport' content='width=device-width, initial-scale=1'>";
|
||||
html += "<style>";
|
||||
html += "body{font-family:Arial;margin:20px;background:#f0f0f0}";
|
||||
@@ -262,6 +431,10 @@ void handleRoot() {
|
||||
html += ".btn-on{background:#4CAF50;color:white}.btn-off{background:#f44336;color:white}";
|
||||
html += ".wh-ok{background:#c8e6c9;color:#1b5e20;padding:10px;border-radius:4px}";
|
||||
html += ".wh-err{background:#ffcdd2;color:#b71c1c;padding:10px;border-radius:4px}";
|
||||
html += ".nfc-state{padding:10px 14px;border-radius:6px;font-weight:bold;font-size:15px;margin-top:4px}";
|
||||
html += ".nfc-idle{background:#f5f5f5;color:#757575}";
|
||||
html += ".nfc-granted{background:#c8e6c9;color:#1b5e20}";
|
||||
html += ".nfc-denied{background:#ffcdd2;color:#b71c1c}";
|
||||
html += "</style>";
|
||||
html += "<script>";
|
||||
// Relay toggle: read current state from data-state attribute, POST, update DOM
|
||||
@@ -287,8 +460,47 @@ void handleRoot() {
|
||||
html += "b.className='relay-btn '+(on?'relay-on':'relay-off');";
|
||||
html += "b.textContent='Relay '+i+': '+(on?'ON':'OFF');";
|
||||
html += "}";
|
||||
// Update NFC section from polled status
|
||||
html += "var nled=document.getElementById('nfc-led');";
|
||||
html += "if(nled)nled.className='led '+(d.nfc_card_present?'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');";
|
||||
html += "if(ncb)ncb.disabled=!d.nfc_card_present;";
|
||||
html += "var nac=document.getElementById('nfc-access');";
|
||||
html += "if(nac){var as=d.nfc_access_state||'idle';";
|
||||
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 += "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='#b71c1c';ad.textContent='None \u2014 no card authorized yet';}}";
|
||||
html += "var rs=document.getElementById('nfc-relay-sel');";
|
||||
html += "if(rs&&document.activeElement!==rs)rs.value=d.nfc_relay_num||1;";
|
||||
html += "var pf=document.getElementById('nfc-pulse-field');";
|
||||
html += "if(pf&&document.activeElement!==pf)pf.value=d.nfc_pulse_ms||3000;";
|
||||
html += "}).catch(function(e){console.error(e);});}";
|
||||
html += "window.addEventListener('load',function(){updateStatus();setInterval(updateStatus,2000);});";
|
||||
html += "function clearNFCAuth(){";
|
||||
html += "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 += ".then(function(r){return r.json();})";
|
||||
html += ".then(function(d){if(d.status==='ok'){alert('Authorized card cleared.');}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 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 += ".then(function(r){return r.json();})";
|
||||
html += ".then(function(d){if(d.status==='ok'){alert('Saved!\\nAuthorized UID: '+(d.auth_uid||'(none)')+'\\nRelay: '+d.relay_num+'\\nAbsent timeout: '+d.pulse_ms+'ms');}else{alert('Error: '+d.error);}})";
|
||||
html += ".catch(function(e){alert('Network error');console.error(e);});}";
|
||||
html += "function copyUID(){";
|
||||
html += "var u=document.getElementById('nfc-uid').textContent;";
|
||||
html += "if(u&&u!=='No card inserted'){var f=document.getElementById('nfc-auth-field');f.value=u;";
|
||||
html += "var b=document.getElementById('nfc-copy-btn');b.textContent='\u2713 Copied!';";
|
||||
html += "setTimeout(function(){b.textContent='\u2193 Use as authorized';},1500);}}";
|
||||
html += "window.addEventListener('load',function(){updateStatus();setInterval(updateStatus,2000);});";
|
||||
html += "</script>";
|
||||
html += "</head><body>";
|
||||
html += "<h1>ESP32-C6 Control Panel</h1>";
|
||||
@@ -336,6 +548,74 @@ void handleRoot() {
|
||||
html += "<button class='btn btn-off' onclick='fetch(\"/led/off\",{method:\"POST\"}).then(()=>location.reload())'>LED OFF</button>";
|
||||
html += " Status: <strong>" + String(led_state ? "ON" : "OFF") + "</strong></div>";
|
||||
|
||||
// ── 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 += "<div class='card'><h2>NFC Access Control (PN532 — UEXT1)</h2>";
|
||||
|
||||
// ── Live status row ────────────────────────────────────────────────────
|
||||
html += "<div style='display:flex;align-items:center;gap:14px;margin-bottom:12px'>";
|
||||
html += "<div class='led " + String(nfc_present_now ? "led-on" : "led-off") + "' id='nfc-led'></div>";
|
||||
html += "<div style='flex:1'>";
|
||||
html += "<div style='font-size:11px;color:#888;margin-bottom:4px'>Detected card UID</div>";
|
||||
html += "<div id='nfc-uid' style='font-size:20px;font-weight:bold;font-family:monospace;letter-spacing:2px'>";
|
||||
html += (nfc_last_uid.length() > 0 ? nfc_last_uid : "No card inserted");
|
||||
html += "</div></div>";
|
||||
html += "<button id='nfc-copy-btn' class='btn' onclick='copyUID()' style='white-space:nowrap'" + String(nfc_present_now ? "" : " disabled") + ">↓ Use as authorized</button>";
|
||||
html += "</div>";
|
||||
html += "<div id='nfc-access' class='nfc-state " + ac_class + "'>" + ac_text + "</div>";
|
||||
|
||||
// ── Current Settings (read-only) ───────────────────────────────────────
|
||||
html += "<h3 style='font-size:14px;color:#555;margin:18px 0 8px'>Current Settings</h3>";
|
||||
html += "<table style='width:100%;border-collapse:collapse;font-size:13px'>";
|
||||
// Authorized card row
|
||||
html += "<tr style='border-bottom:1px solid #eee'>";
|
||||
html += "<td style='padding:8px 6px;color:#666;width:160px'>Authorized card</td>";
|
||||
if (strlen(nfc_auth_uid) > 0) {
|
||||
html += "<td style='padding:8px 6px;font-family:monospace;font-weight:bold;color:#1b5e20' id='nfc-auth-display'>" + String(nfc_auth_uid) + "</td>";
|
||||
html += "<td style='padding:8px 6px;text-align:right'><button class='btn btn-off' style='font-size:12px;padding:4px 10px' onclick='clearNFCAuth()'>✕ Remove</button></td>";
|
||||
} else {
|
||||
html += "<td colspan='2' style='padding:8px 6px;color:#b71c1c' id='nfc-auth-display'>None — no card authorized yet</td>";
|
||||
}
|
||||
html += "</tr>";
|
||||
// Relay row
|
||||
html += "<tr style='border-bottom:1px solid #eee'>";
|
||||
html += "<td style='padding:8px 6px;color:#666'>Trigger relay</td>";
|
||||
html += "<td colspan='2' style='padding:8px 6px;font-weight:bold'>Relay " + String(nfc_relay_num) + "</td>";
|
||||
html += "</tr>";
|
||||
// Timeout row
|
||||
html += "<tr>";
|
||||
html += "<td style='padding:8px 6px;color:#666'>Absent timeout</td>";
|
||||
html += "<td colspan='2' style='padding:8px 6px;font-weight:bold'>" + String(nfc_pulse_ms) + " ms</td>";
|
||||
html += "</tr>";
|
||||
html += "</table>";
|
||||
|
||||
// ── Edit Settings form ─────────────────────────────────────────────────
|
||||
html += "<h3 style='font-size:14px;color:#555;margin:18px 0 8px'>Edit Settings</h3>";
|
||||
html += "<div style='display:grid;grid-template-columns:1fr 1fr 1fr auto;gap:8px;align-items:end'>";
|
||||
html += "<div><div style='font-size:12px;color:#666;margin-bottom:4px'>Authorized UID</div>";
|
||||
html += "<input id='nfc-auth-field' style='width:100%;padding:8px;border:1px solid #ccc;border-radius:4px;font-family:monospace;box-sizing:border-box' placeholder='e.g. 04:AB:CD:EF' value='" + String(nfc_auth_uid) + "'></div>";
|
||||
html += "<div><div style='font-size:12px;color:#666;margin-bottom:4px'>Trigger relay</div>";
|
||||
html += "<select id='nfc-relay-sel' style='width:100%;padding:8px;border:1px solid #ccc;border-radius:4px'>";
|
||||
for (int r = 1; r <= 4; r++) {
|
||||
html += "<option value='" + String(r) + "'" + String(nfc_relay_num == r ? " selected" : "") + ">Relay " + String(r) + "</option>";
|
||||
}
|
||||
html += "</select></div>";
|
||||
html += "<div><div style='font-size:12px;color:#666;margin-bottom:4px'>Absent timeout (ms)</div>";
|
||||
html += "<input id='nfc-pulse-field' type='number' min='100' max='60000' style='width:100%;padding:8px;border:1px solid #ccc;border-radius:4px;box-sizing:border-box' value='" + String(nfc_pulse_ms) + "'></div>";
|
||||
html += "<button class='btn btn-on' onclick='saveNFCConfig()' style='padding:8px 18px'>Save</button>";
|
||||
html += "</div>";
|
||||
if (!nfc_initialized) {
|
||||
html += "<p style='color:#b71c1c;font-size:13px;margin:8px 0 0'>✗ PN532 not detected — check UEXT1 wiring (TX=GPIO4, RX=GPIO5)</p>";
|
||||
}
|
||||
if (nfc_initialized && strlen(nfc_auth_uid) == 0) {
|
||||
html += "<p style='color:#e65100;font-size:13px;margin:8px 0 0'>⚠ No authorized UID saved — present a card, click “Use as authorized” then Save.</p>";
|
||||
}
|
||||
html += "</div>";
|
||||
|
||||
// Home Assistant Webhook Status
|
||||
html += "<div class='card'><h2>Home Assistant Webhook</h2>";
|
||||
if (ha_registered && strlen(ha_callback_url) > 0) {
|
||||
@@ -348,7 +628,7 @@ void handleRoot() {
|
||||
html += "<div class='card'><h2>API Endpoints</h2>";
|
||||
html += "GET /api/status POST /relay/on?relay=1-4 POST /relay/off?relay=1-4<br>";
|
||||
html += "GET /input/status?input=1-4 POST /led/on POST /led/off<br>";
|
||||
html += "POST /register?callback_url=...</div>";
|
||||
html += "GET /nfc/status GET /nfc/config POST /nfc/config?auth_uid=&relay=&pulse_ms= POST /register?callback_url=...</div>";
|
||||
|
||||
html += "</body></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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user