Files
MTGcompanion/README.md

348 lines
15 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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** (7-pin) | CS | **15** | dedicated chip select |
| | DC | **2** | data/command |
| | RST | **4** | panel reset |
| | SDA | **23** | shared MOSI (data) |
| | SCL | **18** | shared SCK (clock) |
| | VCC / GND | 3.3V / GND | panel power (no separate LED 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 ──┐ │
│ GND 23 ── MOSI ─┼─────┐ │
│ 19 ── MISO ─┘ │ │
│ 5 ── SDA ───────┤ MFRC522 22 ── RST
│ 22 ── RST ───────┤ 3V3/GND
│ 15 ── CS ────────┤ ST7789 (7-pin)
│ 2 ── DC ────────┤ SDA <-> MOSI, SCL <-> SCK
│ 4 ── RST ───────┤ VCC = 3V3, GND = GND
│ 32 ── CLK ────────┤ EC11 33 ── DT 25 ── SW/GND
│ 27 ── Din ────────┤ WS2812B (5V + GND)
│ 14 ── SIG ────────┤ Buzzer (GND)
└────────────────────────────┘
```
> **7-pin ST7789 wiring recap:** connect the display's **SCL → ESP32 GPIO 18**,
> **SDA → GPIO 23**, **CS → 15**, **DC → 2**, **RST → 4**, **VCC → 3.3V**,
> **GND → GND**. The MFRC522 taps the same SCL/SDA lines (its own CS is GPIO 5).
- 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:
```powershell
# 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](#changing-the-wifi-without-the-toolchain)";**
> the section below is optional.
### 2. Build
```powershell
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:
```powershell
# 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](#2-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) →
3. 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
```powershell
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 |
| Flash fails: `Serial data stream stopped` / must unplug the display to flash | Disconnect the ST7789 (or power it down) during `idf.py flash`: this board's display shares strapping pins GPIO2/GPIO15 and the panel can pull the 3.3V rail / strap pins at reset, corrupting the serial link mid-write. Flash, then reconnect the display. |
| RFID step of boot log: `Buffers content missmatch`, buffer2 = `13 33 37` | The reader now answers (SPI OK) but glitches on the write/read race. `RC522_SPI_CLK_HZ` is already 2 MHz; lower further to `1000000` in `rfid_manager.c`, or check the MFRC522's separate power header gets a clean 3.3V and confirm MISO=GPIO19 is actually connected |
| 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.