ESP32-WROOM-32E · PlatformIO · Arduino + FreeRTOS · WiFiClientSecure MQTT
Production offline-first payment terminal for campus transport. Built from System Architecture Document (SAD) Rev 1.0.
| Component | GPIO / Interface |
|---|---|
| ESP32-WROOM-32E 38-pin | — |
| RC522 RFID Kit | VSPI: CS=5, RST=27, SCK=18, MISO=19, MOSI=23 |
| LCD 1602 + PCF8574 I2C backpack | SDA=21, SCL=22 (addr 0x27) |
| 4×4 Hard Matrix Keypad | Rows: 13,14,26,25 · Cols: 32,33,15,12 |
| 5mm Green LED + 220 Ω | GPIO 2 |
| 5mm Red LED + 220 Ω | GPIO 4 |
| TMB12A05 Active Buzzer | GPIO 0 |
| 18650 3200 mAh + Charge Controller | → Rocker Switch → ESP32 Vin |
⚠️ SD card modules are NOT used. Storage is LittleFS on internal flash.⚠️ SIM800L is replaced by Wi-Fi in this build. Power the ESP32 from 5 V.
ctransit-firmware/
├── platformio.ini # Build config — platform pinned to espressif32 @ 6.5.0
├── partitions.csv # App0 1.5 MB · App1 1.5 MB · LittleFS 960 KB
├── include/
│ └── config.h # Every pin, constant, and tunable in one place
├── src/
│ └── main.cpp # Boot, task creation, state dispatch loop
├── lib/
│ ├── logger/ # Tagged serial macros [RFID] [GSM] [AUTH] …
│ ├── power/ # WDT arm + brownout detection
│ ├── storage/ # All LittleFS I/O, free-space guard, atomic swap
│ ├── display/ # LCD 16×2 via I2C PCF8574
│ ├── ui/ # Green/red LEDs + active buzzer
│ ├── rfid/ # MFRC522 VSPI driver + 8-second debounce
│ ├── keypad/ # 4×4 matrix scanner, blocking PIN collector
│ ├── auth/ # 5-step offline validation tree + staff 2FA
│ ├── transaction/ # RTC from millis offset + tx.log write
│ ├── sync/ # WiFiClientSecure MQTT, QoS1, LWT, diff sync
│ └── statemachine/ # 11-state FSM, session persistence
└── data/ # Files uploaded to LittleFS via --target uploadfs
├── drv.dat # DEADBEEF,1234 ← replace before deployment
├── adm.dat # CAFEBABE,9999 ← replace before deployment
├── wl.dat # Whitelisted UIDs (one per line)
├── bl.dat # Blacklisted UIDs (empty at first boot)
├── tx.log # Transaction queue (empty at first boot)
├── sess.dat # Session state seed: 0,NONE
└── sync.dat # Sync timestamp seed: 0
This project enforces the following rules in every file that touches the FS:
- LittleFS only — SPIFFS is deprecated for frequent read/write on ESP32.
partitions.csvsubtype column =littlefs(notspiffs).- Mount call:
LittleFS.begin(true, "/data", 10, "littlefs")— the 4th argument must match the Name column inpartitions.csv. platformio.ini:board_build.filesystem = littlefs— do not add-D ARDUINO_ESP32_LITTLEFS; that flag is for the oldlorol/LittleFS_esp32library and conflicts with the built-in.- Transaction writes always use append mode
"a". Files are deleted only after a confirmed MQTT PUBACK. - Free space is checked before every write (
FS_HEADROOM_BYTES = 8 KB).
If LittleFS.begin() returns false, check all three match:
| Where | Must be |
|---|---|
partitions.csv subtype |
littlefs |
LittleFS.begin() 4th arg |
"littlefs" |
platformio.ini |
board_build.filesystem = littlefs |
A mismatch on any one causes a silent mount failure.
BOOT ──► [sess.dat active?] ──► READY (power-loss recovery)
──► [wl.dat empty?] ──► COLD_SYNC
──► [default] ──► OFFLINE_LOCKED
OFFLINE_LOCKED ──[driver tap]──► DRIVER_LOGIN ──[PIN ok]──► READY
OFFLINE_LOCKED ──[admin tap] ──► DRIVER_LOGIN ──[PIN ok]──► REGISTER_MODE
READY ──[student tap]──► PROCESSING ──[APPROVED]──► APPROVED ──► READY
──[DENIED] ──► DENIED ──► READY
READY ──[3-hr timeout]──► HARD_LOCKDOWN ──[sync done]──► READY
READY ──[tx.log full] ──► HARD_LOCKDOWN
REGISTER_MODE ──[0 or timeout]──► OFFLINE_LOCKED
COLD_SYNC ──[SYNC_COMPLETE]──► OFFLINE_LOCKED
| Topic | Direction | Purpose |
|---|---|---|
ctransit/TERM_01/tx |
Publish | Transaction payload flush |
ctransit/TERM_01/rx |
Subscribe | Differential whitelist / blacklist updates |
ctransit/TERM_01/status |
LWT | ONLINE / OFFLINE presence |
Uplink payload format (pipe-delimited, no JSON):
TERM_01:A1B2C3D4,-200,1708000500,DEADBEEF|E5F6G7H8,-200,1708000545,DEADBEEF
Downlink differential format:
ADD:BL,E5F6G7H8|REM:WL,A1B2C3D4
Cold-sync chunks:
SYS:WL,A1B2C3D4|E5F6G7H8|J9K0L1M2
SYS:SYNC_COMPLETE
The UI task runs at 500 ms. It writes two sync indicators into row 0:
| Column | Symbol | Meaning |
|---|---|---|
| 14 | ^ |
Upload active (MQTT publish fired within last 400 ms) |
| 15 | v |
Download active (MQTT message received within last 400 ms) |
Driven by extern volatile uint32_t g_last_upload_ms and
g_last_download_ms defined in sync.cpp, read in main.cpp.
pip install platformio # or install PlatformIO IDE in VS CodeEdit platformio.ini:
-D WIFI_SSID=\"YourNetworkName\"
-D WIFI_PASS=\"YourPassword\"Edit data/drv.dat with real driver UIDs and PINs:
AABBCCDD,5678 # UID (8 hex chars), comma, 4-digit PIN
pio run --target uploadfsMust be done before uploading firmware on a blank device. Also run this any time you edit files in the
data/folder.
pio run --target uploadpio device monitor --baud 115200pio run --target erase
pio run --target uploadfs
pio run --target upload| Tag | Module |
|---|---|
[MAIN] |
Boot, task creation, state dispatch |
[RFID] |
Card detection, debounce |
[AUTH] |
Validation tree, staff login |
[TX] |
Transaction recording, RTC |
[STORAGE] |
LittleFS reads/writes, free-space checks |
[SYNC] |
WiFi, MQTT publish/subscribe |
[DISPLAY] |
LCD writes |
[UI] |
LED + buzzer events |
[POWER] |
WDT, brownout, reset reason |
[SM] |
State machine transitions |
[KEYPAD] |
Key presses, PIN entry |
Disable all output for a silent production build:
// include/config.h
#define DEBUG_MODE 0| Symptom | Cause | Fix |
|---|---|---|
LittleFS.begin() failed |
Partition subtype / label mismatch | Check all three: partitions.csv, LittleFS.begin() 4th arg, platformio.ini filesystem |
| LCD blank after power-on | Contrast pot not adjusted | Turn PCF8574 blue pot until characters appear |
| LCD address not found | I2C addr is 0x3F, not 0x27 | Run I2C scanner, update LCD_I2C_ADDR in config.h |
| RFID never reads | Wrong SPI pins or bus shared | Verify VSPI: CS=5, RST=27, SCK=18, MISO=19, MOSI=23 |
| WiFi connect timeout | Wrong SSID/password | Update WIFI_SSID / WIFI_PASS in platformio.ini build_flags |
| MQTT connect fails | Wrong broker/port or TLS issue | Verify MQTT_HOST, MQTT_PORT=8883; setInsecure() bypasses cert for dev |
| Taps rejected after 3 h | 3-hour kill switch (sync.dat) | Confirm WiFi + MQTT syncing and PUBACK writing sync.dat |
| WDT reset on boot | Hardware init freeze | Read serial backtrace — which module is hung? |
| Driver PIN never accepted | drv.dat not on device | Run pio run --target uploadfs then reflash |