Skip to content

Serial Protocol Specification

Hyacinthe-primus edited this page Jul 17, 2026 · 1 revision

Serial Protocol Specification

Newline-delimited JSON, one object per line, UTF-8. Baud rate: 2000000.

Python to ESP32

type Fields Description
add uid, name, registered, valid_days Register a new user. Omit fields for admin badge.
batch_add users (array of {uid, name, registered*, valid_days*}) Add multiple users in one round-trip (import mode only).
import_bin bytes (int) Announce incoming raw-binary import transfer of N bytes (import mode only).
remove uid Delete a user
clear_all - Delete ALL users
remove_all_except uids (array) Delete every user NOT in uids
rename uid, name Rename an existing user
find uid Single-UID O(log n) binary search
find_name query Case-insensitive substring match on name (device-side, only matches cross Serial)
list - Request the full user list
export_bin - Export DB as raw binary stream (followed by N raw bytes)
sync_begin - Stateless query: device's db_crc32 + count, first step of sync
sync_manifest - Request the (uid, per-record CRC32) manifest (followed by N raw entries)
sync_apply remove, add, replace (counts) Announce a merge-diff, then stream raw remove-list + add-records + replace-records in that order
enter_scan_mode - Next card read reported, not checked
status - DB path + LittleFS usage + db_crc32
net_status - Wi-Fi connection state
get_time - Device's current local time
ntp_sync - Force NTP resync
import_begin - Enter batch-import mode
import_end - Finalize import: persist once
enter_renewal_mode valid_days Enter tag renewal mode
exit_renewal_mode - Exit renewal mode, return to idle
configure_wifi ssid, password Store Wi-Fi credentials and connect
configure_timezone gmt_offset_sec, daylight_offset_sec* Set + persist timezone, resync NTP immediately. *daylight_offset_sec optional, defaults to 0.

*registered and valid_days are optional in add. When either is missing the firmware treats the badge as admin.

ESP32 to Python

{"status":"ok"}
{"status":"error","message":"Duplicate UID"}
{"status":"ok","users":[...],"scan_us":12345}
{"status":"ok","type":"uid_detected","uid":"04AABBCCDD"}
{"status":"ok","type":"remove_all_except","removed_count":3}
{"status":"ok","type":"wifi_status","connected":true,"message":"..."}
{"status":"ok","type":"timezone","applied":true,"gmt_offset_sec":3600,"daylight_offset_sec":0,"message":"..."}
{"status":"ok","type":"net_status","connected":true,"ssid":"...","ip":"...","rssi":-58,"time_synced":true}
{"status":"ok","type":"time","epoch":1783107800,"formatted":"2026-07-03 19:43:20"}
{"status":"ok","type":"ntp_sync","synced":true,"message":"2026-07-03 19:45:00"}
{"status":"ok","type":"import_result","added":1500,"errors":0,"json_parse_ms":120,"batch_loop_ms":340,"ack_serialize_ms":85,"save_ms":2100,"save_encode_ms":800,"save_write_ms":1100,"save_finalize_ms":200,"batches":15,"users_profiled":1500,"transport_wait_ms":50,"transport_wait_count":15}
{"status":"ok","type":"batch_add_result","added":100,"errors":0,"failed":[]}
{"status":"ok","type":"batch_add_result","added":98,"errors":2,"failed":[{"uid":"...","message":"Duplicate UID"},...]}
{"status":"ok","type":"find_result","uid":"...","name":"...","registered":"...","valid_days":30,"search_us":42}
{"status":"error","type":"find_result","message":"UID not found","search_us":38}
{"status":"ok","type":"renewal_result","uid":"...","name":"...","registered":"...","valid_days":30}
{"status":"ok","type":"export_bin","bytes":335000,"count":5000}
{"status":"ok","type":"import_bin_result","added":5000,"errors":0}
{"status":"ok","type":"sync_begin","db_crc32":3234567890,"count":5000}
{"status":"ok","type":"sync_manifest","bytes":75000,"count":5000}
{"status":"ok","type":"sync_result","removed":2,"added":3,"replaced":1,"errors":0,"db_crc32":1234567890}
{"status":"error","type":"sync_result","message":"Failed to persist database","removed":2,"added":3,"replaced":1,"errors":0,"db_crc32":3234567890}

sync: raw wire formats

sync reuses the existing 74-byte record format (see import_bin/export_bin) for ADD and REPLACE entries. Two smaller formats are specific to sync:

  • Manifest entry (sync_manifest's raw payload, one per user): uidLen(1) + uidBytes(10, zero-padded) + recordCrc32(4) = 15 bytes. recordCrc32 is the same trailing per-record CRC32 already embedded in the 74-byte record format -- not a separately computed value.
  • Remove entry (sync_apply's raw "remove" list, one per uid): uidLen(1) + uidBytes(10, zero-padded) = 11 bytes.

sync_apply's raw payload is remove_count remove-entries, followed by add_count 74-byte records, followed by replace_count 74-byte records, concatenated with no framing between phases -- the device already knows each phase's byte length from the remove/add/replace counts announced in the preceding sync_apply message.

RFID Access Control ESP32-S3 · PN532 · Python CLI


🚀 Getting Started

🐍 Using the CLI

🧭 Reference


📦 README 🐛 Issues

Clone this wiki locally