- C++ 72.4%
- C 27.6%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .cache/clangd/index | ||
| .vscode | ||
| docs | ||
| splash_art | ||
| src | ||
| .DS_Store | ||
| .gitignore | ||
| compile_commands.json | ||
| platformio.ini | ||
ChipScanner
Firmware for a handheld 134.2 kHz FDX-B microchip scanner, codename Tranymon built on an ESP32 basis. It reads ISO 11784/11785 transponders over a RFID module, shows results on a 128×64 OLED screen, logs readings to a microSD card with an RTC timestamp, and can look up IDs against a backend APIover WiFi. Navigation is done via four buttons; an inactivity timeout enters a power saving mode.
Disclaimer: The majority of this code was build using Claude Opus 4.8. The underlying electrical logic, prototype and PCB were entirely designed by humans.
- MCU board: ESP32-WROOM-32 (DevKit) or Wemos/LOLIN D1 Mini ESP32
- Framework: Arduino-ESP32, built with PlatformIO
- Version: 0.9.1
Features
- Boot and sleep splash screens
- Button-navigable menu UI
- One-line status bar: clock · WiFi signal bars · battery gauge · SD card
- Scans FDX-B chips and show the conventional
country + nationalID (wip: temp and animal bit) - Save readings to
microSDas CSV, browse them later, view details, delete recordings - Backend lookup of a scanned ID via a database (wip)
- Persistent settings (NVS): WiFi on/off, brightness, sleep timeout, 24 h clock, plus stored WiFi/API credentials
- WiFi info screen (status / SSID / IP / RSSI)
- Battery gauge
- Deep sleep on inactivity, wake on a button press
- OTA firmware updates
- Network time sync
- Sound on scan
Hardware
| Part | Role | Bus |
|---|---|---|
| ESP32-WROOM-32 / D1 Mini ESP32 | MCU | — |
| SSD1309 128×64 OLED | display | I²C 0x3C |
| DS3231 (HW-084 module) | real-time clock | I²C 0x68 (board also has AT24C32 0x57, unused) |
| HW-125 microSD adapter | data storage | SPI |
| RFID sub module + LM358 | RFID tag reader | LEDC carrier + GPIO edge ISR |
| 4 × buttons | UP / DOWN / OK / BACK | GPIO |
| SM5308 power module | power; cell voltage read via ADC | ADC1 (BAT) |
| Buzzer | Sound output | LEDC PWM (GPIO) |
Pin map (config.h)
| Function | GPIO |
|---|---|
| I²C SDA / SCL | 21 / 22 |
| SD SCK / MISO / MOSI / CS | 18 / 19 / 23 / 5 |
| Buttons UP / DOWN / OK / BACK | 32 / 33 / 25 / 26 |
| RFID carrier out (→ carrier amplifier) | 4 |
| RFID demod data in (← LM358 receive) | 2 |
| RFID front-end enable (active-high) | 14 |
| Buzzer | 16 |
| Battery ADC (SM5308 BAT node via 2:1 divider) | 34 |
| Peripheral power gate | 27 |
| SM5308 KEY (hardware power-off) | 17 |
RFID front-end
The reader is decoded on-chip: the ESP32 generates the 134.2 kHz carrier on the
carrier pin (LEDC, channel 0), which drives the antenna amplifier / LC tank,
and time-stamps the demodulated edges arriving on the data pin (a CHANGE
interrupt) to recover the FDX-B telegram. The enable pin gates the analog
front-end so it only draws current while the Scan screen is active. Note the
carrier (4) and data (2) pins are ESP32 boot-strapping pins, so the board
must not hold them at a boot-blocking level; verify the enable pin/polarity
against your board (RFID_EN_PIN / RFID_EN_ON in config.h).
SM5308 BAT node via 2:1 divider
Taps BAT pin (6). Per the data sheet, BAT is the boost input pin, connected directly to the Li-ion positive terminal, with a working range of 3.0–4.4 V.
BAT (B+) --[R1 100k]--+--> GPIO34
|
[R2 100k] 4.2 V -> 2.1 V at the pin
|
GND
Buttons
Buttons are wired between their GPIO and GND (internal pull-ups, active-low).
OK doubles as the deep-sleep wake source (ext0).
Display panel/ UI
The UI is optimized for a 128x64 pixel panel: a 12 px status bar (clock · WiFi · battery · SD Card), a four-row menu, the full chip ID on the result screen, a thermometer glyph for temperature. If the module stays blank, switch the constructor to the NONAME2 variant in main.cpp, or set OLED_RESET_PIN if your module has a RES line.
Power model
The "Sleep" menu item and the inactivity timeout both enter ESP32 deep sleep (OLED off); pressing OK wakes it with a full restart through setup().
Hardware poweroff via Settings -> Menu or double pressings the power button
Hardware power on: 1x short press
Build & flash
Requires PlatformIO Core (CLI) or the PlatformIO IDE extension.
pio run -e esp32dev -t upload # build + flash over USB
pio device monitor # serial console @ 115200
pio run -e esp32dev_ota -t upload # build + flash wirelessly (see Firmware updates)
Dependencies are pinned in platformio.ini (U8g2, RTClib, ArduinoJson);
PlatformIO fetches them automatically. SD, SPI, Wire, WiFi, HTTPClient,
HTTPUpdate, ArduinoOTA, Preferences and FS come from the ESP32 Arduino core.
The board uses the min_spiffs partition table (two ~1.9 MB app slots); stored on the SD card, not internal flash, so the SPIFFS partition is unused. Changing the partition table needs one USB flash — do
the first flash over cable, after which OTA works.
Editor note (VSCodium): the PlatformIO build engine works fine, but Microsoft's
C/C++ IntelliSense extension is licensed for Microsoft products only. Use the
clangd extension instead and run pio run -t compiledb to generate
compile_commands.json, or just build from the terminal.
Using the device
Navigation: UP/DOWN move the highlight, OK selects, BACK goes up a level. UP/DOWN auto-repeat when held. A button is ignored until it has been seen released once after boot, which immunizes the UI against floating-pin noise.
Home menu: Scan chip, Past readings, Settings, Sleep.
Scanning: choose Scan chip, hold the tag within ~4 cm. On a successful read the result screen shows the full chip ID, the registering country decoded from the ID's country field, and a temperature line if one was decoded. OK saves it to SD, UP runs an optional API lookup that shows returned info inline, and BACK discards and keeps scanning. Holding the same chip near the coil will not re-trigger within a short time window.
Past readings: newest-first list; OK opens a detail view. In detail, OK runs the optional backend lookup (if WiFi + API are configured).
Settings: WiFi on/off, Brightness (Min/Low/Med/High/Max), Set time, Sleep timeout (Off / 30 s / 1 m / 2 m / 5 m), WiFi info, Update firmware, Deep sleep, Power off, About, Back. Every change is written to flash immediately. Update firmware runs an on-device pull OTA from FW_URL — see Firmware updates.
Set time: UP/DOWN change the underlined field, OK advances to the next, BACK cancels. Time is synced automatically on start up when connected to WiFi.
Settings & persistence
Setting values are stored in ESP32 NVS (namespace cfg) in a flash partition, so they
survive power-off, deep sleep, and re-flashing.
Defaults on first boot:
WiFi off, 60 s sleep, 24 h clock, High brightness, empty credentials.
To restore defaults, erase flash: pio run -t erase.
WiFi/API credentials can be provided via src/secrets.h.
// src/secrets.h
#define WIFI_SSID "YourNetwork"
#define WIFI_PASS "YourPassword"
#define API_URL "api.gaycats.fyi" // optional lookup backend, "" if unused
#define OTA_PASSWORD "s3cret" // password for wireless (push) OTA
#define FW_URL "firmware.gaycats.fyi" // firmware.bin URL for on-device (pull) OTA
When a SSID is compiled in, WiFi defaults to on. You can still toggle WiFi on/off at runtime from the Settings menu.
SD card format
Readings are appended to /readings.csv:
epoch,country,national,hexid,tempC,datahex
1717000000,900,123456789012,0384001C8AF34B,,
epoch— unix time from the RTC (0 if the clock isn't set)country/national— decoded FDX-B fieldshexid— the raw 7 ID bytes as hextempC— decoded temperature, blank when nonedatahex— raw FDX-B data-block bytes, blank when the reader sends none
The SD card should be formatted in FAT32.
Temperature support
Temperature-sensing FDX-B chips carry their reading in the optional 24-bit data block. Not all RFID reader modules support temperature readout (e.g. WL-134).
WiFi backend lookup
When WiFi is enabled, connected, and an API URL is set, the detail screen's OK action POSTs the scanned ID to your backend:
POST <api> Content-Type: application/json
{ "uid": "<base64 of the 7 raw ID bytes>" }
It expects a JSON response and reads a text field named info (falling back to
name) for display. Adapt those field names in Api::lookup() to your backend.
Connect/read timeouts are capped at 3s so a slow server can't lock the UI.
WIP.
Battery sensing
Power is supplied by an SM5308 module (single-chip Li-ion charger + 5V boost +
battery-level LED display). The firmware reads the cell voltage with the ESP32
ADC, tapping the module's BAT node through a 2:1 divider into GPIO34:
BAT (B+) --[220k]+------+--> GPIO34
| |
[220k] [100nF]
| |
+------+
GND
Reads use analogReadMilliVolts(), averaged over 8 samples
and scaled by BATT_DIVIDER, mapped between BATT_EMPTY_MV/BATT_FULL_MV
(default 3300/4200 mV — set BATT_FULL_MV to your SM5308 float-voltage variant).
An unconnected pin / USB-only power reads outside the plausible range and shows
as "no battery" (hollow icon). BATT_LOW_PCT sets the low-battery blink
threshold. ESP32 ADC linearity is imperfect — calibrate the end points against a
meter if you need accuracy. The divider draws a little current from the cell
continuously.
Firmware updates (OTA)
Two over-the-air paths, both requiring WiFi, with OLED progress bar.
Wireless push (replaces the cable, for development)
Build in PlatformIO and upload over WiFi with the esp32dev_ota environment:
pio run -e esp32dev_ota -t upload
The device advertises OTA_HOSTNAME.local (default tranymonscanner.local, set
in config.h) via mDNS; upload_port in platformio.ini must match it. If mDNS
name resolution is flaky on your network, target the IP instead (the WiFi info
screen shows it):
pio run -e esp32dev_ota -t upload --upload-port 192.168.1.42
Requirements and notes:
- The device must be awake and connected to receive a push. Deep sleep drops the WiFi/OTA listener, so during development set Sleep timeout to Off.
- Set
OTA_PASSWORDinsecrets.hand uncommentupload_flags = --auth=...in theesp32dev_otaenv. Without a password, anyone on the LAN can flash the device. The transfer itself is unencrypted. - The device reboots into the new image automatically when the transfer completes.
On-device pull
Set FW_URL in secrets.h to an https:// firmware .bin URL, then choose
Settings → Update firmware. The device downloads the image over TLS,
self-flashes with a progress bar, and reboots. HTTP_UPDATE_NO_UPDATES
("Up to date") is returned if your server is version-aware and 304s the request
(the current FW_VERSION is sent as a header); a plain static file server will
simply re-flash on each run.
TLS / certificate. The pull path uses WiFiClientSecure and download the ISRG Root X1, valid to 2035. Paste the PEM into OTA_ROOT_CA in drivers.cpp
Integrity. The Update layer verifies the image's own SHA-256 (and that it
fits the OTA slot) before ever switching to it, so a corrupt or truncated
download can't boot — this is automatic, and over TLS the transport is
integrity-checked too.
Diagnostics
Set DEBUG_SERIAL to 1 in config.h (default on) for a 115200-baud console:
- boot banner + wake cause
- I²C scan (expect
0x3COLED,0x68RTC,0x57EEPROM) - RTC / SD / WiFi status
- raw RFID bytes as hex while the scan screen is open. Two reader output formats
are auto-detected: a binary frame (
AA 0F 08 00 … XOR BB) and an ASCII frame (02STX, hex digits, optional CR/LF,03ETX). Both carry the same 7 ID bytes (2 country + 5 national) and decode identically. - twice-a-second button states
BTN U1 D1 O1 B1(1 = released, 0 = pressed) — a pin stuck at0untouched is a wiring/floating fault
Set it to 0 for normal use to quiet the serial traffic.
Project layout
chipscanner/
├── platformio.ini build config + pinned libraries
├── README.md
├── .gitignore excludes secrets.h, .pio/, editor cruft
└── src/
├── config.h pins, constants, the Reading struct, wiring notes
├── secrets.example.h credential template (committed)
├── secrets.h your real credentials (git-ignored, you create this)
├── drivers.h service-layer declarations
├── drivers.cpp Settings/Buttons/Power/Batt/Rfid/Store/Net/Api/Ota
├── country.h/.cpp FDX-B country-code -> name lookup
├── datetime.h small date helpers (daysInMonth, leap-aware)
├── boot_splash.h 128x64 XBM shown on cold boot (startup logo)
├── credits.h third-party attributions (Credits screen text)
├── sleep_splash.h 128x64 XBM shown when entering deep sleep
└── main.cpp UI state machine, screens, rendering, setup/loop
Roadmap
- Optional buzzer for audible read confirmation.
License & credits
Third-party works reused in the firmware are attributed on the device under
Settings → About → OK (a scrollable Credits screen).
--> Not fully done yet.