Firmware for the ESP32 rfid scanning tool aka TranymonScanner
  • C++ 72.4%
  • C 27.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-08-23 20:57:41 +02:00
.cache/clangd/index add src, LED pin change to GPIO13 2026-05-31 23:32:10 +02:00
.vscode Initial commit 2026-05-31 22:09:05 +02:00
docs Change GPIO Pinout; Add debug build option; 2026-08-23 20:57:41 +02:00
splash_art Add splash art PNGs; Fix time-set accepting impossible dates, 2026-08-15 11:40:13 +02:00
src Change GPIO Pinout; Add debug build option; 2026-08-23 20:57:41 +02:00
.DS_Store Change GPIO Pinout; Add debug build option; 2026-08-23 20:57:41 +02:00
.gitignore Update battery management 2026-08-13 14:28:51 +02:00
compile_commands.json Initial commit 2026-05-31 22:09:05 +02:00
platformio.ini Change GPIO Pinout; Add debug build option; 2026-08-23 20:57:41 +02:00

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 + national ID (wip: temp and animal bit)
  • Save readings to microSD as 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 fields
  • hexid — the raw 7 ID bytes as hex
  • tempC — decoded temperature, blank when none
  • datahex — 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_PASSWORD in secrets.h and uncomment upload_flags = --auth=... in the esp32dev_ota env. 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 0x3C OLED, 0x68 RTC, 0x57 EEPROM)
  • 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 (02 STX, hex digits, optional CR/LF, 03 ETX). 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 at 0 untouched 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.