-
-
Notifications
You must be signed in to change notification settings - Fork 1
CHANGELOG
-
save()had a no-file crash window: the bulk rewrite path didLittleFS.remove(USERS_DB_PATH)thenLittleFS.rename(tmp, USERS_DB_PATH)as two separate calls. A power loss between them left nousers.binat all, andbegin()would silently recreate an empty database on the next boot. Fixed: the tmp file is now verified by re-reading it and comparing its CRC32 against the in-memory database before touching the canonical file, and finalization prefers a directLittleFS.rename()over the existing destination (a single atomic commit) rather than remove-then- rename, falling back to the old two-step sequence only if that direct rename is ever rejected outright. -
save() stack overflow: the fixed-width binary database format (below) was first implemented with its ~8KB record-flush buffer as a stack array. The default Arduino loop task stack is 8192 bytes total, so that one buffer alone consumed nearly all of it, leaving nothing for the rest of
save()'s call chain -- crashed with a Guru Meditation Error on the firstsave(). Moved to a PSRAM heap allocation (same allocator already used forusers_/uidIndex_), freed via an RAII guard so every early return still cleans it up. -
TWDT reboot every ~10.25s:
RFID_Access_Control.inosubscribesloopTaskto the Task Watchdog Timer insetup()but nothing in the normalloop()path ever calledesp_task_wdt_reset(), so the TWDT panicked ~10.2s after boot regardless of load. Fixed by addingesp_task_wdt_reset()at the top ofloop(). -
listreboots with >~15000 users:sendUserList()streams the entire user list inside a singleloop()iteration -- at USB-CDC speeds, tens of thousands of users take longer than the TWDT timeout. Fixed by callingesp_task_wdt_reset()every 256 users inside the streaming loop. -
removeUser()afterfs::File::truncate()build error: the arduino- esp32 core'sfs::Filehas notruncate()member (compile error, not runtime).saveSuffixFrom_()now falls back to a fullsave()when the array shrinks (removeUser), since there is no way to shrink a file in place. rename/renew (no size change) still get true single-record seek+write. -
importdidn't accept.binfiles: any file not ending in.jsonfell into the CSV parser, which choked on binary bytes. Fixed:importnow detects.binby extension, validates the header, and sends the bytes straight through toimport_bin. -
export ... .binproduced a headerless file:cmd_export()saved the raw bytes from theexport_binwire transfer as-is, which omits the 7-byte file header (magic"RUD1"+ version + record size) since that data travels in the JSON control line, not the raw stream. The resulting.binfile would fail_parse_import_bin()on re-import ("missing/bad header"). Fixed:cmd_export()now prependsconvert.header_bytes()to the output. Verified end-to-end: a round-trip export->import now produces 0 skips, 5/5 matching entries.
-
synccommand: makes the device database exactly match a local JSON/CSV/.binfile via a merge-diff (remove/add/replace) against a compact (uid, per-record CRC32) manifest, instead ofimport's additive-or-fully-destructive model. One raw binary transfer for the whole diff, one flash write, and a finaldb_crc32round-trip so the host can confirm the two databases actually match rather than just trusting an "ok" status. Seesync_begin/sync_manifest/sync_applyin the Serial Protocol Specification. -
Binary user database:
users.json(NDJSON) replaced with a fixed-width binary format (users.bin) -- seeDatabaseManager.hfor the full record layout. Existingusers.jsondatabases are migrated automatically on first boot with this firmware and kept asusers.json.bak. Per-record CRC32 preserves the old format's graceful-degradation property (a corrupted record is skipped, not the whole database). -
Sorted array + targeted save strategies:
users_is now kept physically sorted by UID at all times. Lookups use binary search (O(log n)) directly over the sorted array -- the olduidIndex_std::mapwas removed as redundant. Save strategies depend on the operation:renameUser()/renewUser()use a single-record seek+write (saveSingleRecord_);addUser()rewrites only from the insertion point onward (saveSuffixFrom_);removeUser()and bulk operations still do a full rewrite. -
Raw binary import/export sub-protocol: new
import_binandexport_binserial commands transfer the database as raw fixed-width records with no JSON framing or ArduinoJson parsing on the device. The CLI defaults to this path for bothimportandexport, with--json-transportfalling back to the older JSON-based pipeline..binfiles are sent/received byte-for-byte with no re-encoding. -
CRC32 fingerprint skip:
statusnow includesdb_crc32(a CRC32 over every encoded record in sorted order). The CLI computes the same CRC locally and skips the entire transfer + save if it matches -- common when re-importing the same export. -
findby UID (O(log n)):find --uiddoes a device-side binary search over the sorted array. Response includessearch_us(micros spent in the search only, excluding serial I/O). -
find_name(device-side):find --namenow filters on-device (case-insensitive substring match on name) instead of transferring the full database to the CLI for client-side filtering. Only matching records cross Serial. Response includesscan_us. -
Backup format prompt:
remove --forceandimport --clearnow prompt for backup format (JSON or.bin) before wiping.--no-backupskips the backup entirely. -
Import accepts
.binfiles:import users.binreads the on-disk binary format directly and sends it byte-for-byte toimport_bin(no decode-then-re-encode round trip). -
Export to
.bin:export users.binwrites the raw on-disk format (header + records) directly from theexport_binwire transfer. Any other extension (.json,.txt, etc.) always produces JSON -- never raw binary under a misleading filename. -
File encoding fallback: import now tries utf-8-sig, then cp1252, then latin-1 for JSON/CSV files, with a warning when a non-UTF-8 encoding is used. Fixes "can't decode byte 0xe9" failures from Excel-exported CSVs on Windows/French locales.
-
import_bin/export_binraw binary transfer: the device reads/writes fixed-width records directly off Serial with no JSON framing. A stalled transfer is detected (short read) and reported as an error requiring reconnect -- there is no mid-stream resync for raw binary on a shared serial line. -
ImportProfiler instrumentation: device-side timing of every phase during an import (JSON parse, batch loop, ACK serialize, save breakdown, transport wait). Reported in the
import_resultresponse. Not part of the stable protocol contract.
-
MAX_USERS: 10000 -> 70000 (
Config.h). -
Serial baud rate: 921600 -> 2000000 (
SERIAL_BAUDinConfig.h). The Python CLI auto-matches this rate; a v1.0.0 CLI talking to v1.0.1 firmware (or vice versa) will see garbled output until both sides are updated. -
Line buffer:
kLineBufCapacityraised from 4096 to 16384 to support ~100 users perbatch_addline. Serial RX ring buffer is now sized from this constant so the two can't diverge. -
Batch import size:
IMPORT_BATCH_SIZEraised from 15 to 100 (matches the new 16KB line buffer with >2.5x headroom). -
Boot wait: CLI
_wait_for_ready()timeout raised from 15s to 45s to cover the first boot after flash whereLittleFS.format()on the 12MB partition can take 10-30s. Post-boot backlog drain added to prevent stalestatusreplies from contaminating the next real command. -
partitions.csv: coredump partition added (64KB at 0xF10000).
-
JSON escaping: a user name containing
"or\corruptedusers.jsonon the next save, which then failed to parse on the next boot and silently wiped the entire database. Names are now properly JSON-escaped everywhere they're written (DatabaseManager::save(),SerialProtocol::sendUserList()). Control characters (newline/tab/etc.) are now rejected at validation time since they'd still garble the LCD even when escaped correctly. -
O(log n) lookups: every badge scan,
remove,rename, andtag-renewdid a linear O(n) scan over the user list despite an index existing --uidIndex_is now a real uid-to-position map, so those operations are O(log n). -
Idempotency-aware retries: the Python CLI could report a false failure ("Duplicate UID" / "UID not found") for an
add/remove/renamethat actually succeeded, if the device's reply arrived after the 2s timeout and got retried. Retries are now aware these commands aren't idempotent.
-
Automatic backup before wipe:
remove --forceandimport --clearnow save an automatic timestamped backup of the current device database topython_cli/backups/before wiping anything (best-effort -- a backup failure is logged but does not block the operation you asked for). -
Anti-brute-force lockout: after
MAX_CONSECUTIVE_DENIALS(default 5) consecutive denied badges, the reader stops accepting cards forLOCKOUT_DURATION_MS(default 30s). Both tunable inConfig.h. The serial/CLI link stays fully usable during a lockout. -
Runtime timezone: timezone is now configurable at runtime via
python cli.py timezone --offset SECONDS [--dst SECONDS]-- persisted on the device (NVS), no reflash required.Config.h'sNTP_GMT_OFFSET_SEC/NTP_DAYLIGHT_OFFSET_SECare now only the first-boot default.
RFID Access Control · v1.0.1 · ESP32-S3 + PN532
README · Changelog · Issues · MIT License