Files
MTGcompanion/README.md

14 KiB
Raw Blame History

MTG RFID Companion

A hand-held Magic: The Gathering companion built on an ESP32-WROOM-32. Tap an RFID/NFC-tagged card to a built-in reader and the device:

  • renders the card's cover art and metadata on a 2.0" ST7789 TFT (240x320),
  • flashes a WS2812B Neopixel in the card's mana color and plays a short buzzer chirp,
  • keeps a life counter (20/40) you adjust with a rotary encoder,
  • caches cards locally on LittleFS and refetches from the Scryfall API over Wi-Fi,
  • runs a SoftAP registration server (MTG-Companion) so you can pair an unknown tag to a card name from your phone.

All code is pure ESP-IDF v5.x (C) / FreeRTOS — no Arduino.


Features

🃏 RFID scanning MFRC522, NTAG21x / MIFARE tags, 4- and 7-byte UIDs
🖥️ Display ST7789 240x320 RGB565, 5x7 text, scaled digits, streaming JPEG decode
🌐 Network Wi-Fi STA auto-connect, Scryfall HTTPS with local cache
📡 Pairing SoftAP MTG-Companion (192.168.4.1) web SPA for UID ↔ card name
🔄 UX EC11 rotary encoder, backlit life counter, mana-colored LED, audio cues
💾 Storage LittleFS /spiffs (~1.9MB) seeded with card mapping + example art

Hardware Requirements

  • ESP32-WROOM-32 dev board (4MB flash, no PSRAM required)
  • MFRC522 RFID reader (13.56 MHz, SPI)
  • ST7789 SPI TFT, 240x320 (e.g. common 2.0-2.4" breakout)
  • EC11 rotary encoder with push switch
  • WS2812B / Neopixel (single LED)
  • Passive piezo buzzer
  • Antenna-friendly NTAG213/NTAG215 stickers or MIFARE cards, ~3.3V–5V supply

Wiring Guide

Do not change these GPIO assignments in code unless you update both main/hw_pins.h and this table. The ST7789 and MFRC522 share one SPI bus (SPI2_HOST).

Pin map

Peripheral Function ESP32 GPIO Notes
Shared SPI SCK 18 SPI2_HOST (VSPI), 1 MHz modules
MOSI 23
MISO 19
MFRC522 SDA (CS) 5 dedicated chip select
RST 22 hard reset
3.3V / GND — feed from board 3.3V rail
ST7789 CS 15 dedicated chip select
DC (A0) 2 data/command
RST 4 panel reset
LED 3.3V or 5V optional backlight pin
EC11 CLK 32 interrupt input, pull-up
DT 33 quadrature B
SW 25 button, active low, pull-up
WS2812B DIN 27 RMT Tx; supply 5V/VDD, GND common
Buzzer SIG 14 LEDC PWM

Suggested breadboard layout

        ESP32-WROOM-32
  ┌────────────────────────────┐
  │ 3V3  18 ──── SCK ───────┤ MFRC522  5 ── SDA(CS)  22 ── RST
  │ GND  23 ──── MOSI ──────┤ (SPI)    3V3/GND
  │       19 ──── MISO ─────┤
  │       15 ──── CS ───────┤ ST7789   2 ── DC  4 ── RST
  │       4 ─────────────────┤ (SPI)
  │       32 ── CLK ─────────┤ EC11     33 ── DT  25 ── SW/GND
  │       27 ── Din ─────────┤ WS2812B  (5V + GND)
  │       14 ── SIG ─────────┤ Buzzer   (GND)
  └────────────────────────────┘
  • Keep SPI signal runs short (< 10 cm) for reliable 20 MHz display transfers.
  • NTAG213/215 FPC stickers need to sit flat and close to the MFRC522 antenna.
  • The WS2812B draws up to ~60 mA; power it from the 5V rail, never from a GPIO.

Getting Started

1. Toolchain

Install ESP-IDF v5.5 (any 5.x works): https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/

Verify:

# Windows (Espressif "ESP-IDF 5.5 PowerShell" terminal)
. C:\Espressif\Initialize-Idf.ps1        # or: . $IDF_PATH\export.ps1
idf.py --version

Never used Espressif tools before? Skip ahead to "Changing the Wi-Fi without the toolchain"; the section below is optional.

2. Build

idf.py set-target esp32
idf.py build

The custom partition table (partitions.csv) reserves:

Offset Size Subtype Purpose
0x10000 2 MB app/factory firmware
0x210000 ~1.9 MB data/littlefs /spiffs cache (seeded from storage/)

3. Configure Wi-Fi

Two equivalent ways:

# a) interactive menu (recommended)
idf.py menuconfig        # -> "MTG RFID Companion" -> SSID + password

# b) edit the preset file directly
#    sdkconfig.defaults.esp32

Leaving the SSID empty (or keeping the YOUR_... placeholders) disables station mode. The SoftAP pairing server runs regardless, so the device stays usable as a fully standalone scanner — no home network required.

What "station mode disabled" actually means

The Wi-Fi radio still turns on, but only as an access point:

  1. wifi_manager_start() sees the blank/placeholder SSID and returns early — it never calls esp_wifi_connect(), so the device won't try to join any router (and won't pretend to be online).
  2. web_server_start() starts unconditionally: SoftAP MTG-Companion at 192.168.4.1 (own DHCP) plus the /api/last_uid and /api/bind endpoints.
  3. net_task skips the 60-second connect wait and the boot-time prefetch. Card lookups only run when you explicitly trigger one, and those fail fast (no internet route) with a scryfall log line — they never block the UI.

What works and what doesn't

Feature In AP-only mode Notes
RFID scanning, life counter, LED/buzzer ✔ fully local
Showing already-cached cards (art + stats) ✔ served from /spiffs
Binding a UID to a card name ✔ written to /spiffs/mappings.json immediately
Scryfall lookup + art download ✘ the device has no internet here; the fetch fails and is logged. It completes automatically on a later boot with Wi-Fi configured — the boot-time prefetch walks mappings.json and downloads anything missing
Scryfall autocomplete in the pairing page ⚠️ the AP doesn't pass internet through (no NAT). A phone with cellular data keeps its mobile connection, so autocomplete works there; on a device with no other internet path, type the exact card name manually

Use case: AP-only mode is the normal "deck on the table" flow — mid-game you almost never need live Scryfall lookups. When you do (new card, missing art), configure Wi-Fi once, boot briefly, and the binding + art cache update themselves via prefetch.


Changing the Wi-Fi without the toolchain

If you're new to this and haven't installed any Espressif software yet, start here.

The Wi-Fi network the device connects to is baked into the firmware at compile time — there is no on-device settings menu yet. To change it you must edit one text file and produce a new firmware image. Here is the shortest ESPRESSIF-FREE path to do that, with zero drivers or toolchains.

The one file you edit

Open sdkconfig.defaults.esp32 in any text editor (Notepad is fine) and find these two lines:

CONFIG_MTG_WIFI_SSID="YOUR_SSID_HERE"
CONFIG_MTG_WIFI_PASS="YOUR_PASSWORD_HERE"

Replace the placeholders with your network name and password, keeping the quotes:

CONFIG_MTG_WIFI_SSID="MyHomeWiFi"
CONFIG_MTG_WIFI_PASS="correct horse battery staple"
  • Keep the file UTF-8 without BOM, and don't add spaces inside the quotes.
  • If your password has a " or \ in it, this file format can't represent it (rare) — pick that one character out of the password or use the toolchain menu instead.
  • Want to go back to offline-only later? Restore the YOUR_... placeholders and rebuild.

Getting a new firmware image from this file

The repo cannot build firmware without the toolchain. Your options, easiest first:

Path What's needed How
Ask whoever gave you the device a minute of their time Send them the edited file; they run idf.py build and hand you a new .bin
One-time toolchain install (~45 min, guided) the Espressif installer (free, Windows/macOS/Linux) Follow the official guide: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/ then run the 5 commands in Build
CI build (GitHub Actions etc.) a GitHub account The repo can build on ubuntu in CI; push the edited file and download the built mtg-rfid-companion.bin artifact

Flashing the new firmware (no toolchain needed)

Flash using Espressif Flash Download Tool (graphical, no toolchain): https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/linux-setup-scratch.html#esp32-flash-download-tool or esptool.py if you already have Python:

pip install esptool
esptool.py -p COM3 --chip esp32 --before default_reset write_flash --flash_mode dio --flash_size 4MB 0x1000 bootloader.bin 0x8000 partition-table.bin 0x10000 mtg-rfid-companion.bin

TL;DR

  1. Edit sdkconfig.defaults.esp32 → 2. rebuild (yours or someone's toolchain) →
  2. flash the .bin → 4. done. If you don't need your home Wi-Fi, skip the whole thing: the device works standalone and has its own AP.

4. Flash & watch

idf.py -p COMx flash monitor      # COMx = your serial port

Expected boot log if everything is wired right:

I storage: LittleFS mounted at /spiffs: <size> bytes total ...
I storage: boot_count read back = 1 (persistence across reboots OK)
I rfid:    RC522 polling task pinned to Core 0
I display: ST7789 240x320 ready ...
I web:     SoftAP 'MTG-Companion' at 192.168.4.1, web server ready
I ui:      UI task on core 1

RFID reader missing / not wired? That's OK. A reader self-test failure no longer aborts boot — the device logs a warning and continues into the display, SoftAP and web-pairing screens without card scanning:


Usage

Scan a card

Place a tagged card on the reader:

  1. Mapped UID → chirp, mana-colored LED flash, then the card view (cover art + name/type/P·T/price/mana).
  2. Unknown UID → registration screen with the pairing address.

Life counter

Press the encoder button to toggle SCANNER ↔ LIFE_COUNTER. Rotate to adjust (20/40 preset or whatever you last set; press again to reset to the preset).

Web pairing (registration mode)

  1. Connect your phone to the MTG-Companion access point. A phone with cellular data keeps its mobile internet, so Scryfall autocomplete works; otherwise type the name manually.
  2. Open http://192.168.4.1.
  3. Scan an unmapped card — the UID appears in the page.
  4. Type / pick a Scryfall-autocomplete name → Pair UID with card.
  5. The mapping persists to /spiffs/mappings.json. The card data/art fetch runs immediately if the device has Wi-Fi, or is picked up automatically by the boot-time prefetch the next time you boot with Wi-Fi configured.

Storage Layout (/spiffs)

/spiffs/
├── mappings.json              UID -> Card Name map
├── cards/
│   ├── sample.json            example metadata
│   ├── sample.jpg             example cover art (baked into flash image)
│   ├── <UID>.json             Scryfall metadata cache
│   └── <UID>.jpg              cached cover art

The storage/ folder is compiled into the LittleFS partition image at build time (littlefs_create_partition_image), so a fresh flash already contains mappings.json and the example art.


Troubleshooting

Symptom Fix
Display = black / garbage Check CS/DC/RST wiring; try toggling esp_lcd_panel_invert_color and LCD_RGB_ELEMENT_ORDER_* in display_manager.c
Colors inverted Same toggle as above — panel clones differ
No UID printed on scan Verify MFRC522 wiring, tag type (ISO14443A), antenna positioning; confirm rfid: RC522 polling task pinned to Core 0 in the log
Boot log: rc522: FIFO length missmatch / RTOS: RFID unavailable Reader self-test failed. Check the MFRC522 has 3.3V power (many clones have a separate power pin), CS=GPIO5 / RST=GPIO22, and SCK/MOSI/MISO=18/23/19. Try lowering RC522_SPI_CLK_HZ in rfid_manager.c (e.g. 2000000); some clones need a slower clock on a shared bus
Wi-Fi never connects Re-run idf.py menuconfig, confirm you replaced the YOUR_... placeholders
Scryfall autocomplete shows nothing The AP has no internet passthrough — the phone needs its own mobile data connection, or type the card name manually
Can't reach 192.168.4.1 Ensure your phone joined MTG-Companion, not a cached network
Card renders but no art Card not cached yet — check scryfall log lines; art downloads are best-effort
App grows near 2MB mbedTLS + HTTP pulls are large; if needed, enable CONFIG_MBEDTLS_CERTIFICATE_BUNDLE (smaller)

Project Layout

├── CMakeLists.txt              top-level; bakes LittleFS storage image
├── partitions.csv              custom partition table
├── sdkconfig.defaults          shared ESP32 defaults
├── sdkconfig.defaults.esp32    Wi-Fi credential preset (edit me)
├── storage/                    files pre-seeded into /spiffs
├── tools/                      host-side helpers (sample JPEG generator)
└── main/
    ├── hw_pins.h               GPIO assignments (authoritative)
    ├── spi_bus_manager.c       shared SPI2_HOST bus
    ├── storage_manager.c       LittleFS mount + JSON helpers
    ├── rfid_manager.c          MFRC522 / Core 0 polling
    ├── display_manager.c       ST7789 + fonts + streaming JPEG
    ├── ui_task.c               state machine + screens (Core 1)
    ├── wifi_manager.c          STA connect/reconnect
    ├── scryfall_client.c       Scryfall HTTPS + cache (net_task)
    ├── web_server.c            SoftAP pairing server
    ├── encoder.c               EC11 ISR decode
    ├── led_manager.c           WS2812B RMT
    ├── buzzer.c                LEDC PWM tones
    └── app_state.c             shared app/life-counter state

See AGENTS.md for the detailed architecture, core split, and coding conventions used across this repository.