A small RFID access control project built around an ESP32 with a SSD1306 display and a lightweight Python server + dashboard.
- When connected it syncs with the server using ETag/If-None-Match semantics for minimum filesystem and network strain.
- Stores a compact authorization bitset on the device.
- Capable of authorization during network interruption
- Seamless reconnection.
-
ESP32 firmware (PlatformIO) which reads RFID scans, enforces authorization, and syncs a compact bitset representation of allowed card IDs.
-
A tiny Flask-based server and dashboard (
lib/server.py,lib/dashboard.html) to manage cards and provide the sync API. -
Persistence:
- Bitset snapshot: LittleFS file
/bits.bin - Allow/deny hash lists: LittleFS file
/allow_deny.bin - Small metadata (ETag, max_id): NVS (Preferences)
- Bitset snapshot: LittleFS file
- RFC552
- SSD1306/SSD1315
- Nodemcu-32s / esp-wroom
- Compact on-device authorization bitset (per-card_id bits) for fast local checks.
- Offline allow/deny caches (64-bit FNV-1a hashes) persisted to LittleFS.
- Server-first lookups when online, with fallback to offline caches. Quite easily reversed to be the other way around.
- Efficient sync: server provides
ETagfor the bitset and/api/sync/metafor cheap polling. - Simple web UI to list/add/remove/toggle cards and to show last scanned UID.
- Enrollment mode from dashboard: -Revoke on scan -Grant on scan
src/— ESP32 firmware (PlatformIO project)lib/— Python server and dashboardlib/server.py— Flask applib/dashboard.html— web UI
data/— runtime data and example configcards.db - local— SQLite DB used by the server autogenerated on first runconfig.json - local- server config (rename example and upload to littleFS) / uncomment one time write in main as fallback
.pio_venv/ - local— local Python venv created by PlatformIO (not checked in)platformio.ini— PlatformIO build configuration
GET /api/cards— list cardsPOST /api/cards— add/update a card (body{ "uid": "04A1...", "authorized": true })GET /api/cards/<uid>— get a cardDELETE /api/cards/<uid>— soft-delete a cardPATCH /api/cards/<uid>— updateauthorizedPOST /api/last_scan— device posts scanned UID (body{ "uid": "..." })POST /api/enroll— set enrollment mode{ "mode": "grant" | "revoke" | null }GET /api/status— returns{ last_scanned, enroll_mode }(dashboard polls this)GET /api/sync— full bitset payload{ max_id, bits }(bits as hex); returnsETagheader and supportsIf-None-MatchGET /api/sync/meta— lightweight{ max_id, etag, bits_len }for cheap polling
Configurable autoremoval after periodSleep mode and power-profileOngoing transition to ESP-IDF functions where performance gain outweighs loss of simplicity
Prerequisites
- PlatformIO (VSCode extension or CLI) for building/flashing the ESP32 firmware.
- Python 3.8+ to run the Flask server locally (prefer using the project's
.pio_venv).
Build & flash ESP32 firmware (PowerShell)
# Build (adjust environment name if needed)
-pio run
Run the server locally
```powershell
# Install server deps into the project's venv (if not present)
& .\.pio_venv\Scripts\pip.exe install Flask flask-cors
# Run the server using the venv Python
& .\.pio_venv\Scripts\python.exe -u lib\server.pyThe server listens on port 5000 by default. Open http://<server-ip>:5000/ to view the dashboard.
Conditional GET example (PowerShell + curl)
# Get meta first (recommended)
$response = Invoke-RestMethod -Uri "http://127.0.0.1:5000/api/sync/meta" -Method GET
$etag = $response.etag
# Request full sync only if changed
$code = & curl.exe -s -o sync.json -w "%{http_code}" -H "If-None-Match: $etag" "http://127.0.0.1:5000/api/sync"
if ($code -eq '304') { Write-Host 'No change' } else { Write-Host 'Updated, saved sync.json' }- Bitset snapshot: written to LittleFS
/bits.binandmax_idstored in NVS so the device can reconstruct exact size on boot. - Allow/deny: a compact binary file
/allow_deny.binis stored on LittleFS with counts and rawuint64_thashes. - ETag: SHA1 hex of the bitset returned by the server; stored in NVS under key
bitset_etagand used inIf-None-Matchheader.
-
ModuleNotFoundError: No module named 'flask'— ensure you run the server with the project's venv Python or install Flask in the interpreter you use. -
Dashboard not showing changes: hard reload the browser (Ctrl+F5) or open an incognito tab. The server serves the dashboard with
Cache-Control: no-storebut some caches can still be sticky. -
LittleFS wear: the firmware currently writes allow/deny and bitset files on changes; if scans are extremely frequent you may want to debounce writes.
- Server log/debug prints have been added to surface
last_scannedupdates and status polls — check the terminal runninglib/server.pyto confirm receipt of scan POSTs. - If you want the server to return compact hash metadata or change the sync payload format, update
lib/server.pyand coordinate with the device code insrc/AuthSync.cpp. - (Ensure Python and C/C++ hashing implementations are the same if you change hash format.)
VID_20251129_174131.5.mp4
VID_20251129_174131.6.mp4
Need to fix updating entrollment status symbol (GR) if DB lost during grant enrollment