updated card reader to multiple cards added on nfc
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
/**
|
||||
* ESP32-C6 Home Assistant Integration
|
||||
* Arduino IDE Project
|
||||
*
|
||||
* v18
|
||||
* Board: ESP32C6 Dev Module
|
||||
* Flash Size: 4MB
|
||||
* USB CDC On Boot: Enabled (REQUIRED for serial output!)
|
||||
*
|
||||
* Provides REST API for Home Assistant integration
|
||||
*/
|
||||
// version 2.2.1 Persistent NFC config via NVS
|
||||
// version 2.3.0 C5-parity: secrets.h WiFi/IP, relay toggle, NFC probe, improved NFC polling, WiFi LED
|
||||
#include <WiFi.h>
|
||||
#include <WebServer.h>
|
||||
#include <WiFiClient.h>
|
||||
@@ -42,18 +42,17 @@ bool nfc_initialized = false;
|
||||
String nfc_last_uid = "";
|
||||
unsigned long nfc_last_poll_ms = 0;
|
||||
int nfc_miss_count = 0; // consecutive polls with no card detected
|
||||
unsigned long nfc_absent_since = 0; // millis() when card first went absent (GRANTED→absent)
|
||||
// NFC Access Control
|
||||
char nfc_auth_uid[32] = ""; // authorized card UID; empty = any card triggers
|
||||
#define NFC_MAX_CARDS 10 // maximum authorized cards
|
||||
char nfc_auth_uids[NFC_MAX_CARDS][32] = {}; // authorized UIDs (empty string = unused slot)
|
||||
int nfc_auth_count = 0; // number of active cards
|
||||
int nfc_relay_num = 1; // relay to open on match (1-4)
|
||||
unsigned long nfc_pulse_ms = 5000; // absence timeout: relay closes after this many ms of no card
|
||||
unsigned long nfc_pulse_ms = 0; // relay release delay after card removed (ms); 0 = off after ~1 s debounce
|
||||
char nfc_access_state[8] = "idle"; // "idle" | "granted" | "denied"
|
||||
bool nfc_enabled = false; // access control module on/off; off by default
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// WiFi credentials
|
||||
const char* ssid = "BUON GUSTO PARTER";
|
||||
const char* password = "arleta13";
|
||||
|
||||
// Web server on port 80
|
||||
WebServer server(80);
|
||||
|
||||
@@ -102,6 +101,7 @@ void handleStatus();
|
||||
void handleRelayOn();
|
||||
void handleRelayOff();
|
||||
void handleRelayStatus();
|
||||
void handleRelayToggle();
|
||||
void handleInputStatus();
|
||||
void handleRegister();
|
||||
void handleLEDOn();
|
||||
@@ -111,6 +111,7 @@ void handleNFCStatus();
|
||||
void handleNFCConfigGet();
|
||||
void handleNFCConfigSet();
|
||||
void handleNFCEnable();
|
||||
void handleNFCProbe();
|
||||
void handleDebug();
|
||||
void postNFCEvent(const String& uid);
|
||||
void postInputEvent(int input_num, bool state);
|
||||
@@ -123,15 +124,21 @@ void scanWiFiNetworks();
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
void setup() {
|
||||
// Initialize USB CDC serial
|
||||
// ── USB CDC serial init ───────────────────────────────────────────────────
|
||||
// IMPORTANT: In Arduino IDE set Tools → USB CDC On Boot → Enabled
|
||||
// Without that, Serial maps to UART and you'll see nothing on the USB port.
|
||||
Serial.begin(115200);
|
||||
delay(2000); // Give time for USB CDC to initialize
|
||||
|
||||
// Wait for serial port to be ready (up to 5 seconds)
|
||||
for (int i = 0; i < 10 && !Serial; i++) {
|
||||
delay(500);
|
||||
|
||||
// After a flash-and-reset cycle the USB CDC port re-enumerates. We wait up
|
||||
// to 5 s for the host to open the port, then continue regardless so the
|
||||
// board doesn't hang when running headless (no monitor attached).
|
||||
{
|
||||
unsigned long _t = millis();
|
||||
while (!Serial && millis() - _t < 5000) { delay(100); }
|
||||
}
|
||||
|
||||
// Brief additional settle time for the CDC driver on the host side
|
||||
delay(200);
|
||||
|
||||
Serial.println("\n\n=================================");
|
||||
Serial.println("ESP32-C6 Home Assistant Device");
|
||||
Serial.println("Arduino Framework");
|
||||
@@ -149,27 +156,34 @@ void setup() {
|
||||
pinMode(DIN2_PIN, INPUT_PULLUP);
|
||||
pinMode(DIN3_PIN, INPUT_PULLUP);
|
||||
pinMode(DIN4_PIN, INPUT_PULLUP);
|
||||
// Set all outputs to LOW
|
||||
digitalWrite(LED_PIN, LOW);
|
||||
// Set all outputs to initial safe state
|
||||
digitalWrite(LED_PIN, HIGH); // HIGH = LED OFF (active LOW) — will turn ON when WiFi connects
|
||||
digitalWrite(RELAY_1_PIN, LOW);
|
||||
digitalWrite(RELAY_2_PIN, LOW);
|
||||
digitalWrite(RELAY_3_PIN, LOW);
|
||||
digitalWrite(RELAY_4_PIN, LOW);
|
||||
|
||||
Serial.println("GPIO initialized");
|
||||
Serial.println("GPIO initialized - LED OFF (will turn ON when WiFi connects)");
|
||||
|
||||
// ── Load persistent NFC config from NVS ──────────────────────────────────
|
||||
prefs.begin("nfc_cfg", true); // read-only namespace
|
||||
String saved_uid = prefs.getString("auth_uid", "");
|
||||
if (saved_uid.length() > 0 && saved_uid.length() < sizeof(nfc_auth_uid)) {
|
||||
saved_uid.toCharArray(nfc_auth_uid, sizeof(nfc_auth_uid));
|
||||
}
|
||||
nfc_relay_num = prefs.getInt("relay_num", 1);
|
||||
nfc_pulse_ms = prefs.getULong("pulse_ms", 5000);
|
||||
nfc_pulse_ms = prefs.getULong("pulse_ms", 0);
|
||||
nfc_enabled = prefs.getBool("enabled", false);
|
||||
nfc_auth_count = 0;
|
||||
for (int i = 0; i < NFC_MAX_CARDS; i++) {
|
||||
String key = "uid" + String(i);
|
||||
String saved = prefs.getString(key.c_str(), "");
|
||||
if (saved.length() > 0 && saved.length() < 32) {
|
||||
saved.toCharArray(nfc_auth_uids[i], 32);
|
||||
nfc_auth_count++;
|
||||
} else {
|
||||
nfc_auth_uids[i][0] = '\0';
|
||||
}
|
||||
}
|
||||
prefs.end();
|
||||
Serial.printf("NFC config loaded: auth='%s' relay=%d pulse=%lu ms\n",
|
||||
nfc_auth_uid, nfc_relay_num, nfc_pulse_ms);
|
||||
Serial.printf("NFC config loaded: %d authorized card(s), relay=%d, pulse=%lu ms\n",
|
||||
nfc_auth_count, nfc_relay_num, nfc_pulse_ms);
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// ── Load persistent HA registration from NVS ─────────────────────────────
|
||||
@@ -197,16 +211,36 @@ void setup() {
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.setAutoReconnect(true);
|
||||
|
||||
// Static IP
|
||||
IPAddress staticIP(192, 168, 0, 240);
|
||||
IPAddress gateway(192, 168, 0, 1);
|
||||
IPAddress subnet(255, 255, 255, 0);
|
||||
WiFi.config(staticIP, gateway, subnet);
|
||||
// Parse static IP configuration from secrets.h
|
||||
// Note: WiFi.config() must be called right before WiFi.begin() on ESP32-C6;
|
||||
// calling it before disconnect(true)/mode() causes it to be silently cleared.
|
||||
IPAddress _staticIP, _gateway, _subnet, _dns1, _dns2;
|
||||
bool _have_static = false;
|
||||
if (USE_STATIC_IP) {
|
||||
if (_staticIP.fromString(STATIC_IP_ADDR) &&
|
||||
_gateway.fromString(STATIC_GATEWAY) &&
|
||||
_subnet.fromString(STATIC_SUBNET) &&
|
||||
_dns1.fromString(STATIC_DNS1) &&
|
||||
_dns2.fromString(STATIC_DNS2)) {
|
||||
_have_static = true;
|
||||
Serial.printf("Static IP will be applied: %s\n", STATIC_IP_ADDR);
|
||||
} else {
|
||||
Serial.println("⚠ Invalid static IP configuration — falling back to DHCP");
|
||||
}
|
||||
} else {
|
||||
Serial.println("Using DHCP for IP configuration");
|
||||
}
|
||||
|
||||
bool wifi_ok = false;
|
||||
for (int pass = 1; pass <= 3 && !wifi_ok; pass++) {
|
||||
Serial.printf("Connecting to WiFi: %s (attempt %d/3)\n", ssid, pass);
|
||||
WiFi.begin(ssid, password);
|
||||
Serial.printf("Connecting to WiFi: %s (attempt %d/3)\n", WIFI_SSID, pass);
|
||||
// Apply static IP immediately before begin() so it is never cleared by a
|
||||
// preceding disconnect(true) call.
|
||||
if (_have_static) {
|
||||
WiFi.config(_staticIP, _gateway, _subnet, _dns1, _dns2);
|
||||
Serial.printf("Static IP configured: %s\n", STATIC_IP_ADDR);
|
||||
}
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
||||
for (int t = 0; t < 40 && WiFi.status() != WL_CONNECTED; t++) {
|
||||
delay(500);
|
||||
Serial.print(".");
|
||||
@@ -218,20 +252,23 @@ void setup() {
|
||||
WiFi.disconnect(true);
|
||||
delay(1000);
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.config(staticIP, gateway, subnet);
|
||||
}
|
||||
}
|
||||
|
||||
if (wifi_ok) {
|
||||
Serial.println("\n\u2713 WiFi connected!");
|
||||
Serial.println("\n✓ WiFi connected!");
|
||||
Serial.print(" IP : "); Serial.println(WiFi.localIP());
|
||||
Serial.print(" RSSI : "); Serial.print(WiFi.RSSI()); Serial.println(" dBm");
|
||||
Serial.print(" MAC : "); Serial.println(WiFi.macAddress());
|
||||
digitalWrite(LED_PIN, LOW); // LED steady ON = WiFi connected (active LOW)
|
||||
led_state = true;
|
||||
} else {
|
||||
Serial.println("\n\u2717 WiFi connection failed after 3 attempts.");
|
||||
Serial.println("\n✗ WiFi connection failed after 3 attempts.");
|
||||
Serial.print(" Status code: "); Serial.println(WiFi.status());
|
||||
Serial.println(" Check: correct SSID/password, 2.4 GHz band, board in range.");
|
||||
Serial.println(" HTTP server will start anyway \u2014 accessible once WiFi reconnects.");
|
||||
Serial.println(" Check: correct WIFI_SSID/WIFI_PASSWORD, 2.4 GHz band, board in range.");
|
||||
Serial.println(" HTTP server will start anyway — accessible once WiFi reconnects.");
|
||||
digitalWrite(LED_PIN, HIGH); // LED OFF = no WiFi
|
||||
led_state = false;
|
||||
}
|
||||
|
||||
// ── NTP time sync ────────────────────────────────────────────────────────────
|
||||
@@ -260,9 +297,10 @@ void setup() {
|
||||
server.on("/", handleRoot);
|
||||
server.on("/api/status", HTTP_GET, handleStatus);
|
||||
// Relay endpoints
|
||||
server.on("/relay/on", HTTP_POST, handleRelayOn);
|
||||
server.on("/relay/off", HTTP_POST, handleRelayOff);
|
||||
server.on("/relay/status", HTTP_GET, handleRelayStatus);
|
||||
server.on("/relay/on", HTTP_POST, handleRelayOn);
|
||||
server.on("/relay/off", HTTP_POST, handleRelayOff);
|
||||
server.on("/relay/toggle", HTTP_POST, handleRelayToggle);
|
||||
server.on("/relay/status", HTTP_GET, handleRelayStatus);
|
||||
// Input endpoints
|
||||
server.on("/input/status", HTTP_GET, handleInputStatus);
|
||||
// Home Assistant webhook registration
|
||||
@@ -275,6 +313,7 @@ void setup() {
|
||||
server.on("/nfc/config", HTTP_GET, handleNFCConfigGet);
|
||||
server.on("/nfc/config", HTTP_POST, handleNFCConfigSet);
|
||||
server.on("/nfc/enable", HTTP_POST, handleNFCEnable);
|
||||
server.on("/nfc/probe", HTTP_POST, handleNFCProbe);
|
||||
server.on("/debug", HTTP_GET, handleDebug);
|
||||
|
||||
// ── Collect HMAC auth headers for verifyAPIRequest() ─────────────────────────
|
||||
@@ -293,57 +332,60 @@ void setup() {
|
||||
|
||||
// ── NFC (PN532 HSU) initialisation — multi-baud auto-detect ─────────────
|
||||
// The PN532 default HSU baud is 115200, but some modules ship at 9600.
|
||||
// We try both, plus RX/TX swapped, so the board will find the module
|
||||
// regardless of those two variables.
|
||||
// Only the correct pin pair (NFC_RX_PIN / NFC_TX_PIN) is tried — the board
|
||||
// has a fixed UEXT1 wiring so pin-swap probing is unnecessary and causes
|
||||
// hangs when no module is connected (the PN532 library uses its own blocking
|
||||
// read loop that does not respect HardwareSerial::setTimeout()).
|
||||
Serial.println("--- NFC (PN532 HSU) Initialization ---");
|
||||
|
||||
// Baud rates to try, in order
|
||||
const long NFC_BAUDS[] = {115200, 9600, 57600, 38400};
|
||||
const int NFC_NBAUDS = 4;
|
||||
// Pin pairs to try: {RX, TX}. Second pair = swapped.
|
||||
const int NFC_PINS[2][2] = {{NFC_RX_PIN, NFC_TX_PIN},
|
||||
{NFC_TX_PIN, NFC_RX_PIN}};
|
||||
const long NFC_BAUDS[] = {115200, 9600, 57600, 38400};
|
||||
const int NFC_NBAUDS = 4;
|
||||
|
||||
uint32_t versiondata = 0;
|
||||
long found_baud = 0;
|
||||
int found_rx = NFC_RX_PIN;
|
||||
int found_tx = NFC_TX_PIN;
|
||||
|
||||
for (int pi = 0; pi < 2 && !versiondata; pi++) {
|
||||
int rx = NFC_PINS[pi][0];
|
||||
int tx = NFC_PINS[pi][1];
|
||||
for (int bi = 0; bi < NFC_NBAUDS && !versiondata; bi++) {
|
||||
long baud = NFC_BAUDS[bi];
|
||||
Serial.printf(" Trying baud=%-7ld RX=GPIO%d TX=GPIO%d ...\n", baud, rx, tx);
|
||||
nfcSerial.begin(baud, SERIAL_8N1, rx, tx);
|
||||
// Serve HTTP and feed watchdog during probe delay instead of blocking
|
||||
{ unsigned long _t = millis(); while (millis() - _t < 500) { server.handleClient(); yield(); } }
|
||||
nfc.begin(); // PN532_HSU::begin() is a no-op — pins already set
|
||||
versiondata = nfc.getFirmwareVersion();
|
||||
if (!versiondata) {
|
||||
unsigned long _t = millis(); while (millis() - _t < 200) { server.handleClient(); yield(); }
|
||||
} else { found_baud = baud; found_rx = rx; found_tx = tx; }
|
||||
for (int bi = 0; bi < NFC_NBAUDS && !versiondata; bi++) {
|
||||
long baud = NFC_BAUDS[bi];
|
||||
Serial.printf(" Trying baud=%-7ld RX=GPIO%d TX=GPIO%d ...\n", baud, NFC_RX_PIN, NFC_TX_PIN);
|
||||
nfcSerial.begin(baud, SERIAL_8N1, NFC_RX_PIN, NFC_TX_PIN);
|
||||
nfcSerial.setTimeout(800);
|
||||
// Drain any garbage from a floating / unconnected RX line
|
||||
while (nfcSerial.available()) nfcSerial.read();
|
||||
// Let the bus settle; keep HTTP alive during wait
|
||||
{ unsigned long _t = millis(); while (millis() - _t < 300) { server.handleClient(); yield(); } }
|
||||
nfc.begin();
|
||||
versiondata = nfc.getFirmwareVersion();
|
||||
if (!versiondata) {
|
||||
while (nfcSerial.available()) nfcSerial.read();
|
||||
unsigned long _t = millis(); while (millis() - _t < 100) { server.handleClient(); yield(); }
|
||||
} else {
|
||||
found_baud = baud;
|
||||
}
|
||||
}
|
||||
|
||||
if (!versiondata) {
|
||||
Serial.println("\u2717 PN532 not detected with any baud/pin combination.");
|
||||
Serial.println(" NFC capabilities are NOT available \u2014 all other features remain active.");
|
||||
Serial.println(" (Connect a PN532 via UEXT1 and reboot to enable NFC.)");
|
||||
Serial.println(" Hardware checklist:");
|
||||
Serial.println(" 1. DIP/solder-jumpers on PN532 board: BOTH = 0 (HSU mode)");
|
||||
Serial.println(" Some boards label them SEL0/SEL1 or I0/I1 — both must be LOW.");
|
||||
Serial.println(" 2. Power: UEXT1 pin 1 = 3V3, pin 2 = GND. Measure with multimeter.");
|
||||
Serial.println(" 3. Wiring: UEXT1 pin 3 (GPIO4) ↔ PN532 RXD");
|
||||
Serial.println(" UEXT1 pin 4 (GPIO5) ↔ PN532 TXD");
|
||||
Serial.println(" 3. Wiring: UEXT1 pin 3 (GPIO4) \u2194 PN532 RXD");
|
||||
Serial.println(" UEXT1 pin 4 (GPIO5) \u2194 PN532 TXD");
|
||||
Serial.println(" 4. Some PN532 breakouts need a 100 ohm series resistor on TX line.");
|
||||
nfcSerial.end(); // release UART — don't hold pins in an indeterminate state
|
||||
nfc_initialized = false;
|
||||
} else {
|
||||
Serial.printf("\u2713 PN532 found! baud=%ld RX=GPIO%d TX=GPIO%d\n",
|
||||
found_baud, found_rx, found_tx);
|
||||
found_baud, NFC_RX_PIN, NFC_TX_PIN);
|
||||
Serial.print(" Chip: PN5");
|
||||
Serial.println((versiondata >> 24) & 0xFF, HEX);
|
||||
Serial.printf(" Firmware: %d.%d\n",
|
||||
(versiondata >> 16) & 0xFF, (versiondata >> 8) & 0xFF);
|
||||
// Re-init serial with confirmed settings
|
||||
nfcSerial.begin(found_baud, SERIAL_8N1, found_rx, found_tx);
|
||||
nfcSerial.begin(found_baud, SERIAL_8N1, NFC_RX_PIN, NFC_TX_PIN);
|
||||
nfc.SAMConfig();
|
||||
nfc_initialized = true;
|
||||
Serial.println("\u2713 NFC ready \u2014 waiting for ISO14443A / Mifare cards");
|
||||
@@ -366,23 +408,64 @@ void loop() {
|
||||
checkInputChanges();
|
||||
}
|
||||
|
||||
// ── NFC: two-phase polling ────────────────────────────────────────────────
|
||||
// IDLE : fast poll every NFC_POLL_MS (500 ms), 50 ms RF timeout
|
||||
// GRANTED/DENIED: slow presence-check every nfc_pulse_ms, 500 ms RF timeout
|
||||
// 2 consecutive misses required to confirm card is gone
|
||||
// ── WiFi LED status: steady ON = connected, OFF = disconnected ───────────
|
||||
static bool last_wifi_state = false;
|
||||
bool wifi_now = (WiFi.status() == WL_CONNECTED);
|
||||
if (wifi_now != last_wifi_state) {
|
||||
last_wifi_state = wifi_now;
|
||||
if (wifi_now) {
|
||||
digitalWrite(LED_PIN, LOW); // LED ON — WiFi reconnected (active LOW)
|
||||
led_state = true;
|
||||
Serial.printf("\u2713 WiFi reconnected: %s\n", WiFi.localIP().toString().c_str());
|
||||
} else {
|
||||
digitalWrite(LED_PIN, HIGH); // LED OFF — WiFi lost
|
||||
led_state = false;
|
||||
Serial.println("\u26a0 WiFi connection lost");
|
||||
}
|
||||
}
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// ── NFC polling ──────────────────────────────────────────────────────────
|
||||
// State machine with a "card present" flag:
|
||||
//
|
||||
// IDLE → poll every 1 s with 100 ms RF timeout (quick new-card scan)
|
||||
// card found + authorized → relay ON, state = GRANTED
|
||||
// card found + denied → state = DENIED
|
||||
//
|
||||
// GRANTED → poll every 1 s with 500 ms RF timeout (card needs time to
|
||||
// power back up after inRelease on the previous poll)
|
||||
// card still present → reset absence timer, keep relay ON
|
||||
// card absent → start absence timer
|
||||
// absent > limit → relay OFF, state = IDLE
|
||||
// limit = nfc_pulse_ms if > 0, otherwise default 5 000 ms
|
||||
//
|
||||
// DENIED → poll every 1 s; card gone → state = IDLE
|
||||
{
|
||||
bool is_active_state = (strcmp(nfc_access_state, "granted") == 0 ||
|
||||
strcmp(nfc_access_state, "denied") == 0);
|
||||
unsigned long nfc_interval = is_active_state ? nfc_pulse_ms : (unsigned long)NFC_POLL_MS;
|
||||
if (nfc_enabled && nfc_initialized && millis() - nfc_last_poll_ms >= nfc_interval) {
|
||||
const unsigned long NFC_POLL_INTERVAL = 1000UL;
|
||||
|
||||
bool is_granted = (strcmp(nfc_access_state, "granted") == 0);
|
||||
bool is_denied = (strcmp(nfc_access_state, "denied") == 0);
|
||||
|
||||
// Absent limit: configurable via nfc_pulse_ms (web UI), default 5 s
|
||||
unsigned long absent_limit = (nfc_pulse_ms > 0) ? nfc_pulse_ms : 5000UL;
|
||||
|
||||
if (nfc_enabled && nfc_initialized && millis() - nfc_last_poll_ms >= NFC_POLL_INTERVAL) {
|
||||
nfc_last_poll_ms = millis();
|
||||
uint16_t rf_timeout = is_active_state ? 500 : 50;
|
||||
|
||||
// GRANTED: use a longer RF timeout — after inRelease the card returns to
|
||||
// its IDLE state and needs 300-500 ms to be powered up and respond.
|
||||
// Using 100 ms here is why presence checks were failing every poll.
|
||||
uint16_t rf_timeout = is_granted ? 500 : 100;
|
||||
|
||||
uint8_t uid[7] = {0};
|
||||
uint8_t uidLen = 0;
|
||||
while (nfcSerial.available()) nfcSerial.read(); // flush stale UART bytes
|
||||
bool found = nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLen, rf_timeout);
|
||||
|
||||
if (found && uidLen > 0) {
|
||||
nfc.inRelease(1); // deselect → card returns to ISO14443A IDLE state for next poll
|
||||
nfc_miss_count = 0;
|
||||
nfc.inRelease(1); // deselect — card returns to ISO14443A IDLE for next poll
|
||||
|
||||
// Build UID string
|
||||
String uid_str = "";
|
||||
for (uint8_t i = 0; i < uidLen; i++) {
|
||||
if (uid[i] < 0x10) uid_str += "0";
|
||||
@@ -390,26 +473,28 @@ void loop() {
|
||||
if (i < uidLen - 1) uid_str += ":";
|
||||
}
|
||||
uid_str.toUpperCase();
|
||||
if (strcmp(nfc_access_state, "granted") == 0) {
|
||||
// Presence check passed — card still on reader
|
||||
Serial.printf("NFC: card present UID=%s\n", uid_str.c_str());
|
||||
} else if (strcmp(nfc_access_state, "denied") == 0 && uid_str == nfc_last_uid) {
|
||||
// Same card that was already denied is still on reader — do nothing
|
||||
Serial.printf("NFC: denied card still present UID=%s\n", uid_str.c_str());
|
||||
} else {
|
||||
// New card — authenticate
|
||||
nfc_last_uid = uid_str;
|
||||
Serial.printf("NFC: card UID=%s\n", uid_str.c_str());
|
||||
postNFCEvent(uid_str);
|
||||
// Require an explicit authorized UID — empty = no card is authorized yet
|
||||
if (strlen(nfc_auth_uid) == 0) {
|
||||
strcpy(nfc_access_state, "denied");
|
||||
Serial.printf("NFC: ACCESS DENIED — no authorized UID configured. Set one in the web UI.\n");
|
||||
} else if (uid_str == String(nfc_auth_uid)) {
|
||||
|
||||
// Card is physically present — reset absence timer regardless of state
|
||||
nfc_absent_since = 0;
|
||||
|
||||
bool is_authorized = false;
|
||||
if (nfc_auth_count > 0) {
|
||||
for (int i = 0; i < NFC_MAX_CARDS; i++) {
|
||||
if (strlen(nfc_auth_uids[i]) > 0 && uid_str == String(nfc_auth_uids[i])) {
|
||||
is_authorized = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (is_authorized) {
|
||||
if (!is_granted) {
|
||||
// First detection of this authorized card: open relay
|
||||
strcpy(nfc_access_state, "granted");
|
||||
int gpin = nfcRelayPin(nfc_relay_num);
|
||||
if (gpin >= 0) {
|
||||
digitalWrite(gpin, HIGH);
|
||||
nfc_last_uid = uid_str;
|
||||
int pin = nfcRelayPin(nfc_relay_num);
|
||||
if (pin >= 0) {
|
||||
digitalWrite(pin, HIGH);
|
||||
switch (nfc_relay_num) {
|
||||
case 1: relay1_state = true; break;
|
||||
case 2: relay2_state = true; break;
|
||||
@@ -417,19 +502,34 @@ void loop() {
|
||||
case 4: relay4_state = true; break;
|
||||
}
|
||||
}
|
||||
Serial.printf("NFC: ACCESS GRANTED relay=%d (presence-check every %lums)\n",
|
||||
nfc_relay_num, nfc_pulse_ms);
|
||||
} else {
|
||||
Serial.printf("NFC: ACCESS GRANTED UID=%s — relay %d ON\n",
|
||||
uid_str.c_str(), nfc_relay_num);
|
||||
postNFCEvent(uid_str);
|
||||
}
|
||||
// Already GRANTED and card still present — absence timer already
|
||||
// reset above; relay stays ON, no further action needed.
|
||||
|
||||
} else {
|
||||
// Card present but not in the authorized list
|
||||
if (!is_denied || uid_str != nfc_last_uid) {
|
||||
strcpy(nfc_access_state, "denied");
|
||||
nfc_last_uid = uid_str;
|
||||
Serial.printf("NFC: ACCESS DENIED UID=%s\n", uid_str.c_str());
|
||||
postNFCEvent(uid_str);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// No card this poll
|
||||
if (strcmp(nfc_access_state, "granted") == 0) {
|
||||
nfc_miss_count++;
|
||||
if (nfc_miss_count >= 2) { // 2 consecutive presence-check failures = card gone
|
||||
nfc_miss_count = 0;
|
||||
// No card detected this poll
|
||||
if (is_granted) {
|
||||
// Start the absence timer on first missed poll
|
||||
if (nfc_absent_since == 0) {
|
||||
nfc_absent_since = millis();
|
||||
Serial.printf("NFC: card not detected — relay OFF if absent > %lu ms\n",
|
||||
absent_limit);
|
||||
}
|
||||
if (millis() - nfc_absent_since >= absent_limit) {
|
||||
// Card has been absent long enough — close relay
|
||||
int pin = nfcRelayPin(nfc_relay_num);
|
||||
if (pin >= 0) {
|
||||
digitalWrite(pin, LOW);
|
||||
@@ -440,27 +540,23 @@ void loop() {
|
||||
case 4: relay4_state = false; break;
|
||||
}
|
||||
}
|
||||
nfc_absent_since = 0;
|
||||
nfc_last_uid = "";
|
||||
strcpy(nfc_access_state, "idle");
|
||||
nfc_last_uid = "";
|
||||
Serial.printf("NFC: card removed — relay %d closed\n", nfc_relay_num);
|
||||
} else {
|
||||
Serial.printf("NFC: presence miss %d/2 — retrying\n", nfc_miss_count);
|
||||
}
|
||||
} else if (strcmp(nfc_access_state, "denied") == 0) {
|
||||
nfc_miss_count++;
|
||||
if (nfc_miss_count >= 2) {
|
||||
Serial.printf("NFC: denied card removed\n");
|
||||
strcpy(nfc_access_state, "idle");
|
||||
nfc_last_uid = "";
|
||||
nfc_miss_count = 0;
|
||||
Serial.printf("NFC: card absent %lu ms — relay %d OFF\n",
|
||||
absent_limit, nfc_relay_num);
|
||||
}
|
||||
} else {
|
||||
// DENIED or IDLE — card gone, just return to idle
|
||||
nfc_absent_since = 0;
|
||||
nfc_last_uid = "";
|
||||
strcpy(nfc_access_state, "idle");
|
||||
}
|
||||
// idle: keep fast-polling, no action needed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delay(10);
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
yield();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -617,6 +713,8 @@ void handleRoot() {
|
||||
html += "table.settings tr:last-child{border-bottom:none}";
|
||||
html += "table.settings .val{font-weight:700;color:var(--text)}";
|
||||
html += "table.settings .mono{font-family:monospace;color:#1b5e20}";
|
||||
html += ".uid-tag{display:inline-flex;align-items:center;gap:4px;background:var(--card2,#e8f5e9);color:#1b5e20;border-radius:4px;padding:3px 8px;margin:2px 4px 2px 0;font-family:monospace;font-size:13px}";
|
||||
html += ".uid-tag-del{cursor:default}";
|
||||
html += ".api-list{font-size:12px;color:var(--text2);line-height:2}";
|
||||
html += "</style>";
|
||||
// ── JavaScript ─────────────────────────────────────────────────────────
|
||||
@@ -647,7 +745,7 @@ void handleRoot() {
|
||||
html += "b.className='relay-btn '+(on?'relay-on':'relay-off');";
|
||||
html += "b.textContent='Relay '+i+': '+(on?'ON':'OFF');}";
|
||||
html += "var nled=document.getElementById('nfc-led');";
|
||||
html += "if(nled)nled.className='led '+(d.nfc_card_present?'led-on':'led-off');";
|
||||
html += "if(nled)nled.className='led '+(d.nfc_last_uid?'led-on':'led-off');";
|
||||
html += "var nuid=document.getElementById('nfc-uid');";
|
||||
html += "if(nuid)nuid.textContent=d.nfc_last_uid||'No card inserted';";
|
||||
html += "var ncb=document.getElementById('nfc-copy-btn');if(ncb)ncb.disabled=!d.nfc_card_present;";
|
||||
@@ -656,12 +754,13 @@ void handleRoot() {
|
||||
html += "nac.className='nfc-state nfc-'+as;";
|
||||
html += "nac.textContent=as==='granted'?'ACCESS GRANTED':as==='denied'?'ACCESS DENIED':'Waiting for card';}";
|
||||
html += "var ef=document.getElementById('nfc-auth-field');";
|
||||
html += "if(ef&&document.activeElement!==ef)ef.value=d.nfc_auth_uid||'';";
|
||||
html += "if(ef&&document.activeElement!==ef)ef.value=d.nfc_last_uid||'';";
|
||||
html += "var ad=document.getElementById('nfc-auth-display');";
|
||||
html += "if(ad){if(d.nfc_auth_uid){ad.style.color='#1b5e20';ad.textContent=d.nfc_auth_uid;}";
|
||||
html += "else{ad.style.color='var(--wh-err-c)';ad.textContent='None \u2014 no card authorized yet';}}";
|
||||
html += "if(ad){var uids=d.nfc_auth_uids||[];";
|
||||
html += "if(uids.length>0){ad.style.color='#1b5e20';ad.innerHTML=uids.map(function(u,i){return '<span class=\\'uid-tag\\'>'+u+'</span>';}).join('');}";
|
||||
html += "else{ad.style.color='var(--wh-err-c)';ad.innerHTML='None \u2014 no card authorized yet';}}";
|
||||
html += "var rs=document.getElementById('nfc-relay-sel');if(rs&&document.activeElement!==rs)rs.value=d.nfc_relay_num||1;";
|
||||
html += "var pf=document.getElementById('nfc-pulse-field');if(pf&&document.activeElement!==pf)pf.value=d.nfc_pulse_ms||3000;";
|
||||
html += "var pf=document.getElementById('nfc-pulse-field');if(pf&&document.activeElement!==pf)pf.value=d.nfc_pulse_ms!=null?d.nfc_pulse_ms:0;";
|
||||
html += "var nmb=document.getElementById('nfc-module-btn');";
|
||||
html += "if(nmb){var ne=!!d.nfc_enabled;nmb.dataset.enabled=ne?'1':'0';";
|
||||
html += "nmb.className='btn '+(ne?'nfc-module-on':'nfc-module-off');";
|
||||
@@ -681,18 +780,33 @@ void handleRoot() {
|
||||
html += "if(ns){ns.textContent=ne?'Active':'Disabled';ns.style.color=ne?'#4CAF50':'#f44336';}}";
|
||||
html += "else{alert('Error: '+d.error);}}).catch(function(e){alert('Network error');});}";
|
||||
// NFC helpers
|
||||
html += "function clearNFCAuth(){if(!confirm('Remove the authorized card?'))return;";
|
||||
html += "fetch('/nfc/config?auth_uid=&relay='+document.getElementById('nfc-relay-sel').value+'&pulse_ms='+document.getElementById('nfc-pulse-field').value,{method:'POST'})";
|
||||
html += "function getCurrentUIDs(){";
|
||||
html += "var tags=document.querySelectorAll('#nfc-uid-list .uid-tag-del');";
|
||||
html += "return Array.from(tags).map(function(t){return t.dataset.uid;}).filter(function(u){return u&&u.length>0;});}";
|
||||
html += "function renderUIDList(uids){";
|
||||
html += "var list=document.getElementById('nfc-uid-list');if(!list)return;";
|
||||
html += "list.innerHTML='';";
|
||||
html += "uids.forEach(function(u){";
|
||||
html += "var s=document.createElement('span');s.className='uid-tag uid-tag-del';s.dataset.uid=u;";
|
||||
html += "s.innerHTML=u+' <button onclick=\\'removeCard(\\\"'+u+'\\\")\\' style=\\'background:none;border:none;cursor:pointer;color:inherit;font-size:14px;padding:0 2px;\\'>✕</button>';";
|
||||
html += "list.appendChild(s);});";
|
||||
html += "if(uids.length===0)list.innerHTML='<span style=\\'color:var(--wh-err-c)\\'>None \u2014 no card authorized yet</span>';}";
|
||||
html += "function removeCard(uid){";
|
||||
html += "var uids=getCurrentUIDs().filter(function(u){return u!==uid;});";
|
||||
html += "fetch('/nfc/config?auth_uids='+encodeURIComponent(uids.join(','))+'&relay='+document.getElementById('nfc-relay-sel').value+'&pulse_ms='+document.getElementById('nfc-pulse-field').value,{method:'POST'})";
|
||||
html += ".then(function(r){return r.json();})";
|
||||
html += ".then(function(d){if(d.status==='ok'){alert('Authorized card cleared.');}else{alert('Error: '+d.error);}})";
|
||||
html += ".then(function(d){if(d.status==='ok'){renderUIDList(d.auth_uids||[]);}else{alert('Error: '+d.error);}})";
|
||||
html += ".catch(function(e){alert('Network error');});}";
|
||||
html += "function saveNFCConfig(){";
|
||||
html += "var uid=document.getElementById('nfc-auth-field').value.trim().toUpperCase();";
|
||||
html += "var newuid=document.getElementById('nfc-auth-field').value.trim().toUpperCase();";
|
||||
html += "var uids=getCurrentUIDs();";
|
||||
html += "if(newuid.length>0){if(uids.indexOf(newuid)===-1){if(uids.length>=10){alert('Maximum 10 authorized cards reached. Remove one first.');return;}uids.push(newuid);}";
|
||||
html += "document.getElementById('nfc-auth-field').value='';}";
|
||||
html += "var relay=document.getElementById('nfc-relay-sel').value;";
|
||||
html += "var pulse=document.getElementById('nfc-pulse-field').value;";
|
||||
html += "fetch('/nfc/config?auth_uid='+encodeURIComponent(uid)+'&relay='+relay+'&pulse_ms='+pulse,{method:'POST'})";
|
||||
html += "fetch('/nfc/config?auth_uids='+encodeURIComponent(uids.join(','))+'&relay='+relay+'&pulse_ms='+pulse,{method:'POST'})";
|
||||
html += ".then(function(r){return r.json();})";
|
||||
html += ".then(function(d){if(d.status==='ok'){alert('Saved!\\nUID: '+(d.auth_uid||'(none)')+'\\nRelay: '+d.relay_num+'\\nTimeout: '+d.pulse_ms+'ms');}else{alert('Error: '+d.error);}})";
|
||||
html += ".then(function(d){if(d.status==='ok'){renderUIDList(d.auth_uids||[]);alert('Saved! '+d.auth_uids.length+' card(s) authorized.');}else{alert('Error: '+d.error);}})";
|
||||
html += ".catch(function(e){alert('Network error');});}";
|
||||
html += "function copyUID(){var u=document.getElementById('nfc-uid').textContent;";
|
||||
html += "if(u&&u!=='No card inserted'){var f=document.getElementById('nfc-auth-field');f.value=u;";
|
||||
@@ -776,37 +890,44 @@ void handleRoot() {
|
||||
// Current settings table
|
||||
html += "<h3>Current Settings</h3>";
|
||||
html += "<table class='settings'>";
|
||||
html += "<tr><td style='width:150px'>Authorized card</td>";
|
||||
if (strlen(nfc_auth_uid) > 0) {
|
||||
html += "<td class='val mono' id='nfc-auth-display'>" + String(nfc_auth_uid) + "</td>";
|
||||
html += "<td style='text-align:right'><button class='btn btn-off' style='font-size:12px;padding:5px 10px' onclick='clearNFCAuth()'>✕ Remove</button></td>";
|
||||
html += "<tr><td style='width:150px;vertical-align:top;padding-top:6px'>Authorized cards</td>";
|
||||
html += "<td colspan='2' id='nfc-uid-list' style='padding:4px 0'>";
|
||||
if (nfc_auth_count == 0) {
|
||||
html += "<span style='color:var(--wh-err-c)'>None — no card authorized yet</span>";
|
||||
} else {
|
||||
html += "<td colspan='2' style='color:var(--wh-err-c)' id='nfc-auth-display'>None — no card authorized yet</td>";
|
||||
for (int i = 0; i < NFC_MAX_CARDS; i++) {
|
||||
if (strlen(nfc_auth_uids[i]) > 0) {
|
||||
html += "<span class='uid-tag uid-tag-del' data-uid='" + String(nfc_auth_uids[i]) + "'>";
|
||||
html += String(nfc_auth_uids[i]);
|
||||
html += " <button onclick='removeCard(\"" + String(nfc_auth_uids[i]) + "\")' style='background:none;border:none;cursor:pointer;color:inherit;font-size:14px;padding:0 2px;'>✕</button>";
|
||||
html += "</span>";
|
||||
}
|
||||
}
|
||||
}
|
||||
html += "</tr>";
|
||||
html += "</td></tr>";
|
||||
html += "<tr><td>Trigger relay</td><td colspan='2' class='val'>Relay " + String(nfc_relay_num) + "</td></tr>";
|
||||
html += "<tr><td>Absent timeout</td><td colspan='2' class='val'>" + String(nfc_pulse_ms) + " ms</td></tr>";
|
||||
html += "<tr><td>Absent timeout</td><td colspan='2' class='val'>" + String(nfc_pulse_ms) + " ms (0 = 5 s default)</td></tr>";
|
||||
html += "</table>";
|
||||
// Edit settings form
|
||||
html += "<h3>Edit Settings</h3>";
|
||||
html += "<div class='nfc-edit'>";
|
||||
html += "<div><div class='field-label'>Authorized UID</div>";
|
||||
html += "<input type='text' id='nfc-auth-field' placeholder='e.g. 04:AB:CD:EF' value='" + String(nfc_auth_uid) + "' style='font-family:monospace'></div>";
|
||||
html += "<div><div class='field-label'>Add authorized UID <small style='color:var(--text3)'>(max 10 cards)</small></div>";
|
||||
html += "<input type='text' id='nfc-auth-field' placeholder='e.g. 04:AB:CD:EF or use "Use as authorized" button' style='font-family:monospace;width:100%'></div>";
|
||||
html += "<div><div class='field-label'>Trigger relay</div>";
|
||||
html += "<select id='nfc-relay-sel'>";
|
||||
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 class='field-label'>Absent timeout (ms)</div>";
|
||||
html += "<input type='number' id='nfc-pulse-field' min='100' max='60000' value='" + String(nfc_pulse_ms) + "'></div>";
|
||||
html += "<button class='btn btn-on nfc-save-btn' onclick='saveNFCConfig()' style='padding:9px 18px'>Save</button>";
|
||||
html += "<div><div class='field-label'>Absent timeout (ms, 0 = 5 s default)</div>";
|
||||
html += "<input type='number' id='nfc-pulse-field' min='0' max='60000' value='" + String(nfc_pulse_ms) + "'></div>";
|
||||
html += "<button class='btn btn-on nfc-save-btn' onclick='saveNFCConfig()' style='padding:9px 18px'>Add card & Save</button>";
|
||||
html += "</div>";
|
||||
if (!nfc_initialized) {
|
||||
html += "<p style='color:var(--wh-err-c);font-size:13px;margin-top:10px'>✗ 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-top:10px'>⚠ No authorized UID — present a card, click “Use as authorized” then Save.</p>";
|
||||
if (nfc_initialized && nfc_auth_count == 0) {
|
||||
html += "<p style='color:#e65100;font-size:13px;margin-top:10px'>⚠ No authorized UID — present a card, click “Use as authorized” then “Add card & Save”.</p>";
|
||||
}
|
||||
html += "</div>"; // close NFC card
|
||||
|
||||
@@ -821,7 +942,7 @@ void handleRoot() {
|
||||
|
||||
// API reference
|
||||
html += "<div class='card'><h2>API Endpoints</h2><div class='api-list'>";
|
||||
html += "GET /api/status • POST /relay/on?relay=1-4 • POST /relay/off?relay=1-4<br>";
|
||||
html += "GET /api/status • POST /relay/on?relay=1-4 • POST /relay/off?relay=1-4 • POST /relay/toggle?relay=1-4<br>";
|
||||
html += "GET /input/status?input=1-4 • POST /led/on • POST /led/off<br>";
|
||||
html += "GET /nfc/status • GET /nfc/config • POST /nfc/config?auth_uid=&relay=&pulse_ms=<br>";
|
||||
html += "POST /nfc/enable?state=0|1 • POST /register?callback_url=...";
|
||||
@@ -937,12 +1058,16 @@ void handleStatus() {
|
||||
json += "\"relay3\":" + String(relay3_state ? "true" : "false") + ",";
|
||||
json += "\"relay4\":" + String(relay4_state ? "true" : "false") + ",";
|
||||
json += "\"led\":" + String(led_state ? "true" : "false") + ",";
|
||||
bool nfc_present = nfc_initialized && (strcmp(nfc_access_state, "granted") == 0);
|
||||
bool nfc_present = nfc_initialized && nfc_last_uid.length() > 0;
|
||||
json += "\"nfc_initialized\":" + String(nfc_initialized ? "true" : "false") + ",";
|
||||
json += "\"nfc_card_present\":" + String(nfc_present ? "true" : "false") + ",";
|
||||
json += "\"nfc_last_uid\":\"" + nfc_last_uid + "\",";
|
||||
json += "\"nfc_access_state\":\"" + String(nfc_access_state) + "\",";
|
||||
json += "\"nfc_auth_uid\":\"" + String(nfc_auth_uid) + "\",";
|
||||
json += "\"nfc_auth_uid\":\"" + (nfc_auth_count > 0 ? String(nfc_auth_uids[0]) : "") + "\",";
|
||||
// Full list for multi-card support
|
||||
json += "\"nfc_auth_uids\":[";
|
||||
{ bool f=true; for(int i=0;i<NFC_MAX_CARDS;i++){if(strlen(nfc_auth_uids[i])>0){if(!f)json+=",";json+="\""+String(nfc_auth_uids[i])+"\"";f=false;}} }
|
||||
json += "],";
|
||||
json += "\"nfc_relay_num\":" + String(nfc_relay_num) + ",";
|
||||
json += "\"nfc_pulse_ms\":" + String(nfc_pulse_ms) + ",";
|
||||
json += "\"nfc_enabled\":" + String(nfc_enabled ? "true" : "false");
|
||||
@@ -1012,6 +1137,36 @@ void handleRelayOff() {
|
||||
Serial.printf("Relay %d OFF\n", relay_num);
|
||||
}
|
||||
|
||||
void handleRelayToggle() {
|
||||
if (!verifyAPIRequest()) return;
|
||||
if (!server.hasArg("relay")) {
|
||||
server.send(400, "application/json", "{\"error\":\"Missing relay parameter\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
int relay_num = server.arg("relay").toInt();
|
||||
int pin = -1;
|
||||
bool *state_ptr = nullptr;
|
||||
|
||||
switch (relay_num) {
|
||||
case 1: pin = RELAY_1_PIN; state_ptr = &relay1_state; break;
|
||||
case 2: pin = RELAY_2_PIN; state_ptr = &relay2_state; break;
|
||||
case 3: pin = RELAY_3_PIN; state_ptr = &relay3_state; break;
|
||||
case 4: pin = RELAY_4_PIN; state_ptr = &relay4_state; break;
|
||||
default:
|
||||
server.send(400, "application/json", "{\"error\":\"Invalid relay number (1-4)\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Flip current state (active HIGH: HIGH = ON, LOW = OFF)
|
||||
bool new_state = !(*state_ptr);
|
||||
digitalWrite(pin, new_state ? HIGH : LOW);
|
||||
*state_ptr = new_state;
|
||||
String json = "{\"status\":\"ok\",\"state\":" + String(new_state ? "true" : "false") + "}";
|
||||
server.send(200, "application/json", json);
|
||||
Serial.printf("Relay %d TOGGLE → %s\n", relay_num, new_state ? "ON" : "OFF");
|
||||
}
|
||||
|
||||
void handleRelayStatus() {
|
||||
if (!verifyAPIRequest()) return;
|
||||
if (!server.hasArg("relay")) {
|
||||
@@ -1247,6 +1402,84 @@ void handleNFCEnable() {
|
||||
String("{\"status\":\"ok\",\"nfc_enabled\":") + (nfc_enabled ? "true" : "false") + "}");
|
||||
}
|
||||
|
||||
void handleNFCProbe() {
|
||||
// Manual NFC detection at runtime with timeout protection
|
||||
if (!requireAuth()) return;
|
||||
|
||||
server.setContentLength(CONTENT_LENGTH_UNKNOWN);
|
||||
server.send(200, "text/plain");
|
||||
server.sendContent("Starting NFC probe (with 300ms timeout per attempt)...\n\n");
|
||||
|
||||
const long NFC_BAUDS[] = {115200, 9600, 57600, 38400};
|
||||
const int NFC_NBAUDS = 4;
|
||||
const int NFC_PINS[2][2] = {{NFC_RX_PIN, NFC_TX_PIN},
|
||||
{NFC_TX_PIN, NFC_RX_PIN}};
|
||||
|
||||
uint32_t versiondata = 0;
|
||||
long found_baud = 0;
|
||||
int found_rx = NFC_RX_PIN;
|
||||
int found_tx = NFC_TX_PIN;
|
||||
|
||||
for (int pi = 0; pi < 2 && !versiondata; pi++) {
|
||||
int rx = NFC_PINS[pi][0];
|
||||
int tx = NFC_PINS[pi][1];
|
||||
for (int bi = 0; bi < NFC_NBAUDS && !versiondata; bi++) {
|
||||
long baud = NFC_BAUDS[bi];
|
||||
char msg[128];
|
||||
snprintf(msg, sizeof(msg), " Trying baud=%-7ld RX=GPIO%d TX=GPIO%d ... ", baud, rx, tx);
|
||||
server.sendContent(msg);
|
||||
|
||||
nfcSerial.begin(baud, SERIAL_8N1, rx, tx);
|
||||
delay(100);
|
||||
while (nfcSerial.available()) nfcSerial.read();
|
||||
nfc.begin();
|
||||
|
||||
unsigned long probe_start = millis();
|
||||
versiondata = nfc.getFirmwareVersion();
|
||||
unsigned long probe_time = millis() - probe_start;
|
||||
|
||||
if (versiondata) {
|
||||
found_baud = baud; found_rx = rx; found_tx = tx;
|
||||
snprintf(msg, sizeof(msg), "✓ FOUND in %lu ms\n", probe_time);
|
||||
server.sendContent(msg);
|
||||
} else {
|
||||
server.sendContent("✗\n");
|
||||
}
|
||||
server.handleClient();
|
||||
yield();
|
||||
}
|
||||
}
|
||||
|
||||
server.sendContent("\n");
|
||||
if (!versiondata) {
|
||||
server.sendContent("✗ PN532 not detected with any baud/pin combination.\n\n");
|
||||
server.sendContent("Hardware checklist:\n");
|
||||
server.sendContent("1. DIP/solder-jumpers on PN532: BOTH = 0 for HSU mode\n");
|
||||
server.sendContent(" (Some boards label them SEL0/SEL1 or I0/I1 — both must be LOW)\n");
|
||||
server.sendContent("2. Power: UEXT1 pin 1 = 3.3V, pin 2 = GND (measure with multimeter)\n");
|
||||
server.sendContent("3. Wiring: UEXT1 pin 3 (GPIO4) ↔ PN532 RXD\n");
|
||||
server.sendContent(" UEXT1 pin 4 (GPIO5) ↔ PN532 TXD\n");
|
||||
server.sendContent("4. Some PN532 boards need 100Ω series resistor on TX line\n");
|
||||
server.sendContent("5. Power off the module, power on again, then run probe\n");
|
||||
nfc_initialized = false;
|
||||
} else {
|
||||
char msg[256];
|
||||
snprintf(msg, sizeof(msg),
|
||||
"✓ PN532 found! baud=%ld RX=GPIO%d TX=GPIO%d\n"
|
||||
" Chip: PN5%02X\n"
|
||||
" Firmware: %d.%d\n\n",
|
||||
found_baud, found_rx, found_tx,
|
||||
(versiondata >> 24) & 0xFF,
|
||||
(versiondata >> 16) & 0xFF, (versiondata >> 8) & 0xFF);
|
||||
server.sendContent(msg);
|
||||
nfcSerial.begin(found_baud, SERIAL_8N1, found_rx, found_tx);
|
||||
nfc.SAMConfig();
|
||||
nfc_initialized = true;
|
||||
server.sendContent("✓ NFC initialized successfully — ready for card detection\n");
|
||||
}
|
||||
server.sendContent(""); // Finish response
|
||||
}
|
||||
|
||||
void handleNFCStatus() {
|
||||
if (!verifyAPIRequest()) return;
|
||||
bool present = nfc_initialized && nfc_enabled && (strcmp(nfc_access_state, "granted") == 0);
|
||||
@@ -1256,7 +1489,10 @@ void handleNFCStatus() {
|
||||
json += "\"card_present\":" + String(present ? "true" : "false") + ",";
|
||||
json += "\"last_uid\":\"" + nfc_last_uid + "\",";
|
||||
json += "\"access_state\":\"" + String(nfc_access_state) + "\",";
|
||||
json += "\"auth_uid\":\"" + String(nfc_auth_uid) + "\",";
|
||||
json += "\"auth_uid\":\"" + String(nfc_auth_count > 0 ? nfc_auth_uids[0] : "") + "\",";
|
||||
json += "\"auth_uids\":[";
|
||||
{ bool f=true; for(int i=0;i<NFC_MAX_CARDS;i++){if(strlen(nfc_auth_uids[i])>0){if(!f)json+=",";json+="\""+String(nfc_auth_uids[i])+"\"";f=false;}} }
|
||||
json += "],";
|
||||
json += "\"relay_num\":" + String(nfc_relay_num) + ",";
|
||||
json += "\"pulse_ms\":" + String(nfc_pulse_ms);
|
||||
json += "}";
|
||||
@@ -1298,7 +1534,16 @@ int nfcRelayPin(int rnum) {
|
||||
void handleNFCConfigGet() {
|
||||
if (!verifyAPIRequest()) return;
|
||||
String json = "{";
|
||||
json += "\"auth_uid\":\"" + String(nfc_auth_uid) + "\",";
|
||||
json += "\"auth_uids\":[";
|
||||
bool first = true;
|
||||
for (int i = 0; i < NFC_MAX_CARDS; i++) {
|
||||
if (strlen(nfc_auth_uids[i]) > 0) {
|
||||
if (!first) json += ",";
|
||||
json += "\"" + String(nfc_auth_uids[i]) + "\"";
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
json += "],";
|
||||
json += "\"relay_num\":" + String(nfc_relay_num) + ",";
|
||||
json += "\"pulse_ms\":" + String(nfc_pulse_ms) + ",";
|
||||
json += "\"nfc_enabled\":" + String(nfc_enabled ? "true" : "false");
|
||||
@@ -1308,17 +1553,34 @@ void handleNFCConfigGet() {
|
||||
|
||||
void handleNFCConfigSet() {
|
||||
if (!verifyAPIRequest()) return;
|
||||
if (server.hasArg("auth_uid")) {
|
||||
String u = server.arg("auth_uid");
|
||||
u.trim();
|
||||
u.toUpperCase();
|
||||
if (u.length() < sizeof(nfc_auth_uid)) {
|
||||
u.toCharArray(nfc_auth_uid, sizeof(nfc_auth_uid));
|
||||
} else {
|
||||
server.send(400, "application/json", "{\"error\":\"auth_uid too long (max 31 chars)\"}");
|
||||
return;
|
||||
|
||||
// auth_uids: comma-separated list of UIDs, e.g. "04:AB:CD:EF,04:11:22:33"
|
||||
// Sending auth_uids= (empty) clears all authorized cards.
|
||||
if (server.hasArg("auth_uids")) {
|
||||
String raw = server.arg("auth_uids");
|
||||
raw.trim();
|
||||
raw.toUpperCase();
|
||||
// Clear all slots first
|
||||
for (int i = 0; i < NFC_MAX_CARDS; i++) nfc_auth_uids[i][0] = '\0';
|
||||
nfc_auth_count = 0;
|
||||
if (raw.length() > 0) {
|
||||
int start = 0;
|
||||
int slot = 0;
|
||||
while (slot < NFC_MAX_CARDS) {
|
||||
int comma = raw.indexOf(',', start);
|
||||
String uid = (comma == -1) ? raw.substring(start) : raw.substring(start, comma);
|
||||
uid.trim();
|
||||
if (uid.length() > 0 && uid.length() < 32) {
|
||||
uid.toCharArray(nfc_auth_uids[slot], 32);
|
||||
slot++;
|
||||
nfc_auth_count++;
|
||||
}
|
||||
if (comma == -1) break;
|
||||
start = comma + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (server.hasArg("relay")) {
|
||||
int r = server.arg("relay").toInt();
|
||||
if (r >= 1 && r <= 4) {
|
||||
@@ -1330,26 +1592,58 @@ void handleNFCConfigSet() {
|
||||
}
|
||||
if (server.hasArg("pulse_ms")) {
|
||||
long p = server.arg("pulse_ms").toInt();
|
||||
if (p >= 100 && p <= 60000) {
|
||||
if (p >= 0 && p <= 60000) {
|
||||
nfc_pulse_ms = (unsigned long)p;
|
||||
} else {
|
||||
server.send(400, "application/json", "{\"error\":\"pulse_ms range: 100-60000\"}");
|
||||
server.send(400, "application/json", "{\"error\":\"pulse_ms range: 0-60000\"}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
Serial.printf("NFC config: auth='%s' relay=%d pulse=%lu ms\n",
|
||||
nfc_auth_uid, nfc_relay_num, nfc_pulse_ms);
|
||||
// ── Persist to NVS so settings survive power cycles ──────────────────────
|
||||
prefs.begin("nfc_cfg", false); // read-write namespace
|
||||
prefs.putString("auth_uid", nfc_auth_uid);
|
||||
prefs.putInt("relay_num", nfc_relay_num);
|
||||
prefs.putULong("pulse_ms", nfc_pulse_ms);
|
||||
|
||||
Serial.printf("NFC config: %d card(s) authorized, relay=%d, pulse=%lu ms\n",
|
||||
nfc_auth_count, nfc_relay_num, nfc_pulse_ms);
|
||||
|
||||
// If relay was ON from a previous grant, turn it off before resetting state
|
||||
if (strcmp(nfc_access_state, "granted") == 0) {
|
||||
int pin = nfcRelayPin(nfc_relay_num);
|
||||
if (pin >= 0) {
|
||||
digitalWrite(pin, LOW);
|
||||
switch (nfc_relay_num) {
|
||||
case 1: relay1_state = false; break;
|
||||
case 2: relay2_state = false; break;
|
||||
case 3: relay3_state = false; break;
|
||||
case 4: relay4_state = false; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
strcpy(nfc_access_state, "idle");
|
||||
nfc_last_uid = "";
|
||||
nfc_miss_count = 0;
|
||||
nfc_absent_since = 0;
|
||||
|
||||
// ── Persist to NVS ────────────────────────────────────────────────────────
|
||||
prefs.begin("nfc_cfg", false);
|
||||
prefs.putInt("relay_num", nfc_relay_num);
|
||||
prefs.putULong("pulse_ms", nfc_pulse_ms);
|
||||
for (int i = 0; i < NFC_MAX_CARDS; i++) {
|
||||
String key = "uid" + String(i);
|
||||
prefs.putString(key.c_str(), nfc_auth_uids[i]);
|
||||
}
|
||||
prefs.end();
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
String json = "{\"status\":\"ok\","
|
||||
"\"auth_uid\":\"" + String(nfc_auth_uid) + "\","
|
||||
"\"relay_num\":" + String(nfc_relay_num) + ","
|
||||
"\"pulse_ms\":" + String(nfc_pulse_ms) + "}";
|
||||
|
||||
// Build response with current uid list
|
||||
String json = "{\"status\":\"ok\",\"auth_uids\":[";
|
||||
bool first = true;
|
||||
for (int i = 0; i < NFC_MAX_CARDS; i++) {
|
||||
if (strlen(nfc_auth_uids[i]) > 0) {
|
||||
if (!first) json += ",";
|
||||
json += "\"" + String(nfc_auth_uids[i]) + "\"";
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
json += "],\"relay_num\":" + String(nfc_relay_num) +
|
||||
",\"pulse_ms\":" + String(nfc_pulse_ms) + "}";
|
||||
server.send(200, "application/json", json);
|
||||
}
|
||||
|
||||
@@ -1382,7 +1676,12 @@ void handleDebug() {
|
||||
out += "NFC last UID:" + String(nfc_last_uid.length() > 0 ? nfc_last_uid : "(none)") + "\n";
|
||||
out += "NFC state: " + String(nfc_access_state) + "\n";
|
||||
out += "NFC relay: " + String(nfc_relay_num) + "\n";
|
||||
out += "NFC auth: " + String(strlen(nfc_auth_uid) > 0 ? nfc_auth_uid : "(any)") + "\n";
|
||||
out += "NFC auth: " + String(nfc_auth_count) + " card(s)\n";
|
||||
for (int i = 0; i < NFC_MAX_CARDS; i++) {
|
||||
if (strlen(nfc_auth_uids[i]) > 0) {
|
||||
out += " [" + String(i) + "] " + String(nfc_auth_uids[i]) + "\n";
|
||||
}
|
||||
}
|
||||
out += "\n";
|
||||
out += "Relay states: R1=" + String(relay1_state) + " R2=" + String(relay2_state)
|
||||
+ " R3=" + String(relay3_state) + " R4=" + String(relay4_state) + "\n";
|
||||
|
||||
@@ -10,6 +10,20 @@
|
||||
// WEB_USER / WEB_PASSWORD — credentials for the browser control panel.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// ─ WiFi Credentials ───────────────────────────────────────────────────────
|
||||
#define WIFI_SSID "your_wifi_network"
|
||||
#define WIFI_PASSWORD "your_wifi_password"
|
||||
|
||||
// ─ Static IP Configuration ────────────────────────────────────────────────
|
||||
// Set USE_STATIC_IP to true for a fixed IP; false = use DHCP
|
||||
#define USE_STATIC_IP true
|
||||
#define STATIC_IP_ADDR "192.168.0.240" // C6-EVB board IP
|
||||
#define STATIC_GATEWAY "192.168.0.1" // Local network gateway
|
||||
#define STATIC_SUBNET "255.255.255.0" // Subnet mask
|
||||
#define STATIC_DNS1 "8.8.8.8" // Google DNS primary
|
||||
#define STATIC_DNS2 "8.8.4.4" // Google DNS secondary
|
||||
|
||||
// ─ Web Server Credentials ─────────────────────────────────────────────────
|
||||
#define API_SECRET "REPLACE_WITH_OUTPUT_OF_python3_-c_import_secrets_token_hex_32"
|
||||
#define WEB_USER "your_username"
|
||||
#define WEB_PASSWORD "your_password"
|
||||
|
||||
Reference in New Issue
Block a user