/** * ESP32-C6 Home Assistant Integration * Arduino IDE Project * * Board: ESP32C6 Dev Module * Flash Size: 4MB * USB CDC On Boot: Enabled (REQUIRED for serial output!) * * Provides REST API for Home Assistant integration */ // 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"; // Web server on port 80 WebServer server(80); // GPIO pins - Olimex ESP32-C6-EVB board configuration const int LED_PIN = 8; // Onboard LED const int BUT_PIN = 9; // Onboard button const int RELAY_1_PIN = 10; // Relay 1 const int RELAY_2_PIN = 11; // Relay 2 const int RELAY_3_PIN = 22; // Relay 3 const int RELAY_4_PIN = 23; // Relay 4 const int DIN1_PIN = 1; // Digital Input 1 const int DIN2_PIN = 2; // Digital Input 2 const int DIN3_PIN = 3; // Digital Input 3 const int DIN4_PIN = 15; // Digital Input 4 // State tracking bool relay1_state = false; bool relay2_state = false; bool relay3_state = false; bool relay4_state = false; bool led_state = false; // Input state tracking - for change detection bool input1_state = true; // HIGH when not pressed (pull-up) bool input2_state = true; bool input3_state = true; bool input4_state = true; bool last_input1_state = true; bool last_input2_state = true; bool last_input3_state = true; bool last_input4_state = true; unsigned long last_input_check = 0; // Home Assistant callback configuration char ha_callback_url[256] = ""; // URL to POST input events to bool ha_registered = false; // Temperature simulation float temperature = 25.0; unsigned long last_temp_update = 0; void setup() { // Initialize USB CDC serial Serial.begin(115200); delay(2000); // Give time for USB CDC to initialize // Wait for serial port to be ready (up to 5 seconds) for (int i = 0; i < 10 && !Serial; i++) { delay(500); } Serial.println("\n\n================================="); Serial.println("ESP32-C6 Home Assistant Device"); Serial.println("Arduino Framework"); Serial.println("================================="); // Initialize GPIO - Outputs pinMode(LED_PIN, OUTPUT); pinMode(RELAY_1_PIN, OUTPUT); pinMode(RELAY_2_PIN, OUTPUT); pinMode(RELAY_3_PIN, OUTPUT); pinMode(RELAY_4_PIN, OUTPUT); // Initialize GPIO - Inputs with pull-up pinMode(BUT_PIN, INPUT_PULLUP); pinMode(DIN1_PIN, INPUT_PULLUP); pinMode(DIN2_PIN, INPUT_PULLUP); pinMode(DIN3_PIN, INPUT_PULLUP); pinMode(DIN4_PIN, INPUT_PULLUP); // Set all outputs to LOW digitalWrite(LED_PIN, LOW); digitalWrite(RELAY_1_PIN, LOW); digitalWrite(RELAY_2_PIN, LOW); digitalWrite(RELAY_3_PIN, LOW); digitalWrite(RELAY_4_PIN, LOW); 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); // 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); 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(); 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 server.on("/", handleRoot); server.on("/api/status", HTTP_GET, handleStatus); // Relay endpoints server.on("/relay/on", HTTP_POST, handleRelayOn); server.on("/relay/off", HTTP_POST, handleRelayOff); server.on("/relay/status", HTTP_GET, handleRelayStatus); // Input endpoints server.on("/input/status", HTTP_GET, handleInputStatus); // Home Assistant webhook registration server.on("/register", HTTP_POST, handleRegister); // LED endpoints 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(); Serial.println("\n✓ HTTP server started on port 80"); Serial.println("\n================================="); Serial.println("Ready! Try these endpoints:"); Serial.print(" http://"); 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 — 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); } // ============================================ // WiFi Debug Function // ============================================ void scanWiFiNetworks() { Serial.println("\n--- Available WiFi Networks ---"); int n = WiFi.scanNetworks(); if (n == 0) { Serial.println("No networks found!"); } else { Serial.print("Found "); Serial.print(n); Serial.println(" networks:"); for (int i = 0; i < n; i++) { Serial.print(i + 1); Serial.print(". "); Serial.print(WiFi.SSID(i)); Serial.print(" ("); Serial.print(WiFi.RSSI(i)); Serial.print(" dBm) "); Serial.println(WiFi.encryptionType(i) == WIFI_AUTH_OPEN ? "Open" : "Secured"); } } Serial.println("-------------------------------\n"); } // ============================================ // API Handlers // ============================================ void handleRoot() { // Read all inputs first to get current state input1_state = digitalRead(DIN1_PIN); input2_state = digitalRead(DIN2_PIN); input3_state = digitalRead(DIN3_PIN); input4_state = digitalRead(DIN4_PIN); String html = "ESP32-C6 Device"; html += ""; html += ""; html += ""; html += ""; html += "

ESP32-C6 Control Panel

"; // Device Info html += "

Device Info

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

Inputs

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

Relays

"; bool relayStates[5] = {false, relay1_state, relay2_state, relay3_state, relay4_state}; for (int i = 1; i <= 4; i++) { bool on = relayStates[i]; html += ""; } html += "
"; // close Relays card html += "
"; // close .card-row // LED Control html += "

LED Control

"; html += ""; 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) { html += "
✓ Connected — " + String(ha_callback_url) + "
"; } else { html += "
✗ Not registered — waiting for Home Assistant...
"; } html += "
"; html += "

API Endpoints

"; html += "GET /api/status   POST /relay/on?relay=1-4   POST /relay/off?relay=1-4
"; html += "GET /input/status?input=1-4   POST /led/on   POST /led/off
"; html += "GET /nfc/status   GET /nfc/config   POST /nfc/config?auth_uid=&relay=&pulse_ms=   POST /register?callback_url=...
"; html += ""; server.send(200, "text/html", html); } // Status request monitoring static unsigned long last_status_log = 0; static int status_request_count = 0; void handleStatus() { status_request_count++; // Read fresh input states input1_state = digitalRead(DIN1_PIN); input2_state = digitalRead(DIN2_PIN); input3_state = digitalRead(DIN3_PIN); input4_state = digitalRead(DIN4_PIN); String json = "{"; json += "\"input1\":" + String(input1_state ? "true" : "false") + ","; json += "\"input2\":" + String(input2_state ? "true" : "false") + ","; json += "\"input3\":" + String(input3_state ? "true" : "false") + ","; json += "\"input4\":" + String(input4_state ? "true" : "false") + ","; json += "\"relay1\":" + String(relay1_state ? "true" : "false") + ","; 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") + ","; 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 if (millis() - last_status_log > 10000) { last_status_log = millis(); Serial.printf("API: %d requests/10sec, Free heap: %d bytes, Uptime: %lus\n", status_request_count, ESP.getFreeHeap(), millis() / 1000); status_request_count = 0; } } void handleRelayOn() { if (!server.hasArg("relay")) { server.send(400, "application/json", "{\"error\":\"Missing relay parameter\"}"); return; } int relay_num = server.arg("relay").toInt(); int pin = -1; bool *state_ptr = nullptr; switch(relay_num) { case 1: pin = RELAY_1_PIN; state_ptr = &relay1_state; break; case 2: pin = RELAY_2_PIN; state_ptr = &relay2_state; break; case 3: pin = RELAY_3_PIN; state_ptr = &relay3_state; break; case 4: pin = RELAY_4_PIN; state_ptr = &relay4_state; break; default: server.send(400, "application/json", "{\"error\":\"Invalid relay number\"}"); return; } digitalWrite(pin, HIGH); *state_ptr = true; server.send(200, "application/json", "{\"status\":\"ok\",\"state\":true}"); Serial.printf("Relay %d ON\n", relay_num); } void handleRelayOff() { if (!server.hasArg("relay")) { server.send(400, "application/json", "{\"error\":\"Missing relay parameter\"}"); return; } int relay_num = server.arg("relay").toInt(); int pin = -1; bool *state_ptr = nullptr; switch(relay_num) { case 1: pin = RELAY_1_PIN; state_ptr = &relay1_state; break; case 2: pin = RELAY_2_PIN; state_ptr = &relay2_state; break; case 3: pin = RELAY_3_PIN; state_ptr = &relay3_state; break; case 4: pin = RELAY_4_PIN; state_ptr = &relay4_state; break; default: server.send(400, "application/json", "{\"error\":\"Invalid relay number\"}"); return; } digitalWrite(pin, LOW); *state_ptr = false; server.send(200, "application/json", "{\"status\":\"ok\",\"state\":false}"); Serial.printf("Relay %d OFF\n", relay_num); } void handleRelayStatus() { if (!server.hasArg("relay")) { server.send(400, "application/json", "{\"error\":\"Missing relay parameter\"}"); return; } int relay_num = server.arg("relay").toInt(); bool state = false; switch(relay_num) { case 1: state = relay1_state; break; case 2: state = relay2_state; break; case 3: state = relay3_state; break; case 4: state = relay4_state; break; default: server.send(400, "application/json", "{\"error\":\"Invalid relay number\"}"); return; } String json = "{\"state\":" + String(state ? "true" : "false") + "}"; server.send(200, "application/json", json); } void handleInputStatus() { if (!server.hasArg("input")) { server.send(400, "application/json", "{\"error\":\"Missing input parameter\"}"); return; } int input_num = server.arg("input").toInt(); int pin = -1; switch(input_num) { case 1: pin = DIN1_PIN; break; case 2: pin = DIN2_PIN; break; case 3: pin = DIN3_PIN; break; case 4: pin = DIN4_PIN; break; default: server.send(400, "application/json", "{\"error\":\"Invalid input number\"}"); return; } int level = digitalRead(pin); String json = "{\"state\":" + String(level ? "true" : "false") + "}"; server.send(200, "application/json", json); Serial.printf("Input %d status: %d\n", input_num, level); } void handleLEDOn() { digitalWrite(LED_PIN, LOW); // LED is active-low led_state = true; server.send(200, "application/json", "{\"status\":\"ok\"}"); Serial.println("LED ON"); } void handleLEDOff() { digitalWrite(LED_PIN, HIGH); // LED is active-low led_state = false; server.send(200, "application/json", "{\"status\":\"ok\"}"); Serial.println("LED OFF"); } void handleNotFound() { String message = "404: Not Found\n\n"; message += "URI: " + server.uri() + "\n"; message += "Method: " + String((server.method() == HTTP_GET) ? "GET" : "POST") + "\n"; server.send(404, "text/plain", message); } // ============================================ // Home Assistant Integration // ============================================ void handleRegister() { if (!server.hasArg("callback_url")) { server.send(400, "application/json", "{\"error\":\"Missing callback_url parameter\"}"); return; } String url = server.arg("callback_url"); if (url.length() > 255) { server.send(400, "application/json", "{\"error\":\"URL too long (max 255 chars)\"}"); return; } url.toCharArray(ha_callback_url, 256); ha_registered = true; Serial.printf("Home Assistant webhook registered: %s\n", ha_callback_url); server.send(200, "application/json", "{\"status\":\"ok\",\"message\":\"Webhook registered\"}"); } void checkInputChanges() { if (!ha_registered) return; // Only check if HA is registered // Read all input states bool curr_input1 = digitalRead(DIN1_PIN); bool curr_input2 = digitalRead(DIN2_PIN); bool curr_input3 = digitalRead(DIN3_PIN); bool curr_input4 = digitalRead(DIN4_PIN); // Check for changes and POST events if (curr_input1 != last_input1_state) { last_input1_state = curr_input1; postInputEvent(1, curr_input1); } if (curr_input2 != last_input2_state) { last_input2_state = curr_input2; postInputEvent(2, curr_input2); } if (curr_input3 != last_input3_state) { last_input3_state = curr_input3; postInputEvent(3, curr_input3); } if (curr_input4 != last_input4_state) { last_input4_state = curr_input4; postInputEvent(4, curr_input4); } } // ============================================ // 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); int protocol_end = url_str.indexOf("://"); if (protocol_end < 0) return false; int host_start = protocol_end + 3; 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_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); WiFiClient client; client.setTimeout(3000); // 3-second connect/read timeout if (!client.connect(host.c_str(), port)) { Serial.printf("HA POST: failed to connect to %s:%d\n", host.c_str(), port); return false; } client.println("POST " + path + " HTTP/1.1"); client.println("Host: " + host); client.println("Content-Type: application/json"); client.println("Content-Length: " + String(json.length())); client.println("Connection: close"); client.println(); client.print(json); unsigned long deadline = millis() + 3000; while (client.connected() && millis() < deadline) { if (client.available()) client.read(); else delay(1); } client.stop(); 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); }