ESP RFID Webui
Architecture and call graph
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1#include <freertos/queue.h>
2#include "TimerHandle.h"
3#include "AuthSync.h"
5#include "HardwareSerial.h"
6#include "HashUtils.h"
7#include <ArduinoJson.h>
8#include <HTTPClient.h>
9#include <LittleFS.h>
10#include <MFRC522.h>
11#include <SPI.h>
12#include <U8x8lib.h>
13#include <WiFi.h>
14#include <Wire.h>
15
16
17
18
19
20/*
21 Runtime flow (high level)
22
23 1) Boot and setup()
24 - Initialize peripherals (display, SPI, RFID).
25 - Mount LittleFS and try to read `/config.json`.
26 * If present, parse and populate SSID/PASS/SERVER_BASE.
27 * If missing or parse fails, the strings remain empty and network/server
28 related features are skipped.
29 - Create `AuthSync` at runtime if a `server_base` was provided in config.
30 - Call `WiFi.begin(SSID, PASS)` using the loaded credentials (may fail if
31 no config provided).
32 - If Wi‑Fi connects, call `authSync->begin()` (initial server sync) when
33 `AuthSync` exists.
34
35 2) Main loop
36 - Periodically poll `/api/status` (only when Wi‑Fi connected and
37 SERVER_BASE is configured) to update enroll mode.
38 - On RFID scan:
39 * Post the scan to `/api/last_scan` (if configured/online).
40 * Ask `authSync` whether the UID is authorized. `AuthSync` first
41 consults its persistent hashed allow/deny caches (offline), then
42 falls back to a server lookup / bitset when online. Results are
43 learned and persisted for offline use.
44 - `AuthSync::update()` runs periodically to refresh the authorization
45 bitset from the server when online.
46
47 Notes:
48 - Configuration file format: JSON with keys `ssid`, `password`,
49 and `server_base`. Place it in the project `data/` folder and use
50 `pio run -t uploadfs` to write it to LittleFS as `/config.json`.
51 - The app is defensive: if no network/server config exists it still
52 runs and accepts scans, but server operations are skipped.
53
54*/
55
56/* RFID pins
57#define RST_PIN 17
58#define SS_PIN 5*/
59constexpr uint8_t RST_PIN = 17;
60constexpr uint8_t SS_PIN = 5;
61static constexpr unsigned long ENROLL_POLL_INTERVAL_MS = 5000;
63
64// Display
65U8X8_SSD1315_128X64_NONAME_SW_I2C u8x8(
66 /* clock=*/22,
67 /* data=*/21,
68 /* reset=*/U8X8_PIN_NONE);
69
70// ----------------- CONFIG -----------------
71// Network and server configuration are moved out of the firmware and
72// loaded from LittleFS at boot. This prevents embedding credentials in
73// the binary and allows convenient updates by writing `/config.json` to
74// the filesystem (use PlatformIO `uploadfs` from the project `data/` dir).
75//
76// If the file is missing the strings remain empty and server-related
77// functionality is skipped.
78String SSID = "";
79String PASS = "";
80String SERVER_BASE = "";
81
82// Authorization sync (created after config load) — allocated at runtime
83// so it can use the runtime `SERVER_BASE` value read from the JSON file.
84AuthSync *authSync = nullptr;
85
86// ----------------- State -----------------
87String lastUID = "NONE";
88String enrollMode = "none";
89bool lastAuthorized = false;
90uint64_t lastHash = 0; // Last computed hash for display
91bool serverReachable = false; // Track server/database reachability
92unsigned long lastDisplayUpdate = 0;
93unsigned long enrollBlinkMillis = 0;
94bool enrollBlinkState = false;
95// Simple millis-based enroll-mode poll
96
97static unsigned long lastEnrollPoll = 0;
98
99// Display state tracking (to avoid unnecessary redraws)
100String displayedUID = "";
101bool displayedAuth = false;
102uint64_t displayedHash = 0;
106
107String getUidString();
108void updateEnrollStatus();
109void updateDisplay();
110void drawHeader();
111void drawEnrollIndicator(bool on);
112void NetworkTask(void *pv);
113bool postLastScan(const String &uid, JsonDocument &out);
114
115// Queue for deferred network posting of scanned UIDs
116struct ScanItem {
117 char uid[21];
118};
119static QueueHandle_t scanQueue = nullptr;
120//---------------- FreeRTOS timers -----------------
121// AuthSync timer (non-blocking): callback only sets a flag; NetworkTask does
122// the work
123static volatile bool authSyncRequested = false;
124// Display update flag set by timer callback
125static volatile bool displayUpdateRequested = false;
126static void displayTimerCallback(TimerHandle_t xTimer) { (void)xTimer; displayUpdateRequested = true; }
127
128// ----------------- SETUP -----------------
129void setup() {
130 Serial.begin(115200);
131 vTaskDelay(500 / portTICK_PERIOD_MS);
132 Serial.println(" hello world!");
133
134 // Force first auth line to render even if initial authorization is false
135 // by priming displayedAuth opposite of lastAuthorized.
137
138 Wire.begin(21, 22);
139 u8x8.begin();
140 u8x8.setFont(u8x8_font_chroma48medium8_r);
142 u8x8.drawString(0, 2, "FS Init...");
143
144 SPI.begin();
145 rfid.PCD_Init();
146
147 // Ensure FS is mounted before trying to load config
148 if (LittleFS.begin()) {
150 Serial.println("Config loaded from LittleFS");
151 Serial.println("SSID: " + SSID);
152 Serial.println("PASS: " + PASS);
153 Serial.println("SERVER_BASE: " + SERVER_BASE);
154 u8x8.drawString(0, 2, "FS OK ");
155 // Create AuthSync early so we can load offline caches from NVS
156 if (SERVER_BASE.length() > 0) {
158 // AuthSync constructed — delay offline preload until after WiFi
159 // initialization so any network-related state is stable.
160 } else {
161 Serial.println("SERVER_BASE empty; offline authorization disabled "
162 "until configured");
163 }
164 } /* ------------- If failing to flash config.json, run auto-provisioning
165 ------------- Replace this placeholder with your desired network
166 details to have the device write config.json on first boot.
167 ------------- ------------- ------------- ------------- -------------
168 -------------*/
169 /*else {
170 Serial.println("config.json missing -> auto-provisioning defaults");
171 // One-time provisioning: write a default config, then reload.
172 if (ConfigManager::saveConfig("SSID", "PASSWORD",
173 "http://SERVER_Base")) { if (ConfigManager::loadConfig(SSID, PASS,
174 SERVER_BASE)) { Serial.println("Provisioned default config.json");
175 Serial.println("SSID: " + SSID);
176 Serial.println("PASS: " + PASS);
177 Serial.println("SERVER_BASE: " + SERVER_BASE);
178 u8x8.drawString(0, 2, "PROVISION");
179 } else {
180 Serial.println("Provision write ok but reload failed");
181 u8x8.drawString(0, 2, "PROV ERR");
182 }
183 } else {
184 Serial.println("Failed to auto-provision config.json");
185 u8x8.drawString(0, 2, "PROV FAIL");
186 }
187 }
188 ConfigManager::listFiles();
189 } else {
190 Serial.println("LittleFS mount failed, formatting...");
191 LittleFS.format();
192 if (LittleFS.begin()) {
193 Serial.println("LittleFS formatted and remounted");
194 if (ConfigManager::loadConfig(SSID, PASS, SERVER_BASE)) {
195 Serial.println("Config loaded from LittleFS");
196 ConfigManager::listFiles();
197 }
198 } else {
199 Serial.println("LittleFS format/remount failed");
200 u8x8.drawString(0, 2, "FS FAIL");
201 }*/
202 }
203 vTaskDelay(100 /
204 portTICK_PERIOD_MS); // Give some time for LittleFS to stabilize
205
206 WiFi.begin(SSID.c_str(), PASS.c_str());
207 // After Wi-Fi initialization, perform the offline preload (loads NVS + FS
208 // caches) so AuthSync can have its cached data ready before any network
209 // sync attempts. Doing this after WiFi.begin keeps the ordering stable.
210 if (authSync) {
213 Serial.println("[AuthSync] Offline cache preloaded (after WiFi init)");
214 }
215 int tries = 0;
216 while (WiFiClass::status() != WL_CONNECTED && tries < 80) {
217 vTaskDelay(500 / portTICK_PERIOD_MS);
218 Serial.print(".");
219 tries++;
220 }
221
222 if (WiFiClass::status() == WL_CONNECTED) {
223 Serial.println("");
224 Serial.println("WiFi connected");
225 Serial.print("IP address: ");
226 Serial.println(WiFi.localIP());
227 u8x8.drawString(0, 2, "WiFi OK ");
228 // Wifi modem sleep
229 WiFi.setSleep(true);
230 vTaskDelay(100 / portTICK_PERIOD_MS);
231
232 // Attempt an online sync now that WiFi is connected (we already loaded
233 // offline cache)
234 bool syncOk = false;
235 if (authSync) {
236 // Use public begin() to perform an immediate sync (reloads NVS + attempts
237 // server sync)
238 syncOk = authSync->begin();
239 }
240 if (syncOk) {
241 u8x8.drawString(0, 3, "DB OK");
242 serverReachable = true;
244 } else {
245 u8x8.drawString(0, 3, "DB OFFLINE ");
246 serverReachable = false;
248 Serial.println(
249 "[AuthSync] Using offline cache (sync failed or server unreachable)");
250 }
251 } else {
252 u8x8.drawString(0, 2, "WiFi FAIL");
253 serverReachable = false;
255 }
256 vTaskDelay(100 / portTICK_PERIOD_MS);
257
258 // Create queue and network task (pin to core 0, lower priority than loop for
259 // RFID responsiveness)
260 //Note: this was implemented in a phase where there was no NetworkTask yet
261 // Could probably just be lower priority
262 if (!scanQueue) {
263 scanQueue = xQueueCreate(10, sizeof(ScanItem));
264
265
266 //Configure UNICORE flag in platformio.ini if using a single-core ESP32
267#if defined(CONFIG_FREERTOS_UNICORE)
268
269 xTaskCreate(
270 NetworkTask,
271 "net_task",
272 4096,
273 nullptr,
274 tskIDLE_PRIORITY,
275 nullptr);
276
277 Serial.println("[Tasks] NetworkTask started on IdleTask priority");
278#else
279 xTaskCreatePinnedToCore(
281 "net_task",
282 4096,
283 nullptr,
284 tskIDLE_PRIORITY,
285 nullptr,
286 0);
287 Serial.println("[Tasks] NetworkTask started on IdleTask priority");
288#endif
289 // Runtime fallback: query chip info (works on Arduino and esp-idf)
290 if (scanQueue) {
291 Serial.println("[Tasks] scanQueue created");
292 } else {
293 Serial.println("[Tasks] Failed to create scanQueue");
294 }
295 }
296 // Create timers using centralized helpers (TimerHandle.cpp)
297 if (!createDisplayTimer(displayTimerCallback, pdMS_TO_TICKS(500))) {
298 Serial.println("[Tasks] Failed to create/start display timer");
299 } else {
300 Serial.println("[Tasks] Display timer started");
301 }
302
303}
304
305void loop() {
306 // Server reachability check now handled by FreeRTOS timer in NetworkTask
307
308 if (rfid.PICC_IsNewCardPresent() && rfid.PICC_ReadCardSerial()) {
309 String uid = getUidString();
310 Serial.println("Scanned: " + uid);
311 lastUID = uid;
312
313 // Compute hash for display (same method as AuthSync) ----------- FOR
314 // DEBUGGING ----
315 /*String normalized = uid;
316 normalized.trim();
317 normalized.toUpperCase();
318 uint64_t hash = 0xcbf29ce484222325ULL;
319 for (size_t i = 0; i < normalized.length(); i++) {
320 constexpr uint64_t prime = 0x100000001b3ULL;
321 hash ^= static_cast<uint8_t>(normalized[i]);
322 hash *= prime;
323 }*/
324
327 updateEnrollStatus(); // Refresh after scan
329 rfid.PICC_HaltA();
330 rfid.PCD_StopCrypto1();
331 vTaskDelay(100 / portTICK_PERIOD_MS); // Debounce recommended delay
332 // Defer network POST of last scan to network task via queue
333 if (scanQueue) {
334 ScanItem item{};
335 uid.toCharArray(item.uid, sizeof(item.uid));
336 if (xQueueSend(scanQueue, &item, 0) != pdPASS) {
337 Serial.println("[Queue] scanQueue full; dropping UID post");
338 } else {
339 Serial.printf("[Queue] Enqueued UID=%s\n", item.uid);
340 }
341 }
342 }
343
344 // Periodic sync handled by NetworkTask
345
346 // Display updates are requested by a timer; perform the actual update in
347 // loop() context to keep display code single-threaded and safe for the
348 // U8x8 library.
352 lastDisplayUpdate = millis();
353 }
354
355 // Blink indicator when waiting for enroll
356 if (enrollMode != "none" && millis() - enrollBlinkMillis > 500) {
358 enrollBlinkMillis = millis();
360 }
361
362 // Simple millis-based enroll-mode poll
363 if (millis() - lastEnrollPoll > ENROLL_POLL_INTERVAL_MS) {
364 lastEnrollPoll = millis();
366 }
367
368#ifdef AUTH_TEST_HOOK
369 // Test hook: press 'm' on serial to print memory stats
370 if (Serial.available()) {
371 int c = Serial.read();
372 if (c == 'm' || c == 'M') {
373 if (authSync) authSync->TEST_dumpMemoryStats();
374 }
375 }
376#endif
377}
378
379/* -------------------------------- helpers ---------------------------------- */
381{
382 String uid = "";
383 for (byte i = 0; i < rfid.uid.size; i++) {
384 if (rfid.uid.uidByte[i] < 0x10)
385 uid += "0";
386 uid += String(rfid.uid.uidByte[i], HEX);
387 }
388 uid.toUpperCase();
389 return uid;
390}
391
393{
394 static bool headerDrawn = false;
395 if (!headerDrawn) {
396 u8x8.clear();
397 u8x8.drawString(0, 0, "RFID Access");
398 headerDrawn = true;
399 }
400}
401
403{
404 drawHeader(); // Only draws once
405
406 // Update DB status if changed
408 if (serverReachable) {
409 u8x8.drawString(0, 3, "DB OK ");
410 } else {
411 u8x8.drawString(0, 3, "DB LOST ");
412 }
414 }
415
416 // Only update UID if changed
417 if (lastUID != displayedUID) {
418 String line = "UID:" + lastUID;
419 if (line.length() > 16)
420 line = line.substring(0, 16);
421 // Pad with spaces to clear old text
422 while (line.length() < 16)
423 line += " ";
424 u8x8.drawString(0, 1, line.c_str());
426 }
427
428 // Only update auth status if changed
430 u8x8.drawString(
431 0, 4, (String("Auth:") + (lastAuthorized ? "YES" : "NO ")).c_str());
433 }
434
435 // Update hash display (last 8 hex digits on bottom row)
436 if (lastHash != displayedHash) {
437 char hashStr[17];
438 snprintf(hashStr, sizeof(hashStr), "H:%08X",
439 static_cast<uint32_t>(lastHash & 0xFFFFFFFF));
440 u8x8.drawString(0, 7, hashStr);
442 }
443
444 // Update enroll indicator if mode changed
447 }
448}
449
451{
452 String currentMode = enrollMode;
453 bool currentBlink = on;
454
455 // Only redraw if mode or blink state changed - Full redraws are visible
456 if (currentMode != displayedEnrollMode ||
457 currentBlink != displayedEnrollBlink) {
458 if (enrollMode == "none") {
459 u8x8.drawString(14, 0, " ");
460 } else if (on) {
461 u8x8.drawString(14, 0, enrollMode == "grant" ? "GR" : "RV");
462 } else {
463 u8x8.drawString(14, 0, " ");
464 }
465 displayedEnrollMode = currentMode;
466 displayedEnrollBlink = currentBlink;
467 }
468}
469
470bool postLastScan(const String &uid, JsonDocument &out)
471{
472 out.clear();
473 // Guard: if we're offline or server not configured, return empty doc.
474 // This avoids making invalid HTTP calls when no server_base is provided
475 // (e.g. on first-boot before provisioning /config.json).
476 if (WiFi.status() != WL_CONNECTED)
477 return false;
478 if (SERVER_BASE.length() == 0)
479 return false;
480 // Escape: if we already marked serverReachable false, skip HTTP entirely
481 if (!serverReachable) {
482 // Uncomment for verbose logging: Serial.println("[postLastScan] Skipped
483 // (serverReachable=false)");
484 return false;
485 }
486 HTTPClient http;
487 http.setTimeout(1500); // shorter timeout to avoid long blocking
488 http.begin(String(SERVER_BASE) + "/api/last_scan");
489 http.addHeader("Content-Type", "application/json");
490 String body = R"({"uid":")" + uid + "\"}";
491 int code = http.POST(body);
492 Serial.printf("[HTTP] POST /api/last_scan -> code=%d, body=%s\n", code, body.c_str());
493 if (code < 200 || code >= 300) {
494 Serial.printf("postLastScan failed: %d\n", code);
495 http.end();
496 return false;
497 }
498 String payload = http.getString();
499 Serial.printf("[HTTP] /api/last_scan payload: %s\n", payload.c_str());
500 http.end();
501 // Parse into caller-provided document (prefer StaticJsonDocument in caller)
502 DeserializationError err = deserializeJson(out, payload);
503 if (err) {
504 Serial.printf("postLastScan: JSON parse error: %s\n", err.c_str());
505 out.clear();
506 return false;
507 }
508 return true;
509}
510
511
513{
514 // Skip poll if offline or no server configured. Keeps display consistent
515 // and avoids pointless HTTP requests when not provisioned.
516 if (WiFi.status() != WL_CONNECTED || SERVER_BASE.length() == 0) {
517 enrollMode = "none";
518 serverReachable = false;
519 return;
520 }
521 // Simple synchronous status poll (called from loop() on a millis timer)
522 HTTPClient http;
523 http.setTimeout(1500);
524 String url = SERVER_BASE + "/api/status";
525 http.begin(url);
526 int code = http.GET();
527 if (code > 0 && code < 400)
528 serverReachable = true;
529 String payload = http.getString();
530 JsonDocument doc;
531 DeserializationError err = deserializeJson(doc, payload);
532 if (!err) {
533 const char *m = doc["enroll_mode"] | nullptr;
534
535 if (m && strlen(m) > 0) {
536 enrollMode = m;
537 } else {
538 enrollMode = "none";
539 }
540 } else {
541 serverReachable = false;
542 enrollMode = "none";
543 }
544 http.end();
545}
546
547// Timer callback for server reachability check
548void serverCheckTimerCallback(TimerHandle_t xTimer)
549{
550 bool nowReachable = false;
551 if (WiFiClass::status() == WL_CONNECTED && SERVER_BASE.length() > 0) {
552 HTTPClient http;
553 http.setTimeout(1500);
554 http.begin(SERVER_BASE + "/api/status");
555 int code = http.GET();
556 http.end();
557 nowReachable = (code == 200);
558 }
559 if (nowReachable != serverReachable) {
560 serverReachable = nowReachable;
561 Serial.printf("[DB] Reachable=%d\n", serverReachable);
562 // Keep AuthSync's cached probe state in sync with the central timer so
563 // both modules make decisions from the same reachability information.
564 if (authSync) {
565 authSync->setServerProbeResult(nowReachable, millis());
566 }
567 }
568}
569
570// Non-blocking timer callback for triggering AuthSync work.
571
572void authSyncTimerCallback(TimerHandle_t xTimer)
573{
574 authSyncRequested = true;
575}
576
577// ----------- Network Task (core 0) ------------
578void NetworkTask(void *pv)
579{
580 Serial.printf("[Tasks] NetworkTask running on core %d\n", xPortGetCoreID());
581
582
583 // Create and start the timer (5000ms period, auto-reload)
585 Serial.println("[Tasks] Failed to create/start server check timer");
586 } else {
587 Serial.println("[Tasks] Server check timer started");
588 }
589
590 // Create and start the auth sync timer (non-blocking callback)
591 if (!createAuthSyncTimer(authSyncTimerCallback, pdMS_TO_TICKS(5000))) {
592 Serial.println("[Tasks] Failed to create/start auth sync timer");
593 } else {
594 Serial.println("[Tasks] AuthSync timer started");
595 }
596
597 for (;;) {
598 // AuthSync periodic sync — triggered by timer flag (non-blocking timer
599 // callback)
601 authSyncRequested = false; // clear flag before doing work
603 Serial.println("[Tasks] Auth sync requested");
604 }
605
606 // Drain scan queue: post last_scan events (limit per cycle)
607 if (serverReachable && scanQueue) {
608 for (int i = 0; i < 3;
609 ++i) {
610 // process up to 3 per loop to avoid starving
611 ScanItem item;
612 if (xQueueReceive(scanQueue, &item, 0) == pdPASS) {
613 // Post scan to server and handle enrollment side-effects returned by
614 // the server.
615 Serial.printf("[Queue] Posting UID=%s\n", item.uid);
616 JsonDocument resp;
617
618 if (postLastScan(String(item.uid), resp)) {
619 Serial.printf("[Queue] postLastScan returned size=%u\n", static_cast<unsigned>(resp.size()));
620 // If server acknowledged enrollment, clear enroll mode and redraw
621 // indicator immediately
622 if (resp.size() > 0) {
623 bool enrolled = false;
624 if (resp["enrolled"].is<bool>()) {
625 enrolled = resp["enrolled"].as<bool>();
626 }
627 if (enrolled) {
628 enrollMode = "none";
629 // Request main loop to redraw the enroll indicator (display
630 // operations must run from loop context to be thread-safe).
632 Serial.println("[Queue] Enrollment cleared (requested display update)");
633 }
634 }
635 } else {
636 Serial.println("[Queue] postLastScan: no valid response");
637 }
638 } else {
639 break;
640 }
641 }
642 } else if (!serverReachable && scanQueue) {
643 // When offline, keep queued scans for later (do not drop them).
644 // Optionally we could limit queue size elsewhere, but avoid clearing here.
645 }
646
647 vTaskDelay(pdMS_TO_TICKS(50));
648 }
649}
bool createServerCheckTimer(TimerCallbackFunction_t callback, TickType_t periodTicks)
bool createDisplayTimer(TimerCallbackFunction_t callback, TickType_t periodTicks)
bool createAuthSyncTimer(TimerCallbackFunction_t callback, TickType_t periodTicks)
bool update()
Definition AuthSync.cpp:151
void setServerProbeResult(bool ok, unsigned long probeMillis)
Definition AuthSync.cpp:669
bool begin()
Definition AuthSync.cpp:120
bool preloadOffline()
Definition AuthSync.cpp:136
void dumpMemoryStats() const
Definition AuthSync.cpp:649
bool isAuthorized(const String &uid)
Definition AuthSync.cpp:158
AuthSync(const String &serverBase)
Definition AuthSync.cpp:49
static bool loadConfig(String &ssid, String &pass, String &serverBase)
bool displayedAuth
Definition main.cpp:101
AuthSync * authSync
Definition main.cpp:84
void NetworkTask(void *pv)
Definition main.cpp:578
static volatile bool authSyncRequested
Definition main.cpp:123
bool displayedEnrollBlink
Definition main.cpp:104
bool postLastScan(const String &uid, JsonDocument &out)
Definition main.cpp:470
String PASS
Definition main.cpp:79
String displayedEnrollMode
Definition main.cpp:103
String SERVER_BASE
Definition main.cpp:80
unsigned long lastDisplayUpdate
Definition main.cpp:92
void setup()
Definition main.cpp:129
uint64_t lastHash
Definition main.cpp:90
U8X8_SSD1315_128X64_NONAME_SW_I2C u8x8(22, 21, U8X8_PIN_NONE)
bool enrollBlinkState
Definition main.cpp:94
String SSID
Definition main.cpp:78
String lastUID
Definition main.cpp:87
String enrollMode
Definition main.cpp:88
constexpr uint8_t RST_PIN
Definition main.cpp:59
MFRC522 rfid(SS_PIN, RST_PIN)
bool lastAuthorized
Definition main.cpp:89
static volatile bool displayUpdateRequested
Definition main.cpp:125
void drawEnrollIndicator(bool on)
Definition main.cpp:450
bool displayedServerReachable
Definition main.cpp:105
static constexpr unsigned long ENROLL_POLL_INTERVAL_MS
Definition main.cpp:61
void updateEnrollStatus()
Definition main.cpp:512
void authSyncTimerCallback(TimerHandle_t xTimer)
Definition main.cpp:572
static void displayTimerCallback(TimerHandle_t xTimer)
Definition main.cpp:126
void serverCheckTimerCallback(TimerHandle_t xTimer)
Definition main.cpp:548
bool serverReachable
Definition main.cpp:91
String getUidString()
Definition main.cpp:380
static unsigned long lastEnrollPoll
Definition main.cpp:97
void updateDisplay()
Definition main.cpp:402
void drawHeader()
Definition main.cpp:392
constexpr uint8_t SS_PIN
Definition main.cpp:60
unsigned long enrollBlinkMillis
Definition main.cpp:93
static QueueHandle_t scanQueue
Definition main.cpp:119
uint64_t displayedHash
Definition main.cpp:102
String displayedUID
Definition main.cpp:100
void loop()
Definition main.cpp:305
uint64_t hashUid(const String &s)
Definition HashUtils.cpp:18
char uid[21]
Definition main.cpp:117