Make RFID failure non-fatal (self-test retries, 4MHz clock, diagnostic log), add beginner Wi-Fi guide + RFID troubleshooting to README

This commit is contained in:
2026-09-09 21:52:40 +02:00
parent 7599f0e6b2
commit 96d14c0538
6 changed files with 167 additions and 19 deletions

119
README.md
View File

@@ -98,6 +98,10 @@ Verify:
idf.py --version 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 ### 2. Build
```powershell ```powershell
@@ -124,9 +128,104 @@ idf.py menuconfig # -> "MTG RFID Companion" -> SSID + password
# sdkconfig.defaults.esp32 # sdkconfig.defaults.esp32
``` ```
Leaving the SSID empty (or keeping the `YOUR_...` placeholders) disables station Leaving the SSID empty (or keeping the `YOUR_...` placeholders) **disables
mode — the SoftAP pairing server still runs, so you can use the device station mode**. The SoftAP pairing server runs regardless, so the device stays
without a home network. 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 ### 4. Flash & watch
@@ -145,6 +244,10 @@ I web: SoftAP 'MTG-Companion' at 192.168.4.1, web server ready
I ui: UI task on core 1 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 ## Usage
@@ -159,11 +262,15 @@ Press the encoder button to toggle **SCANNER ↔ LIFE_COUNTER**. Rotate to adjus
(20/40 preset or whatever you last set; press again to reset to the preset). (20/40 preset or whatever you last set; press again to reset to the preset).
### Web pairing (registration mode) ### Web pairing (registration mode)
1. Connect your phone to the `MTG-Companion` access point. 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`. 2. Open `http://192.168.4.1`.
3. Scan an unmapped card — the UID appears in the page. 3. Scan an unmapped card — the UID appears in the page.
4. Type / pick a Scryfall-autocomplete name → **Pair UID with card**. 4. Type / pick a Scryfall-autocomplete name → **Pair UID with card**.
5. The mapping persists to `/spiffs/mappings.json` and the card is fetched & cached. 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.
--- ---
@@ -192,7 +299,9 @@ time (`littlefs_create_partition_image`), so a fresh flash already contains
| Display = black / garbage | Check CS/DC/RST wiring; try toggling `esp_lcd_panel_invert_color` and `LCD_RGB_ELEMENT_ORDER_*` in `display_manager.c` | | 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 | | 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 | | 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 | | 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 | | 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 | | 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) | | App grows near 2MB | mbedTLS + HTTP pulls are large; if needed, enable `CONFIG_MBEDTLS_CERTIFICATE_BUNDLE` (smaller) |

View File

@@ -21,8 +21,13 @@ void app_main(void)
ESP_ERROR_CHECK(storage_manager_init()); ESP_ERROR_CHECK(storage_manager_init());
ESP_ERROR_CHECK(display_manager_init()); ESP_ERROR_CHECK(display_manager_init());
ESP_ERROR_CHECK(rfid_manager_init()); /* RFID is not fatal: the reader may be unpowered, unwired, or a clone
ESP_ERROR_CHECK(rfid_manager_start()); * that needs a slower SPI clock. Continue booting so the display, SoftAP
* and web pairing all stay usable; scanning is simply unavailable. */
esp_err_t ret = rfid_manager_init();
if (ret != ESP_OK || rfid_manager_start() != ESP_OK) {
ESP_LOGW(TAG, "RFID unavailable (%s) - continuing without card scanning", esp_err_to_name(ret));
}
ESP_ERROR_CHECK(wifi_manager_init()); ESP_ERROR_CHECK(wifi_manager_init());
ESP_ERROR_CHECK(wifi_manager_start()); ESP_ERROR_CHECK(wifi_manager_start());
@@ -31,7 +36,7 @@ void app_main(void)
ESP_ERROR_CHECK(ui_task_start()); ESP_ERROR_CHECK(ui_task_start());
ESP_LOGI(TAG, "Boot sequence complete. Ready for Phase 7+."); ESP_LOGI(TAG, "Boot sequence complete.");
while (1) { while (1) {
vTaskDelay(pdMS_TO_TICKS(1000)); vTaskDelay(pdMS_TO_TICKS(1000));

View File

@@ -21,6 +21,11 @@ static const char *TAG = "rfid";
#define RC522_TASK_STACK_SIZE (6 * 1024) #define RC522_TASK_STACK_SIZE (6 * 1024)
#define RC522_TASK_PRIORITY (4) #define RC522_TASK_PRIORITY (4)
/* Conservative clock for MFRC522 clones. If the FIFO self-test still
* reports a mismatch at boot, lower this (e.g. 2000000) or check power. */
#define RC522_SPI_CLK_HZ (4000000)
#define RC522_START_ATTEMPTS (3)
static rc522_driver_handle_t s_driver; static rc522_driver_handle_t s_driver;
static rc522_handle_t s_scanner; static rc522_handle_t s_scanner;
@@ -63,6 +68,7 @@ esp_err_t rfid_manager_init(void)
.bus_config = NULL, .bus_config = NULL,
.dev_config = { .dev_config = {
.spics_io_num = HW_RC522_CS_GPIO, .spics_io_num = HW_RC522_CS_GPIO,
.clock_speed_hz = RC522_SPI_CLK_HZ,
}, },
.dma_chan = SPI_DMA_CH_AUTO, .dma_chan = SPI_DMA_CH_AUTO,
.rst_io_num = HW_RC522_RST_GPIO, .rst_io_num = HW_RC522_RST_GPIO,
@@ -96,9 +102,21 @@ esp_err_t rfid_manager_init(void)
esp_err_t rfid_manager_start(void) esp_err_t rfid_manager_start(void)
{ {
esp_err_t ret = rc522_start(s_scanner); esp_err_t ret = ESP_FAIL;
for (int attempt = 1; attempt <= RC522_START_ATTEMPTS; attempt++) {
ret = rc522_start(s_scanner);
if (ret == ESP_OK) {
break;
}
ESP_LOGW(TAG, "rc522_start attempt %d/%d failed: %s",
attempt, RC522_START_ATTEMPTS, esp_err_to_name(ret));
vTaskDelay(pdMS_TO_TICKS(250));
}
if (ret != ESP_OK) { if (ret != ESP_OK) {
ESP_LOGE(TAG, "rc522_start failed: %s", esp_err_to_name(ret)); ESP_LOGE(TAG, "MFRC522 self-test failed (%s). Check: reader powered "
"(3.3V), SPI wiring (SCK=18/MOSI=23/MISO=19, CS=5, RST=22), "
"and if it is a clone try lowering RC522_SPI_CLK_HZ", esp_err_to_name(ret));
return ret; return ret;
} }

View File

@@ -360,16 +360,16 @@ static void net_task(void *arg)
#ifndef CONFIG_MTG_WIFI_SSID #ifndef CONFIG_MTG_WIFI_SSID
#error "Missing CONFIG_MTG_WIFI_SSID" #error "Missing CONFIG_MTG_WIFI_SSID"
#endif #endif
if (strlen(CONFIG_MTG_WIFI_SSID) > 0) { if (wifi_manager_sta_configured()) {
if (wifi_manager_wait_connected(pdMS_TO_TICKS(60000)) != ESP_OK) { if (wifi_manager_wait_connected(pdMS_TO_TICKS(60000)) != ESP_OK) {
ESP_LOGW(TAG, "Wi-Fi connect timed out, continuing without network"); ESP_LOGW(TAG, "Wi-Fi connect timed out, continuing without network");
} else {
#ifdef CONFIG_MTG_SCAN_PREFETCH
prefetch_mappings();
#endif
} }
} }
#ifdef CONFIG_MTG_SCAN_PREFETCH
prefetch_mappings();
#endif
while (1) { while (1) {
fetch_request_t req; fetch_request_t req;
BaseType_t got = xQueueReceive(s_fetch_queue, &req, portMAX_DELAY); BaseType_t got = xQueueReceive(s_fetch_queue, &req, portMAX_DELAY);

View File

@@ -76,22 +76,32 @@ esp_err_t wifi_manager_init(void)
return ESP_OK; return ESP_OK;
} }
bool wifi_manager_sta_configured(void)
{
#ifndef CONFIG_MTG_WIFI_SSID
return false;
#else
const char *ssid = CONFIG_MTG_WIFI_SSID;
return strlen(ssid) > 0 && strstr(ssid, "YOUR_") == NULL;
#endif
}
esp_err_t wifi_manager_start(void) esp_err_t wifi_manager_start(void)
{ {
#ifndef CONFIG_MTG_WIFI_SSID #ifndef CONFIG_MTG_WIFI_SSID
#error "Missing CONFIG_MTG_WIFI_SSID - run idf.py menuconfig" #error "Missing CONFIG_MTG_WIFI_SSID - run idf.py menuconfig"
#endif #endif
const char *ssid = CONFIG_MTG_WIFI_SSID;
const char *pass = CONFIG_MTG_WIFI_PASS;
/* Empty SSID, or an untouched sdkconfig.defaults.esp32 placeholder: /* Empty SSID, or an untouched sdkconfig.defaults.esp32 placeholder:
* keep station mode disabled so we don't hammer a bogus AP at boot. */ * keep station mode disabled so we don't hammer a bogus AP at boot. */
if (strlen(ssid) == 0 || strstr(ssid, "YOUR_") != NULL) { if (!wifi_manager_sta_configured()) {
ESP_LOGW(TAG, "no SSID configured - Wi-Fi station skipped (set CONFIG_MTG_WIFI_SSID)"); ESP_LOGW(TAG, "no SSID configured - Wi-Fi station skipped (set CONFIG_MTG_WIFI_SSID)");
return ESP_OK; return ESP_OK;
} }
const char *ssid = CONFIG_MTG_WIFI_SSID;
const char *pass = CONFIG_MTG_WIFI_PASS;
wifi_config_t wifi_config = { wifi_config_t wifi_config = {
.sta = { .sta = {
.threshold.authmode = WIFI_AUTH_WPA2_PSK, .threshold.authmode = WIFI_AUTH_WPA2_PSK,

View File

@@ -25,6 +25,12 @@ esp_err_t wifi_manager_init(void);
*/ */
esp_err_t wifi_manager_start(void); esp_err_t wifi_manager_start(void);
/**
* @brief True if a real SSID is configured (non-empty, not the
* `YOUR_...` placeholder). Station mode will actually run.
*/
bool wifi_manager_sta_configured(void);
/** /**
* @brief True if the STA has a valid IP. * @brief True if the STA has a valid IP.
*/ */