Add Gitea OTA firmware update, dual-slot partition layout, and Gitea Actions release workflow

This commit is contained in:
2026-09-12 10:49:39 +02:00
parent 8ac465229c
commit 1d954287a2
82 changed files with 7611 additions and 395 deletions

View File

@@ -0,0 +1,57 @@
name: Build & Release Firmware
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
build:
name: Build ESP32 Firmware
runs-on: ubuntu-latest
container:
image: espressif/idf:v5.5
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Compile ESP32 firmware
shell: bash
run: |
. $IDF_PATH/export.sh
idf.py build
- name: Bundle release artifacts
shell: bash
run: |
mkdir -p release-dist
# Primary OTA binary required by on-device update manager:
cp build/mtg-rfid-companion.bin release-dist/
# USB flashing bundle:
cp build/bootloader/bootloader.bin release-dist/
cp build/partition_table/partition-table.bin release-dist/
cp build/storage.bin release-dist/
cd release-dist
tar -czvf full-flash-bundle-${{ gitea.ref_name || 'latest' }}.tar.gz *.bin
cd ..
- name: Publish to Gitea Release
uses: https://gitea.com/actions/release-action@v1
continue-on-error: true
with:
files: |
release-dist/mtg-rfid-companion.bin
release-dist/full-flash-bundle-${{ gitea.ref_name || 'latest' }}.tar.gz
name: "Release ${{ gitea.ref_name }}"
draft: false
prerelease: false
api_key: ${{ secrets.RELEASE_TOKEN || gitea.token }}
- name: Upload Artifacts to Workflow
uses: actions/upload-artifact@v4
with:
name: mtg-firmware-${{ gitea.ref_name || 'manual' }}
path: release-dist/*

28
.gitignore vendored
View File

@@ -1,5 +1,29 @@
# ESP-IDF build output and temporary files
build/
managed_components/
sdkconfig.old
sdkconfig.defaults.bak
dependencies.lock
dependencies.lock
# ESP Component Manager downloads
managed_components/
# Editor & IDE configurations
.vscode/
.idea/
*.code-workspace
*.sublime-project
*.sublime-workspace
# Python and virtual environments
__pycache__/
*.py[cod]
*$py.class
.venv/
venv/
ENV/
# Operating System files
.DS_Store
Thumbs.db
ehthumbs.db
[Dd]esktop.ini

View File

@@ -32,7 +32,6 @@ The **MTG RFID Companion** is a hand-held hardware utility for Magic: The Gather
| **MFRC522 RFID**| CS (SDA) / RST | GPIO 5 / GPIO 22 | Dedicated CS & Reset |
| **ST7789 Display**| CS / DC / RST | GPIO 15 / GPIO 2 / GPIO 4 | Dedicated CS, Data/Cmd & Reset |
| **Rotary Encoder**| CLK / DT / SW | GPIO 32 / GPIO 33 / GPIO 25 | Active low / Interrupt driven |
| **Indicators** | Neopixel (WS2812B) | GPIO 27 | RMT driver |
| **Audio** | Piezo Buzzer | GPIO 14 | LEDC PWM channel |
---
@@ -124,7 +123,7 @@ Build Commands
Monitor Logs: idf.py -p <PORT> monitor
Partition Table: Uses custom partitions.csv (Flash allocation: 2MB App, 1.5MB LittleFS data).
Partition Table: Uses custom partitions.csv (Flash allocation: 2x 1.56MB dual-OTA app slots, 832KB LittleFS data, 8KB otadata, 16KB NVS).
8. Guidance for AI Agents Generating Code
@@ -148,8 +147,8 @@ Build Commands
| `wifi_manager.c` | STA auto-connect + reconnect | event loop |
| `scryfall_client.c` | Scryfall HTTPS, cJSON cache, prefetch | net_task / 0 |
| `web_server.c` | SoftAP MTG-Companion 192.168.4.1 + pairing SPA | httpd task |
| `ota_manager.c` | Gitea release API, HTTPS OTA flash, rollback cancel | Core 0 |
| `encoder.c` | EC11 ISR decode + button | ISR |
| `led_manager.c` | WS2812B RMT | ISR + ui_task |
| `buzzer.c` | LEDC PWM tones | ui_task |
| `app_state.c` | STATE_* + life counter values | all |

342
README.md
View File

@@ -1,14 +1,28 @@
# MTG RFID Companion
# MTG RFID Companion 🧙‍♂️✨
A feature-packed, handheld **Magic: The Gathering** companion built on the **ESP32-WROOM-32**. Slip an RFID/NFC tag into your card sleeves and tap them to the device for instant card lookups, official art display, life tracking, dice rolling, and complete deck management.
All firmware is written in **pure ESP-IDF v5.x (C) / FreeRTOS** — no Arduino overhead, dual-core task segregation, and strictly optimized for devices without external PSRAM (< 520 KB SRAM).
A dedicated tabletop companion for **Magic: The Gathering** players. Slip micro RFID/NFC stickers into your card sleeves and tap them to the device for instant card lookups, official full-color art, life tracking, dice rolling, and complete deck management.
---
## 🌟 Feature Tour & Modes
## 🃏 Part 1: Player's Guide (Non-Technical Overview)
The companion boots directly into an interactive, rotary-controlled interface designed specifically for tabletop MTG gameplay:
### What is the MTG Companion?
The MTG Companion is a handheld gadget designed to sit on your playmat during Commander (EDH), Modern, or casual kitchen-table games.
Instead of fumbling for your phone to check complex Oracle card text, searching for spindown dice, or asking *"Wait, how much commander tax am I at?"*, the Companion handles everything with a turn of a knob and a tap of a card:
* **⚡ Tap-to-Identify:** Tap any sleeved card with an NFC sticker to instantly display its official illustration, mana cost, updated Oracle text, type line, power/toughness, and current market price.
* **👑 Legendary Fanfares:** An 8-bit stinger plays whenever you cast a Mythic Rare or Legendary spell!
* **❤️ Life & Multi-Counter:** Tracks starting life (40 for Commander, 20 for Standard/Modern), **Poison Counters** (tracks to 10 lethal), and **Commander Combat Damage** (tracks to 21 lethal). Sounds an 8-bit defeat tone if a player dies.
* **🎲 Spindown D20, D6 & Fair Coin:** Roll animated dice on screen with ticking sound effects and special fanfares for Natural 20s and Natural 1s.
* **🪙 Token & Counter Spawner:** Digital counters for **Treasures, Foods, Clues, 1/1 Soldiers, 1/1 Goblins, 2/2 Zombies, 3/3 Beasts**, and **+1/+1 Counters**.
* **👑 Commander Tax & Turn Tracker:** Automatically calculates the extra {2} mana tax every time your commander returns to the command zone, alongside a table turn counter.
* **📱 Wireless Phone Pairing:** Connect your phone or laptop over your home Wi-Fi (or the device's hotspot) to easily assign card names to tags with live Scryfall search autocomplete.
* **🔄 Over-The-Air (OTA) Updates:** Update to new software versions wirelessly over Wi-Fi right from the on-device menu.
---
### How to Use at the Table
```
┌──────────────────────────────┐
@@ -20,160 +34,115 @@ The companion boots directly into an interactive, rotary-controlled interface de
│ [4] Commander Tax │
│ [5] Token & Counters │
│ [6] Deck Storage │
│ [7] Device Info │
│ [7] Device & WiFi Info │
│ [8] Check Firmware OTA │
└──────────────────────────────┘
```
### 1. Interactive Boot Menu (`APP_STATE_MENU`)
On power-up, the device greets you with the Main Menu.
* **Rotate the knob** to highlight any of the 7 modes with an active amber cursor.
* **Click the knob** to enter the highlighted mode.
* **Universal Back (Hold knob $\ge$ 0.75s):** Press and hold the rotary switch from **any** screen to sound a double-chirp, flash purple on the WS2812B LED, and instantly return to the Main Menu.
* **Universal Card Interrupt:** Tapping a card tag from **any** screen or menu immediately transitions into that card's view.
#### Controls
* **Turn the knob:** Scroll through menus, adjust life totals, change token quantities, or pick dice modes.
* **Click the knob:** Select an option, roll dice, or toggle between Life, Poison, and Commander Damage modes.
* **Long Press (Hold knob for 0.75s):** Sounds a chirp and instantly takes you back to the **Main Menu** from any screen.
* **Tap a Card Anytime:** Tapping a card sleeve immediately pulls up that card's details, regardless of which screen you are on.
---
### 2. Universal RFID Scanner & Art Crop Display (`APP_STATE_CARD_VIEW`)
* **Instant Tag Identification:** Supports MFRC522, ISO/IEC 14443A, NTAG213/215 stickers, and standard MIFARE 1K cards (4-byte and 7-byte UIDs).
* **True RGB565 Scryfall Art:** Cards display official card illustrations pre-scaled to a crisp 200×110 format, decoded via hardware-assisted Baseline JPEG in ~10 ms with true color accuracy.
* **Card Metadata:** Shows card name, mana cost, type line, oracle text, power/toughness, loyalty, and market pricing.
* **8-Bit Legendary Stinger & Neopixel Feedback:**
* **Legendary / Mythic Stinger:** Scanning any Legendary creature or Mythic Rare plays an upward 8-bit stinger and pulses the LED in gold!
* **Mana Color LEDs:** WS2812B Neopixel illuminates in the card's native mana identity (White, Blue, Black, Red, Green, Gold, Colorless).
* **Unmapped Card Support:** Tapping an unmapped tag switches to the Registration view, displaying the UID and pairing instructions.
* **Smart Cache Validation:** When a card tag is re-paired or changed in the web UI, the firmware automatically detects the change, purges old cached assets, and downloads the fresh card details and art.
### Tabletop Modes
#### 1. Card Scanner & Art Viewer
Tap your card against the scanner area. The device checks its internal memory and displays:
* Full-color official art crop streamed from Scryfall.
* Mana cost, card type, power / toughness, and loyalty.
* Updated Oracle card text with rules text formatting.
* Market price in USD.
#### 2. Life & Metric Tracker
* Starts at **40 life** (Commander) or **20 life** (1v1).
* Rotate clockwise to gain life; counter-clockwise to take damage.
* **Click the knob** to cycle between:
1. **Life Total** (Standard life score).
2. **Poison Counters** (Counts 0 to 10; triggers a defeat tone at 10).
3. **Commander Combat Damage** (Counts 0 to 21; triggers lethal banner at 21).
#### 3. Spindown D20 / D6 / Fair Coin Flip
* Select **D20 Spindown**, **D6**, or **Coin Flip** and click to roll.
* An animated roll plays with sound effects.
* **Natural 20:** Plays a triumphant 8-bit Victory Fanfare!
* **Natural 1:** Sounds a critical failure buzz.
* **Coin Flip:** Clean 50/50 toss landing on **HEADS** or **TAILS**.
#### 4. Built-in Token & Counter Spawner
Never dig through your token box again for missing tokens:
* Pre-loaded with MTG staples: **Treasure, Food, Clue, Soldier (1/1), Zombie (2/2), Goblin (1/1), Beast (3/3)**, and **+1/+1 Counters**.
* Rotate to select a token, click to edit, and rotate to change quantity (persists throughout your game).
#### 5. Commander Tax & Turn Tracker
* Rotate the knob each time your commander is recast from the command zone; the screen calculates the extra mana required (e.g. `Cast 3 = +4 Mana`).
* Click to switch to the table turn counter.
#### 6. Pairing Cards from your Phone (No App Needed!)
When you scan a new card tag that hasn't been programmed yet:
1. The screen displays:
```
PAIR VIA WI-FI:
http://192.168.0.57
(or Hotspot: MTG-Companion @ 192.168.4.1)
```
2. Open that address in your smartphone or laptop browser.
3. Tap the card sleeve to the device—its unique ID pops up in the web form automatically.
4. Start typing the card name (e.g. `"Sol Ring"`). Real-time Scryfall suggestions appear.
5. Click **Pair Card**. The device downloads the high-res card art and text and saves it to its permanent memory.
6. **Deck Overview:** The web page also lets you view your entire scanned deck with card art thumbnails and unbind old cards with one click.
#### 7. Over-The-Air (OTA) Updates
* Select **8. CHECK FIRMWARE OTA** from the menu (or let it auto-check on boot when connected to Wi-Fi).
* If a new version is detected, the screen shows the new version number and asks you to click to install or turn the knob to dismiss.
* While updating, a live progress bar shows the download. When finished, the device verifies the update and restarts automatically.
---
### 3. Life & Multi-Counter (`APP_STATE_LIFE_COUNTER`)
A dedicated, high-contrast tournament-grade counter supporting the three most important Magic metrics:
* **Life Total (Starts at 40 Commander / 20 Standard):**
* Turn clockwise to gain life (green LED flash); turn counter-clockwise to lose life (red LED flash).
* **Click to Cycle Counter Types:**
1. **Life Total:** Standard life tracking.
2. **Poison Counters (0–10):** In MTG, taking 10 poison counters loses the game.
3. **Commander Combat Damage (0–21):** Dealing 21 combat damage with a single commander eliminates a player.
* **8-Bit Dramatic Loss Tone:** Reaching 0 life or 10 poison triggers an 8-bit descending defeat stinger and a red alert banner.
## 🛠️ Part 2: Technical Specifications & Build Guide
### Hardware Architecture
| Component | Part / Model | Purpose |
| :--- | :--- | :--- |
| **MCU** | ESP32-WROOM-32 (4MB Flash, no external PSRAM) | Dual-core 240 MHz processor |
| **Display** | 2.0" ST7789 IPS TFT LCD (240x320 resolution) | 16-bit RGB565 high-contrast screen |
| **RFID Reader** | MFRC522 (13.56 MHz, SPI Interface) | Reads ISO/IEC 14443A tags & stickers |
| **User Input** | EC11 Rotary Encoder with push button | Tabletop navigation & life adjustment |
| **Audio** | Passive Piezo Buzzer (GPIO 14) | LEDC PWM multi-frequency tones |
| **Tags** | NTAG213 / NTAG215 micro FPC stickers | Thin adhesive tags embedded inside sleeves |
| **Capacitor** | **2200 µF Electrolytic Capacitor** | **CRITICAL**: Placed across MFRC522 VCC/GND to absorb RF transmit current spikes |
---
### 4. Spindown D20 / D6 / Fair Coin Flip (`APP_STATE_DICE_ROLLER`)
* **Rotate knob** to select between **D20 (Spindown)**, **D6**, or **Coin Flip**, then **click to roll**.
* Plays an interactive rolling shuffle animation with rapid tick sounds.
* **Natural 20 (Critical Hit):** Plays the 8-bit **Victory Fanfare** and flashes brilliant green.
* **Natural 1 (Critical Fail):** Sounds a buzzer tone and flashes red.
* **D6 Mode:** Quick six-sided die roll for ability triggers and random player selection.
* **Coin Flip Mode:** Random 50/50 flip landing on **HEADS (Green)** or **TAILS (Amber)**.
### GPIO Wiring & Pinout
---
### 5. Built-in Token & Counter Spawner (`APP_STATE_TOKEN_SPAWNER`)
An interactive tabletop tool for spawning and managing tokens on the battlefield:
* **Pre-configured Tokens:**
* **Treasure** (Artifact Token) — `{T}, Sacrifice: Add one mana of any color.`
* **Food** (Artifact Token) — `{2}, {T}, Sacrifice: You gain 3 life.`
* **Clue** (Artifact Token) — `{2}, Sacrifice: Draw a card.`
* **1/1 Soldier** (White Creature Token) — With full P/T box.
* **2/2 Zombie** (Black Creature Token) — With full P/T box.
* **1/1 Goblin** (Red Creature Token) — With full P/T box.
* **3/3 Beast** (Green Creature Token) — With full P/T box.
* **+1/+1 Counter** (Permanent Modifier) — Universal stat counter.
* **Controls:**
* **Pick Mode:** Rotate the knob to cycle through tokens (`< 1 OF 8 >`).
* **Edit Mode:** Click the knob to select; rotate to increment/decrement count (`0` to `99`). Click again to save. Token quantities persist throughout the play session.
---
### 6. Commander Tax & Turn Tracker (`APP_STATE_COMMANDER_TRACKER`)
In Commander (EDH), casting your commander costs an additional {2} generic mana for each time it has previously been cast from the command zone.
* **Commander Casts:** Rotate the knob to adjust casts; the screen automatically calculates and displays the total tax (e.g., `Cast 3 = +4 Mana`).
* **Turn Counter:** Click the button to switch to the table turn tracker.
---
### 7. Deck & Storage Manager (`APP_STATE_STORAGE_MANAGER`)
View flash disk usage and manage cards stored in LittleFS:
* **Storage Metrics:** Displays total storage space (~1.9 MB), used bytes, and counts of cached JSON cards and art crop images.
* **Wipe All Cards (New Deck Mode):** Switching to a new deck? Rotate to highlight "Wipe All Cards", click, and confirm. This purges `/spiffs/cards/*` and resets `mappings.json`, freeing space for a completely new 100-card deck with full artwork.
---
### 8. Web Registration, SoftAP Pairing & Scanned Cards Overview (`web_server`)
Pair tags and manage your entire scanned deck directly from your smartphone browser—no host software required:
1. Connect your phone or laptop to the Wi-Fi network: **`MTG-Companion`** (open network, default IP `192.168.4.1`).
2. Open `http://192.168.4.1` in your browser.
3. **Pair New Tag:**
* Tap an RFID tag to the reader — the UID populates automatically.
* Type the card name (Scryfall autocomplete suggestions appear in real time).
* Click **Pair Card**. The binding is saved to `/spiffs/mappings.json` and the firmware fetches the card details and cover art over Wi-Fi.
4. **Scanned / Paired Cards Overview:**
* View an alphabetized list of all registered cards in your deck with live card counter (`Paired Cards (X)`).
* Displays the card title, tag UID, and cover art thumbnails streamed directly from LittleFS flash storage (`/api/art?uid=...`).
* **Unbind Card:** Click "Unbind" next to any card to immediately delete its mapping and clean up its cached JSON and JPEG files from flash memory.
* **Refresh:** Click "Refresh" to re-sync the list after scans or edits.
---
### 9. Device Info Screen (`APP_STATE_DEVICE_INFO`)
Real-time diagnostic screen showing:
* Wi-Fi STA connection status, SSID, and assigned IP address.
* SoftAP network details (`MTG-Companion`, `192.168.4.1`).
* Free internal heap SRAM (typically ~180 KB free during active rendering).
* System uptime and LittleFS partition status.
---
## 🎮 Quick Controls Cheat Sheet
| Action | Control |
| :--- | :--- |
| **Navigate / Adjust** | Rotate EC11 rotary knob clockwise / counter-clockwise |
| **Select / Roll / Toggle** | Short press encoder button |
| **Universal Back (To Menu)** | **Long press encoder button ($\ge$ 0.75s)** (double chirp + purple flash) |
| **Instant Card View** | Tap any RFID card sleeve to reader from **any** screen |
---
## 🛠️ Hardware Requirements
- **ESP32-WROOM-32** development board (4MB flash, dual-core LX6, no external PSRAM needed).
- **ST7789** SPI TFT LCD, 240x320 resolution (2.0" or 2.4" breakout).
- **MFRC522** RFID reader module (13.56 MHz, SPI).
- **EC11** rotary encoder with integrated push switch.
- **WS2812B / Neopixel** (single addressable RGB LED).
- **Passive piezo buzzer** (driven via LEDC PWM).
- **Antenna-friendly NTAG213/NTAG215** micro FPC stickers (for card sleeves) or standard MIFARE cards.
- **2200 µF Electrolytic Capacitor** (placed directly across MFRC522 VCC and GND) — **CRITICAL**: absorbs RF transmit spikes and prevents 3.3V rail voltage dips that trigger ESP32 brownouts or MFRC522 register resets.
---
## 🔌 Peripheral Wiring & Pinout
The display and RFID reader use **two separate SPI host controllers** to prevent SPI bus contention and clock mismatch:
The display and RFID reader run on **two separate SPI host controllers** to prevent SPI bus contention and clock frequency mismatches:
- **ST7789 Display:** `SPI2_HOST` (VSPI) at 20 MHz.
- **MFRC522 Reader:** `SPI3_HOST` (HSPI) at 1 MHz.
| Peripheral | Function | ESP32 GPIO | Notes |
| Peripheral | Functional Pin | ESP32 GPIO | Notes |
| :--- | :--- | :---: | :--- |
| **ST7789 Display** (SPI2) | SCL (Clock) | **18** | SPI2_HOST (VSPI) clock |
| | SDA (MOSI) | **23** | SPI2_HOST MOSI |
| | CS | **21** | Dedicated Chip Select (non-strapping pin) |
| | DC | **19** | Data / Command select (non-strapping pin) |
| | RST | **4** | Display Reset |
| **ST7789 Display** (SPI2) | SCL (Clock) | **GPIO 18** | SPI2_HOST (VSPI) Clock |
| | SDA (MOSI) | **GPIO 23** | SPI2_HOST MOSI |
| | CS | **GPIO 21** | Dedicated Chip Select (non-strapping) |
| | DC | **GPIO 19** | Data / Command Select (non-strapping) |
| | RST | **GPIO 4** | Display Reset |
| | VCC / GND | 3.3V / GND | Display logic & backlight power |
| **MFRC522 RFID** (SPI3) | SCK | **16** | SPI3_HOST (HSPI) clock |
| | MOSI | **17** | SPI3_HOST MOSI |
| | MISO | **13** | SPI3_HOST MISO |
| | SDA (CS) | **26** | Dedicated Chip Select |
| | RST | **22** | Hard reset line |
| **MFRC522 RFID** (SPI3) | SCK | **GPIO 16** | SPI3_HOST (HSPI) Clock |
| | MOSI | **GPIO 17** | SPI3_HOST MOSI |
| | MISO | **GPIO 13** | SPI3_HOST MISO |
| | SDA (CS) | **GPIO 26** | Dedicated Chip Select |
| | RST | **GPIO 22** | Reader Reset |
| | 3.3V / GND | 3.3V / GND | **Attach 2200 µF capacitor across these pins!** |
| **EC11 Rotary Encoder** | CLK (A) | **32** | Quadrature A (interrupt-driven, internal pull-up) |
| | DT (B) | **33** | Quadrature B (internal pull-up) |
| | SW (Button) | **25** | Push button (active LOW, internal pull-up) |
| **Indicators** | WS2812B DIN | **27** | RMT peripheral Tx (supply 5V to VDD, common GND) |
| **Audio** | Buzzer SIG | **14** | LEDC PWM channel |
| **EC11 Rotary Encoder** | CLK (A) | **GPIO 32** | Quadrature A (interrupt-driven) |
| | DT (B) | **GPIO 33** | Quadrature B |
| | SW (Button) | **GPIO 25** | Push button (active LOW) |
| **Audio** | Buzzer SIG | **GPIO 14** | LEDC PWM audio output |
### Wiring Diagram
#### Wiring Diagram
```
ESP32-WROOM-32
┌────────────────────────────┐
@@ -182,13 +151,12 @@ The display and RFID reader use **two separate SPI host controllers** to prevent
│ 13 ── MISO ────┼──┘ │
│ 26 ── SDA(CS) ─┘ │
│ 22 ── RST ─┐ │
│ 21 ── CS ──┼───────┤ ST7789 (7-pin, SPI2_HOST)
│ 21 ── CS ──┼───────┤ ST7789 Display (SPI2_HOST)
│ 19 ── DC ──┤ │ SCL=18, SDA=23, CS=21, DC=19, RST=4
│ 4 ── RST ─┘ │ VCC=3.3V, GND=GND
│ 18 ── SCL ─────────┘
│ 23 ── SDA ─────────
│ 32 ── CLK ────────┤ EC11 Rotary Encoder (CLK=32, DT=33, SW=25)
│ 27 ── Din ────────┤ WS2812B LED (GPIO 27, 5V rail)
│ 14 ── SIG ────────┤ Piezo Buzzer (GPIO 14)
└────────────────────────────┘
│ │
@@ -200,57 +168,93 @@ The display and RFID reader use **two separate SPI host controllers** to prevent
└────────────────────────────┘
```
> **Note on Flashing:** GPIO21 (CS) and GPIO19 (DC) are deliberate non-strapping pins. The ST7789 display can remain fully connected during `idf.py flash`.
---
### Flash Partition Layout (4MB Dual-OTA)
Configured in [`partitions.csv`](file:///c:/Users/rsben/Desktop/Rasmus/Programmering/espMTG/partitions.csv):
| Partition | Type | SubType | Offset | Size | Purpose |
| :--- | :--- | :--- | :--- | :--- | :--- |
| `nvs` | data | nvs | `0x9000` | 16 KB | Wi-Fi credentials & system settings |
| `otadata` | data | ota | `0xd000` | 8 KB | Active OTA slot pointer & rollback state |
| `phy_init` | data | phy | `0xf000` | 4 KB | RF calibration data |
| `ota_0` | app | ota_0 | `0x10000` | 1,600 KB | Primary firmware slot |
| `ota_1` | app | ota_1 | `0x1a0000` | 1,600 KB | Secondary firmware slot |
| `storage` | data | littlefs | `0x330000` | 832 KB | LittleFS flash database (`/spiffs`) |
* **Rollback Safety:** Uses `CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y`. On first boot of a new firmware version, the app must call `esp_ota_mark_app_valid_cancel_rollback()`. If an update is corrupt or crashes on boot, the bootloader automatically reverts to the previous working slot.
---
## 💻 Software Setup & Build
### FreeRTOS Dual-Core Architecture
### 1. Requirements
- **ESP-IDF v5.x** (v5.0 through v5.5 supported):
```powershell
. C:\Espressif\frameworks\esp-idf-v5.5\export.ps1
idf.py --version
```
* **Core 0 (Networking, Storage & Hardware I/O):**
* `rc522_polling_task`: Continuously polls the MFRC522 reader.
* `net_task`: Manages Wi-Fi auto-reconnection and Scryfall HTTPS API downloads (`esp_http_client`).
* `httpd`: Runs the embedded web server (serves the pairing SPA and REST APIs).
* `ota_check` / `ota_update`: Queries Gitea release API and streams firmware updates into the inactive OTA bank.
* **Core 1 (User Interface & Rendering):**
* `ui_task`: Drives the LVGL display subsystem, rotary encoder state machine, and screen animations at 40 FPS without I/O stutter.
### 2. Configure Wi-Fi
Configure home Wi-Fi credentials for automatic Scryfall fetching:
---
### Software Setup & Build Commands
#### 1. Prerequisites
Install [ESP-IDF v5.x](https://docs.espressif.com/projects/esp-idf/en/v5.5/esp32/get-started/):
```powershell
. C:\Espressif\frameworks\esp-idf-v5.5\export.ps1
idf.py --version
```
#### 2. Configuration
Configure Wi-Fi credentials and Gitea instance details:
```powershell
idf.py menuconfig
# Navigate to: MTG RFID Companion -> Wi-Fi SSID & Password
# Navigate to: "MTG RFID Companion"
# - Wi-Fi SSID & Password
# - Gitea instance base URL (e.g. https://git.example.com)
# - Gitea repository owner & repo name
```
*(Or edit `sdkconfig.defaults.esp32` directly).*
### 3. Build & Flash
#### 3. Build & Flash (Initial USB Provisioning)
```powershell
idf.py set-target esp32
idf.py build
idf.py -p COM4 flash monitor
idf.py -p COM4 erase-flash flash monitor
```
---
## 📂 LittleFS Storage Structure
### Automated CI / Gitea Actions Workflow
The repository includes a Gitea Actions workflow in [`.gitea/workflows/releases.yml`](file:///c:/Users/rsben/Desktop/Rasmus/Programmering/espMTG/.gitea/workflows/releases.yml):
* **Trigger:** Pushing any version tag (e.g. `git tag v1.0.1 && git push origin v1.0.1`).
* **Environment:** Official `docker://espressif/idf:v5.5` container.
* **Artifacts:** Compiles the firmware and automatically attaches `mtg-rfid-companion.bin` (for wireless OTA updates) and `full-flash-bundle-*.tar.gz` (for USB cable flashing) directly to the Gitea release.
---
### LittleFS Storage Structure
All local card details and artwork are stored in LittleFS mounted at `/spiffs`:
```
/spiffs/
├── mappings.json UID -> Card Name JSON map
├── mappings.json # Scanned UID -> Card Name mapping table
└── cards/
├── <UID>.json Scryfall attributes (Mana, Type, Oracle, P/T)
└── <UID>.jpg Cached 200x110 Baseline JPEG art crop
├── <UID>.json # Cached Scryfall card data (Mana, Oracle, Type, P/T)
└── <UID>.jpg # Cached 200x110 Baseline JPEG art crop
```
The `storage/` repository directory is compiled directly into the LittleFS partition image at build time (`littlefs_create_partition_image`).
---
## ❓ Troubleshooting
### ❓ Troubleshooting
| Symptom | Cause & Solution |
| :--- | :--- |
| **ESP32 reboots when scanning card** | 3.3V rail voltage sag caused by the MFRC522 RF transmitter. **Add a 2200 µF capacitor** directly across the MFRC522 VCC and GND pins. |
| **Display colors inverted (blues look red)** | Check `flags.swap_color_bytes = 1` in JPEG config and `esp_lcd_panel_invert_color()` in `display_manager.c`. Panel clones differ between RGB and BGR. |
| **Card shows "PARSE FAILED"** | Corrupt card JSON in cache. The firmware auto-purges corrupt entries on reboot, or use **Deck Storage -> Wipe** to reset. |
| **Scanned card shows previously paired card** | Re-pairing a card now automatically purges the old cache. Ensure firmware is up to date, or clear via **Deck Storage -> Wipe**. |
| **Art crop doesn't show** | Ensure device has Wi-Fi connectivity to download from Scryfall. Images are cached locally on flash once downloaded. |
| **Flash fails: `Failed to connect to ESP32`** | Make sure no other terminal is monitoring COM4 (`Ctrl + ]` to exit monitor). If using older GPIO15/2 wiring, hold the BOOT button during connect. |
| **Device reboots when scanning a card** | Power rail sag caused by the RF transmitter. **Solder a 2200 µF capacitor** directly across the MFRC522 VCC and GND pins. |
| **Display colors inverted (blues look red)** | Check `flags.swap_color_bytes = 1` in JPEG config and `esp_lcd_panel_invert_color()` in `display_manager.c`. |
| **Card displays "Could not fetch card"** | Device is unable to reach Scryfall. Verify your home Wi-Fi credentials in `menuconfig` or verify card spelling on the web pairing page. |
| **Router displays device name as "espressif"** | Hostname is set to `mtg-companion`. Clear DHCP lease on your router or reboot the ESP32 after connecting. |
| **Flash fails: `Failed to connect to ESP32`** | Ensure no serial monitor is holding COM4 open (`Ctrl + ]` to exit monitor). Hold the device BOOT button while plugging into USB. |

View File

@@ -0,0 +1,75 @@
---
Language: Cpp
BasedOnStyle: WebKit
AlignConsecutiveBitFields: true
AlignConsecutiveMacros: true
AlignEscapedNewlines: Right
AlignOperands: true
AlignTrailingComments: true
AllowAllArgumentsOnNextLine: false
AllowAllConstructorInitializersOnNextLine: true
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortBlocksOnASingleLine: true
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: Empty
AllowShortIfStatementsOnASingleLine: Never
AllowShortLambdasOnASingleLine: All
AllowShortLoopsOnASingleLine: false
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: Yes
BinPackArguments: false
BreakBeforeBraces: Custom
BitFieldColonSpacing: Before
PenaltyBreakBeforeFirstCallParameter: 0
BraceWrapping:
AfterCaseLabel: false
AfterClass: true
AfterControlStatement: false
AfterEnum: true
AfterFunction: true
AfterNamespace: false
AfterObjCDeclaration: true
AfterStruct: true
AfterUnion: true
AfterExternBlock: false
BeforeCatch: true
BeforeElse: true
BeforeLambdaBody: false
BeforeWhile: true
IndentBraces: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: false
BreakBeforeBinaryOperators: NonAssignment
BreakBeforeTernaryOperators: true
BreakConstructorInitializers: BeforeColon
ColumnLimit: 120
CompactNamespaces: true
Cpp11BracedListStyle: false
FixNamespaceComments: true
IndentCaseLabels: true
IndentWidth: 4
KeepEmptyLinesAtTheStartOfBlocks: false
MaxEmptyLinesToKeep: 1
NamespaceIndentation: Inner
PointerAlignment: Right
ReflowComments: true
SortIncludes: false
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: false
SpaceBeforeAssignmentOperators: true
SpaceBeforeInheritanceColon: false
SpaceBeforeParens: ControlStatements
SpaceBeforeRangeBasedForLoopColon: false
SpaceInEmptyParentheses: false
SpacesInAngles: false
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
Standard: Auto
TabWidth: 4
UseTab: Never
InsertNewlineAtEOF: true
InsertTrailingCommas: Wrapped
LineEnding: LF

View File

@@ -0,0 +1 @@
3302c1e7e19ee75c37e3e7284e86d0f624b56b7c5c55929c4ed1a1bdd28b62c5

6
components/rc522/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
/dist
**/build/
sdkconfig.old
sdkconfig
.vscode
dependencies.lock

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,47 @@
SET (RC522_REQUIRES
esp_event
esp_driver_gpio
esp_driver_i2c
esp_timer
)
if(${IDF_TARGET} STREQUAL "linux") # tests
list(APPEND RC522_REQUIRES driver)
else()
list(APPEND RC522_REQUIRES esp_driver_spi)
endif()
idf_component_register(
INCLUDE_DIRS
include
internal
SRCS
src/rc522.c
src/rc522_helpers.c
src/rc522_pcd.c
src/rc522_picc.c
src/picc/rc522_mifare.c
src/picc/rc522_nxp.c
src/rc522_driver.c
src/driver/rc522_spi.c
src/driver/rc522_i2c.c
REQUIRES
${RC522_REQUIRES}
)
target_compile_options(${COMPONENT_LIB} PRIVATE
-Werror # Warning as error
#-pedantic
-Wextra
-Wall
-Wshadow
-Wcast-qual
-Wswitch-default
#-Wswitch-enum
#-Wconversion
-Wunreachable-code
-Wuninitialized
-Winit-self
-Wpointer-arith
-Werror-implicit-function-declaration
)

14
components/rc522/Kconfig Normal file
View File

@@ -0,0 +1,14 @@
menu "RC522"
config RC522_PREVENT_SECTOR_TRAILER_WRITE
bool "Prevent writing to the Sector Trailer block"
default y
help
The Sector Trailer block stores authentication keys
and access bits. Enabling this option prevents writing
to the Trailer block. This is enabled by default to protect
inexperienced users from accidentally overwriting keys or
writing incorrect access bits, which could render the sector
unusable.
endmenu

202
components/rc522/LICENSE Normal file
View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [2024] [Alija Bobija]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

109
components/rc522/README.md Normal file
View File

@@ -0,0 +1,109 @@
# esp-idf-rc522
![CI](https://img.shields.io/github/actions/workflow/status/abobija/esp-idf-rc522/validate.yaml?branch=main&style=for-the-badge&logo=githubactions&logoColor=white) [![Component Registry](https://img.shields.io/github/v/release/abobija/esp-idf-rc522?sort=date&display_name=release&style=for-the-badge&logo=espressif&logoColor=white&label=Latest%20version)](https://components.espressif.com/components/abobija/rc522)
This repository contains [ESP-IDF](https://github.com/espressif/esp-idf) library (component) for communication with RFID cards using [MFRC522](https://www.nxp.com/docs/en/data-sheet/MFRC522.pdf) reader.
![read-write-example](docs/img/read-write-example.png)
Library takes care of polling the cards and managing the card lifecycle. It also fires events when a card is detected, removed, or when the card changes to any state described in ISO-14443. Additionally, it provides an API for reading and writing to card memory blocks.
## Installation
To install latest version of this component to your project, run:
```bash
idf.py add-dependency "abobija/rc522"
```
Read more about esp-idf component manager in [official documentation](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/tools/idf-component-manager.html).
## Support
- Full support: `MIFARE 1K`, `MIFARE 4K`, `MIFARE Mini`
- Partial support: Ultralight & NTAG families
- Supported commands: `GET_VERSION`, `READ`, `FAST_READ`, `WRITE`, `READ_CNT`,
`READ_SIG`, `PWD_AUTH`
- Supported features: PICC identification, read/write, page counts, password
authentication
- Unsupported commands: `INCR_CNT`, `WRITE_SIG`, `LOCK_SIG`, `AUTHENTICATE`,
`COMPAT_WRITE`, `VCSL`
- Unsupported features: signature validation, key authentication, high-level
configuration access (raw read + bit-manipulation only)
- No support: ISO-14443-4 compatible PICCs, other proprietary PICC protocols
- Communication protocols: `SPI` and `I2C`
See [examples](examples/) for example projects.
## Create project from example
> [!TIP]
> To find more interesting examples (like [`memory_dump`](examples/memory_dump)), go to [examples](examples) folder.
To run [`basic`](examples/basic) example, create it as follows:
```bash
idf.py create-project-from-example "abobija/rc522:basic"
```
Then build and flash it as usual:
```bash
cd basic
idf.py build flash monitor
```
> [!NOTE]
> [`basic`](examples/basic) example uses SPI communication. Find defined GPIO configuration in [basic.c](examples/basic/main/basic.c) file.
## Pin Layout
Pin layout is configurable by the user. To configure the GPIOs, check the `#define` statements in the [basic example](examples/basic/main/basic.c). If you are not using the RST pin, you can connect it to the 3.3V.
## Unit testing
To run unit tests, go to [`test`](test) directory and set target to `linux`:
```bash
cd test
idf.py --preview set-target linux
```
Then build the project and run tests:
```bash
idf.py build && ./build/test.elf
```
## Security
- Mifare Classic cards use the Crypto-1 cipher for authentication and encryption, which has been [broken](https://eprint.iacr.org/2008/166) for a long time. As a result, it is not advisable to use Mifare Classic cards for security-sensitive applications. Instead, consider using Mifare Plus or Desfire cards, which utilize AES encryption.
- Even though block zero, which contains the UID, is typically considered as read-only, there are certain cards known as "magic" or "Chinese magic" cards that can be used to modify the UID. As a result, relying on the UID of a card as a secure identifier is not recommended.
## Terms
| Term | Description |
| ---- | ----------- |
| PCD | Proximity Coupling Device (the card reader). In our case this is MFRC522 module |
| PICC | Proximity Integrated Circuit Card (e.g: rfid card, tag, ...) |
## References
- [ISO/IEC 14443](https://en.wikipedia.org/wiki/ISO/IEC_14443) Identification cards - Contactless integrated circuit cards
- [ISO/IEC 14443-2](http://www.emutag.com/iso/14443-2.pdf) Radio frequency power and signal interface
- [ISO/IEC 14443-3](http://www.emutag.com/iso/14443-3.pdf) Initialization and anticollision
- [ISO/IEC 14443-4](http://www.emutag.com/iso/14443-4.pdf) Transmission protocol
- [MFRC522](https://www.nxp.com/docs/en/data-sheet/MFRC522.pdf) MFRC522 - Standard performance MIFARE and NTAG frontend
- [AN10833](https://www.nxp.com/docs/en/application-note/AN10833.pdf) MIFARE type identification procedure
- [AN10834](https://www.nxp.com/docs/en/application-note/AN10834.pdf) MIFARE ISO/IEC 14443 PICC selection
- [MF1S50YYX_V1](https://www.nxp.com/docs/en/data-sheet/MF1S50YYX_V1.pdf) MIFARE Classic EV1 1K
- [MF1S70YYX_V1](https://www.nxp.com/docs/en/data-sheet/MF1S70YYX_V1.pdf) MIFARE Classic EV1 4K
## Author
GitHub: [abobija](https://github.com/abobija)<br />
Homepage: [abobija.com](https://abobija.com)
## License
This component is provided under Apache 2.0 license, see [LICENSE](LICENSE) file for details.

View File

@@ -0,0 +1,8 @@
# For more information about build system see
# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html
# The following five lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(basic)

View File

@@ -0,0 +1,2 @@
idf_component_register(SRCS "basic.c"
INCLUDE_DIRS ".")

View File

@@ -0,0 +1,55 @@
#include <esp_log.h>
#include "rc522.h"
#include "driver/rc522_spi.h"
#include "rc522_picc.h"
static const char *TAG = "rc522-basic-example";
#define RC522_SPI_BUS_GPIO_MISO (25)
#define RC522_SPI_BUS_GPIO_MOSI (23)
#define RC522_SPI_BUS_GPIO_SCLK (19)
#define RC522_SPI_SCANNER_GPIO_SDA (22)
#define RC522_SCANNER_GPIO_RST (-1) // soft-reset
static rc522_spi_config_t driver_config = {
.host_id = SPI3_HOST,
.bus_config = &(spi_bus_config_t){
.miso_io_num = RC522_SPI_BUS_GPIO_MISO,
.mosi_io_num = RC522_SPI_BUS_GPIO_MOSI,
.sclk_io_num = RC522_SPI_BUS_GPIO_SCLK,
},
.dev_config = {
.spics_io_num = RC522_SPI_SCANNER_GPIO_SDA,
},
.rst_io_num = RC522_SCANNER_GPIO_RST,
};
static rc522_driver_handle_t driver;
static rc522_handle_t scanner;
static void on_picc_state_changed(void *arg, esp_event_base_t base, int32_t event_id, void *data)
{
rc522_picc_state_changed_event_t *event = (rc522_picc_state_changed_event_t *)data;
rc522_picc_t *picc = event->picc;
if (picc->state == RC522_PICC_STATE_ACTIVE) {
rc522_picc_print(picc);
}
else if (picc->state == RC522_PICC_STATE_IDLE && event->old_state >= RC522_PICC_STATE_ACTIVE) {
ESP_LOGI(TAG, "Card has been removed");
}
}
void app_main()
{
rc522_spi_create(&driver_config, &driver);
rc522_driver_install(driver);
rc522_config_t scanner_config = {
.driver = driver,
};
rc522_create(&scanner_config, &scanner);
rc522_register_events(scanner, RC522_EVENT_PICC_STATE_CHANGED, on_picc_state_changed, NULL);
rc522_start(scanner);
}

View File

@@ -0,0 +1,3 @@
dependencies:
abobija/rc522:
version: '*'

View File

@@ -0,0 +1 @@
CONFIG_LOG_COLORS=y

View File

@@ -0,0 +1,8 @@
# For more information about build system see
# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html
# The following five lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(basic_i2c)

View File

@@ -0,0 +1,2 @@
idf_component_register(SRCS "basic_i2c.c"
INCLUDE_DIRS ".")

View File

@@ -0,0 +1,56 @@
#include <esp_log.h>
#include "rc522.h"
#include "driver/rc522_i2c.h"
#include "rc522_picc.h"
static const char *TAG = "rc522-basic-i2c-example";
#define RC522_I2C_ADDRESS (0x28)
#define RC522_I2C_GPIO_SDA (18)
#define RC522_I2C_GPIO_SCL (21)
#define RC522_SCANNER_GPIO_RST (-1) // soft-reset
static rc522_i2c_config_t driver_config = {
.bus_config = &(i2c_master_bus_config_t){
.i2c_port = I2C_NUM_0,
.sda_io_num = RC522_I2C_GPIO_SDA,
.scl_io_num = RC522_I2C_GPIO_SCL,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = true,
.clk_source = I2C_CLK_SRC_DEFAULT,
},
.dev_config = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = RC522_I2C_ADDRESS,
.scl_speed_hz = 100000,
},
.rst_io_num = RC522_SCANNER_GPIO_RST,
};
static rc522_driver_handle_t driver;
static rc522_handle_t scanner;
static void on_picc_state_changed(void *arg, esp_event_base_t base, int32_t event_id, void *data)
{
rc522_picc_state_changed_event_t *event = (rc522_picc_state_changed_event_t *)data;
if (event->picc->state != RC522_PICC_STATE_ACTIVE) {
return;
}
rc522_picc_print(event->picc);
}
void app_main()
{
rc522_i2c_create(&driver_config, &driver);
rc522_driver_install(driver);
rc522_config_t scanner_config = {
.driver = driver,
};
rc522_create(&scanner_config, &scanner);
rc522_register_events(scanner, RC522_EVENT_PICC_STATE_CHANGED, on_picc_state_changed, NULL);
rc522_start(scanner);
}

View File

@@ -0,0 +1,3 @@
dependencies:
abobija/rc522:
version: '*'

View File

@@ -0,0 +1 @@
CONFIG_LOG_COLORS=y

View File

@@ -0,0 +1,8 @@
# For more information about build system see
# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html
# The following five lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(memory_dump)

View File

@@ -0,0 +1,2 @@
idf_component_register(SRCS "memory_dump.c"
INCLUDE_DIRS ".")

View File

@@ -0,0 +1,3 @@
dependencies:
abobija/rc522:
version: '*'

View File

@@ -0,0 +1,187 @@
#include <esp_log.h>
#include <esp_check.h>
#include <string.h>
#include "rc522.h"
#include "driver/rc522_spi.h"
#include "picc/rc522_mifare.h"
static const char *TAG = "rc522-memory-dump-example";
#define RC522_SPI_BUS_GPIO_MISO (25)
#define RC522_SPI_BUS_GPIO_MOSI (23)
#define RC522_SPI_BUS_GPIO_SCLK (19)
#define RC522_SPI_SCANNER_GPIO_SDA (22)
#define RC522_SCANNER_GPIO_RST (-1) // soft-reset
static rc522_spi_config_t driver_config = {
.host_id = SPI3_HOST,
.bus_config = &(spi_bus_config_t){
.miso_io_num = RC522_SPI_BUS_GPIO_MISO,
.mosi_io_num = RC522_SPI_BUS_GPIO_MOSI,
.sclk_io_num = RC522_SPI_BUS_GPIO_SCLK,
},
.dev_config = {
.spics_io_num = RC522_SPI_SCANNER_GPIO_SDA,
},
.rst_io_num = RC522_SCANNER_GPIO_RST,
};
static rc522_driver_handle_t driver;
static rc522_handle_t scanner;
#define DUMP(format, ...) esp_log_write(ESP_LOG_INFO, TAG, format, ##__VA_ARGS__)
static void dump_header()
{
DUMP("Sector Block Bytes AccessBits\n");
DUMP(" 0 1 2 3 4 5 6 7 8 9 11 12 15 c1 c2 c3\n");
}
static void dump_block(rc522_mifare_sector_block_t *block, uint8_t sector_index)
{
if (block->type == RC522_MIFARE_BLOCK_TRAILER) {
DUMP("%*d", 6, sector_index);
}
else {
DUMP("%*s", 6, "");
}
// Block address
DUMP(" %*d", 5, block->address);
// Data
DUMP(" ");
for (uint8_t i = 0; i < 16; i++) {
DUMP("%02" RC522_X "", block->bytes[i]);
if ((i % 4) == 3) {
DUMP(" ");
}
}
// Access bits
DUMP(" %d %d %d", block->access_bits.c1, block->access_bits.c2, block->access_bits.c3);
// String representation (if it's data block)
if (block->type == RC522_MIFARE_BLOCK_DATA) {
DUMP(" |");
for (uint8_t i = 0; i < 16; i++) {
if (block->bytes[i] >= 32 && block->bytes[i] <= 126) { // standard ascii codes
DUMP(LOG_COLOR(LOG_COLOR_GREEN) "%c" LOG_RESET_COLOR, block->bytes[i]);
}
else {
DUMP("%c", '.');
}
}
DUMP("|");
}
// Value (if it's value block)
else if (block->type == RC522_MIFARE_BLOCK_VALUE) {
DUMP(" (val=%ld (0x%04l" RC522_X "), adr=0x%02" RC522_X ")",
block->value_info.value,
block->value_info.value,
block->value_info.addr);
}
// Errors and warnings
if (block->type == RC522_MIFARE_BLOCK_TRAILER && block->error == RC522_ERR_MIFARE_ACCESS_BITS_INTEGRITY_VIOLATION) {
DUMP(" ABITS_ERR");
}
if (block->type == RC522_MIFARE_BLOCK_VALUE && block->error == RC522_ERR_MIFARE_VALUE_BLOCK_INTEGRITY_VIOLATION) {
DUMP(" VAL_ERR");
}
// Termination
DUMP("\n");
}
static esp_err_t dump_memory(rc522_handle_t scanner, rc522_picc_t *picc)
{
rc522_mifare_key_t key = {
.value = { RC522_MIFARE_KEY_VALUE_DEFAULT },
};
rc522_mifare_desc_t mifare;
ESP_RETURN_ON_ERROR(rc522_mifare_get_desc(picc, &mifare), TAG, "");
DUMP("\n");
dump_header();
// Start from the highest sector
uint8_t sector_index = mifare.number_of_sectors - 1;
do {
rc522_mifare_sector_desc_t sector;
ESP_RETURN_ON_ERROR(rc522_mifare_get_sector_desc(sector_index, &sector), TAG, "");
ESP_RETURN_ON_ERROR(rc522_mifare_auth_sector(scanner, picc, &sector, &key), TAG, "");
rc522_mifare_sector_block_t trailer;
ESP_RETURN_ON_ERROR(rc522_mifare_read_sector_trailer_block(scanner, picc, &sector, &trailer), TAG, "");
dump_block(&trailer, sector_index);
// Start from the highest (non-trailer) block
uint8_t block_offset = sector.number_of_blocks - 2;
do {
rc522_mifare_sector_block_t block;
ESP_RETURN_ON_ERROR(rc522_mifare_read_sector_block(scanner, picc, &sector, &trailer, block_offset, &block),
TAG,
"");
dump_block(&block, sector_index);
}
while (block_offset--);
}
while (sector_index--);
DUMP("\n");
return ESP_OK;
}
static void on_picc_state_changed(void *arg, esp_event_base_t base, int32_t event_id, void *data)
{
rc522_picc_state_changed_event_t *event = (rc522_picc_state_changed_event_t *)data;
rc522_picc_t *picc = event->picc;
if (picc->state != RC522_PICC_STATE_ACTIVE) {
return;
}
rc522_picc_print(picc);
if (!rc522_mifare_type_is_classic_compatible(picc->type)) {
ESP_LOGW(TAG, "Card is not supported by this example");
return;
}
if (dump_memory(scanner, picc) == ESP_OK) {
ESP_LOGI(TAG, "Memory dump success");
}
else {
ESP_LOGE(TAG, "Memory dump failed");
}
if (rc522_mifare_deauth(scanner, picc) != ESP_OK) {
ESP_LOGW(TAG, "Deauth failed");
}
}
void app_main()
{
rc522_spi_create(&driver_config, &driver);
rc522_driver_install(driver);
rc522_config_t scanner_config = {
.driver = driver,
};
rc522_create(&scanner_config, &scanner);
rc522_register_events(scanner, RC522_EVENT_PICC_STATE_CHANGED, on_picc_state_changed, NULL);
rc522_start(scanner);
}

View File

@@ -0,0 +1 @@
CONFIG_LOG_COLORS=y

View File

@@ -0,0 +1,8 @@
# For more information about build system see
# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html
# The following five lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(multiple_scanners)

View File

@@ -0,0 +1,2 @@
idf_component_register(SRCS "multiple_scanners.c"
INCLUDE_DIRS ".")

View File

@@ -0,0 +1,3 @@
dependencies:
abobija/rc522:
version: '*'

View File

@@ -0,0 +1,117 @@
#include <esp_log.h>
#include "rc522.h"
#include "driver/rc522_spi.h"
#include "rc522_picc.h"
static const char *TAG = "rc522-multiple-scanners-example";
// SPI bus GPIOs
#define RC522_SPI_BUS_GPIO_MISO (25)
#define RC522_SPI_BUS_GPIO_MOSI (23)
#define RC522_SPI_BUS_GPIO_SCLK (19)
// CS GPIOs for each scanner
#define RC522_SPI_SCANNER_1_GPIO_SDA (22)
#define RC522_SPI_SCANNER_2_GPIO_SDA (26)
// {{ Scanner configurations
static rc522_spi_config_t scanner_1_config = {
.host_id = SPI3_HOST,
.bus_config = &(spi_bus_config_t){
.miso_io_num = RC522_SPI_BUS_GPIO_MISO,
.mosi_io_num = RC522_SPI_BUS_GPIO_MOSI,
.sclk_io_num = RC522_SPI_BUS_GPIO_SCLK,
},
.dev_config = {
.spics_io_num = RC522_SPI_SCANNER_1_GPIO_SDA,
},
.rst_io_num = -1, // soft-reset
};
// Second scanner does not need bus configuration,
// since first scanner will initialize the bus.
static rc522_spi_config_t scanner_2_config = {
.host_id = SPI3_HOST,
.dev_config = {
.spics_io_num = RC522_SPI_SCANNER_2_GPIO_SDA,
},
.rst_io_num = -1, // soft-reset
};
// }}
// Drivers
static rc522_driver_handle_t driver_1;
static rc522_driver_handle_t driver_2;
// Scanners
static rc522_handle_t scanner_1;
static rc522_handle_t scanner_2;
// Event handler
static void on_picc_state_changed(void *arg, esp_event_base_t base, int32_t event_id, void *data)
{
rc522_handle_t scanner = (rc522_handle_t)arg;
rc522_picc_state_changed_event_t *event = (rc522_picc_state_changed_event_t *)data;
rc522_picc_t *picc = event->picc;
uint8_t scanner_no = 1;
if (scanner == scanner_2) {
scanner_no = 2;
}
if (picc->state == RC522_PICC_STATE_ACTIVE) {
ESP_LOGI(TAG, "Card detected on the scanner #%d", scanner_no);
rc522_picc_print(picc);
}
else if (picc->state == RC522_PICC_STATE_IDLE && event->old_state >= RC522_PICC_STATE_ACTIVE) {
ESP_LOGI(TAG, "Card has been removed from the scanner #%d", scanner_no);
}
}
// App entry point
void app_main()
{
// Create drivers
rc522_spi_create(&scanner_1_config, &driver_1);
rc522_spi_create(&scanner_2_config, &driver_2);
// Install drivers
rc522_driver_install(driver_1);
rc522_driver_install(driver_2);
// Create scanners
rc522_create(
&(rc522_config_t) {
.driver = driver_1,
},
&scanner_1);
rc522_create(
&(rc522_config_t) {
.driver = driver_2,
},
&scanner_2);
// Register events for each scanner
rc522_register_events(scanner_1, RC522_EVENT_PICC_STATE_CHANGED, on_picc_state_changed, scanner_1);
rc522_register_events(scanner_2, RC522_EVENT_PICC_STATE_CHANGED, on_picc_state_changed, scanner_2);
// Start scanners
rc522_start(scanner_1);
rc522_start(scanner_2);
}

View File

@@ -0,0 +1 @@
CONFIG_LOG_COLORS=y

View File

@@ -0,0 +1,4 @@
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(picc_nxp)

View File

@@ -0,0 +1,65 @@
I (98) main_task: Started on CPU0
I (108) main_task: Calling app_main()
I (128) rc522: PCD (fw=unknown) initialized
I (128) main_task: Returned from app_main()
I (5088) rc522:
I (5088) rc522: ╔══════════════╗
I (5088) rc522: ║ ║ Type: NTAG215
I (5098) rc522: ║ RFID ║ UID: 04 26 F8 F5 9F 61 80
I (5098) rc522: ║ CARD ║ ATQA: 0x4400
I (5098) rc522: ║ ║ SAK: 0x00
I (5098) rc522: ╚══════════════╝
I (5098) rc522:
I (5098) rc522-picc-nxp-example: User pages: 126/135
I (5098) rc522-picc-nxp-example: Writing to address 05: R=00
I (5108) rc522-picc-nxp-example: Reading from address 05: R=00
I (5108) rc522-picc-nxp-example: Data read/write match
I (5108) rc522-picc-nxp-example: Pages 5 to 8: 10 20 30 40 F3 E7 42 BD 00 00 00 00 00 00 00 00
I (5108) rc522-picc-nxp-example: Dumping 3 pages: R=00
I (5108) rc522-picc-nxp-example: Page 004: 67 B6 A5 B4
I (5108) rc522-picc-nxp-example: Page 005: 10 20 30 40
I (5108) rc522-picc-nxp-example: Page 006: F3 E7 42 BD
I (5108) rc522-picc-nxp-example: Password auth: R=00
I (5118) rc522-picc-nxp-example: Read config bytes: R=00
I (5118) rc522-picc-nxp-example: NFC_CNT_EN bit: 0
I (5118) rc522-picc-nxp-example: Counter disabled; skipping
I (5118) rc522-picc-nxp-example: Read signature: R=00
I (5118) rc522-picc-nxp-example: Signature: B6 54 F4 00 A4 A5 6E 28 96 90 76 84 39 CA B4 CE 8E FC 3D 08 65 A3 29 60 1C 36 EA E4 B5 DA 74 7C
Page | Offset: +0 +1 +2 +3
00 | 04 26 F8 52 F5 9F 61 80 8B 48 00 00 E1 10 3E 00
04 | 67 B6 A5 B4* 10 20 30 40 F3 E7 42 BD 00 00 00 00 * Start of user memory (0x04)
08 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0C | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
10 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
14 | 1A 2B 00 00 00 00 00 00 00 00 00 00 00 00 00 00
18 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
1C | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
20 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
24 | 00 00 00 00 00 00 00 00 1A 2B 00 3C 00 00 00 00
28 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
2C | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
30 | 2A 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
34 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
38 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
3C | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
40 | 2A 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
44 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
48 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
4C | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
50 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
54 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
58 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
5C | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
60 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
64 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
68 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
6C | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
70 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
74 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
78 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
7C | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
80 | 00 00 00 00 00 00 00 00* 00 00 00 BD 04 00 00 FF * End of user memory (0x81)
84 | 00 05 00 00 00 00 00 00 00 00 00 00 04 26 F8 52
I (5258) rc522-picc-nxp-example: Memory dump success

View File

@@ -0,0 +1,2 @@
idf_component_register(SRCS "picc_nxp.c"
INCLUDE_DIRS ".")

View File

@@ -0,0 +1,3 @@
dependencies:
abobija/rc522:
version: '*'

View File

@@ -0,0 +1,283 @@
#include <esp_log.h>
#include <esp_check.h>
#include <string.h>
#include "driver/spi_master.h"
#include "rc522.h"
#include "rc522_pcd.h"
#include "driver/rc522_spi.h"
#include "picc/rc522_mifare.h"
#include "picc/rc522_nxp.h"
#include "rc522_picc.h"
static const char *TAG = "rc522-picc-nxp-example";
#define RC522_SPI_BUS_GPIO_MISO (16)
#define RC522_SPI_BUS_GPIO_MOSI (15)
#define RC522_SPI_BUS_GPIO_SCLK (17)
#define RC522_SPI_SCANNER_GPIO_SDA (1)
#define RC522_SCANNER_GPIO_RST (-1) // soft-reset
static rc522_spi_config_t driver_config = {
.host_id = SPI3_HOST,
.bus_config = &(spi_bus_config_t){
.miso_io_num = RC522_SPI_BUS_GPIO_MISO,
.mosi_io_num = RC522_SPI_BUS_GPIO_MOSI,
.sclk_io_num = RC522_SPI_BUS_GPIO_SCLK,
},
.dev_config = {
.spics_io_num = RC522_SPI_SCANNER_GPIO_SDA,
},
.rst_io_num = RC522_SCANNER_GPIO_RST,
};
static rc522_driver_handle_t driver;
static rc522_handle_t scanner;
static char str_buf[128];
#define DUMP(format, ...) esp_log_write(ESP_LOG_INFO, TAG, format, ##__VA_ARGS__)
static void dump_header()
{
DUMP("Page | Offset: +0 +1 +2 +3\n");
// 00 | 10 20 30 40 50 60 70 80 90 A0 B0 C0 D0 E0 F0 11
}
// Dump four pages (size of a single READ) to output, annotating the start
// and end pages of user memory if present
static void dump_pages(uint8_t address, uint8_t page[RC522_NXP_READ_SIZE], rc522_picc_type_t picc_type)
{
// Get the start and end pages of user memory so we can mark them in the dump
uint8_t start_page = rc522_nxp_get_user_mem_start(picc_type);
uint8_t end_page = rc522_nxp_get_user_mem_end(picc_type);
// 0: nothing of interest, 1: start page, 2: end page
uint8_t found = 0;
DUMP(" %02X |", address);
for (uint8_t i = 0; i < RC522_NXP_READ_SIZE; i++) {
DUMP(" %02X", page[i]);
if (i / 4 + address == start_page && i % 4 == 3) {
found = 1;
DUMP("*");
}
else if (i / 4 + address == end_page && i % 4 == 3) {
found = 2;
DUMP("*");
}
else if (i % 4 == 3) {
DUMP(" ");
}
}
if (found == 1) {
DUMP(" * Start of user memory (0x%02X)", start_page);
}
else if (found == 2) {
DUMP(" * End of user memory (0x%02X)", end_page);
}
DUMP("\n");
}
// Dump all memory to output
static esp_err_t dump_memory(rc522_handle_t scanner, rc522_picc_t *picc)
{
DUMP("\n");
dump_header();
uint8_t page_count = rc522_nxp_get_page_count(picc->type);
// Standard read is 4 pages; allocate an appropriate buffer and increment by
// 4 pages on each loop
uint8_t recv[RC522_NXP_PAGE_SIZE * 4];
for (uint8_t i = 0; i < page_count; i += 4) {
ESP_RETURN_ON_ERROR(rc522_nxp_read(scanner, picc, i, recv), TAG, "");
dump_pages(i, recv, picc->type);
}
DUMP("\n");
return ESP_OK;
}
static void buf_to_hex(const uint8_t *buffer, uint8_t buflen, char *strbuf, uint8_t strbuflen)
{
const char *format = "%02X ";
uint16_t len = 0;
for (uint16_t i = 0; i < buflen; i++) {
len += sprintf(strbuf + len, format, buffer[i]);
}
strbuf[len - 1] = 0x00;
}
// Example: Get the total and user memory page count of an NXP PICC
static void example_page_counts(rc522_handle_t rc522, rc522_picc_t *picc)
{
uint8_t page_count = rc522_nxp_get_page_count(picc->type);
uint8_t user_page_count = rc522_nxp_get_user_page_count(picc->type);
ESP_LOGI(TAG, "User pages: %d/%d", user_page_count, page_count);
}
// Example: Write to and read from user memory, verifying write works
static esp_err_t example_read_write(rc522_handle_t rc522, rc522_picc_t *picc)
{
uint8_t page = 5;
uint8_t write_buf[4] = { 0x10, 0x20, 0x30, 0x40 };
esp_err_t ret = rc522_nxp_write(scanner, picc, page, write_buf);
ESP_LOGI(TAG, "Writing to address %02X: R=%02X", page, ret);
ESP_RETURN_ON_ERROR(ret, TAG, "Error in WRITE");
uint8_t read_buf[16] = { 0 };
ret = rc522_nxp_read(scanner, picc, page, read_buf);
ESP_LOGI(TAG, "Reading from address %02X: R=%02X", page, ret);
ESP_RETURN_ON_ERROR(ret, TAG, "Error in READ");
if (memcmp(write_buf, read_buf, 4) == 0) {
ESP_LOGI(TAG, "Data read/write match");
}
else {
ESP_LOGW(TAG, "Data read/write mismatch");
}
buf_to_hex(read_buf, 16, str_buf, sizeof(str_buf));
ESP_LOGI(TAG, "Pages %d to %d: %s", page, page + 3, str_buf);
return ESP_OK;
}
// Example: Use of FAST_READ command for variable-size read
static esp_err_t example_fast_read(rc522_handle_t rc522, rc522_picc_t *picc)
{
uint8_t page_count = 3;
uint8_t start_page = 4;
// Need to allocate 4 bytes per page
uint8_t *mem_buf = calloc(page_count, 4);
ESP_RETURN_ON_FALSE(mem_buf != NULL, ESP_FAIL, TAG, "Out of memory");
rc522_nxp_fast_read_data_t mem_data = {
.bytes = mem_buf,
.buffer_size = page_count * 4,
};
esp_err_t ret = rc522_nxp_fast_read(scanner,
picc,
start_page,
start_page + page_count - 1, // end is inclusive, so subtract 1
&mem_data);
ESP_LOGI(TAG, "Dumping %d pages: R=%02X", page_count, ret);
ESP_RETURN_ON_ERROR(ret, TAG, "Error in FAST_READ");
for (uint8_t i = 0; i < page_count; i++) {
buf_to_hex(&mem_buf[i * 4], 4, str_buf, 128);
ESP_LOGI(TAG, "Page %03d: %s", start_page + i, str_buf);
}
free(mem_buf);
return ESP_OK;
}
// Example: Password authentication with PWD/PACK
static esp_err_t example_pwd_auth(rc522_handle_t rc522, rc522_picc_t *picc)
{
// Note both PWD and PACK can and should be programmed during initial
// PICC setup
esp_err_t ret = rc522_nxp_pwd_auth(scanner,
picc,
RC522_NXP_DEFAULT_PWD, // 4-byte password, default is 0xFFFFFFFF
RC522_NXP_DEFAULT_PACK, // 2-byte acknowledge, default is 0x0000
&picc->state // Ensure the card state is updated if successful
);
ESP_LOGI(TAG, "Password auth: R=%02X", ret);
return ret;
}
// Example: Counter reading
static esp_err_t example_read_cnt(rc522_handle_t rc522, rc522_picc_t *picc)
{
// Counters can only be read if they're enabled, otherwise the PICC will
// just respond with a NAK. On the NTAG21x, the single counter can be
// enabled in the config section, so read it and check if it's enabled.
// 0x83 for NTAG215; address will vary on other PICCs. Check your datasheet
uint8_t read_buf[16];
esp_err_t ret = rc522_nxp_read(scanner, picc, 0x83, read_buf);
ESP_LOGI(TAG, "Read config bytes: R=%02X", ret);
ESP_RETURN_ON_ERROR(ret, TAG, "Error in READ");
uint8_t count_en = (read_buf[4] >> 4) & 0x01;
ESP_LOGI(TAG, "NFC_CNT_EN bit: %hhu", count_en);
if (count_en) { // Avoid a NAK since then we need to wake the chip back up
uint32_t counter = 0;
// Counter argument is always 0x02 on NTAG21x; Ultralight PICCs with
// counters usually have 3 and the argument selects which to read
ret = rc522_nxp_read_cnt(scanner, picc, 0x02, &counter);
ESP_LOGI(TAG, "Read counter: R=%02X", ret);
ESP_RETURN_ON_ERROR(ret, TAG, "Error in READ_CNT");
ESP_LOGI(TAG, "Counter value: %lu", counter);
}
else {
ESP_LOGI(TAG, "Counter disabled; skipping");
}
return ESP_OK;
}
// Example: Reading the originality signature from the PICC
static esp_err_t example_get_signature(rc522_handle_t rc522, rc522_picc_t *picc)
{
// Signatures can be either 32 or 48 bytes - here we default to 48 and let
// the library fill as appropriate. The sig_size field is updated with the
// correct size
uint8_t sig_buf[48];
rc522_nxp_sig_t sig = { .bytes = sig_buf, .buffer_size = sizeof(sig_buf) };
esp_err_t ret = rc522_nxp_read_sig(scanner, picc, &sig);
ESP_LOGI(TAG, "Read signature: R=%02X", ret);
ESP_RETURN_ON_ERROR(ret, TAG, "Error in READ_SIG");
buf_to_hex(sig_buf, sig.sig_size, str_buf, 128);
ESP_LOGI(TAG, "Signature: %s", str_buf);
return ESP_OK;
}
static void on_picc_state_changed(void *arg, esp_event_base_t base, int32_t event_id, void *data)
{
rc522_picc_state_changed_event_t *event = (rc522_picc_state_changed_event_t *)data;
rc522_picc_t *picc = event->picc;
if (picc->state != RC522_PICC_STATE_ACTIVE) {
return;
}
if (picc->type != RC522_PICC_TYPE_MIFARE_UL) {
return;
}
// get_type MUST be called before rc522_nxp_* functions to ensure the PICC
// has been correctly identified
rc522_nxp_get_type(scanner, picc, &picc->type);
rc522_picc_print(picc);
example_page_counts(scanner, picc);
example_read_write(scanner, picc);
example_fast_read(scanner, picc);
example_pwd_auth(scanner, picc);
example_read_cnt(scanner, picc);
example_get_signature(scanner, picc);
esp_err_t ret = dump_memory(scanner, picc);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Memory dump failed: E=%02X", ret);
return;
}
ESP_LOGI(TAG, "Memory dump success");
}
void app_main()
{
rc522_spi_create(&driver_config, &driver);
rc522_driver_install(driver);
rc522_config_t scanner_config = {
.driver = driver,
};
rc522_create(&scanner_config, &scanner);
rc522_register_events(scanner, RC522_EVENT_PICC_STATE_CHANGED, on_picc_state_changed, NULL);
rc522_start(scanner);
}

View File

@@ -0,0 +1 @@
CONFIG_LOG_COLORS=y

View File

@@ -0,0 +1,8 @@
# For more information about build system see
# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html
# The following five lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(read_write)

View File

@@ -0,0 +1,2 @@
idf_component_register(SRCS "read_write.c"
INCLUDE_DIRS ".")

View File

@@ -0,0 +1,3 @@
dependencies:
abobija/rc522:
version: '*'

View File

@@ -0,0 +1,163 @@
#include <esp_log.h>
#include <esp_check.h>
#include <string.h>
#include <time.h>
#include <stdlib.h>
#include "rc522.h"
#include "driver/rc522_spi.h"
#include "picc/rc522_mifare.h"
static const char *TAG = "rc522-read-write-example";
#define RC522_SPI_BUS_GPIO_MISO (25)
#define RC522_SPI_BUS_GPIO_MOSI (23)
#define RC522_SPI_BUS_GPIO_SCLK (19)
#define RC522_SPI_SCANNER_GPIO_SDA (22)
#define RC522_SCANNER_GPIO_RST (-1) // soft-reset
static rc522_spi_config_t driver_config = {
.host_id = SPI3_HOST,
.bus_config = &(spi_bus_config_t){
.miso_io_num = RC522_SPI_BUS_GPIO_MISO,
.mosi_io_num = RC522_SPI_BUS_GPIO_MOSI,
.sclk_io_num = RC522_SPI_BUS_GPIO_SCLK,
},
.dev_config = {
.spics_io_num = RC522_SPI_SCANNER_GPIO_SDA,
},
.rst_io_num = RC522_SCANNER_GPIO_RST,
};
static rc522_driver_handle_t driver;
static rc522_handle_t scanner;
static void dump_block(uint8_t buffer[RC522_MIFARE_BLOCK_SIZE])
{
for (uint8_t i = 0; i < RC522_MIFARE_BLOCK_SIZE; i++) {
esp_log_write(ESP_LOG_INFO, TAG, "%02" RC522_X " ", buffer[i]);
}
esp_log_write(ESP_LOG_INFO, TAG, "\n");
}
static esp_err_t read_write(rc522_handle_t scanner, rc522_picc_t *picc)
{
const char *data_to_write = "rc522 is dope";
const uint8_t block_address = 4;
rc522_mifare_key_t key = {
.value = { RC522_MIFARE_KEY_VALUE_DEFAULT },
};
if (strlen(data_to_write) > 14) {
ESP_LOGW(TAG, "Please make sure that data length is no more than 14 characters");
ESP_LOGW(TAG, "since we are going to use random values for last two bytes");
return ESP_ERR_INVALID_ARG;
}
ESP_RETURN_ON_ERROR(rc522_mifare_auth(scanner, picc, block_address, &key), TAG, "auth fail");
uint8_t read_buffer[RC522_MIFARE_BLOCK_SIZE];
uint8_t write_buffer[RC522_MIFARE_BLOCK_SIZE];
// Read
ESP_LOGI(TAG, "Reading data from the block %d", block_address);
ESP_RETURN_ON_ERROR(rc522_mifare_read(scanner, picc, block_address, read_buffer), TAG, "read fail");
ESP_LOGI(TAG, "Current data:");
dump_block(read_buffer);
// ~Read
// Write
strncpy((char *)write_buffer, data_to_write, RC522_MIFARE_BLOCK_SIZE);
// Set random values for the last two bytes in the write buffer
// so we are using new data on each call of read_write function
int r = rand();
write_buffer[RC522_MIFARE_BLOCK_SIZE - 2] = ((r >> 8) & 0xFF);
write_buffer[RC522_MIFARE_BLOCK_SIZE - 1] = ((r >> 0) & 0xFF);
ESP_LOGI(TAG, "Writing data (%s) to the block %d:", data_to_write, block_address);
dump_block(write_buffer);
ESP_RETURN_ON_ERROR(rc522_mifare_write(scanner, picc, block_address, write_buffer), TAG, "write fail");
// ~Write
// Read again
ESP_LOGI(TAG, "Write done. Verifying...");
ESP_RETURN_ON_ERROR(rc522_mifare_read(scanner, picc, block_address, read_buffer), TAG, "read fail");
ESP_LOGI(TAG, "New data in the block %d:", block_address);
dump_block(read_buffer);
// ~Read again
// Validate
bool rw_missmatch = false;
uint8_t i;
for (i = 0; i < RC522_MIFARE_BLOCK_SIZE; i++) {
if (write_buffer[i] != read_buffer[i]) {
rw_missmatch = true;
break;
}
}
// ~Validate
// Feedback
if (!rw_missmatch) {
ESP_LOGI(TAG, "Verified.");
}
else {
ESP_LOGE(TAG,
"Write failed. RW missmatch on the byte %d (w:%02" RC522_X ", r:%02" RC522_X ")",
i,
write_buffer[i],
read_buffer[i]);
dump_block(write_buffer);
dump_block(read_buffer);
}
// ~Feedback
return ESP_OK;
}
static void on_picc_state_changed(void *arg, esp_event_base_t base, int32_t event_id, void *data)
{
rc522_picc_state_changed_event_t *event = (rc522_picc_state_changed_event_t *)data;
rc522_picc_t *picc = event->picc;
if (picc->state != RC522_PICC_STATE_ACTIVE) {
return;
}
rc522_picc_print(picc);
if (!rc522_mifare_type_is_classic_compatible(picc->type)) {
ESP_LOGW(TAG, "Card is not supported by this example");
return;
}
if (read_write(scanner, picc) == ESP_OK) {
ESP_LOGI(TAG, "Read/Write success");
}
else {
ESP_LOGE(TAG, "Read/Write failed");
}
if (rc522_mifare_deauth(scanner, picc) != ESP_OK) {
ESP_LOGW(TAG, "Deauth failed");
}
}
void app_main()
{
srand(time(NULL)); // Initialize random generator
rc522_spi_create(&driver_config, &driver);
rc522_driver_install(driver);
rc522_config_t scanner_config = {
.driver = driver,
};
rc522_create(&scanner_config, &scanner);
rc522_register_events(scanner, RC522_EVENT_PICC_STATE_CHANGED, on_picc_state_changed, NULL);
rc522_start(scanner);
}

View File

@@ -0,0 +1 @@
CONFIG_LOG_COLORS=y

View File

@@ -0,0 +1,28 @@
#pragma once
#include "driver/i2c_master.h"
#include "driver/i2c_types.h"
#include "rc522_driver.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct
{
i2c_master_bus_handle_t bus;
i2c_master_bus_config_t *bus_config; // Only required if `bus` is NULL
i2c_device_config_t dev_config;
/**
* GPIO number of the RC522 RST pin.
* Set to -1 if the RST pin is not connected.
*/
gpio_num_t rst_io_num;
} rc522_i2c_config_t;
esp_err_t rc522_i2c_create(const rc522_i2c_config_t *config, rc522_driver_handle_t *driver);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,32 @@
#pragma once
#include <driver/spi_master.h>
#include <driver/gpio.h>
#include "rc522_driver.h"
#ifdef __cplusplus
extern "C" {
#endif
#define RC522_SPI_WRITE (0)
#define RC522_SPI_READ (1)
typedef struct
{
spi_host_device_t host_id;
spi_bus_config_t *bus_config;
spi_device_interface_config_t dev_config;
spi_dma_chan_t dma_chan;
/**
* GPIO number of the RC522 RST pin.
* Set to -1 if the RST pin is not connected.
*/
gpio_num_t rst_io_num;
} rc522_spi_config_t;
esp_err_t rc522_spi_create(const rc522_spi_config_t *config, rc522_driver_handle_t *driver);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,193 @@
#pragma once
#include "rc522_types.h"
#include "rc522_picc.h"
#ifdef __cplusplus
extern "C" {
#endif
#define RC522_ERR_MIFARE_BASE (RC522_ERR_BASE + 0xFF)
#define RC522_ERR_MIFARE_ACCESS_BITS_INTEGRITY_VIOLATION (RC522_ERR_MIFARE_BASE + 1)
#define RC522_ERR_MIFARE_VALUE_BLOCK_INTEGRITY_VIOLATION (RC522_ERR_MIFARE_BASE + 2)
#define RC522_ERR_MIFARE_NACK (RC522_ERR_MIFARE_BASE + 3)
#define RC522_ERR_MIFARE_AUTHENTICATION_FAILED (RC522_ERR_MIFARE_BASE + 4)
#define RC522_ERR_MIFARE_SECTOR_TRAILER_WRITE_NOT_ALLOWED (RC522_ERR_MIFARE_BASE + 5)
#define RC522_MIFARE_SECTOR_INDEX_MAX (39) // No MIFARE Classic has more than 40 sectors
#define RC522_MIFARE_BLOCK_SIZE (16)
#define RC522_MIFARE_ACK (0x0A) // The MIFARE Classic uses a 4 bit ACK/NAK. Any other value than 0xA is NAK.
#define RC522_MIFARE_KEY_SIZE (6)
#define RC522_MIFARE_KEY_VALUE_DEFAULT 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
typedef struct
{
uint8_t number_of_sectors;
} rc522_mifare_desc_t;
typedef struct
{
uint8_t index; // Zero-based index of Sector
uint8_t number_of_blocks; // Total number of blocks inside of Sector
uint8_t block_0_address; // Zero-based index of the first Block inside of MIFARE memory
} rc522_mifare_sector_desc_t;
typedef enum
{
RC522_MIFARE_KEY_A = 0,
RC522_MIFARE_KEY_B,
} rc522_mifare_key_type_t;
typedef struct
{
rc522_mifare_key_type_t type;
uint8_t value[RC522_MIFARE_KEY_SIZE];
} rc522_mifare_key_t;
typedef enum
{
RC522_MIFARE_BLOCK_UNDEFINED = 0,
RC522_MIFARE_BLOCK_TRAILER,
RC522_MIFARE_BLOCK_DATA,
RC522_MIFARE_BLOCK_VALUE,
RC522_MIFARE_BLOCK_MANUFACTURER_DATA,
} rc522_mifare_block_type_t;
typedef struct
{
uint8_t c1 :1;
uint8_t c2 :1;
uint8_t c3 :1;
} rc522_mifare_access_bits_t;
typedef struct
{
rc522_mifare_access_bits_t access_bits[4]; // [3] = Trailer, [2] = Block 2, [1] = Block 1, [0] = Block 0
} rc522_mifare_sector_trailer_info_t;
typedef struct
{
int32_t value; // Value stored in the block
uint8_t addr; // Value block address (this is not the memory address)
} rc522_mifare_sector_value_block_info_t;
typedef struct
{
uint8_t address;
rc522_mifare_block_type_t type;
uint8_t bytes[RC522_MIFARE_BLOCK_SIZE];
union
{
rc522_mifare_sector_trailer_info_t trailer_info; // Valid only if type == RC522_MIFARE_BLOCK_TRAILER
rc522_mifare_sector_value_block_info_t value_info; // Valid only if type == RC522_MIFARE_BLOCK_VALUE
};
rc522_mifare_access_bits_t access_bits;
esp_err_t error; // e.g. acces bits or value block integrity violation
} rc522_mifare_sector_block_t;
// {{ MIFARE_Specific_Functions
/**
* @brief Authenticate a block read/write operations.
*
* After a successful authentication, communication between
* the PCD and the PICC is secured with MF1xxS20, MF1xxS70 or MF1xxS50 encryption.
*
* Once when read/write operations are authenticated for one block inside of a sector,
* read/write operations are authenticated for all blocks inside of that sector as well.
*
* @note After read/write operations are done, the PICC must be deauthenticated using @c rc522_mifare_deauth().
*
* @param rc522 RC522 handle
* @param picc PICC that is currently selected
* @param block_address Address of the block to authenticate
* @param key Key to authenticate with
*/
esp_err_t rc522_mifare_auth(
const rc522_handle_t rc522, const rc522_picc_t *picc, uint8_t block_address, const rc522_mifare_key_t *key);
/**
* @brief Read from a block at the given @c block_address on the PICC.
*
* @note The block must be authenticated before calling this function.
*
* @param rc522 RC522 handle
* @param picc PICC that is currently selected
* @param block_address Address of the block to read
* @param[out] out_buffer Buffer of exactly @c RC522_MIFARE_BLOCK_SIZE bytes to store the retrived data in
*/
esp_err_t rc522_mifare_read(const rc522_handle_t rc522, const rc522_picc_t *picc, uint8_t block_address,
uint8_t out_buffer[RC522_MIFARE_BLOCK_SIZE]);
/**
* @brief Write to a block at the given @c block_address on the PICC.
*
* @note The block must be authenticated before calling this function.
*
* @param rc522 RC522 handle
* @param picc PICC that is currently selected
* @param block_address Address of the block to write
* @param[in] buffer Buffer of exactly @c RC522_MIFARE_BLOCK_SIZE that contains the data to write
*/
esp_err_t rc522_mifare_write(const rc522_handle_t rc522, const rc522_picc_t *picc, uint8_t block_address,
const uint8_t buffer[RC522_MIFARE_BLOCK_SIZE]);
// }}
// {{ MIFARE_Utility_Functions
/**
* @brief Deauthenticates read/write operations and allows PCD to perform other commands
*
* @param rc522 RC522 handle
* @param picc PICC that is currently selected
*/
esp_err_t rc522_mifare_deauth(const rc522_handle_t rc522, const rc522_picc_t *picc);
/**
* @brief Authenticates read/write operations
*/
esp_err_t rc522_mifare_auth_sector(const rc522_handle_t rc522, const rc522_picc_t *picc,
const rc522_mifare_sector_desc_t *sector_desc, const rc522_mifare_key_t *key);
/**
* @brief Checks if the PICC is MIFARE Classic
*/
bool rc522_mifare_type_is_classic_compatible(rc522_picc_type_t type);
/**
* @brief Get MIFARE description (e.g number of sectors)
*/
esp_err_t rc522_mifare_get_desc(const rc522_picc_t *picc, rc522_mifare_desc_t *out_mifare_desc);
/**
* @brief Get MIFARE sector description (e.g number of blocks, block 0 address)
*/
esp_err_t rc522_mifare_get_sector_desc(uint8_t sector_index, rc522_mifare_sector_desc_t *out_sector_desc);
/**
* @brief Read and parse MIFARE sector trailer block
*/
esp_err_t rc522_mifare_read_sector_trailer_block(const rc522_handle_t rc522, const rc522_picc_t *picc,
const rc522_mifare_sector_desc_t *sector_desc, rc522_mifare_sector_block_t *out_trailer);
/**
* @brief Read and parse MIFARE sector (non-trailer) block
*/
esp_err_t rc522_mifare_read_sector_block(const rc522_handle_t rc522, const rc522_picc_t *picc,
const rc522_mifare_sector_desc_t *sector_desc, const rc522_mifare_sector_block_t *trailer, uint8_t block_offset,
rc522_mifare_sector_block_t *out_block);
esp_err_t rc522_mifare_get_number_of_sectors(rc522_picc_type_t type, uint8_t *out_result);
uint8_t rc522_mifare_get_sector_index_by_block_address(uint8_t block_address);
esp_err_t rc522_mifare_get_number_of_blocks_in_sector(uint8_t sector_index, uint8_t *out_result);
esp_err_t rc522_mifare_get_sector_block_0_address(uint8_t sector_index, uint8_t *out_result);
// }}
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,377 @@
/** Functions to support interfacing with NXP tags
*
* This header defines functions for working with ISO14443-3 compliant NXP
* PICCs, primarily the Ultralight and NTAG families. These PICCs use
* predominately the same command set, with some small variations.
*
* For MIFARE Classic PICCs, see `include/picc/rc522_mifare.h`.
*
* Authentication:
* Ultralight {C,AES}: Key-based (3DES and AES respectively)
* Ultralight EV1, NTAG 21x: Password-based
*
* Some functions have been commented out; ideally these would be implemented
* at some stage to implement full support for these PICC functions, but I don't
* have any of the actual PICCs to properly test with.
*
* CMD/PICC | UL | UL C | UL EV1 | UL AES | UL Nano | NTAG21x |
* -------------|---------|---------|---------|---------|---------|---------|
* GET_VERSION | | | y | y | y | y |
* READ | y | y | y | y | y | y |
* FAST_READ | | | y | y | | y |
* WRITE | y | y | y | y | y | y |
* COMPAT_WRITE | y | y | y | | y | y |
* READ_CNT | | | 3 | 3 | | 1 |
* INCR_CNT | | | y | y | | |
* READ_SIG | | | 32 | 48 | 32 | 32 | (bytes)
* WRITE_SIG | | | | y | y | |
* LOCK_SIG | | | | y | y | |
* AUTHENTICATE | | 3DES | | AES | | |
* PWD_AUTH | | | y | | | y |
* VCSL | | | y | y | | |
*/
#pragma once
#include "rc522_picc.h"
#include "rc522_types.h"
#ifdef __cplusplus
extern "C" {
#endif
#define RC522_ERR_NXP_BASE (RC522_ERR_BASE + 0XF0)
#define RC522_ERR_NXP_NACK (RC522_ERR_NXP_BASE + 1)
#define RC522_ERR_NXP_AUTHENTICATION_FAILED (RC522_ERR_NXP_BASE + 2)
#define RC522_ERR_NXP_PACK_MISMATCH (RC522_ERR_NXP_BASE + 3)
#define RC522_NXP_PWD_SIZE 4
#define RC522_NXP_PACK_SIZE 2
#define RC522_NXP_PAGE_SIZE 4
#define RC522_NXP_READ_SIZE (RC522_NXP_PAGE_SIZE * 4)
extern const uint8_t RC522_NXP_DEFAULT_PWD[RC522_NXP_PWD_SIZE];
extern const uint8_t RC522_NXP_DEFAULT_PACK[RC522_NXP_PACK_SIZE];
enum
{
/**
* Get PICC information.
*/
RC522_NXP_GET_VERSION = 0x60,
/**
* Read four pages from memory.
*/
RC522_NXP_READ = 0x30,
/**
* Read pages from START to END in memory.
*/
RC522_NXP_FAST_READ = 0x3A,
/**
* Write a page to memory.
*/
RC522_NXP_WRITE = 0xA2,
/**
* Write the first 4 bytes of 16 to a page in memory.
*/
RC522_NXP_COMPAT_WRITE = 0xA0,
/**
* Read an onboard counter.
*/
RC522_NXP_READ_CNT = 0x39,
/**
* Increment an onboard counter.
*/
RC522_NXP_INCR_CNT = 0xA5,
/**
* Read the originality signature.
*/
RC522_NXP_READ_SIG = 0x3C,
/**
* Write a new originality signature.
*/
RC522_NXP_WRITE_SIG = 0xA9,
/**
* Control access to the originality signature.
*/
RC522_NXP_LOCK_SIG = 0xAC,
/**
* Perform key-based authentication, stage 1.
*/
RC522_NXP_AUTHENTICATE = 0x1A,
/**
* Perform key-based authentication, stage 2.
*/
RC522_NXP_AUTHENTICATE_2 = 0xAF,
/**
* Perform password-based authentication.
*/
RC522_NXP_PWD_AUTH = 0x1B,
RC522_NXP_VCSL = 0x4B,
};
/**
* L3 GET_VERSION: product_type field
*/
typedef enum
{
RC522_NXP_PRODUCT_TYPE_UNKNOWN,
RC522_NXP_PRODUCT_TYPE_UL = 0x03,
RC522_NXP_PRODUCT_TYPE_NTAG = 0x04,
} rc522_nxp_product_type_t;
/**
* L3 GET_VERSION: major_version field
*/
typedef enum
{
RC522_NXP_MAJ_VER_UNKNOWN = 0,
RC522_NXP_MAJ_VER_NTAG21 = 0x01,
RC522_NXP_MAJ_VER_UL_EV1 = 0x01,
RC522_NXP_MAJ_VER_UL_NANO = 0x02,
RC522_NXP_MAJ_VER_UL_AES = 0x04,
} rc522_nxp_major_version_t;
/**
* L3 GET_VERSION result
*/
typedef struct
{
uint8_t header;
uint8_t vendor;
rc522_nxp_product_type_t product_type :8;
uint8_t product_subtype;
rc522_nxp_major_version_t major_version :8;
uint8_t minor_version;
uint8_t storage_size;
uint8_t protocol_type;
} rc522_nxp_picc_version_t;
/**
* L3 FAST_READ result
*
* Contains a buffer, the size of the buffer, and the amount of space occupied
* by the data after a read
*/
typedef struct
{
uint8_t *bytes;
uint8_t buffer_size;
uint8_t read_size;
} rc522_nxp_fast_read_data_t;
/**
* Variable-length protection signature
*
* UL EV1: 32 bytes
* UL AES: 48 bytes
* UL Nano: 32 bytes
* NTAG213/215/216: 32 bytes
*/
typedef struct
{
uint8_t *bytes;
uint8_t buffer_size;
uint8_t sig_size;
} rc522_nxp_sig_t;
/**
* @brief Determine the total memory size of an NXP PICC
*
* Given an NXP PICC type, returns the total size of the memory in pages. This
* is not equivalent to generic user memory.
*
* For invalid types, unknown types, or types without pages, returns 0.
*
* @sa rc522_nxp_get_user_page_count()
*/
uint8_t rc522_nxp_get_page_count(rc522_picc_type_t type);
/**
* @brief Determine the user memory size of an NXP PICC
*
* Given an NXP PICC type (RC522_PICC_TYPE_MIFARE_UL_* or RC522_PICC_TYPE_NTAG*),
* returns the total size of the user memory in pages. The user memory always
* starts at page 4.
*
* For invalid types, unknown types, or types without pages, returns 0.
*/
uint8_t rc522_nxp_get_user_page_count(rc522_picc_type_t type);
/**
* @brief Determine the start page of user memory for an NXP PICC
*
* Currently this is 0x04 for all PICCs. It's unlikely to change in future
* for compatibility reasons, but this function exists in case (and also
* as a reference).
*
* For invalid or unknown types, returns 0.
*/
uint8_t rc522_nxp_get_user_mem_start(rc522_picc_type_t type);
/**
* @brief Determine the final page of user memory for an NXP PICC
*
* Returns the address of the final page of user memory for a given PICC
* type. This is the last page that can be safely written to without changing
* the configuration of the PICC.
*
* For invalid or unknown types, returns 0.
*/
uint8_t rc522_nxp_get_user_mem_end(rc522_picc_type_t type);
/**
* @brief Determine the type of an NXP PICC
*
* The Ultralight and NTAG families both use SAK=0, and need further processing
* to identify. This function attempts to determine the PICC in use following
* the procedure in NXP Application Note 10833.
*/
esp_err_t rc522_nxp_get_type(const rc522_handle_t rc522, const rc522_picc_t *picc, rc522_picc_type_t *out_type);
/**
* @brief NXP Level 3 GET_VERSION
*
* If supported, returns information about the PICC, including manufacturer,
* product, and memory size.
*/
esp_err_t rc522_nxp_get_version(
const rc522_handle_t rc522, const rc522_picc_t *picc, rc522_nxp_picc_version_t *out_version);
/**
* @brief NXP Level 3 READ
*
* Reads 4 pages (16 bytes) from a given page address on the PICC.
*/
esp_err_t rc522_nxp_read(
const rc522_handle_t rc522, const rc522_picc_t *picc, uint8_t address, uint8_t out_buffer[RC522_NXP_PAGE_SIZE * 4]);
/**
* NXP Level 3 FAST_READ
*
* If supported, allows a variable number of pages to be read at once, instead
* of the fixed 4 in READ.
*
* @param start Page address to start reading from
* @param end Page address to end reading (inclusive)
* @param out_buffer Output buffer; should be at least (end-start+1) * 4 bytes
*
* Supported PICCs: UL EV1, UL AES, NTAG21x
*/
esp_err_t rc522_nxp_fast_read(const rc522_handle_t rc522, const rc522_picc_t *picc, uint8_t start, uint8_t end,
rc522_nxp_fast_read_data_t *out_buffer);
/**
* @brief NXP Level 3 WRITE
*
* Writes 4 bytes of data to a single page.
*/
esp_err_t rc522_nxp_write(
const rc522_handle_t rc522, const rc522_picc_t *picc, uint8_t address, const uint8_t buffer[RC522_NXP_PAGE_SIZE]);
/**
* @brief NXP Level 3 READ_CNT
*
* If supported, reads a counter from the PICC. Some PICCs have more than one
* counter; if so counter_no specifies the counter to address.
*
* Supported PICCs: UL EV1 (3), UL AES (3), NTAG21x (1)
*/
esp_err_t rc522_nxp_read_cnt(
const rc522_handle_t rc522, const rc522_picc_t *picc, uint8_t counter_no, uint32_t *out_count);
/**
* @brief NXP Level 3 INCR_CNT
*
* If supported, increments a counter on the PICC. If the PICC has more than one
* counter, counter_no specifies the counter to target.
*
* TODO
*/
// esp_err_t rc522_nxp_incr_cnt(const rc522_handle_t rc522, const rc522_picc_t *picc,
// uint8_t counter_no);
/**
* @brief Checks for support for AUTHENTICATE in L3 on a PICC
*
* Used to determine if a chip supports AUTHENTICATE, to differentiate between
* Ultralight and Ultralight C. Does not perform actual authentication.
*/
esp_err_t rc522_nxp_keyauth_supported(const rc522_handle_t rc522, const rc522_picc_t *picc);
/**
* @brief NXP Level 3 AUTHENTICATE
*
* If supported, performs key-based authentication with a PICC. Note support
* for DES and/or AES algorithms is required.
*
* TODO
*/
// esp_err_t rc522_nxp_key_auth(const rc522_handle_t rc522, const rc522_picc_t *picc,
// rc522_nxp_key_t *key);
/**
* @brief NXP Level 3 PWD_AUTH
*
* If supported, performs password-based authentication with a PICC.
*
* @param pwd Password to send to pick
* @param pack Password ACKnowledgement expected to be returned by PICC
* @param[out] out_state PICC state on return. Correct PWD with mismatched PACK
* will still result in AUTHENTICATED
*
* Supported PICCs: UL EV1, NTAG21x
*/
esp_err_t rc522_nxp_pwd_auth(const rc522_handle_t rc522, const rc522_picc_t *picc,
const uint8_t pwd[RC522_NXP_PWD_SIZE], const uint8_t pack[RC522_NXP_PACK_SIZE], rc522_picc_state_t *out_state);
/**
* @brief NXP Level 3 READ_SIG
*
* If supported, reads the ECC signature from the PICC. This signature can
* be used to verify originality. Does not actually perform verification.
*
* TODO: Implement verification per AN11350?
*
* Supported PICCs: UL EV1, UL AES, UL Nano, NTAG21x
*/
esp_err_t rc522_nxp_read_sig(const rc522_handle_t rc522, const rc522_picc_t *picc, rc522_nxp_sig_t *out_sig);
/**
* @brief NXP Level 3 WRITE_SIG
*
* If supported, writes a new signature to the PICC.
*
* TODO
*/
// esp_err_t rc522_nxp_write_sig(const rc522_handle_t rc522, const rc522_picc_t *picc,
// rc522_nxp_sig_t *sig);
/**
* @brief NXP Level 3 LOCK_SIG
*
* If supported, controls the originality signature lock on the PICC.
*
* @param argument 0x00 for unlock, 0x01 for lock, 0x02 for permanent lock
*
* TODO
*/
// esp_err_t rc522_nxp_lock_sig(const rc522_handle_t rc522, const rc522_picc_t *picc,
// rc522_nxp_sig_arg_t argument);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,26 @@
#pragma once
#include "rc522_types.h"
#ifdef __cplusplus
extern "C" {
#endif
ESP_EVENT_DECLARE_BASE(RC522_EVENTS);
esp_err_t rc522_create(const rc522_config_t *config, rc522_handle_t *out_rc522);
esp_err_t rc522_register_events(
const rc522_handle_t rc522, rc522_event_t event, esp_event_handler_t event_handler, void *event_handler_arg);
esp_err_t rc522_unregister_events(const rc522_handle_t rc522, rc522_event_t event, esp_event_handler_t event_handler);
esp_err_t rc522_start(rc522_handle_t rc522);
esp_err_t rc522_pause(rc522_handle_t rc522);
esp_err_t rc522_destroy(rc522_handle_t rc522);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,15 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
typedef struct rc522_driver_handle *rc522_driver_handle_t;
esp_err_t rc522_driver_install(const rc522_driver_handle_t driver);
esp_err_t rc522_driver_uninstall(const rc522_driver_handle_t driver);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,33 @@
#pragma once
#include "rc522_types.h"
#ifdef __cplusplus
extern "C" {
#endif
enum // RC522_PCD_RF_CFG_REG
{
RC522_PCD_RX_GAIN_2_BIT = BIT6,
RC522_PCD_RX_GAIN_1_BIT = BIT5,
RC522_PCD_RX_GAIN_0_BIT = BIT4,
};
/**
* Receiver (antenna) gain
*/
typedef enum
{
RC522_PCD_18_DB_RX_GAIN = (RC522_PCD_RX_GAIN_1_BIT), /* 18 dB */
RC522_PCD_23_DB_RX_GAIN = (RC522_PCD_RX_GAIN_1_BIT | RC522_PCD_RX_GAIN_0_BIT), /* 23 dB */
RC522_PCD_33_DB_RX_GAIN = (RC522_PCD_RX_GAIN_2_BIT), /* 33 dB */
RC522_PCD_38_DB_RX_GAIN = (RC522_PCD_RX_GAIN_2_BIT | RC522_PCD_RX_GAIN_0_BIT), /* 38 dB */
RC522_PCD_43_DB_RX_GAIN = (RC522_PCD_RX_GAIN_2_BIT | RC522_PCD_RX_GAIN_1_BIT), /* 43 dB */
RC522_PCD_48_DB_RX_GAIN = (RC522_PCD_RX_GAIN_2_BIT | RC522_PCD_RX_GAIN_1_BIT | RC522_PCD_RX_GAIN_0_BIT), /* 48 dB */
} rc522_pcd_rx_gain_t;
esp_err_t rc522_pcd_set_rx_gain(const rc522_handle_t rc522, rc522_pcd_rx_gain_t gain);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,170 @@
#pragma once
#include "rc522_types.h"
#ifdef __cplusplus
extern "C" {
#endif
#define RC522_PICC_UID_SIZE_MAX (10)
#define RC522_PICC_UID_SIZE_MIN (4)
#define RC522_PICC_UID_STR_BUFFER_SIZE_MAX (RC522_PICC_UID_SIZE_MAX * 3)
typedef struct
{
uint8_t value[RC522_PICC_UID_SIZE_MAX];
uint8_t length;
} rc522_picc_uid_t;
typedef enum
{
RC522_PICC_TYPE_UNKNOWN = -1,
RC522_PICC_TYPE_UNDEFINED = 0,
RC522_PICC_TYPE_ISO_14443_4, // PICC compliant with ISO/IEC 14443-4
RC522_PICC_TYPE_ISO_18092, // PICC compliant with ISO/IEC 18092 (NFC)
RC522_PICC_TYPE_MIFARE_MINI, // MIFARE Classic protocol, 320 bytes
RC522_PICC_TYPE_MIFARE_1K, // MIFARE Classic protocol, 1KB
RC522_PICC_TYPE_MIFARE_4K, // MIFARE Classic protocol, 4KB
RC522_PICC_TYPE_MIFARE_UL, // MIFARE Ultralight or Ultralight C
RC522_PICC_TYPE_MIFARE_PLUS, // MIFARE Plus
RC522_PICC_TYPE_MIFARE_DESFIRE, // MIFARE DESFire
RC522_PICC_TYPE_TNP3XXX, // Only mentioned in NXP AN 10833 MIFARE Type Identification Procedure
RC522_PICC_TYPE_MIFARE_UL_, // Ultralight MF0ICU1; 16x 4-byte pages
RC522_PICC_TYPE_MIFARE_UL_C, // Ultralight C MF0ICU2; 48x 4-byte pages
RC522_PICC_TYPE_MIFARE_UL_EV1_1, // Ultralight EV1 MF0UL11; 20x 4-byte pages
RC522_PICC_TYPE_MIFARE_UL_EV1_2, // Ultralight EV1 MF0UL21; 41x 4-byte pages
RC522_PICC_TYPE_MIFARE_UL_NANO, // Ultralight NANO; 40 bytes (probably 10x 4-byte pages)
RC522_PICC_TYPE_MIFARE_UL_AES, // Ultralight AES MF0AES(H)20; 60x 4-byte pages
RC522_PICC_TYPE_NTAG2xx, // NTAG 2xx; further information unknown
RC522_PICC_TYPE_NTAG213, // NTAG213; 45x 4-byte pages
RC522_PICC_TYPE_NTAG215, // NTAG215; 135x 4-byte pages
RC522_PICC_TYPE_NTAG216, // NTAG216; 231x 4-byte pages
} rc522_picc_type_t;
typedef enum
{
/**
* Not used! Ignore this one.
*
* In the POWER-OFF State, the PICC is not powered
* due to a lack of carrier energy.
*/
RC522_PICC_STATE_POWER_OFF = -1,
/**
* PICC is maybe in the field.
*
* In the IDLE State, the PICC shall recognize REQA and WUPA Commands.
* The PICC enters the READY State after it has received a valid
* REQA or WUPA Command and transmitted its ATQA.
*/
RC522_PICC_STATE_IDLE = 0,
/**
* In the READY State, either the bit frame anticollision or a proprietary
* anticollision method can be applied. Cascade levels are handled inside
* this state to get the complete UID.
*
* The PICC enters the ACTIVE State when it is selected with its complete UID.
*/
RC522_PICC_STATE_READY,
/**
* PICC is in the field.
*
* In the ACTIVE State, the PICC listens to any higher layer message.
* The PICC enters the HALT State when a valid HLTA Command is received.
*
* @note
* In the higher layer protocol, specific commands may be defined
* to return the PICC to its HALT State.
*/
RC522_PICC_STATE_ACTIVE,
/**
* In the HALT State, the PICC shall respond only to a WUPA Command.
*
* The PICC enters the READY* state after it has received a valid
* WUPA Command and transmitted its ATQA.
*/
RC522_PICC_STATE_HALT,
/**
* READY* State
*
* PICC is woken from HALT state by WUPA command.
*
* The READY* State is similar to the READY State, either the bit frame
* anticollision or a proprietary anticollision method can be applied.
* Cascade levels are handled inside this state to get complete UID.
*
* The PICC enters the ACTIVE* State when it is selected with its complete UID.
*/
RC522_PICC_STATE_READY_H,
/**
* ACTIVE* State
*
* PICC is still in the field. PICC enters this state
* when it is SELECTED in the READY* state.
*
* The ACTIVE* State is similar to the ACTIVE State, the PICC is selected and
* listens to any higher layer message.
*
* The PICC enters the HALT State when a valid HLTA Command is received.
*/
RC522_PICC_STATE_ACTIVE_H,
/**
* AUTHENTICATED State
*
* PICC has been authenticated, via either keys or passwords, and accessing
* protected areas is now possible. This is a Level 3 extension on some NXP
* PICCs.
*/
RC522_PICC_STATE_AUTHENTICATED,
} rc522_picc_state_t;
/**
* All RFU bits shall be set to zero.
*/
typedef struct
{
uint16_t source;
uint8_t rfu4 :4;
uint8_t prop_coding :4;
uint8_t uid_size :2; // 00b - single (4 bytes), 01b - double (7 bytes), 10b - triple (10 bytes)
uint8_t rfu1 :1;
uint8_t anticollision :5;
} rc522_picc_atqa_desc_t;
typedef struct
{
rc522_picc_atqa_desc_t atqa;
rc522_picc_uid_t uid;
uint8_t sak;
rc522_picc_type_t type;
rc522_picc_state_t state;
} rc522_picc_t;
typedef struct
{
rc522_picc_state_t old_state;
rc522_picc_t *picc;
} rc522_picc_state_changed_event_t;
char *rc522_picc_type_name(rc522_picc_type_t type);
/**
* @brief Convert PICC UID to (null-terminated) string
*/
esp_err_t rc522_picc_uid_to_str(const rc522_picc_uid_t *uid, char *buffer, uint8_t buffer_size);
/**
* @brief Print PICC information in a fancy way
*/
esp_err_t rc522_picc_print(const rc522_picc_t *picc);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,52 @@
#pragma once
#include <esp_err.h>
#include <esp_event.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "rc522_driver.h"
#include "rc522_picc.h"
#ifdef __cplusplus
extern "C" {
#endif
#define RC522_X "X"
#define RC522_ERR_BASE (0xF522)
#define RC522_ERR_COLLISION (RC522_ERR_BASE + 1)
#define RC522_ERR_COLLISION_UNSOLVABLE (RC522_ERR_BASE + 2)
#define RC522_ERR_CRC_WRONG (RC522_ERR_BASE + 3)
#define RC522_ERR_INVALID_ATQA (RC522_ERR_BASE + 4)
#define RC522_ERR_RX_TIMEOUT (RC522_ERR_BASE + 5)
#define RC522_ERR_RX_TIMER_TIMEOUT (RC522_ERR_BASE + 6)
#define RC522_ERR_INVALID_SAK (RC522_ERR_BASE + 7)
#define RC522_ERR_PICC_POST_HEARTBEAT_MISSMATCH (RC522_ERR_BASE + 8)
#define RC522_ERR_PCD_FIFO_BUFFER_OVERFLOW (RC522_ERR_BASE + 9)
#define RC522_ERR_PCD_PARITY_CHECK_FAILED (RC522_ERR_BASE + 10)
#define RC522_ERR_PCD_PROTOCOL_ERROR (RC522_ERR_BASE + 11)
#define RC522_ERR_RST_PIN_UNUSED (RC522_ERR_BASE + 12)
#define RC522_ERR_PCD_FIFO_EMPTY (RC522_ERR_BASE + 13)
#define RC522_ERR_HLTA_NOT_ACKED (RC522_ERR_BASE + 14)
typedef struct rc522 *rc522_handle_t;
typedef struct
{
rc522_driver_handle_t driver;
uint16_t poll_interval_ms; /*<! Delay (in milliseconds) between polls */
size_t task_stack_size; /*<! Stack size of rc522 task */
uint8_t task_priority; /*<! Priority of rc522 task */
SemaphoreHandle_t task_mutex; /*<! Mutex for rc522 task */
} rc522_config_t;
typedef enum
{
RC522_EVENT_ANY = ESP_EVENT_ANY_ID,
RC522_EVENT_NONE,
RC522_EVENT_PICC_STATE_CHANGED,
} rc522_event_t;
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,42 @@
#include <driver/gpio.h>
#include "rc522_types_internal.h"
#include "rc522_driver.h"
#define RC522_DRIVER_HARD_RST_PIN_PWR_DOWN_LEVEL (0)
#define RC522_DRIVER_HARD_RST_PULSE_DURATION_MS (15)
typedef esp_err_t (*rc522_driver_install_handler_t)(const rc522_driver_handle_t driver);
typedef esp_err_t (*rc522_driver_send_handler_t)(
const rc522_driver_handle_t driver, uint8_t address, const rc522_bytes_t *bytes);
typedef esp_err_t (*rc522_driver_receive_handler_t)(
const rc522_driver_handle_t driver, uint8_t address, rc522_bytes_t *bytes);
typedef esp_err_t (*rc522_driver_reset_handler_t)(const rc522_driver_handle_t driver);
typedef esp_err_t (*rc522_driver_uninstall_handler_t)(const rc522_driver_handle_t driver);
struct rc522_driver_handle
{
void *config;
void *device;
void *meta; /* <! Additional (driver specific) data */
rc522_driver_install_handler_t install;
rc522_driver_send_handler_t send;
rc522_driver_receive_handler_t receive;
rc522_driver_reset_handler_t reset;
rc522_driver_uninstall_handler_t uninstall;
};
esp_err_t rc522_driver_init_rst_pin(gpio_num_t rst_io_num);
esp_err_t rc522_driver_create(const void *config, size_t config_size, rc522_driver_handle_t *driver);
esp_err_t rc522_driver_send(const rc522_driver_handle_t driver, uint8_t address, const rc522_bytes_t *bytes);
esp_err_t rc522_driver_receive(const rc522_driver_handle_t driver, uint8_t address, rc522_bytes_t *bytes);
esp_err_t rc522_driver_reset(const rc522_driver_handle_t driver);
esp_err_t rc522_driver_destroy(rc522_driver_handle_t driver);

View File

@@ -0,0 +1,21 @@
#pragma once
#include <esp_err.h>
#include <inttypes.h>
#ifdef __cplusplus
extern "C" {
#endif
uint32_t rc522_millis();
void rc522_delay_ms(uint32_t ms);
esp_err_t rc522_buffer_to_hex_str(
const uint8_t *buffer, uint8_t buffer_length, char *str_buffer, uint8_t str_buffer_length);
esp_err_t rc522_nibbles(uint8_t byte, uint8_t *msb, uint8_t *lsb);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,15 @@
#pragma once
#include "rc522.h"
#ifdef __cplusplus
extern "C" {
#endif
void rc522_task(void *arg);
esp_err_t rc522_dispatch_event(const rc522_handle_t rc522, rc522_event_t event, const void *data, size_t data_size);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,448 @@
#pragma once
#include "rc522_types_internal.h"
#include "rc522_pcd.h"
#ifdef __cplusplus
extern "C" {
#endif
#define RC522_PCD_MOD_WIDTH_REG_RESET_VALUE (38)
#define RC522_PCD_TX_MODE_REG_RESET_VALUE (0x00)
#define RC522_PCD_RX_MODE_REG_RESET_VALUE (0x00)
typedef enum
{
// Starts and stops command execution
RC522_PCD_COMMAND_REG = 0x01,
// Communication Interrupt Enable Register.
// Control bits to enable and disable the passing of interrupt requests
RC522_PCD_COM_INT_EN_REG = 0x02,
// Diverted Interrupt Enable Register.
// Control bits to enable and disable the passing of interrupt requests
RC522_PCD_DIV_INT_EN_REG = 0x03,
// Communication Interrupt request bits
RC522_PCD_COM_INT_REQ_REG = 0x04,
// Diverted Interrupt request bits
RC522_PCD_DIV_INT_REQ_REG = 0x05,
// Error bits showing the error status of the last command executed
RC522_PCD_ERROR_REG = 0x06,
// Contains status bits of the receiver, transmitter and data mode detector
RC522_PCD_STATUS_2_REG = 0x08,
// Input and output of 64 byte FIFO buffer
RC522_PCD_FIFO_DATA_REG = 0x09,
// Number of bytes stored in the FIFO buffer
RC522_PCD_FIFO_LEVEL_REG = 0x0A,
// Shows the MFRC522 software version
RC522_PCD_VERSION_REG = 0x37,
// Controls the logical behavior of the antenna driver pins TX1 and TX2
RC522_PCD_TX_CONTROL_REG = 0x14,
// Configures the receiver gain
RC522_PCD_RF_CFG_REG = 0x26,
// Miscellaneous control register
RC522_PCD_CONTROL_REG = 0x0C,
// Adjustments for bit-oriented frames
RC522_PCD_BIT_FRAMING_REG = 0x0D,
// Defines the first bit-collision detected on the RF interface
RC522_PCD_COLL_REG = 0x0E,
// MSB (higher bits) values of the CRC calculation
RC522_PCD_CRC_RESULT_MSB_REG = 0x21,
// LSB (lower bits) values of the CRC calculation
RC522_PCD_CRC_RESULT_LSB_REG = 0x22,
// Sets the modulation width
RC522_PCD_MOD_WIDTH_REG = 0x24,
// Defines the mode of the timer
RC522_PCD_TIMER_MODE_REG = 0x2A,
// Defines the timer prescaler settings
RC522_PCD_TIMER_PRESCALER_REG = 0x2B,
// MSB (higher bits) value of 16-bit timer reload value
RC522_PCD_TIMER_RELOAD_MSB_REG = 0x2C,
// LSB (lower bits) value of 16-bit timer reload value
RC522_PCD_TIMER_RELOAD_LSB_REG = 0x2D,
// Defines general modes for transmitting and receiving
RC522_PCD_MODE_REG = 0x11,
// Controls the setting of the transmission modulation
RC522_PCD_TX_ASK_REG = 0x15,
// Defines the data rate during transmission
RC522_PCD_TX_MODE_REG = 0x12,
// Defines the data rate during reception
RC522_PCD_RX_MODE_REG = 0x13,
} rc522_pcd_register_t;
enum // RC522_PCD_COLL_REG
{
/**
* all received bits will be cleared after a collision
* only used during bitwise anticollision at 106 kBd,
* otherwise it is set to logic 1
*/
RC522_PCD_VALUES_AFTER_COLL_BIT = BIT7,
/**
* no collision detected or the position of the collision is
* out of the range of CollPos[4:0]
*/
RC522_PCD_COLL_POS_NOT_VALID_BIT = BIT5,
};
enum // RC522_PCD_ERROR_REG
{
/**
* data is written into the FIFO buffer by the host during the MFAuthent
* command or if data is written into the FIFO buffer by the host during the
* time between sending the last bit on the RF interface and receiving the
* last bit on the RF interface
*/
RC522_PCD_WR_ERR_BIT = BIT6,
/**
* internal temperature sensor detects overheating, in which case the
* antenna drivers are automatically switched off
*/
RC522_PCD_TEMP_ERR_BIT = BIT5,
/**
* the host or a MFRC522’s internal state machine (e.g. receiver) tries to
* write data to the FIFO buffer even though it is already full
*/
RC522_PCD_BUFFER_OVFL_BIT = BIT4,
/**
* a bit-collision is detected
* cleared automatically at receiver start-up phase
* only valid during the bitwise anticollision at 106 kBd
* always set to logic 0 during communication protocols at 212 kBd,
* 424 kBd and 848 kBd
*/
RC522_PCD_COLL_ERR_BIT = BIT3,
/**
* the RxModeReg register’s RxCRCEn bit is set and the CRC calculation fails
* automatically cleared to logic 0 during receiver start-up phase
*/
RC522_PCD_CRC_ERR_BIT = BIT2,
/**
* parity check failed
* automatically cleared during receiver start-up phase
* only valid for ISO/IEC 14443 A/MIFARE communication at 106 kBd
*/
RC522_PCD_PARITY_ERR_BIT = BIT1,
/**
* set to logic 1 if the SOF is incorrect
* automatically cleared during receiver start-up phase
* bit is only valid for 106 kBd
* during the MFAuthent command, the ProtocolErr bit is set to logic 1 if the
* number of bytes received in one data stream is incorrect
*/
RC522_PCD_PROTOCOL_ERR_BIT = BIT0,
};
enum // RC522_PCD_COM_INT_REQ_REG
{
/**
* 1 - indicates that the marked bits in the ComIrqReg register are set
* 0 - indicates that the marked bits in the ComIrqReg register are cleared
*/
RC522_PCD_SET_1_BIT = BIT7,
/**
* set immediately after the last bit of the transmitted data was sent out
*/
RC522_PCD_TX_IRQ_BIT = BIT6,
/**
* receiver has detected the end of a valid data stream
* if the RxModeReg register’s RxNoErr bit is set to logic 1, the RxIRq bit is
* only set to logic 1 when data bytes are available in the FIFO
*/
RC522_PCD_RX_IRQ_BIT = BIT5,
/**
* If a command terminates, for example, when the CommandReg changes
* its value from any command to the Idle command (see Table 149 on page 70)
* if an unknown command is started, the CommandReg register
* Command[3:0] value changes to the idle state and the IdleIRq bit is set
* The microcontroller starting the Idle command does not set the IdleIRq bit
*/
RC522_PCD_IDLE_IRQ_BIT = BIT4,
/**
* the Status1Reg register’s HiAlert bit is set
* in opposition to the HiAlert bit, the HiAlertIRq bit stores this event and
* can only be reset as indicated by the Set1 bit in this register
*/
RC522_PCD_HI_ALERT_IRQ_BIT = BIT3,
/**
* Status1Reg register’s LoAlert bit is set
* in opposition to the LoAlert bit, the LoAlertIRq bit stores this event and
* can only be reset as indicated by the Set1 bit in this register
*/
RC522_PCD_LO_ALERT_IRQ_BIT = BIT2,
/**
* any error bit in the ErrorReg register is set
*/
RC522_PCD_ERR_IRQ_BIT = BIT1,
/**
* the timer decrements the timer value in register TCounterValReg to zero
*/
RC522_PCD_TIMER_IRQ_BIT = BIT0,
};
enum // RC522_PCD_COMMAND_REG
{
// Soft power-down mode entered
RC522_PCD_POWER_DOWN_BIT = BIT4,
};
enum // RC522_PCD_TX_CONTROL_REG
{
// Output signal on pin TX2 delivers the 13.56 MHz energy carrier modulated by the transmission data
RC522_PCD_TX2_RF_EN_BIT = BIT1,
// Output signal on pin TX1 delivers the 13.56 MHz energy carrier modulated by the transmission data
RC522_PCD_TX1_RF_EN_BIT = BIT0,
};
typedef enum
{
/**
* Places the MFRC522 in Idle mode. The Idle command also terminates itself.
*/
RC522_PCD_IDLE_CMD = 0b0000,
/**
* The FIFO buffer content is transferred to the CRC coprocessor and the CRC calculation is
* started. The calculation result is stored in the CRCResultReg register. The CRC
* calculation is not limited to a dedicated number of bytes. The calculation is not stopped
* when the FIFO buffer is empty during the data stream. The next byte written to the FIFO
* buffer is added to the calculation.
*
* The CRC preset value is controlled by the ModeReg register’s CRCPreset[1:0] bits. The
* value is loaded in to the CRC coprocessor when the command starts.
*
* This command must be terminated by writing a command to the CommandReg register,
* such as, the Idle command.
*
* If the AutoTestReg register’s SelfTest[3:0] bits are set correctly, the MFRC522 enters Self
* Test mode. Starting the CalcCRC command initiates a digital self test. The result of the
* self test is written to the FIFO buffer.
*/
RC522_PCD_CALC_CRC_CMD = 0b0011,
/**
* This command continuously repeats the transmission of data from the FIFO buffer and the
* reception of data from the RF field. The first action is transmit and after transmission the
* command is changed to receive a data stream.
*
* Each transmit process must be started by setting the BitFramingReg register’s StartSend
* bit to logic 1. This command must be cleared by writing any command to the
* CommandReg register
*/
RC522_PCD_TRANSCEIVE_CMD = 0b1100,
/**
* This command manages MIFARE authentication to enable a secure communication to
* any MIFARE Mini, MIFARE 1K and MIFARE 4K card
*/
RC522_PCD_MF_AUTH_CMD = 0b1110,
/**
* This command performs a reset of the device. The configuration data of the internal buffer
* remains unchanged. All registers are set to the reset values. This command automatically
* terminates when finished.
*/
RC522_PCD_SOFT_RESET_CMD = 0b1111,
} rc522_pcd_command_t;
enum // RC522_PCD_TIMER_MODE_REG
{
/**
* When 1:
* - Timer starts automatically at the end of the transmission in
* all communication modes at all speeds
* - If the RxModeReg register’s RxMultiple bit is not set, the
* timer stops immediately after receiving the 5th bit (1 start bit,
* 4 data bits)
* - If the RxMultiple bit is set to logic 1 the timer never stops, in
* which case the timer can be stopped by setting the
* ControlReg register’s TStopNow bit to logic 1
*
* When 0:
* - Indicates that the timer is not influenced by the protocol
*/
RC522_PCD_T_AUTO_BIT = BIT7,
};
enum // RC522_PCD_TX_ASK_REG
{
/**
* Forces a 100 % ASK modulation independent of the ModGsPReg register setting
*/
RC522_PCD_FORCE_100_ASK_BIT = BIT6,
};
enum // RC522_PCD_MODE_REG
{
// Transmitter can only be started if an RF field is generated
RC522_PCD_TX_WAIT_RF_BIT = BIT5,
/**
* Defines the polarity of pin MFIN
*
* 1 - Polarity of pin MFIN is active HIGH
* 0 - Polarity of pin MFIN is active LOW
*/
RC522_PCD_POL_MFIN_BIT = BIT3,
RC522_PCD_CRC_PRESET_1_BIT = BIT1,
RC522_PCD_CRC_PRESET_0_BIT = BIT0,
};
typedef enum
{
RC522_PCD_CRC_PRESET_0000H = (0x00), /* 0000h */
RC522_PCD_CRC_PRESET_6363H = (RC522_PCD_CRC_PRESET_0_BIT), /* 6363h */
RC522_PCD_CRC_PRESET_A671H = (RC522_PCD_CRC_PRESET_1_BIT), /* A671h */
RC522_PCD_CRC_PRESET_FFFFH = (RC522_PCD_CRC_PRESET_1_BIT | RC522_PCD_CRC_PRESET_0_BIT), /* FFFFh */
} rc522_pcd_crc_preset_value_t;
enum // RC522_PCD_DIV_INT_REQ_REG
{
// The CalcCRC command is active and all data is processed (CRC calculation is done)
RC522_PCD_CRC_IRQ_BIT = BIT2,
};
enum // RC522_PCD_FIFO_LEVEL_REG
{
/**
* Immediately clears the internal FIFO buffer’s read and write pointer
* and ErrorReg register’s BufferOvfl bit
*
* Reading this bit always returns 0
*/
RC522_PCD_FLUSH_BUFFER_BIT = BIT7,
};
enum // RC522_PCD_BIT_FRAMING_REG
{
/**
* Starts the transmission of data
* Only valid in combination with the Transceive command
*/
RC522_PCD_START_SEND_BIT = BIT7,
};
enum // RC522_PCD_STATUS_2_REG
{
/**
* Indicates that the MIFARE Crypto1 unit is switched on and
* therefore all data communication with the card is encrypted
* can only be set to logic 1 by a successful execution of the
* MFAuthent command.
* Only valid in Read/Write mode for MIFARE standard cards.
* This bit is cleared by software.
*/
RC522_PCD_MF_CRYPTO1_ON_BIT = BIT3,
};
enum // RC522_PCD_RX_MODE_REG
{
/**
* An invalid received data stream (less than 4 bits received) will
* be ignored and the receiver remains active
*/
RC522_PCD_RX_NO_ERR_BIT = BIT3,
};
typedef enum
{
RC522_PCD_FIRMWARE_CLONE = 0x88, // clone
RC522_PCD_FIRMWARE_00 = 0x90, // v0.0
RC522_PCD_FIRMWARE_10 = 0x91, // v1.0
RC522_PCD_FIRMWARE_20 = 0x92, // v2.0
RC522_PCD_FIRMWARE_COUNTERFEIT = 0x12, // counterfeit chip
} rc522_pcd_firmware_t;
typedef union
{
uint16_t value;
struct
{
uint8_t lsb;
uint8_t msb;
};
} rc522_pcd_crc_t;
esp_err_t rc522_pcd_reset(const rc522_handle_t rc522, uint32_t timeout_ms);
esp_err_t rc522_pcd_calculate_crc(const rc522_handle_t rc522, const rc522_bytes_t *bytes, rc522_pcd_crc_t *result);
esp_err_t rc522_pcd_init(const rc522_handle_t rc522);
esp_err_t rc522_pcd_firmware(const rc522_handle_t rc522, rc522_pcd_firmware_t *result);
char *rc522_pcd_firmware_name(rc522_pcd_firmware_t firmware);
esp_err_t rc522_pcd_stop_active_command(const rc522_handle_t rc522);
esp_err_t rc522_pcd_clear_all_com_interrupts(const rc522_handle_t rc522);
esp_err_t rc522_pcd_fifo_write(const rc522_handle_t rc522, const rc522_bytes_t *bytes);
esp_err_t rc522_pcd_fifo_read(const rc522_handle_t rc522, rc522_bytes_t *bytes);
esp_err_t rc522_pcd_fifo_flush(const rc522_handle_t rc522);
esp_err_t rc522_pcd_start_data_transmission(const rc522_handle_t rc522);
esp_err_t rc522_pcd_stop_data_transmission(const rc522_handle_t rc522);
esp_err_t rc522_pcd_stop_crypto1(const rc522_handle_t rc522);
esp_err_t rc522_pcd_rw_test(const rc522_handle_t rc522);
esp_err_t rc522_pcd_write_n(const rc522_handle_t rc522, rc522_pcd_register_t addr, const rc522_bytes_t *bytes);
esp_err_t rc522_pcd_write(const rc522_handle_t rc522, rc522_pcd_register_t addr, uint8_t val);
esp_err_t rc522_pcd_read_n(const rc522_handle_t rc522, rc522_pcd_register_t addr, rc522_bytes_t *bytes);
esp_err_t rc522_pcd_read(const rc522_handle_t rc522, rc522_pcd_register_t addr, uint8_t *value_ref);
esp_err_t rc522_pcd_set_bits(const rc522_handle_t rc522, rc522_pcd_register_t addr, uint8_t bits);
esp_err_t rc522_pcd_clear_bits(const rc522_handle_t rc522, rc522_pcd_register_t addr, uint8_t bits);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,115 @@
#pragma once
#include "rc522_pcd_internal.h"
#include "rc522_picc.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* Commands sent to the PICC
*/
typedef enum
{
/**
* The commands used by the PCD to manage communication with several PICCs (ISO 14443-3, Type A, section 6.4)
* REQuest command, Type A. Invites PICCs in state IDLE to go to READY and prepare for
* anticollision or selection. 7 bit frame.
*/
RC522_PICC_CMD_REQA = 0x26,
/**
* Wake-UP command, Type A. Invites PICCs in state IDLE and HALT to go to READY(*) and prepare
* for anticollision or selection. 7 bit frame.
*/
RC522_PICC_CMD_WUPA = 0x52,
/**
* Cascade Tag. Not really a command, but used during anti collision.
* TODO: move it from here in that case
*/
RC522_PICC_CMD_CT = 0x88,
/**
* Anti collision/Select, Cascade Level 1
*/
RC522_PICC_CMD_SEL_CL1 = 0x93,
/**
* Anti collision/Select, Cascade Level 2
*/
RC522_PICC_CMD_SEL_CL2 = 0x95,
/**
* Anti collision/Select, Cascade Level 3
*/
RC522_PICC_CMD_SEL_CL3 = 0x97,
/**
* HaLT command, Type A. Instructs an ACTIVE PICC to go to state HALT.
*/
RC522_PICC_CMD_HLTA = 0x50,
/**
* Request command for Answer To Reset.
*
*/
RC522_PICC_CMD_RATS = 0xE0,
/**
* AUTHenticate command for Ultralight C & AES.
*
*/
RC522_PICC_CMD_UL_AUTH = 0x1A,
/**
* GET Version level 3 command for Ultralight EV1 & NANO, and NTAGs.
*/
RC522_PICC_CMD_GETV = 0x60,
} rc522_picc_command_t;
typedef struct
{
rc522_pcd_command_t pcd_command;
rc522_bytes_t bytes;
uint8_t expected_interrupts;
uint8_t rx_align;
uint8_t valid_bits;
bool check_crc;
} rc522_picc_transaction_t;
typedef struct rc522_picc_transaction_context rc522_picc_transaction_context_t;
typedef struct
{
rc522_bytes_t bytes;
uint8_t valid_bits;
} rc522_picc_transaction_result_t;
esp_err_t rc522_picc_send(const rc522_handle_t rc522, const rc522_picc_transaction_t *transaction,
rc522_picc_transaction_context_t *out_context);
esp_err_t rc522_picc_transceive(const rc522_handle_t rc522, const rc522_picc_transaction_t *transaction,
rc522_picc_transaction_result_t *out_result);
esp_err_t rc522_picc_reqa(const rc522_handle_t rc522, rc522_picc_atqa_desc_t *out_atqa);
esp_err_t rc522_picc_wupa(const rc522_handle_t rc522, rc522_picc_atqa_desc_t *out_atqa);
esp_err_t rc522_picc_select(
const rc522_handle_t rc522, rc522_picc_uid_t *out_uid, uint8_t *out_sak, bool skip_anticoll);
esp_err_t rc522_picc_halta(const rc522_handle_t rc522, rc522_picc_t *picc);
esp_err_t rc522_picc_heartbeat(
const rc522_handle_t rc522, const rc522_picc_t *picc, rc522_picc_uid_t *out_uid, uint8_t *out_sak);
rc522_picc_type_t rc522_picc_get_type(const rc522_picc_t *picc);
esp_err_t rc522_picc_set_state(
const rc522_handle_t rc522, rc522_picc_t *picc, rc522_picc_state_t new_state, bool fire_event);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,97 @@
#pragma once
#include <esp_log.h>
#include <esp_check.h>
#include <esp_bit_defs.h>
#include <freertos/FreeRTOS.h>
#include <freertos/event_groups.h>
#include "rc522_types.h"
#include "rc522_picc.h"
#ifdef __cplusplus
extern "C" {
#endif
#define RC522_LOG_TAG "rc522"
#define RC522_POLL_INTERVAL_MS_DEFAULT (120)
#define RC522_POLL_INTERVAL_MS_MIN (50)
#define RC522_TASK_STACK_SIZE_DEFAULT (4 * 1024)
#define RC522_TASK_PRIORITY_DEFAULT (3)
#define RC522_TASK_STOPPED_BIT (BIT0)
#define RC522_LOG_LEVEL LOG_LOCAL_LEVEL
typedef enum
{
RC522_STATE_UNDEFINED = 0,
RC522_STATE_CREATED,
RC522_STATE_POLLING, /*<! Scanning for nearby PICCs */
RC522_STATE_PAUSED,
} rc522_state_t;
struct rc522
{
rc522_config_t *config; /*<! Configuration */
bool exit_requested; /*<! Indicates whether polling task exit is requested */
TaskHandle_t task_handle; /*<! Handle of task */
esp_event_loop_handle_t event_handle; /*<! Handle of event loop */
rc522_state_t state; /*<! Current state */
rc522_picc_t picc;
EventGroupHandle_t bits;
};
typedef struct
{
uint8_t *ptr;
uint8_t length;
} rc522_bytes_t;
#define RC522_LOG_DEFINE_BASE() static const char *TAG = RC522_LOG_TAG
#define RC522_LOG(esp_log_foo, format, ...) esp_log_foo(TAG, "%s(%d): " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
#define RC522_LOGE(format, ...) RC522_LOG(ESP_LOGE, format, ##__VA_ARGS__)
#define RC522_LOGW(format, ...) RC522_LOG(ESP_LOGW, format, ##__VA_ARGS__)
#define RC522_LOGI(format, ...) RC522_LOG(ESP_LOGI, format, ##__VA_ARGS__)
#define RC522_LOGD(format, ...) RC522_LOG(ESP_LOGD, format, ##__VA_ARGS__)
#define RC522_LOGV(format, ...) RC522_LOG(ESP_LOGV, format, ##__VA_ARGS__)
#define RC522_LOG_WRITE(format, ...) esp_log_write(ESP_LOG_INFO, TAG, format, ##__VA_ARGS__)
#define RC522_RETURN_ON_ERROR(x) \
do { \
esp_err_t err_rc_ = (x); \
if (unlikely(err_rc_ != ESP_OK)) { \
ESP_LOGE(TAG, "%s(%d): %04" RC522_X "", __FUNCTION__, __LINE__, err_rc_); \
return err_rc_; \
} \
} \
while (0)
#define RC522_LOG_ON_ERROR(x) \
do { \
esp_err_t err_rc_ = (x); \
if (unlikely(err_rc_ != ESP_OK)) { \
ESP_LOGE(TAG, "%s(%d): %04" RC522_X "", __FUNCTION__, __LINE__, err_rc_); \
} \
} \
while (0)
#define RC522_RETURN_ON_FALSE(a, err_code) ESP_RETURN_ON_FALSE(a, err_code, TAG, "")
#define RC522_CHECK_WITH_MESSAGE(a, message) ESP_RETURN_ON_FALSE(!(a), ESP_ERR_INVALID_ARG, TAG, message)
#define RC522_CHECK_AND_RETURN(a, ret_val) ESP_RETURN_ON_FALSE(!(a), ret_val, TAG, #a)
#define RC522_CHECK(a) ESP_RETURN_ON_FALSE(!(a), ESP_ERR_INVALID_ARG, TAG, #a)
#define RC522_CHECK_BYTES(b) RC522_CHECK(b == NULL || (b)->ptr == NULL || (b)->length < 1)
#define RC522_RETURN_ON_ERROR_SILENTLY(x) \
do { \
esp_err_t err_rc_ = (x); \
if (unlikely(err_rc_ != ESP_OK)) { \
return err_rc_; \
} \
} \
while (0)
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,114 @@
#include <string.h>
#include "rc522_helpers_internal.h"
#include "rc522_types_internal.h"
#include "rc522_driver_internal.h"
#include "driver/rc522_i2c.h"
#define RC522_I2C_RW_TIMEOUT_MS 1000
RC522_LOG_DEFINE_BASE();
static esp_err_t rc522_i2c_install(const rc522_driver_handle_t driver)
{
RC522_CHECK(driver == NULL);
RC522_CHECK(driver->config == NULL);
rc522_i2c_config_t *conf = (rc522_i2c_config_t *)(driver->config);
if (!conf->bus) {
RC522_CHECK(conf->bus_config == NULL);
RC522_RETURN_ON_ERROR(i2c_new_master_bus(conf->bus_config, &conf->bus));
}
RC522_RETURN_ON_ERROR(
i2c_master_bus_add_device(conf->bus, &conf->dev_config, (i2c_master_dev_handle_t *)(&driver->device)));
if (conf->rst_io_num > GPIO_NUM_NC) {
RC522_RETURN_ON_ERROR(rc522_driver_init_rst_pin(conf->rst_io_num));
}
return ESP_OK;
}
static esp_err_t rc522_i2c_send(const rc522_driver_handle_t driver, uint8_t address, const rc522_bytes_t *bytes)
{
RC522_CHECK(driver == NULL);
RC522_CHECK(driver->config == NULL);
RC522_CHECK_BYTES(bytes);
// FIXME: Find a way to send [address + buffer]
// without need for second buffer
uint8_t buffer2[64];
buffer2[0] = address;
memcpy(buffer2 + 1, bytes->ptr, bytes->length);
RC522_RETURN_ON_ERROR(i2c_master_transmit((i2c_master_dev_handle_t)driver->device,
buffer2,
bytes->length + 1,
RC522_I2C_RW_TIMEOUT_MS));
return ESP_OK;
}
static esp_err_t rc522_i2c_receive(const rc522_driver_handle_t driver, uint8_t address, rc522_bytes_t *bytes)
{
RC522_CHECK(driver == NULL);
RC522_CHECK(driver->config == NULL);
RC522_CHECK_BYTES(bytes);
ESP_ERROR_CHECK(i2c_master_transmit_receive((i2c_master_dev_handle_t)driver->device,
&address,
1,
bytes->ptr,
bytes->length,
RC522_I2C_RW_TIMEOUT_MS));
return ESP_OK;
}
static esp_err_t rc522_i2c_reset(const rc522_driver_handle_t driver)
{
RC522_CHECK(driver == NULL);
RC522_CHECK(driver->config == NULL);
rc522_i2c_config_t *conf = (rc522_i2c_config_t *)(driver->config);
if (conf->rst_io_num < 0) {
return RC522_ERR_RST_PIN_UNUSED;
}
RC522_RETURN_ON_ERROR(gpio_set_level(conf->rst_io_num, RC522_DRIVER_HARD_RST_PIN_PWR_DOWN_LEVEL));
rc522_delay_ms(RC522_DRIVER_HARD_RST_PULSE_DURATION_MS);
RC522_RETURN_ON_ERROR(gpio_set_level(conf->rst_io_num, !RC522_DRIVER_HARD_RST_PIN_PWR_DOWN_LEVEL));
rc522_delay_ms(RC522_DRIVER_HARD_RST_PULSE_DURATION_MS);
return ESP_OK;
}
static esp_err_t rc522_i2c_uninstall(const rc522_driver_handle_t driver)
{
RC522_CHECK(driver == NULL);
RC522_CHECK(driver->config == NULL);
RC522_RETURN_ON_ERROR(i2c_master_bus_rm_device((i2c_master_dev_handle_t)driver->device));
// TODO: remove bus if handle was NULL?
return ESP_OK;
}
esp_err_t rc522_i2c_create(const rc522_i2c_config_t *config, rc522_driver_handle_t *driver)
{
RC522_CHECK(config == NULL);
RC522_CHECK(driver == NULL);
RC522_RETURN_ON_ERROR(rc522_driver_create(config, sizeof(rc522_i2c_config_t), driver));
(*driver)->install = rc522_i2c_install;
(*driver)->send = rc522_i2c_send;
(*driver)->receive = rc522_i2c_receive;
(*driver)->reset = rc522_i2c_reset;
(*driver)->uninstall = rc522_i2c_uninstall;
return ESP_OK;
}

View File

@@ -0,0 +1,216 @@
#include <string.h>
#include "rc522_helpers_internal.h"
#include "rc522_types_internal.h"
#include "rc522_driver_internal.h"
#include "driver/rc522_spi.h"
RC522_LOG_DEFINE_BASE();
typedef struct
{
gpio_num_t cs_io_num;
} rc522_spi_meta_t;
static void rc522_spi_transaction_pre_cb(spi_transaction_t *trans);
static void rc522_spi_transaction_post_cb(spi_transaction_t *trans);
static esp_err_t rc522_spi_uninstall(const rc522_driver_handle_t driver);
static esp_err_t rc522_spi_install(const rc522_driver_handle_t driver)
{
RC522_CHECK(driver == NULL);
RC522_CHECK(driver->config == NULL);
esp_err_t ret = ESP_OK;
rc522_spi_meta_t *meta = calloc(1, sizeof(rc522_spi_meta_t));
ESP_GOTO_ON_FALSE(meta != NULL, ESP_ERR_NO_MEM, error, TAG, "nomem");
driver->meta = (void *)meta;
rc522_spi_config_t *conf = (rc522_spi_config_t *)(driver->config);
if (conf->bus_config) {
// overwrite bus config
conf->bus_config->quadwp_io_num = GPIO_NUM_NC;
conf->bus_config->quadhd_io_num = GPIO_NUM_NC;
ESP_GOTO_ON_ERROR(spi_bus_initialize(conf->host_id, conf->bus_config, conf->dma_chan),
error,
TAG,
"spi bus initialization failed");
}
else {
RC522_LOGD("skips spi bus initialization");
}
// {{ overwrite device config
if (conf->dev_config.clock_speed_hz == 0) {
conf->dev_config.clock_speed_hz = 5000000;
}
conf->dev_config.mode = 0;
if (conf->dev_config.queue_size == 0) {
conf->dev_config.queue_size = 7;
}
if (conf->dev_config.flags == 0) {
conf->dev_config.flags = SPI_DEVICE_HALFDUPLEX;
}
conf->dev_config.command_bits = 8;
conf->dev_config.address_bits = 0;
conf->dev_config.dummy_bits = 0;
// }}
// ESP32 SPI bus has limitation of 3 CS lines, so we need to use
// software control for CS line in order to use more devices.
{
meta->cs_io_num = conf->dev_config.spics_io_num;
conf->dev_config.spics_io_num = GPIO_NUM_NC;
gpio_config_t cs_conf = {
.intr_type = GPIO_INTR_DISABLE,
.mode = GPIO_MODE_OUTPUT,
.pin_bit_mask = (1ULL << meta->cs_io_num),
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.pull_up_en = GPIO_PULLUP_DISABLE,
};
ESP_GOTO_ON_ERROR(gpio_config(&cs_conf), error, TAG, "cs gpio config failed");
ESP_GOTO_ON_ERROR(gpio_set_level(meta->cs_io_num, 1), error, TAG, "cs set level failed");
conf->dev_config.pre_cb = &rc522_spi_transaction_pre_cb;
conf->dev_config.post_cb = &rc522_spi_transaction_post_cb;
}
ESP_GOTO_ON_ERROR(spi_bus_add_device(conf->host_id, &conf->dev_config, (spi_device_handle_t *)(&driver->device)),
error,
TAG,
"spi bus add device failed");
if (conf->rst_io_num > GPIO_NUM_NC) {
ESP_GOTO_ON_ERROR(rc522_driver_init_rst_pin(conf->rst_io_num), error, TAG, "init rst pin failed");
}
goto exit;
error:
RC522_LOG_ON_ERROR(rc522_spi_uninstall(driver));
exit:
return ret;
}
static esp_err_t rc522_spi_send(const rc522_driver_handle_t driver, uint8_t address, const rc522_bytes_t *bytes)
{
RC522_CHECK(driver == NULL);
RC522_CHECK(driver->device == NULL);
RC522_CHECK_BYTES(bytes);
uint8_t cmd_byte = (address << 1) & 0x7E;
esp_err_t ret = spi_device_polling_transmit((spi_device_handle_t)(driver->device),
&(spi_transaction_t) {
.cmd = cmd_byte,
.length = 8 * bytes->length,
.tx_buffer = bytes->ptr,
.user = driver,
});
return ret;
}
static esp_err_t rc522_spi_receive(const rc522_driver_handle_t driver, uint8_t address, rc522_bytes_t *bytes)
{
RC522_CHECK(driver == NULL);
RC522_CHECK(driver->device == NULL);
RC522_CHECK_BYTES(bytes);
uint8_t cmd_byte = ((address << 1) & 0x7E) | 0x80;
for (uint8_t i = 0; i < bytes->length; i++) {
RC522_RETURN_ON_ERROR(spi_device_polling_transmit((spi_device_handle_t)(driver->device),
&(spi_transaction_t) {
.cmd = cmd_byte,
.rxlength = 8,
.rx_buffer = (bytes->ptr + i),
.user = driver,
}));
}
return ESP_OK;
}
static esp_err_t rc522_spi_reset(const rc522_driver_handle_t driver)
{
RC522_CHECK(driver == NULL);
RC522_CHECK(driver->config == NULL);
rc522_spi_config_t *conf = (rc522_spi_config_t *)(driver->config);
if (conf->rst_io_num < 0) {
return RC522_ERR_RST_PIN_UNUSED;
}
RC522_RETURN_ON_ERROR(gpio_set_level(conf->rst_io_num, RC522_DRIVER_HARD_RST_PIN_PWR_DOWN_LEVEL));
rc522_delay_ms(RC522_DRIVER_HARD_RST_PULSE_DURATION_MS);
RC522_RETURN_ON_ERROR(gpio_set_level(conf->rst_io_num, !RC522_DRIVER_HARD_RST_PIN_PWR_DOWN_LEVEL));
rc522_delay_ms(RC522_DRIVER_HARD_RST_PULSE_DURATION_MS);
return ESP_OK;
}
static esp_err_t rc522_spi_uninstall(const rc522_driver_handle_t driver)
{
RC522_CHECK(driver == NULL);
if (driver->meta) {
free(driver->meta);
driver->meta = NULL;
}
if (driver->device) {
RC522_LOG_ON_ERROR(spi_bus_remove_device((spi_device_handle_t)(driver->device)));
driver->device = NULL;
}
if (driver->config) {
rc522_spi_config_t *conf = (rc522_spi_config_t *)(driver->config);
if (conf->bus_config) {
RC522_LOG_ON_ERROR(spi_bus_free(conf->host_id));
}
}
return ESP_OK;
}
esp_err_t rc522_spi_create(const rc522_spi_config_t *config, rc522_driver_handle_t *driver)
{
RC522_CHECK(config == NULL);
RC522_CHECK(driver == NULL);
RC522_RETURN_ON_ERROR(rc522_driver_create(config, sizeof(rc522_spi_config_t), driver));
(*driver)->install = rc522_spi_install;
(*driver)->send = rc522_spi_send;
(*driver)->receive = rc522_spi_receive;
(*driver)->reset = rc522_spi_reset;
(*driver)->uninstall = rc522_spi_uninstall;
return ESP_OK;
}
static void rc522_spi_transaction_pre_cb(spi_transaction_t *trans)
{
rc522_driver_handle_t driver = (rc522_driver_handle_t)trans->user;
rc522_spi_meta_t *meta = (rc522_spi_meta_t *)(driver->meta);
RC522_LOG_ON_ERROR(gpio_set_level(meta->cs_io_num, 0));
}
static void rc522_spi_transaction_post_cb(spi_transaction_t *trans)
{
rc522_driver_handle_t driver = (rc522_driver_handle_t)trans->user;
rc522_spi_meta_t *meta = (rc522_spi_meta_t *)(driver->meta);
RC522_LOG_ON_ERROR(gpio_set_level(meta->cs_io_num, 1));
}

View File

@@ -0,0 +1,604 @@
/**
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* # Memory access management via access bits
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* The access conditions for every data block and sector trailer are defined by 3 bits, which
* are stored non-inverted and inverted in the sector trailer of the specified sector.
*
* The access bits control the rights of memory access using the secret keys A and B. The
* access conditions may be altered, provided one knows the relevant key and the current
* access condition allows this operation.
*
* @warning
* With each memory access the internal logic verifies the format of the access
* conditions. If it detects a format violation the whole sector is irreversibly blocked.
*
* +-----------------------------------------------------------------------------------+
* | Sector Trailer |
* +=============+===+===+===+===+===+===+===+===+===+===+====+====+====+====+====+====+
* | Byte number | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
* +-------------+---+---+---+---+---+---+---+---+---+---+----+----+----+----+----+----+
* | Description | Key A | Access Bits | Key B (optional) |
* +-------------+-----------------------+---------------+-----------------------------+
*
* Bytes 6, 7 and 8 of sector trailer block are bytes that holds access bits.
* Next table shows descriptions for each bit of those bytes.
*
* +-----+------+------+------+------+------+------+------+------+
* | | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
* +-----+------+------+------+------+------+------+------+------+
* | [6] | ~C23 | ~C22 | ~C21 | ~C20 | ~C13 | ~C12 | ~C11 | ~C10 |
* +-----+------+------+------+------+------+------+------+------+
* | [7] | C13 | C12 | C11 | C10 | ~C33 | ~C32 | ~C31 | ~C30 |
* +-----+------+------+------+------+------+------+------+------+
* | [8] | C33 | C32 | C31 | C30 | C23 | C22 | C21 | C20 |
* +-----+------+------+------+------+------+------+------+------+
*
* Where Cxy is access bit Cx for the block (group) y, for example:
* - C10 is the access bit C1 for the block (group) 0,
* - C20 is the access bit C2 for the block (group) 0,
* - ...,
* - C33 is the access bit C3 for the sector trailer,
* - ~C10 is the inverted bit C10.
*/
#include <string.h>
#include "rc522_types_internal.h"
#include "rc522_internal.h"
#include "rc522_helpers_internal.h"
#include "rc522_pcd_internal.h"
#include "rc522_picc_internal.h"
#include "picc/rc522_mifare.h"
RC522_LOG_DEFINE_BASE();
/**
* The commands used for MIFARE Classic
*/
enum
{
/**
* Perform authentication with Key A
*/
RC522_MIFARE_AUTH_KEY_A_CMD = 0x60,
/**
* Perform authentication with Key B
*/
RC522_MIFARE_AUTH_KEY_B_CMD = 0x61,
/**
* Reads one 16 byte block from the authenticated sector of the PICC
*/
RC522_MIFARE_READ_CMD = 0x30,
/**
* Writes one 16 byte block to the authenticated sector of the PICC
*/
RC522_MIFARE_WRITE_CMD = 0xA0,
/**
* Decrements the contents of a block and stores the result in the internal data register
*/
RC522_MIFARE_DECREMENT_CMD = 0xC0,
/**
* Increments the contents of a block and stores the result in the internal data register
*/
RC522_MIFARE_INCREMENT_CMD = 0xC1,
/**
* Reads the contents of a block into the internal data register
*/
RC522_MIFARE_RESTORE_CMD = 0xC2,
/**
* Writes the contents of the internal data register to a block
*/
RC522_MIFARE_TRANSFER_CMD = 0xB0,
};
/**
* Checks if PICC type is compatible with MIFARE Classic protocol
*/
bool rc522_mifare_type_is_classic_compatible(rc522_picc_type_t type)
{
return type == RC522_PICC_TYPE_MIFARE_MINI || type == RC522_PICC_TYPE_MIFARE_1K
|| type == RC522_PICC_TYPE_MIFARE_4K;
}
inline esp_err_t rc522_mifare_auth_sector(const rc522_handle_t rc522, const rc522_picc_t *picc,
const rc522_mifare_sector_desc_t *sector_desc, const rc522_mifare_key_t *key)
{
RC522_CHECK(sector_desc == NULL);
RC522_RETURN_ON_ERROR(rc522_mifare_auth(rc522, picc, sector_desc->block_0_address, key));
return ESP_OK;
}
esp_err_t rc522_mifare_auth(
const rc522_handle_t rc522, const rc522_picc_t *picc, uint8_t block_address, const rc522_mifare_key_t *key)
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK(picc == NULL);
RC522_CHECK(key == NULL);
// TODO: Validate block_address
RC522_LOGD("MIFARE AUTH (block_address=%02" RC522_X ")", block_address);
uint8_t send_data[12];
switch (key->type) {
case RC522_MIFARE_KEY_A:
send_data[0] = RC522_MIFARE_AUTH_KEY_A_CMD;
break;
case RC522_MIFARE_KEY_B:
send_data[0] = RC522_MIFARE_AUTH_KEY_B_CMD;
break;
default:
RC522_LOGE("Invalid key type");
return ESP_ERR_INVALID_ARG;
}
send_data[1] = block_address;
memcpy(send_data + 2, key->value, RC522_MIFARE_KEY_SIZE);
// Use the last uid bytes
// section 3.2.5 "MIFARE Classic Authentication".
// The only missed case is the MF1Sxxxx shortcut activation,
// but it requires cascade tag (CT) byte, that is not part of uid.
memcpy(send_data + 8, picc->uid.value + picc->uid.length - 4, 4);
// Start the authentication.
rc522_picc_transaction_t transaction = {
.pcd_command = RC522_PCD_MF_AUTH_CMD,
.expected_interrupts = RC522_PCD_IDLE_IRQ_BIT,
.bytes = { .ptr = send_data, .length = sizeof(send_data) },
};
esp_err_t ret = rc522_picc_send(rc522, &transaction, NULL);
// 10.3.1.9
//
// "If an error occurs during authentication,
// the ErrorReg register’s ProtocolErr bit is set to logic 1
// and the Status2Reg register’s Crypto1On bit is set to logic 0"
// Timer interrupt fires before ProtocolErr bit is set to 1
// so we will only check for Crypto1On bit (even if ret == ESP_OK)
uint8_t status2;
RC522_RETURN_ON_ERROR(rc522_pcd_read(rc522, RC522_PCD_STATUS_2_REG, &status2));
if (!(status2 & RC522_PCD_MF_CRYPTO1_ON_BIT)) {
return RC522_ERR_MIFARE_AUTHENTICATION_FAILED;
}
return ret;
}
esp_err_t rc522_mifare_read(const rc522_handle_t rc522, const rc522_picc_t *picc, uint8_t block_address,
uint8_t out_buffer[RC522_MIFARE_BLOCK_SIZE])
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK(picc == NULL);
RC522_CHECK(out_buffer == NULL);
RC522_LOGD("MIFARE READ (block_address=%02" RC522_X ")", block_address);
uint8_t cmd_buffer[4] = { 0 };
// Build command buffer
cmd_buffer[0] = RC522_MIFARE_READ_CMD;
cmd_buffer[1] = block_address;
// Calculate CRC_A
rc522_pcd_crc_t crc = { 0 };
RC522_RETURN_ON_ERROR(rc522_pcd_calculate_crc(rc522, &(rc522_bytes_t) { .ptr = cmd_buffer, .length = 2 }, &crc));
cmd_buffer[2] = crc.lsb;
cmd_buffer[3] = crc.msb;
uint8_t block_buffer[RC522_MIFARE_BLOCK_SIZE + 2] = { 0 }; // +2 for CRC_A
rc522_picc_transaction_t transaction = {
.bytes = { .ptr = cmd_buffer, .length = sizeof(cmd_buffer) },
.check_crc = true,
};
rc522_picc_transaction_result_t result = {
.bytes = { .ptr = block_buffer, .length = sizeof(block_buffer) },
};
RC522_RETURN_ON_ERROR(rc522_picc_transceive(rc522, &transaction, &result));
RC522_CHECK_AND_RETURN((result.bytes.length - 2) != RC522_MIFARE_BLOCK_SIZE, ESP_FAIL);
memcpy(out_buffer, block_buffer, sizeof(block_buffer) - 2); // -2 cuz of CRC_A
return ESP_OK;
}
static esp_err_t rc522_mifare_send(const rc522_handle_t rc522, const uint8_t *send_data, uint8_t send_length)
{
RC522_CHECK(send_data == NULL);
RC522_CHECK(send_length > RC522_MIFARE_BLOCK_SIZE);
#pragma GCC diagnostic ignored "-Wcast-qual"
uint8_t *sdata = (uint8_t *)send_data;
#pragma GCC diagnostic pop
rc522_pcd_crc_t crc = { 0 };
RC522_RETURN_ON_ERROR(
rc522_pcd_calculate_crc(rc522, &(rc522_bytes_t) { .ptr = sdata, .length = send_length }, &crc));
uint8_t buffer[RC522_MIFARE_BLOCK_SIZE + 2]; // +2 for CRC_A
memcpy(buffer, send_data, send_length);
buffer[send_length] = crc.lsb;
buffer[send_length + 1] = crc.msb;
send_length += 2;
rc522_picc_transaction_t transaction = {
.bytes = { .ptr = buffer, .length = send_length },
};
rc522_picc_transaction_result_t result = {
.bytes = { .ptr = buffer, .length = sizeof(buffer) },
};
RC522_RETURN_ON_ERROR(rc522_picc_transceive(rc522, &transaction, &result));
// The PICC must reply with a 4 bit ACK
if (result.bytes.length != 1 || result.valid_bits != 4) {
return ESP_FAIL; // TODO: use custom err
}
if (result.bytes.ptr[0] != RC522_MIFARE_ACK) {
return RC522_ERR_MIFARE_NACK;
}
return ESP_OK;
}
esp_err_t rc522_mifare_get_number_of_sectors(rc522_picc_type_t type, uint8_t *out_result)
{
RC522_CHECK(rc522_mifare_type_is_classic_compatible(type) == false);
RC522_CHECK(out_result == NULL);
switch (type) {
case RC522_PICC_TYPE_MIFARE_MINI:
// Has 5 sectors * 4 blocks/sector * 16 bytes/block = 320 bytes.
*out_result = 5;
return ESP_OK;
case RC522_PICC_TYPE_MIFARE_1K:
// Has 16 sectors * 4 blocks/sector * 16 bytes/block = 1024 bytes.
*out_result = 16;
return ESP_OK;
case RC522_PICC_TYPE_MIFARE_4K:
// Has (32 sectors * 4 blocks/sector + 8 sectors * 16 blocks/sector) * 16 bytes/block = 4096 bytes.
*out_result = 40;
return ESP_OK;
default:
return ESP_FAIL;
}
}
inline uint8_t rc522_mifare_get_sector_index_by_block_address(uint8_t block_address)
{
if (block_address < 128) {
return block_address / 4;
}
else {
return 32 + ((block_address - 128) / 16);
}
}
inline esp_err_t rc522_mifare_get_number_of_blocks_in_sector(uint8_t sector_index, uint8_t *out_result)
{
RC522_CHECK(sector_index > RC522_MIFARE_SECTOR_INDEX_MAX);
RC522_CHECK(out_result == NULL);
if (sector_index < 32) {
*out_result = 4;
return ESP_OK;
}
*out_result = 16;
return ESP_OK;
}
inline esp_err_t rc522_mifare_get_sector_block_0_address(uint8_t sector_index, uint8_t *out_result)
{
RC522_CHECK(sector_index > RC522_MIFARE_SECTOR_INDEX_MAX);
RC522_CHECK(out_result == NULL);
if (sector_index < 32) {
*out_result = sector_index * 4;
return ESP_OK;
}
*out_result = 128 + (sector_index - 32) * 16;
return ESP_OK;
}
esp_err_t rc522_mifare_get_sector_desc(uint8_t sector_index, rc522_mifare_sector_desc_t *out_sector_desc)
{
RC522_CHECK(sector_index > RC522_MIFARE_SECTOR_INDEX_MAX);
RC522_CHECK(out_sector_desc == NULL);
rc522_mifare_sector_desc_t desc;
memset(&desc, 0, sizeof(desc));
desc.index = sector_index;
RC522_RETURN_ON_ERROR(rc522_mifare_get_number_of_blocks_in_sector(sector_index, &desc.number_of_blocks));
RC522_RETURN_ON_ERROR(rc522_mifare_get_sector_block_0_address(sector_index, &desc.block_0_address));
memcpy(out_sector_desc, &desc, sizeof(rc522_mifare_sector_desc_t));
return ESP_OK;
}
static esp_err_t rc522_mifare_block_at_address_is_sector_trailer(uint8_t block_address, bool *result)
{
RC522_CHECK(result == NULL);
uint8_t sector_index = rc522_mifare_get_sector_index_by_block_address(block_address);
rc522_mifare_sector_desc_t sector = { 0 };
RC522_RETURN_ON_ERROR(rc522_mifare_get_sector_desc(sector_index, &sector));
*result = (block_address == (sector.block_0_address + sector.number_of_blocks - 1));
return ESP_OK;
}
static esp_err_t rc522_mifare_verify_access_bits_integrity(const uint8_t trailer_bytes[RC522_MIFARE_BLOCK_SIZE])
{
RC522_CHECK(trailer_bytes == NULL);
uint8_t c1 = 0, c2 = 0, c3 = 0;
uint8_t c1_ = 0, c2_ = 0, c3_ = 0;
RC522_RETURN_ON_ERROR(rc522_nibbles(trailer_bytes[6], &c2_, &c1_));
RC522_RETURN_ON_ERROR(rc522_nibbles(trailer_bytes[7], &c1, &c3_));
RC522_RETURN_ON_ERROR(rc522_nibbles(trailer_bytes[8], &c3, &c2));
if ((c1 != (~c1_ & 0x0F)) || (c2 != (~c2_ & 0x0F)) || (c3 != (~c3_ & 0x0F))) {
return RC522_ERR_MIFARE_ACCESS_BITS_INTEGRITY_VIOLATION;
}
return ESP_OK;
}
esp_err_t rc522_mifare_write(const rc522_handle_t rc522, const rc522_picc_t *picc, uint8_t block_address,
const uint8_t buffer[RC522_MIFARE_BLOCK_SIZE])
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK(picc == NULL);
RC522_CHECK(buffer == NULL);
RC522_CHECK(!rc522_mifare_type_is_classic_compatible(picc->type));
bool is_trailer = false;
RC522_RETURN_ON_ERROR(rc522_mifare_block_at_address_is_sector_trailer(block_address, &is_trailer));
if (is_trailer) {
#ifdef CONFIG_RC522_PREVENT_SECTOR_TRAILER_WRITE
ESP_LOGE(TAG, "");
ESP_LOGE(TAG,
"The block at address %d that you are trying to update is a Sector Trailer block.",
block_address);
ESP_LOGE(TAG, "");
ESP_LOGE(TAG, "Writing to Sector Trailer blocks is prevented by default in the component configuration");
ESP_LOGE(TAG, "to protect inexperienced users from accidentally overwriting keys");
ESP_LOGE(TAG, "or writing incorrect access bits, which could make the sector unusable.");
ESP_LOGE(TAG, "");
ESP_LOGE(TAG, "If you know what you are doing, please use menuconfig to disable this protection.");
ESP_LOGE(TAG, "");
return RC522_ERR_MIFARE_SECTOR_TRAILER_WRITE_NOT_ALLOWED;
#endif
RC522_RETURN_ON_ERROR(rc522_mifare_verify_access_bits_integrity(buffer));
}
RC522_LOGD("MIFARE WRITE (block_address=%02" RC522_X ")", block_address);
uint8_t cmd_buffer[] = { RC522_MIFARE_WRITE_CMD, block_address };
RC522_RETURN_ON_ERROR(rc522_mifare_send(rc522, cmd_buffer, sizeof(cmd_buffer)));
RC522_RETURN_ON_ERROR(rc522_mifare_send(rc522, buffer, RC522_MIFARE_BLOCK_SIZE));
return ESP_OK;
}
inline esp_err_t rc522_mifare_deauth(const rc522_handle_t rc522, const rc522_picc_t *picc)
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK(picc == NULL);
RC522_RETURN_ON_ERROR(rc522_pcd_stop_crypto1(rc522));
return ESP_OK;
}
esp_err_t rc522_mifare_get_desc(const rc522_picc_t *picc, rc522_mifare_desc_t *out_mifare_desc)
{
RC522_CHECK(picc == NULL);
RC522_CHECK(out_mifare_desc == NULL);
rc522_mifare_desc_t desc;
memset(&desc, 0, sizeof(desc));
RC522_RETURN_ON_ERROR(rc522_mifare_get_number_of_sectors(picc->type, &desc.number_of_sectors));
memcpy(out_mifare_desc, &desc, sizeof(rc522_mifare_desc_t));
return ESP_OK;
}
static esp_err_t rc522_mifare_nibbles_to_access_bits(
uint8_t c1, uint8_t c2, uint8_t c3, uint8_t offset, rc522_mifare_access_bits_t *access_bits)
{
RC522_CHECK(offset > 3);
RC522_CHECK(access_bits == NULL);
rc522_mifare_access_bits_t bits = { 0 };
bits.c1 = (c1 & (1 << offset)) >> offset;
bits.c2 = (c2 & (1 << offset)) >> offset;
bits.c3 = (c3 & (1 << offset)) >> offset;
memcpy(access_bits, &bits, sizeof(bits));
return ESP_OK;
}
static esp_err_t rc522_mifare_parse_sector_trailer(
const uint8_t *trailer_bytes, uint8_t trailer_bytes_size, rc522_mifare_sector_trailer_info_t *out_trailer_info)
{
RC522_CHECK(trailer_bytes == NULL);
RC522_CHECK(trailer_bytes_size != RC522_MIFARE_BLOCK_SIZE);
RC522_CHECK(out_trailer_info == NULL);
RC522_RETURN_ON_ERROR(rc522_mifare_verify_access_bits_integrity(trailer_bytes));
uint8_t c1 = 0, c2 = 0, c3 = 0;
RC522_RETURN_ON_ERROR(rc522_nibbles(trailer_bytes[7], &c1, NULL));
RC522_RETURN_ON_ERROR(rc522_nibbles(trailer_bytes[8], &c3, &c2));
rc522_mifare_sector_trailer_info_t info = { 0 };
for (uint8_t i = 0; i < 4; i++) {
RC522_RETURN_ON_ERROR(rc522_mifare_nibbles_to_access_bits(c1, c2, c3, i, &info.access_bits[i]));
}
memcpy(out_trailer_info, &info, sizeof(info));
return ESP_OK;
}
/**
* Checks if block is Value block based on access bits
*/
inline static bool rc522_mifare_block_is_value(rc522_mifare_access_bits_t access_bits)
{
uint8_t bits = (access_bits.c1 << 2) | (access_bits.c2 << 1) | (access_bits.c3 << 0);
return bits == 0b110 || bits == 0b001;
}
static esp_err_t rc522_mifare_parse_value_block(
const uint8_t *bytes, uint8_t bytes_size, rc522_mifare_sector_value_block_info_t *out_value_block_info)
{
RC522_CHECK(bytes == NULL);
RC522_CHECK(bytes_size != RC522_MIFARE_BLOCK_SIZE);
RC522_CHECK(out_value_block_info == NULL);
rc522_mifare_sector_value_block_info_t value_block_info;
memset(&value_block_info, 0, sizeof(value_block_info));
value_block_info.value = 0;
value_block_info.value |= ((int32_t)(bytes[3]) << (8 * 3));
value_block_info.value |= ((int32_t)(bytes[2]) << (8 * 2));
value_block_info.value |= ((int32_t)(bytes[1]) << (8 * 1));
value_block_info.value |= ((int32_t)(bytes[0]) << (8 * 0));
value_block_info.addr = bytes[12];
// TODO: Check for integrity violation
// Use RC522_ERR_MIFARE_VALUE_BLOCK_INTEGRITY_VIOLATION
memcpy(out_value_block_info, &value_block_info, sizeof(value_block_info));
return ESP_OK;
}
inline static esp_err_t rc522_mifare_get_sector_block_group_index(
const rc522_mifare_sector_desc_t *sector, uint8_t block_offset, uint8_t *out_group)
{
RC522_CHECK(sector == NULL);
RC522_CHECK(out_group == NULL);
RC522_CHECK(block_offset >= sector->number_of_blocks);
if (sector->number_of_blocks > 4) {
*out_group = block_offset / 5;
return ESP_OK;
}
*out_group = block_offset;
return ESP_OK;
}
esp_err_t rc522_mifare_read_sector_trailer_block(const rc522_handle_t rc522, const rc522_picc_t *picc,
const rc522_mifare_sector_desc_t *sector_desc, rc522_mifare_sector_block_t *out_trailer)
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK(picc == NULL);
RC522_CHECK(sector_desc == NULL);
RC522_CHECK(out_trailer == NULL);
rc522_mifare_sector_block_t block;
memset(&block, 0x00, sizeof(block));
block.address = (sector_desc->block_0_address + sector_desc->number_of_blocks - 1);
block.type = RC522_MIFARE_BLOCK_TRAILER;
RC522_RETURN_ON_ERROR(rc522_mifare_read(rc522, picc, block.address, block.bytes));
block.error = rc522_mifare_parse_sector_trailer(block.bytes, sizeof(block.bytes), &block.trailer_info);
memcpy(&block.access_bits, &block.trailer_info.access_bits[3], sizeof(rc522_mifare_access_bits_t));
memcpy(out_trailer, &block, sizeof(block));
return ESP_OK;
}
esp_err_t rc522_mifare_read_sector_block(const rc522_handle_t rc522, const rc522_picc_t *picc,
const rc522_mifare_sector_desc_t *sector_desc, const rc522_mifare_sector_block_t *trailer, uint8_t block_offset,
rc522_mifare_sector_block_t *out_block)
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK(picc == NULL);
RC522_CHECK(sector_desc == NULL);
RC522_CHECK(trailer == NULL);
RC522_CHECK(block_offset >= sector_desc->number_of_blocks);
RC522_CHECK(out_block == NULL);
RC522_CHECK_WITH_MESSAGE(block_offset == sector_desc->number_of_blocks - 1,
"use rc522_mifare_read_sector_trailer_block() to read the sector trailer block");
RC522_CHECK(trailer->type != RC522_MIFARE_BLOCK_TRAILER);
rc522_mifare_sector_block_t block;
memset(&block, 0, sizeof(block));
block.address = (sector_desc->block_0_address + block_offset);
RC522_RETURN_ON_ERROR(rc522_mifare_read(rc522, picc, block.address, block.bytes));
uint8_t group = 0;
RC522_RETURN_ON_ERROR(rc522_mifare_get_sector_block_group_index(sector_desc, block_offset, &group));
memcpy(&block.access_bits, &trailer->trailer_info.access_bits[group], sizeof(rc522_mifare_access_bits_t));
if (block.address == 0) {
block.type = RC522_MIFARE_BLOCK_MANUFACTURER_DATA;
}
else if (rc522_mifare_block_is_value(block.access_bits)) {
block.type = RC522_MIFARE_BLOCK_VALUE;
block.error = rc522_mifare_parse_value_block(block.bytes, sizeof(block.bytes), &block.value_info);
}
else {
block.type = RC522_MIFARE_BLOCK_DATA;
}
memcpy(out_block, &block, sizeof(block));
return ESP_OK;
}

View File

@@ -0,0 +1,618 @@
#include <esp_system.h>
#include <esp_check.h>
#include <string.h>
#include "rc522_picc.h"
#include "rc522_types_internal.h"
#include "rc522_pcd_internal.h"
#include "rc522_picc_internal.h"
#include "picc/rc522_nxp.h"
#include "picc/rc522_mifare.h"
RC522_LOG_DEFINE_BASE();
const uint8_t RC522_NXP_DEFAULT_PWD[RC522_NXP_PWD_SIZE] = { 0xFF, 0xFF, 0xFF, 0xFF };
const uint8_t RC522_NXP_DEFAULT_PACK[RC522_NXP_PACK_SIZE] = { 0x00 };
// Helper to check if the card responded with a NACK
inline esp_err_t rc522_nxp_check_for_nak(rc522_picc_transaction_result_t *result)
{
if (result->bytes.length == 1 && result->valid_bits == 4 && result->bytes.ptr[0] != RC522_MIFARE_ACK) {
return RC522_ERR_NXP_NACK;
}
return ESP_OK;
}
// WRITE supported? (not classic write - see COMPAT_WRITE)
inline bool rc522_nxp_type_has_write(rc522_picc_type_t type)
{
switch (type) {
case RC522_PICC_TYPE_MIFARE_UL_:
case RC522_PICC_TYPE_MIFARE_UL_C:
case RC522_PICC_TYPE_MIFARE_UL_EV1_1:
case RC522_PICC_TYPE_MIFARE_UL_EV1_2:
case RC522_PICC_TYPE_MIFARE_UL_NANO:
case RC522_PICC_TYPE_MIFARE_UL_AES:
case RC522_PICC_TYPE_NTAG2xx:
case RC522_PICC_TYPE_NTAG213:
case RC522_PICC_TYPE_NTAG215:
case RC522_PICC_TYPE_NTAG216:
return true;
default:
return false;
}
}
// FAST_READ supported?
inline bool rc522_nxp_type_has_fast_read(rc522_picc_type_t type)
{
switch (type) {
case RC522_PICC_TYPE_MIFARE_UL_EV1_1:
case RC522_PICC_TYPE_MIFARE_UL_EV1_2:
case RC522_PICC_TYPE_MIFARE_UL_AES:
case RC522_PICC_TYPE_NTAG213:
case RC522_PICC_TYPE_NTAG215:
case RC522_PICC_TYPE_NTAG216:
return true;
default:
return false;
}
}
// READ_CNT supported?
inline bool rc522_nxp_type_has_read_cnt(rc522_picc_type_t type)
{
switch (type) {
case RC522_PICC_TYPE_MIFARE_UL_EV1_1:
case RC522_PICC_TYPE_MIFARE_UL_EV1_2:
case RC522_PICC_TYPE_MIFARE_UL_AES:
case RC522_PICC_TYPE_NTAG213:
case RC522_PICC_TYPE_NTAG215:
case RC522_PICC_TYPE_NTAG216:
return true;
default:
return false;
}
}
// PWD_AUTH supported?
inline bool rc522_nxp_type_has_pwd_auth(rc522_picc_type_t type)
{
switch (type) {
case RC522_PICC_TYPE_MIFARE_UL_EV1_1:
case RC522_PICC_TYPE_MIFARE_UL_EV1_2:
case RC522_PICC_TYPE_NTAG213:
case RC522_PICC_TYPE_NTAG215:
case RC522_PICC_TYPE_NTAG216:
return true;
default:
return false;
}
}
// Get originality signature size, or 0 if not supported
inline uint8_t rc522_nxp_type_sig_size(rc522_picc_type_t type)
{
switch (type) {
case RC522_PICC_TYPE_MIFARE_UL_AES:
return 48;
case RC522_PICC_TYPE_MIFARE_UL_EV1_1:
case RC522_PICC_TYPE_MIFARE_UL_EV1_2:
case RC522_PICC_TYPE_MIFARE_UL_NANO:
case RC522_PICC_TYPE_NTAG213:
case RC522_PICC_TYPE_NTAG215:
case RC522_PICC_TYPE_NTAG216:
return 32;
default:
return 0;
}
}
// Convert GET_VERSION response to type
static rc522_picc_type_t rc522_nxp_type_from_version(rc522_nxp_picc_version_t *version)
{
rc522_nxp_product_type_t type = version->product_type;
rc522_nxp_major_version_t major_version = version->major_version;
uint8_t size = version->storage_size;
if (type == RC522_NXP_PRODUCT_TYPE_NTAG) {
if (major_version != RC522_NXP_MAJ_VER_NTAG21) { // Not an NTAG21x
RC522_LOGW("Unknown NTAG product version: %02" RC522_X, major_version);
}
else if (size == 0x0F) { // 0x0F => between 128 and 256 user bytes
return RC522_PICC_TYPE_NTAG213;
}
else if (size == 0x11) { // 0x11 => between 256 and 512 user bytes
return RC522_PICC_TYPE_NTAG215;
}
else if (size == 0x13) { // 0x13 => between 512 and 1024 user byets
return RC522_PICC_TYPE_NTAG216;
}
else { // Unknown size?
RC522_LOGW("Unknown NTAG size: %02" RC522_X, size);
}
}
else if (type == RC522_NXP_PRODUCT_TYPE_UL) {
if (major_version == RC522_NXP_MAJ_VER_UL_EV1) {
if (size == 0x0B) { // 0x0B => between 32 and 64 user bytes
return RC522_PICC_TYPE_MIFARE_UL_EV1_1;
}
else if (size == 0x0E) { // 0x0E => 128 user bytes
return RC522_PICC_TYPE_MIFARE_UL_EV1_2;
}
}
else if (major_version == RC522_NXP_MAJ_VER_UL_NANO) {
return RC522_PICC_TYPE_MIFARE_UL_NANO;
}
else if (major_version == RC522_NXP_MAJ_VER_UL_AES) {
return RC522_PICC_TYPE_MIFARE_UL_AES;
}
else {
RC522_LOGW("Unknown UL product version: %02" RC522_X, major_version);
}
}
else {
RC522_LOGE("Unknown product type: %02" RC522_X, type);
}
return RC522_PICC_TYPE_UNKNOWN;
}
uint8_t rc522_nxp_get_user_page_count(rc522_picc_type_t type)
{
switch (type) {
case RC522_PICC_TYPE_MIFARE_UL_:
return 12;
case RC522_PICC_TYPE_MIFARE_UL_C:
return 36;
case RC522_PICC_TYPE_MIFARE_UL_EV1_1:
return 12;
case RC522_PICC_TYPE_MIFARE_UL_EV1_2:
return 32;
case RC522_PICC_TYPE_MIFARE_UL_NANO:
return 10;
case RC522_PICC_TYPE_MIFARE_UL_AES:
return 36;
case RC522_PICC_TYPE_NTAG213:
return 36;
case RC522_PICC_TYPE_NTAG215:
return 126;
case RC522_PICC_TYPE_NTAG216:
return 222;
default:
return 0;
}
}
uint8_t rc522_nxp_get_user_mem_start(rc522_picc_type_t type)
{
switch (type) {
case RC522_PICC_TYPE_MIFARE_UL_:
case RC522_PICC_TYPE_MIFARE_UL_C:
case RC522_PICC_TYPE_MIFARE_UL_EV1_1:
case RC522_PICC_TYPE_MIFARE_UL_EV1_2:
case RC522_PICC_TYPE_MIFARE_UL_NANO:
case RC522_PICC_TYPE_MIFARE_UL_AES:
case RC522_PICC_TYPE_NTAG2xx:
case RC522_PICC_TYPE_NTAG213:
case RC522_PICC_TYPE_NTAG215:
case RC522_PICC_TYPE_NTAG216:
return 0x04;
default:
return 0;
}
}
uint8_t rc522_nxp_get_user_mem_end(rc522_picc_type_t type)
{
switch (type) {
case RC522_PICC_TYPE_MIFARE_UL_:
return 0x0F;
case RC522_PICC_TYPE_MIFARE_UL_C:
return 0x27;
case RC522_PICC_TYPE_MIFARE_UL_EV1_1:
return 0x0F;
case RC522_PICC_TYPE_MIFARE_UL_EV1_2:
return 0x23;
case RC522_PICC_TYPE_MIFARE_UL_NANO:
return 0x0D;
case RC522_PICC_TYPE_MIFARE_UL_AES:
return 0x27;
case RC522_PICC_TYPE_NTAG213:
return 0x27;
case RC522_PICC_TYPE_NTAG215:
return 0x81;
case RC522_PICC_TYPE_NTAG216:
return 0xE1;
default:
return 0;
}
}
uint8_t rc522_nxp_get_page_count(rc522_picc_type_t type)
{
switch (type) {
case RC522_PICC_TYPE_MIFARE_UL_:
return 16;
case RC522_PICC_TYPE_MIFARE_UL_C:
return 48;
case RC522_PICC_TYPE_MIFARE_UL_EV1_1:
return 20;
case RC522_PICC_TYPE_MIFARE_UL_EV1_2:
return 41;
case RC522_PICC_TYPE_MIFARE_UL_NANO:
return 14;
case RC522_PICC_TYPE_MIFARE_UL_AES:
return 60;
case RC522_PICC_TYPE_NTAG213:
return 45;
case RC522_PICC_TYPE_NTAG215:
return 135;
case RC522_PICC_TYPE_NTAG216:
return 231;
default:
return 0;
}
}
esp_err_t rc522_nxp_keyauth_supported(const rc522_handle_t rc522, const rc522_picc_t *picc)
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK(picc == NULL);
RC522_LOGD("AUTHENTICATE (support check)");
uint8_t buffer[11] = { 0 }; // combined send(4)/recv(11) buffer
buffer[0] = RC522_PICC_CMD_UL_AUTH;
// buffer[1] is argument; 0x00 is fine for support check
rc522_pcd_crc_t crc = { 0 };
RC522_RETURN_ON_ERROR(rc522_pcd_calculate_crc(rc522, &(rc522_bytes_t) { .ptr = buffer, .length = 2 }, &crc));
buffer[2] = crc.lsb;
buffer[3] = crc.msb;
rc522_picc_transaction_t transaction = {
.bytes = { .ptr = buffer, .length = 4 },
};
rc522_picc_transaction_result_t result = {
.bytes = { .ptr = buffer, .length = sizeof(buffer) },
};
esp_err_t ret = rc522_picc_transceive(rc522, &transaction, &result);
if (ret == ESP_OK && buffer[0] != 0xAF) {
return RC522_ERR_NXP_NACK;
}
return ret;
}
esp_err_t rc522_nxp_get_type(const rc522_handle_t rc522, const rc522_picc_t *picc, rc522_picc_type_t *out_type)
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK(picc == NULL);
RC522_CHECK(out_type == NULL);
RC522_CHECK(picc->type != RC522_PICC_TYPE_MIFARE_UL);
RC522_LOGD("Attempting to get NXP PICC type");
RC522_LOGD("Checking for GET_VERSION support...");
// In case of early exit due to errors
rc522_picc_type_t type = RC522_PICC_TYPE_MIFARE_UL;
rc522_nxp_picc_version_t version_info = { 0 };
esp_err_t ret = rc522_nxp_get_version(rc522, picc, &version_info);
if (ret == ESP_OK) { // GET_VERSION supported
type = rc522_nxp_type_from_version(&version_info);
}
else if (ret == RC522_ERR_RX_TIMER_TIMEOUT) { // No GET_VERSION
// Failure probably resulted in a HALT, so make sure we can still connect
RC522_LOGD("No L3 GET_VERSION; reselecting");
rc522_picc_uid_t uid;
uint8_t sak;
rc522_picc_heartbeat(rc522, picc, &uid, &sak);
// Fall back to checking for authentication availability (C)
RC522_LOGD("Checking for authentication support...");
ret = rc522_nxp_keyauth_supported(rc522, picc);
if (ret == ESP_OK) {
// Chip supports authentication
// TODO: Do we need to complete authentication, or can we leave it here?
// Note datasheet also mentions a "MIFARE Hospitality" here, but there
// doesn't seem to be any more information about it
type = RC522_PICC_TYPE_MIFARE_UL_C;
}
else if (ret == RC522_ERR_RX_TIMER_TIMEOUT || ret == RC522_ERR_MIFARE_NACK) { // Not supported
// Only Ultralight without auth is the original
type = RC522_PICC_TYPE_MIFARE_UL_;
}
else { // Some other error in AUTHENTICATE
RC522_RETURN_ON_ERROR(ret);
}
rc522_picc_heartbeat(rc522, picc, &uid, &sak);
}
else { // A different error occured in GET_VERSION
RC522_RETURN_ON_ERROR(ret);
}
RC522_LOGD("Found %s", rc522_picc_type_name(type));
*out_type = type;
return ESP_OK;
}
esp_err_t rc522_nxp_get_version(
const rc522_handle_t rc522, const rc522_picc_t *picc, rc522_nxp_picc_version_t *out_version)
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK(picc == NULL);
RC522_CHECK(out_version == NULL);
RC522_LOGD("GET_VERSION");
uint8_t buffer[10]; // Combined buffer; response is 8-byte data + 2-byte CRC
buffer[0] = RC522_PICC_CMD_GETV;
rc522_pcd_crc_t crc = { 0 };
RC522_RETURN_ON_ERROR(rc522_pcd_calculate_crc(rc522, &(rc522_bytes_t) { .ptr = buffer, .length = 1 }, &crc));
buffer[1] = crc.lsb;
buffer[2] = crc.msb;
rc522_picc_transaction_t transaction = {
.bytes = { .ptr = buffer, .length = 3 },
};
rc522_picc_transaction_result_t result = {
.bytes = { .ptr = buffer, .length = sizeof(buffer) },
};
RC522_RETURN_ON_ERROR(rc522_picc_transceive(rc522, &transaction, &result));
RC522_RETURN_ON_ERROR(rc522_nxp_check_for_nak(&result));
memcpy(out_version, buffer, sizeof(rc522_nxp_picc_version_t));
return ESP_OK;
}
esp_err_t rc522_nxp_read(
const rc522_handle_t rc522, const rc522_picc_t *picc, uint8_t address, uint8_t out_buffer[RC522_NXP_PAGE_SIZE * 4])
{
// Same protocol as MIFARE cards, so defer
return rc522_mifare_read(rc522, picc, address, out_buffer);
}
esp_err_t rc522_nxp_fast_read(const rc522_handle_t rc522, const rc522_picc_t *picc, uint8_t start_page,
uint8_t end_page, rc522_nxp_fast_read_data_t *out_buffer)
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK(picc == NULL);
RC522_CHECK(out_buffer == NULL);
RC522_CHECK(!rc522_nxp_type_has_fast_read(picc->type));
// some sanity checks - valid range, output buffer sufficiently large
RC522_CHECK(start_page > end_page);
RC522_CHECK(out_buffer->buffer_size < (end_page - start_page + 1) * 4);
RC522_LOGD("NXP FAST_READ (start=%02" RC522_X ", end=%02" RC522_X ")", start_page, end_page);
uint8_t cmd_buffer[5] = { RC522_NXP_FAST_READ, start_page, end_page, 0, 0 };
uint8_t byte_count = (end_page - start_page + 1) * 4;
rc522_pcd_crc_t crc = { 0 };
RC522_RETURN_ON_ERROR(rc522_pcd_calculate_crc(rc522,
&(rc522_bytes_t) {
.ptr = cmd_buffer,
.length = 3,
},
&crc));
cmd_buffer[3] = crc.lsb;
cmd_buffer[4] = crc.msb;
// I'd rather not malloc() here, but we need extra space for the CRC which
// we're not guaranteed to have in out_buffer
// TODO: Allow for separate CRC fetch and processing to avoid this?
uint8_t *recv_buffer = malloc(byte_count + 2); // Allow for CRC_A
RC522_CHECK_AND_RETURN(recv_buffer == NULL, ESP_ERR_NO_MEM);
rc522_picc_transaction_t transaction = {
.bytes = { .ptr = cmd_buffer, .length = sizeof(cmd_buffer) },
.check_crc = true,
};
rc522_picc_transaction_result_t result = {
.bytes = { .ptr = recv_buffer, .length = byte_count + 2 },
};
// Bunch of manual handling here to make sure free() is called
esp_err_t ret = rc522_picc_transceive(rc522, &transaction, &result);
if (ret != ESP_OK) {
free(recv_buffer);
return ret;
}
else if ((result.bytes.length - 2) != byte_count) {
free(recv_buffer);
return ESP_FAIL;
}
memcpy(out_buffer->bytes, recv_buffer, byte_count);
out_buffer->read_size = byte_count;
free(recv_buffer);
return ESP_OK;
}
esp_err_t rc522_nxp_write(
const rc522_handle_t rc522, const rc522_picc_t *picc, uint8_t address, const uint8_t buffer[RC522_NXP_PAGE_SIZE])
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK(picc == NULL);
RC522_CHECK(buffer == NULL);
RC522_CHECK(!rc522_nxp_type_has_write(picc->type));
RC522_LOGD("NXP WRITE (address=%02" RC522_X ")", address);
uint8_t cmd_buffer[RC522_NXP_PAGE_SIZE + 4] = { 0 };
cmd_buffer[0] = RC522_NXP_WRITE;
cmd_buffer[1] = address;
memcpy(&cmd_buffer[2], buffer, RC522_NXP_PAGE_SIZE);
rc522_pcd_crc_t crc = { 0 };
RC522_RETURN_ON_ERROR(rc522_pcd_calculate_crc(rc522,
&(rc522_bytes_t) {
.ptr = cmd_buffer,
.length = sizeof(cmd_buffer) - 2,
},
&crc));
cmd_buffer[2 + RC522_NXP_PAGE_SIZE] = crc.lsb;
cmd_buffer[2 + RC522_NXP_PAGE_SIZE + 1] = crc.msb;
rc522_picc_transaction_t transaction = {
.bytes = { .ptr = cmd_buffer, .length = sizeof(cmd_buffer) },
};
rc522_picc_transaction_result_t result = {
.bytes = { .ptr = cmd_buffer, .length = sizeof(cmd_buffer) },
};
RC522_RETURN_ON_ERROR(rc522_picc_transceive(rc522, &transaction, &result));
return rc522_nxp_check_for_nak(&result);
}
esp_err_t rc522_nxp_read_cnt(
const rc522_handle_t rc522, const rc522_picc_t *picc, uint8_t counter_no, uint32_t *out_count)
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK(picc == NULL);
RC522_CHECK(out_count == NULL);
RC522_CHECK(!rc522_nxp_type_has_read_cnt(picc->type));
RC522_LOGD("NXP READ_CNT (counter=%02" RC522_X ")", counter_no);
uint8_t cmd_buffer[5] = {
// send(4)/recv(5) buffer
RC522_NXP_READ_CNT,
counter_no,
0,
0, // CRC
0 // Space for reply (3 bytes + CRC)
};
rc522_pcd_crc_t crc = { 0 };
RC522_RETURN_ON_ERROR(rc522_pcd_calculate_crc(rc522,
&(rc522_bytes_t) {
.ptr = cmd_buffer,
.length = sizeof(cmd_buffer) - 2,
},
&crc));
cmd_buffer[2] = crc.lsb;
cmd_buffer[3] = crc.msb;
rc522_picc_transaction_t transaction = {
.bytes = { .ptr = cmd_buffer, .length = 4 },
};
rc522_picc_transaction_result_t result = {
.bytes = { .ptr = cmd_buffer, .length = sizeof(cmd_buffer) },
};
RC522_RETURN_ON_ERROR(rc522_picc_transceive(rc522, &transaction, &result));
RC522_RETURN_ON_ERROR(rc522_nxp_check_for_nak(&result));
// TODO: Verify this counter is correct
*out_count = (cmd_buffer[2] << 16) | (cmd_buffer[1] << 8) | cmd_buffer[0];
return ESP_OK;
}
esp_err_t rc522_nxp_pwd_auth(const rc522_handle_t rc522, const rc522_picc_t *picc,
const uint8_t pwd[RC522_NXP_PWD_SIZE], const uint8_t pack[RC522_NXP_PACK_SIZE], rc522_picc_state_t *out_state)
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK(picc == NULL);
RC522_CHECK(pwd == NULL);
RC522_CHECK(pack == NULL);
RC522_CHECK(!rc522_nxp_type_has_pwd_auth(picc->type));
RC522_LOGD("NXP PWD_AUTH");
uint8_t cmd_buffer[RC522_NXP_PWD_SIZE + 3] = { 0 }; // send(11)/recv(4) buffer
cmd_buffer[0] = RC522_NXP_PWD_AUTH;
memcpy(&cmd_buffer[1], pwd, RC522_NXP_PWD_SIZE);
rc522_pcd_crc_t crc = { 0 };
RC522_RETURN_ON_ERROR(rc522_pcd_calculate_crc(rc522,
&(rc522_bytes_t) {
.ptr = cmd_buffer,
.length = sizeof(cmd_buffer) - 2,
},
&crc));
cmd_buffer[1 + RC522_NXP_PWD_SIZE] = crc.lsb;
cmd_buffer[1 + RC522_NXP_PWD_SIZE + 1] = crc.msb;
rc522_picc_transaction_t transaction = {
.bytes = { .ptr = cmd_buffer, .length = sizeof(cmd_buffer) },
};
rc522_picc_transaction_result_t result = {
.bytes = { .ptr = cmd_buffer, .length = sizeof(cmd_buffer) },
};
// If authentication fails (timeout or NAK), the card will HALT, so assume
// it has until we know otherwise
*out_state = RC522_PICC_STATE_HALT;
RC522_RETURN_ON_ERROR(rc522_picc_transceive(rc522, &transaction, &result));
RC522_RETURN_ON_ERROR(rc522_nxp_check_for_nak(&result));
// Successful authentication, set state
*out_state = RC522_PICC_STATE_AUTHENTICATED;
// Validate the PACK - not critical to authenticate the card, but good
// practice to verify
RC522_CHECK_AND_RETURN(memcmp(pack, cmd_buffer, RC522_NXP_PACK_SIZE) != 0, RC522_ERR_NXP_PACK_MISMATCH);
*out_state = RC522_PICC_STATE_AUTHENTICATED;
return ESP_OK;
}
esp_err_t rc522_nxp_read_sig(const rc522_handle_t rc522, const rc522_picc_t *picc, rc522_nxp_sig_t *out_sig)
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK(picc == NULL);
RC522_CHECK(out_sig == NULL);
uint8_t sig_size = rc522_nxp_type_sig_size(picc->type);
RC522_CHECK(sig_size == 0);
RC522_CHECK(out_sig->buffer_size < sig_size);
RC522_LOGD("NXP READ_SIG (size=%d)", sig_size);
uint8_t cmd_buffer[4] = {
RC522_NXP_READ_SIG,
0,
0,
0 // CRC
};
rc522_pcd_crc_t crc = { 0 };
RC522_RETURN_ON_ERROR(rc522_pcd_calculate_crc(rc522,
&(rc522_bytes_t) {
.ptr = cmd_buffer,
.length = sizeof(cmd_buffer) - 2,
},
&crc));
cmd_buffer[2] = crc.lsb;
cmd_buffer[3] = crc.msb;
// TODO: Combine to single malloc() for both buffers?
// Again, I'd rather not do a large malloc() here, but we need the space for
// CRC. See TODO in FAST_READ
uint8_t *recv_buffer = malloc(sig_size + 2); // +2 for CRC
RC522_CHECK_AND_RETURN(recv_buffer == NULL, ESP_ERR_NO_MEM);
rc522_picc_transaction_t transaction = {
.bytes = { .ptr = cmd_buffer, .length = sizeof(cmd_buffer) },
};
rc522_picc_transaction_result_t result = {
.bytes = { .ptr = recv_buffer, .length = sig_size + 2 },
};
esp_err_t ret = rc522_picc_transceive(rc522, &transaction, &result);
if (ret != ESP_OK) {
free(recv_buffer);
return ret;
}
ret = rc522_nxp_check_for_nak(&result);
if (ret != ESP_OK) {
free(recv_buffer);
return ret;
}
memcpy(out_sig->bytes, recv_buffer, sig_size);
out_sig->sig_size = sig_size;
free(recv_buffer);
return ESP_OK;
}

View File

@@ -0,0 +1,342 @@
#include <esp_system.h>
#include <esp_log.h>
#include <esp_check.h>
#include <string.h>
#include <sys/time.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/event_groups.h>
#include "rc522_pcd_internal.h"
#include "rc522_picc_internal.h"
#include "rc522_helpers_internal.h"
#include "rc522_types_internal.h"
#include "rc522_internal.h"
RC522_LOG_DEFINE_BASE();
ESP_EVENT_DEFINE_BASE(RC522_EVENTS);
inline static bool rc522_is_able_to_start(const rc522_handle_t rc522)
{
return rc522->state >= RC522_STATE_CREATED && rc522->state != RC522_STATE_POLLING;
}
static esp_err_t rc522_clone_config(const rc522_config_t *config, rc522_config_t **result)
{
rc522_config_t *config_clone = calloc(1, sizeof(rc522_config_t));
RC522_CHECK_AND_RETURN(config_clone == NULL, ESP_ERR_NO_MEM);
memcpy(config_clone, config, sizeof(rc522_config_t));
// defaults
if (config_clone->poll_interval_ms < RC522_POLL_INTERVAL_MS_MIN) {
config_clone->poll_interval_ms = RC522_POLL_INTERVAL_MS_DEFAULT;
}
if (config_clone->task_stack_size == 0) {
config_clone->task_stack_size = RC522_TASK_STACK_SIZE_DEFAULT;
}
if (config_clone->task_priority == 0) {
config_clone->task_priority = RC522_TASK_PRIORITY_DEFAULT;
}
// ~defaults
*result = config_clone;
return ESP_OK;
}
esp_err_t rc522_register_events(
const rc522_handle_t rc522, rc522_event_t event, esp_event_handler_t event_handler, void *event_handler_arg)
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK(event_handler == NULL);
return esp_event_handler_register_with(rc522->event_handle, RC522_EVENTS, event, event_handler, event_handler_arg);
}
esp_err_t rc522_unregister_events(const rc522_handle_t rc522, rc522_event_t event, esp_event_handler_t event_handler)
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK(event_handler == NULL);
return esp_event_handler_unregister_with(rc522->event_handle, RC522_EVENTS, event, event_handler);
}
esp_err_t rc522_start(rc522_handle_t rc522)
{
RC522_CHECK(rc522 == NULL);
ESP_RETURN_ON_FALSE(rc522 != NULL, ESP_ERR_INVALID_ARG, TAG, "rc522 is null");
ESP_RETURN_ON_FALSE(rc522_is_able_to_start(rc522),
ESP_ERR_INVALID_STATE,
TAG,
"Unable to start (state=%d)",
rc522->state);
if (rc522->state == RC522_STATE_PAUSED) {
// Scanning has been paused. No need for reinitialization. Just resume
rc522->state = RC522_STATE_POLLING;
return ESP_OK;
}
RC522_RETURN_ON_ERROR(rc522_pcd_reset(rc522, 150));
esp_err_t rw_ret = rc522_pcd_rw_test(rc522);
if (rw_ret != ESP_OK) {
ESP_LOGW(TAG, "rw test warning (%s), proceeding with pcd_init...", esp_err_to_name(rw_ret));
}
ESP_RETURN_ON_ERROR(rc522_pcd_init(rc522), TAG, "unable to init pcd");
rc522->state = RC522_STATE_POLLING;
return ESP_OK;
}
esp_err_t rc522_pause(rc522_handle_t rc522)
{
RC522_CHECK(rc522 == NULL);
ESP_RETURN_ON_FALSE(rc522->state == RC522_STATE_POLLING,
ESP_ERR_INVALID_STATE,
TAG,
"Invalid state (state=%d)",
rc522->state);
rc522->state = RC522_STATE_PAUSED;
return ESP_OK;
}
/**
* @brief Exit and delete the task
*/
inline static void rc522_request_task_to_exit(const rc522_handle_t rc522)
{
rc522->exit_requested = true; // task will delete itself
}
esp_err_t rc522_create(const rc522_config_t *config, rc522_handle_t *out_rc522)
{
RC522_CHECK(config == NULL);
RC522_CHECK(out_rc522 == NULL);
rc522_handle_t rc522 = calloc(1, sizeof(struct rc522));
ESP_RETURN_ON_FALSE(rc522 != NULL, ESP_ERR_NO_MEM, TAG, "nomem");
rc522_picc_set_state(rc522, &rc522->picc, RC522_PICC_STATE_IDLE, false);
esp_err_t ret = ESP_OK;
ESP_GOTO_ON_ERROR(rc522_clone_config(config, &(rc522->config)), _error, TAG, "clone config failed");
esp_event_loop_args_t event_args = {
.queue_size = 1,
.task_name = NULL, // no task will be created
};
rc522->bits = xEventGroupCreate();
ESP_GOTO_ON_FALSE(rc522->bits != NULL, ESP_ERR_NO_MEM, _error, TAG, "nomem");
ESP_GOTO_ON_ERROR(esp_event_loop_create(&event_args, &rc522->event_handle),
_error,
TAG,
"Failed to create event loop");
BaseType_t task_create_result = xTaskCreate(rc522_task,
"rc522_poll",
rc522->config->task_stack_size,
rc522,
rc522->config->task_priority,
&rc522->task_handle);
ESP_GOTO_ON_FALSE(task_create_result == pdTRUE, ESP_FAIL, _error, TAG, "task create failed");
goto _success;
_error:
rc522_destroy(rc522);
rc522 = NULL;
goto _return;
_success:
rc522->state = RC522_STATE_CREATED;
*out_rc522 = rc522;
_return:
return ret;
}
esp_err_t rc522_destroy(rc522_handle_t rc522)
{
RC522_CHECK(rc522 == NULL);
ESP_RETURN_ON_FALSE(xTaskGetCurrentTaskHandle() != rc522->task_handle,
ESP_ERR_INVALID_STATE,
TAG,
"Cannot destroy from event handler");
rc522_request_task_to_exit(rc522);
if (rc522->bits) {
xEventGroupWaitBits(rc522->bits, RC522_TASK_STOPPED_BIT, pdFALSE, pdTRUE, portMAX_DELAY);
vEventGroupDelete(rc522->bits);
rc522->bits = NULL;
}
if (rc522->event_handle) {
if (esp_event_loop_delete(rc522->event_handle) != ESP_OK) {
ESP_LOGW(TAG, "Failed to delete event loop");
}
rc522->event_handle = NULL;
}
if (rc522->config) {
free(rc522->config);
rc522->config = NULL;
}
free(rc522);
return ESP_OK;
}
esp_err_t rc522_dispatch_event(const rc522_handle_t rc522, rc522_event_t event, const void *data, size_t data_size)
{
RC522_RETURN_ON_ERROR(esp_event_post_to(rc522->event_handle, RC522_EVENTS, event, data, data_size, portMAX_DELAY));
return esp_event_loop_run(rc522->event_handle, 0);
}
void rc522_task(void *arg)
{
esp_err_t ret = ESP_OK;
rc522_handle_t rc522 = (rc522_handle_t)arg;
uint32_t last_poll_ms = 0;
const uint32_t task_delay_ms = 50;
const uint32_t picc_heartbeat_failure_threshold_ms = (2 * task_delay_ms);
uint32_t picc_heartbeat_failure_at_ms = 0;
bool mutex_taken = false;
const uint16_t mutex_take_timeout_ms = 4000;
xEventGroupClearBits(rc522->bits, RC522_TASK_STOPPED_BIT);
while (!rc522->exit_requested) {
if (mutex_taken && rc522->config->task_mutex != NULL) {
if (xSemaphoreGive(rc522->config->task_mutex) == pdTRUE) {
mutex_taken = false;
}
else {
RC522_LOGW("failed to give mutex");
}
}
if (rc522->state != RC522_STATE_POLLING) {
// waiting for state change to polling
rc522_delay_ms(100);
continue;
}
rc522_delay_ms(task_delay_ms);
if (rc522->config->task_mutex != NULL) {
if (xSemaphoreTake(rc522->config->task_mutex, pdMS_TO_TICKS(mutex_take_timeout_ms)) == pdTRUE) {
mutex_taken = true;
}
else {
RC522_LOGW("failed to take mutex in %d ms", mutex_take_timeout_ms);
continue;
}
}
bool should_poll = (rc522_millis() - last_poll_ms) > rc522->config->poll_interval_ms;
if (rc522->picc.state == RC522_PICC_STATE_IDLE || rc522->picc.state == RC522_PICC_STATE_HALT) {
rc522_picc_atqa_desc_t atqa;
if (rc522->picc.state == RC522_PICC_STATE_IDLE && ((ret = rc522_picc_reqa(rc522, &atqa)) != ESP_OK)) {
continue;
}
if (rc522->picc.state == RC522_PICC_STATE_HALT && ((ret = rc522_picc_wupa(rc522, &atqa)) != ESP_OK)) {
continue;
}
ESP_LOGI(TAG, "Tag detected in field (ATQA=0x%04X)", atqa.source);
// card is present
rc522->picc.atqa = atqa;
if (rc522->picc.state == RC522_PICC_STATE_IDLE) {
rc522_picc_set_state(rc522, &rc522->picc, RC522_PICC_STATE_READY, true);
}
else if (rc522->picc.state == RC522_PICC_STATE_HALT) {
rc522_picc_set_state(rc522, &rc522->picc, RC522_PICC_STATE_READY_H, true);
}
}
if (should_poll
&& (rc522->picc.state == RC522_PICC_STATE_READY || rc522->picc.state == RC522_PICC_STATE_READY_H)) {
rc522_picc_uid_t uid;
uint8_t sak;
ret = rc522_picc_select(rc522, &uid, &sak, false);
last_poll_ms = rc522_millis();
if (ret != ESP_OK) {
if (ret != RC522_ERR_RX_TIMEOUT && ret != RC522_ERR_INVALID_ATQA && ret != RC522_ERR_INVALID_SAK) {
RC522_LOGW("select failed (err=%04" RC522_X ")", ret);
}
else {
RC522_LOGD("select failed (err=%04" RC522_X ")", ret);
}
rc522_picc_set_state(rc522, &rc522->picc, RC522_PICC_STATE_IDLE, true);
continue;
}
memcpy(&rc522->picc.uid, &uid, sizeof(rc522_picc_uid_t));
rc522->picc.sak = sak;
rc522->picc.type = rc522_picc_get_type(&rc522->picc);
ESP_LOGI(TAG, "Tag selected (SAK=0x%02X, length=%d)", sak, uid.length);
if (rc522->picc.state == RC522_PICC_STATE_READY) {
rc522_picc_set_state(rc522, &rc522->picc, RC522_PICC_STATE_ACTIVE, true);
}
else if (rc522->picc.state == RC522_PICC_STATE_READY_H) {
rc522_picc_set_state(rc522, &rc522->picc, RC522_PICC_STATE_ACTIVE_H, true);
}
picc_heartbeat_failure_at_ms = 0;
continue;
}
if (rc522->picc.state == RC522_PICC_STATE_ACTIVE || rc522->picc.state == RC522_PICC_STATE_ACTIVE_H) {
if (picc_heartbeat_failure_at_ms != 0
&& ((rc522_millis() - picc_heartbeat_failure_at_ms) > picc_heartbeat_failure_threshold_ms)) {
picc_heartbeat_failure_at_ms = 0;
rc522_picc_set_state(rc522, &rc522->picc, RC522_PICC_STATE_IDLE, true);
continue;
}
if ((ret = rc522_picc_heartbeat(rc522, &rc522->picc, NULL, NULL)) == ESP_OK) {
picc_heartbeat_failure_at_ms = 0;
}
else if (picc_heartbeat_failure_at_ms == 0) {
picc_heartbeat_failure_at_ms = rc522_millis();
}
if (ret != ESP_OK) {
RC522_LOGD("heartbeat failed (err=%04" RC522_X ")", ret);
}
// card is still in the field
continue;
}
}
xEventGroupSetBits(rc522->bits, RC522_TASK_STOPPED_BIT);
ESP_LOGI(TAG, "Task exited");
vTaskDelete(NULL); // self-delete
}

View File

@@ -0,0 +1,108 @@
#include <string.h>
#include <driver/gpio.h>
#include "rc522_types_internal.h"
#include "rc522_driver_internal.h"
RC522_LOG_DEFINE_BASE();
esp_err_t rc522_driver_init_rst_pin(gpio_num_t rst_io_num)
{
RC522_CHECK(rst_io_num < 0);
gpio_config_t io_conf = {
.intr_type = GPIO_INTR_DISABLE,
.mode = GPIO_MODE_OUTPUT,
.pin_bit_mask = (1ULL << rst_io_num),
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.pull_up_en = GPIO_PULLUP_ENABLE,
};
RC522_RETURN_ON_ERROR(gpio_config(&io_conf));
RC522_RETURN_ON_ERROR(gpio_set_level(rst_io_num, !RC522_DRIVER_HARD_RST_PIN_PWR_DOWN_LEVEL));
return ESP_OK;
}
inline esp_err_t rc522_driver_install(const rc522_driver_handle_t driver)
{
RC522_CHECK(driver == NULL);
return driver->install(driver);
}
inline esp_err_t rc522_driver_send(const rc522_driver_handle_t driver, uint8_t address, const rc522_bytes_t *bytes)
{
RC522_CHECK(driver == NULL);
RC522_CHECK_BYTES(bytes);
return driver->send(driver, address, bytes);
}
inline esp_err_t rc522_driver_receive(const rc522_driver_handle_t driver, uint8_t address, rc522_bytes_t *bytes)
{
RC522_CHECK(driver == NULL);
RC522_CHECK_BYTES(bytes);
return driver->receive(driver, address, bytes);
}
inline esp_err_t rc522_driver_reset(const rc522_driver_handle_t driver)
{
RC522_CHECK(driver == NULL);
return driver->reset(driver);
}
inline esp_err_t rc522_driver_uninstall(const rc522_driver_handle_t driver)
{
RC522_CHECK(driver == NULL);
return driver->uninstall(driver);
}
esp_err_t rc522_driver_create(const void *config, size_t config_size, rc522_driver_handle_t *driver)
{
RC522_CHECK(config == NULL);
RC522_CHECK(config_size == 0);
RC522_CHECK(driver == NULL);
esp_err_t ret = ESP_OK;
rc522_driver_handle_t _driver = calloc(1, sizeof(struct rc522_driver_handle));
ESP_RETURN_ON_FALSE(_driver != NULL, ESP_ERR_NO_MEM, TAG, "nomem");
_driver->config = calloc(1, config_size);
ESP_GOTO_ON_FALSE(_driver->config != NULL, ESP_ERR_NO_MEM, error, TAG, "nomem");
memcpy(_driver->config, config, config_size);
goto success;
error:
rc522_driver_destroy(_driver);
goto exit;
success:
*driver = _driver;
exit:
return ret;
}
esp_err_t rc522_driver_destroy(rc522_driver_handle_t driver)
{
RC522_CHECK(driver == NULL);
if (driver->config) {
free(driver->config);
driver->config = NULL;
}
driver->install = NULL;
driver->send = NULL;
driver->receive = NULL;
driver->uninstall = NULL;
driver->device = NULL;
free(driver);
return ESP_OK;
}

View File

@@ -0,0 +1,65 @@
#include <esp_system.h>
#include <esp_log.h>
#include <esp_check.h>
#include <string.h>
#include <sys/time.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include "rc522_types_internal.h"
#include "rc522_helpers_internal.h"
#include <esp_timer.h>
RC522_LOG_DEFINE_BASE();
uint32_t rc522_millis()
{
return (uint32_t)(esp_timer_get_time() / 1000ULL);
}
void rc522_delay_ms(uint32_t ms)
{
TickType_t ticks = pdMS_TO_TICKS(ms);
if (ticks == 0 && ms > 0) {
ticks = 1;
}
vTaskDelay(ticks);
}
esp_err_t rc522_buffer_to_hex_str(
const uint8_t *buffer, uint8_t buffer_length, char *str_buffer, uint8_t str_buffer_length)
{
RC522_CHECK(buffer == NULL);
RC522_CHECK(buffer_length < 1);
RC522_CHECK(str_buffer == NULL);
const char *format = "%02" RC522_X " ";
const uint8_t formatted_length = 3;
RC522_CHECK(str_buffer_length < (buffer_length * formatted_length));
uint16_t len = 0;
for (uint16_t i = 0; i < buffer_length; i++) {
len += sprintf(str_buffer + len, format, buffer[i]);
}
str_buffer[len - 1] = 0x00;
return ESP_OK;
}
inline esp_err_t rc522_nibbles(uint8_t byte, uint8_t *msb, uint8_t *lsb)
{
RC522_CHECK(msb == NULL && lsb == NULL);
if (msb) {
*msb = byte >> 4;
}
if (lsb) {
*lsb = byte & 0x0F;
}
return ESP_OK;
}

View File

@@ -0,0 +1,370 @@
#include <esp_system.h>
#include <esp_check.h>
#include <string.h>
#include "rc522_types_internal.h"
#include "rc522_helpers_internal.h"
#include "rc522_driver_internal.h"
#include "rc522_pcd_internal.h"
RC522_LOG_DEFINE_BASE();
/**
* @see https://stackoverflow.com/a/48705557
*/
esp_err_t rc522_pcd_calculate_crc(const rc522_handle_t rc522, const rc522_bytes_t *bytes, rc522_pcd_crc_t *result)
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK_BYTES(bytes);
RC522_CHECK(result == NULL);
RC522_RETURN_ON_ERROR(rc522_pcd_stop_active_command(rc522));
RC522_RETURN_ON_ERROR(rc522_pcd_clear_bits(rc522, RC522_PCD_DIV_INT_REQ_REG, RC522_PCD_CRC_IRQ_BIT));
RC522_RETURN_ON_ERROR(rc522_pcd_fifo_flush(rc522));
RC522_RETURN_ON_ERROR(rc522_pcd_fifo_write(rc522, bytes));
RC522_RETURN_ON_ERROR(rc522_pcd_write(rc522, RC522_PCD_COMMAND_REG, RC522_PCD_CALC_CRC_CMD));
uint32_t deadline_ms = rc522_millis() + 90;
bool calculation_done = false;
do {
uint8_t irq;
RC522_RETURN_ON_ERROR(rc522_pcd_read(rc522, RC522_PCD_DIV_INT_REQ_REG, &irq));
if (irq & RC522_PCD_CRC_IRQ_BIT) {
calculation_done = true;
break;
}
rc522_delay_ms(2);
}
while (rc522_millis() < deadline_ms);
if (!calculation_done) { // Deadline reached
return ESP_ERR_TIMEOUT;
}
rc522_pcd_crc_t crc = { 0 };
RC522_RETURN_ON_ERROR(rc522_pcd_stop_active_command(rc522));
RC522_RETURN_ON_ERROR(rc522_pcd_read(rc522, RC522_PCD_CRC_RESULT_MSB_REG, &crc.msb));
RC522_RETURN_ON_ERROR(rc522_pcd_read(rc522, RC522_PCD_CRC_RESULT_LSB_REG, &crc.lsb));
if (RC522_LOG_LEVEL >= ESP_LOG_DEBUG) {
char debug_buffer[64];
rc522_buffer_to_hex_str(bytes->ptr, bytes->length, debug_buffer, sizeof(debug_buffer));
RC522_LOGD("crc(%s) = 0x%04" RC522_X, debug_buffer, crc.value);
}
*result = crc;
return ESP_OK;
}
static esp_err_t rc522_pcd_wait_for_reset(const rc522_handle_t rc522, uint32_t timeout_ms)
{
RC522_CHECK(rc522 == NULL);
esp_err_t ret = ESP_OK;
bool power_down_bit = true;
uint32_t start_ms = rc522_millis();
// Wait for the PowerDown bit in CommandReg to be cleared
do {
rc522_delay_ms(25);
uint8_t cmd;
if ((ret = rc522_pcd_read(rc522, RC522_PCD_COMMAND_REG, &cmd)) != ESP_OK) {
RC522_LOGD("wait for reset error %04" RC522_X, ret);
continue;
}
if (!(power_down_bit = (cmd & RC522_PCD_POWER_DOWN_BIT))) {
break;
}
}
while ((rc522_millis() - start_ms) < timeout_ms);
return power_down_bit ? ESP_ERR_TIMEOUT : ESP_OK;
}
inline static esp_err_t rc522_pcd_hard_reset(const rc522_handle_t rc522, uint32_t timeout_ms)
{
RC522_CHECK(rc522 == NULL);
RC522_LOGD("hard reset");
esp_err_t ret = rc522_driver_reset(rc522->config->driver);
return ret == ESP_OK ? rc522_pcd_wait_for_reset(rc522, timeout_ms) : ret;
}
inline static esp_err_t rc522_pcd_soft_reset(const rc522_handle_t rc522, uint32_t timeout_ms)
{
RC522_CHECK(rc522 == NULL);
RC522_LOGD("soft reset");
RC522_RETURN_ON_ERROR(rc522_pcd_write(rc522, RC522_PCD_COMMAND_REG, RC522_PCD_SOFT_RESET_CMD));
return rc522_pcd_wait_for_reset(rc522, timeout_ms);
}
esp_err_t rc522_pcd_reset(const rc522_handle_t rc522, uint32_t timeout_ms)
{
esp_err_t ret = ESP_OK;
if ((ret = rc522_pcd_hard_reset(rc522, timeout_ms)) != ESP_OK) {
if (ret != RC522_ERR_RST_PIN_UNUSED) {
RC522_LOGW("hard reset failed, trying soft reset");
}
ret = rc522_pcd_soft_reset(rc522, timeout_ms);
}
return ret;
}
inline static esp_err_t rc522_pcd_tx_enable(const rc522_handle_t rc522)
{
return rc522_pcd_set_bits(rc522, RC522_PCD_TX_CONTROL_REG, (RC522_PCD_TX2_RF_EN_BIT | RC522_PCD_TX1_RF_EN_BIT));
}
static esp_err_t rc522_pcd_configure_timer(const rc522_handle_t rc522, uint8_t mode, uint16_t prescaler)
{
uint8_t prescaler_hi = (prescaler >> 8) & 0x0F;
uint8_t timer_mode = (mode & 0xF0) | prescaler_hi;
uint8_t prescaler_lo = (prescaler & 0xFF);
RC522_RETURN_ON_ERROR(rc522_pcd_write(rc522, RC522_PCD_TIMER_MODE_REG, timer_mode));
RC522_RETURN_ON_ERROR(rc522_pcd_write(rc522, RC522_PCD_TIMER_PRESCALER_REG, prescaler_lo));
return ESP_OK;
}
inline static esp_err_t rc522_pcd_set_timer_reload_value(const rc522_handle_t rc522, uint16_t value)
{
RC522_RETURN_ON_ERROR(rc522_pcd_write(rc522, RC522_PCD_TIMER_RELOAD_MSB_REG, (value >> 8) & 0xFF));
RC522_RETURN_ON_ERROR(rc522_pcd_write(rc522, RC522_PCD_TIMER_RELOAD_LSB_REG, value & 0xFF));
return ESP_OK;
}
inline esp_err_t rc522_pcd_set_rx_gain(const rc522_handle_t rc522, rc522_pcd_rx_gain_t gain)
{
uint8_t value = 0;
RC522_RETURN_ON_ERROR(rc522_pcd_read(rc522, RC522_PCD_RF_CFG_REG, &value));
return rc522_pcd_write(rc522, RC522_PCD_RF_CFG_REG, (value & ~0x70) | (gain & 0x70));
}
esp_err_t rc522_pcd_init(const rc522_handle_t rc522)
{
RC522_CHECK(rc522 == NULL);
// Reset baud rates
RC522_RETURN_ON_ERROR(rc522_pcd_write(rc522, RC522_PCD_TX_MODE_REG, RC522_PCD_TX_MODE_REG_RESET_VALUE));
RC522_RETURN_ON_ERROR(rc522_pcd_write(rc522, RC522_PCD_RX_MODE_REG, RC522_PCD_RX_MODE_REG_RESET_VALUE));
// Reset modulation width
RC522_RETURN_ON_ERROR(rc522_pcd_write(rc522, RC522_PCD_MOD_WIDTH_REG, RC522_PCD_MOD_WIDTH_REG_RESET_VALUE));
// When communicating with a PICC we need a timeout if something goes wrong.
// f_timer = 13.56 MHz / (2*TPreScaler+1) where TPreScaler = [TPrescaler_Hi:TPrescaler_Lo].
// TPrescaler_Hi are the four low bits in TModeReg. TPrescaler_Lo is TPrescalerReg.
// TAuto=1; timer starts automatically at the end of the transmission in all communication modes at all speeds
// TPreScaler = TModeReg[3..0]:TPrescalerReg, ie 0x0A9 = 169 => f_timer=40kHz, ie a timer period of 25μs.
RC522_RETURN_ON_ERROR(rc522_pcd_configure_timer(rc522, RC522_PCD_T_AUTO_BIT, 169));
// Reload timer with 0x3E8 = 1000, ie 25ms before timeout.
RC522_RETURN_ON_ERROR(rc522_pcd_set_timer_reload_value(rc522, 1000));
RC522_RETURN_ON_ERROR(rc522_pcd_write(rc522, RC522_PCD_TX_ASK_REG, RC522_PCD_FORCE_100_ASK_BIT));
RC522_RETURN_ON_ERROR(rc522_pcd_write(rc522, RC522_PCD_MODE_REG, 0x3D));
// Enable the antenna driver pins TX1 and TX2 (they were disabled by the reset)
RC522_RETURN_ON_ERROR(rc522_pcd_tx_enable(rc522));
// Set maximum receiver antenna gain (48dB) for compact antenna on MH-ET LIVE module
RC522_RETURN_ON_ERROR(rc522_pcd_set_rx_gain(rc522, RC522_PCD_48_DB_RX_GAIN));
rc522_pcd_firmware_t fw;
ESP_RETURN_ON_ERROR(rc522_pcd_firmware(rc522, &fw), TAG, "read fw version failed");
// When 0x00 or 0xFF is returned, communication probably failed
if (fw == 0x00 || fw == 0xFF) {
RC522_LOGE("Communication failure, is the MFRC522 properly connected?");
return ESP_FAIL; // TODO: use custom err
}
uint8_t mode_reg = 0, tx_ctrl = 0, rf_cfg = 0;
rc522_pcd_read(rc522, RC522_PCD_MODE_REG, &mode_reg);
rc522_pcd_read(rc522, RC522_PCD_TX_CONTROL_REG, &tx_ctrl);
rc522_pcd_read(rc522, RC522_PCD_RF_CFG_REG, &rf_cfg);
ESP_LOGI(TAG, "PCD (fw=%s) initialized [ModeReg=0x%02X TxControlReg=0x%02X RFCfgReg=0x%02X]",
rc522_pcd_firmware_name(fw), mode_reg, tx_ctrl, rf_cfg);
return ESP_OK;
}
esp_err_t rc522_pcd_firmware(const rc522_handle_t rc522, rc522_pcd_firmware_t *fw)
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK(fw == NULL);
uint8_t value;
RC522_RETURN_ON_ERROR(rc522_pcd_read(rc522, RC522_PCD_VERSION_REG, &value));
*fw = (rc522_pcd_firmware_t)value;
return ESP_OK;
}
char *rc522_pcd_firmware_name(rc522_pcd_firmware_t firmware)
{
switch (firmware) {
case RC522_PCD_FIRMWARE_CLONE:
return "clone";
case RC522_PCD_FIRMWARE_00:
return "v0.0";
case RC522_PCD_FIRMWARE_10:
return "v1.0";
case RC522_PCD_FIRMWARE_20:
return "v2.0";
case RC522_PCD_FIRMWARE_COUNTERFEIT:
return "counterfeit_chip";
default:
return "unknown";
}
}
inline esp_err_t rc522_pcd_stop_active_command(const rc522_handle_t rc522)
{
return rc522_pcd_write(rc522, RC522_PCD_COMMAND_REG, RC522_PCD_IDLE_CMD);
}
inline esp_err_t rc522_pcd_clear_all_com_interrupts(const rc522_handle_t rc522)
{
return rc522_pcd_write(rc522, RC522_PCD_COM_INT_REQ_REG, (uint8_t)(~RC522_PCD_SET_1_BIT));
}
inline esp_err_t rc522_pcd_fifo_write(const rc522_handle_t rc522, const rc522_bytes_t *bytes)
{
return rc522_pcd_write_n(rc522, RC522_PCD_FIFO_DATA_REG, bytes);
}
inline esp_err_t rc522_pcd_fifo_read(const rc522_handle_t rc522, rc522_bytes_t *bytes)
{
return rc522_pcd_read_n(rc522, RC522_PCD_FIFO_DATA_REG, bytes);
}
inline esp_err_t rc522_pcd_fifo_flush(const rc522_handle_t rc522)
{
return rc522_pcd_write(rc522, RC522_PCD_FIFO_LEVEL_REG, RC522_PCD_FLUSH_BUFFER_BIT);
}
inline esp_err_t rc522_pcd_start_data_transmission(const rc522_handle_t rc522)
{
return rc522_pcd_set_bits(rc522, RC522_PCD_BIT_FRAMING_REG, RC522_PCD_START_SEND_BIT);
}
inline esp_err_t rc522_pcd_stop_data_transmission(const rc522_handle_t rc522)
{
return rc522_pcd_clear_bits(rc522, RC522_PCD_BIT_FRAMING_REG, RC522_PCD_START_SEND_BIT);
}
inline esp_err_t rc522_pcd_stop_crypto1(const rc522_handle_t rc522)
{
return rc522_pcd_clear_bits(rc522, RC522_PCD_STATUS_2_REG, RC522_PCD_MF_CRYPTO1_ON_BIT);
}
esp_err_t rc522_pcd_rw_test(const rc522_handle_t rc522)
{
RC522_CHECK(rc522 == NULL);
uint8_t tmp;
ESP_RETURN_ON_ERROR(rc522_pcd_read(rc522, RC522_PCD_FIFO_LEVEL_REG, &tmp), TAG, "Cannot read FIFO length");
ESP_RETURN_ON_ERROR(rc522_pcd_fifo_flush(rc522), TAG, "Cannot flush FIFO");
uint8_t buffer1[] = { 0x13, 0x33, 0x37 };
const uint8_t buffer_size = sizeof(buffer1);
uint8_t buffer2[buffer_size];
ESP_RETURN_ON_ERROR(rc522_pcd_fifo_write(rc522, &(rc522_bytes_t) { .ptr = buffer1, .length = buffer_size }),
TAG,
"Cannot write to FIFO");
RC522_RETURN_ON_ERROR(rc522_pcd_read(rc522, RC522_PCD_FIFO_LEVEL_REG, &tmp));
ESP_RETURN_ON_FALSE(tmp == buffer_size, ESP_FAIL, TAG, "FIFO length missmatch after write");
RC522_RETURN_ON_ERROR(rc522_pcd_fifo_read(rc522, &(rc522_bytes_t) { .ptr = buffer2, .length = buffer_size }));
if (memcmp(buffer1, buffer2, buffer_size) != 0) {
RC522_LOGE("Buffers content missmatch");
RC522_LOGE("Buffer1: ");
ESP_LOG_BUFFER_HEX_LEVEL(TAG, buffer1, buffer_size, ESP_LOG_ERROR);
RC522_LOGE("Buffer2: ");
ESP_LOG_BUFFER_HEX_LEVEL(TAG, buffer2, buffer_size, ESP_LOG_ERROR);
return ESP_FAIL;
}
return ESP_OK;
}
inline esp_err_t rc522_pcd_write_n(const rc522_handle_t rc522, rc522_pcd_register_t addr, const rc522_bytes_t *bytes)
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK_BYTES(bytes);
if (RC522_LOG_LEVEL >= ESP_LOG_VERBOSE) {
char debug_buffer[64];
rc522_buffer_to_hex_str(bytes->ptr, bytes->length, debug_buffer, sizeof(debug_buffer));
RC522_LOGV("pcd [0x%02" RC522_X "] <<< %s", addr, debug_buffer);
}
RC522_RETURN_ON_ERROR(rc522_driver_send(rc522->config->driver, addr, bytes));
return ESP_OK;
}
inline esp_err_t rc522_pcd_write(const rc522_handle_t rc522, rc522_pcd_register_t addr, uint8_t val)
{
return rc522_pcd_write_n(rc522, addr, &(rc522_bytes_t) { .ptr = &val, .length = 1 });
}
inline esp_err_t rc522_pcd_read_n(const rc522_handle_t rc522, rc522_pcd_register_t addr, rc522_bytes_t *bytes)
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK_BYTES(bytes);
esp_err_t ret = rc522_driver_receive(rc522->config->driver, addr, bytes);
if (RC522_LOG_LEVEL >= ESP_LOG_VERBOSE) {
char debug_buffer[64];
rc522_buffer_to_hex_str(bytes->ptr, bytes->length, debug_buffer, sizeof(debug_buffer));
RC522_LOGV("pcd [0x%02" RC522_X "] >>> %s", addr, debug_buffer);
}
return ret;
}
inline esp_err_t rc522_pcd_read(const rc522_handle_t rc522, rc522_pcd_register_t addr, uint8_t *value_ref)
{
return rc522_pcd_read_n(rc522, addr, &(rc522_bytes_t) { .ptr = value_ref, .length = 1 });
}
inline esp_err_t rc522_pcd_set_bits(const rc522_handle_t rc522, rc522_pcd_register_t addr, uint8_t bits)
{
uint8_t value;
RC522_RETURN_ON_ERROR(rc522_pcd_read(rc522, addr, &value));
return rc522_pcd_write(rc522, addr, value | bits);
}
inline esp_err_t rc522_pcd_clear_bits(const rc522_handle_t rc522, rc522_pcd_register_t addr, uint8_t bits)
{
uint8_t value;
ESP_RETURN_ON_ERROR(rc522_pcd_read(rc522, addr, &value), TAG, "");
return rc522_pcd_write(rc522, addr, value & (~bits));
}

View File

@@ -0,0 +1,858 @@
#include <esp_system.h>
#include <esp_check.h>
#include <string.h>
#include "rc522_internal.h"
#include "rc522_types_internal.h"
#include "rc522_helpers_internal.h"
#include "rc522_pcd_internal.h"
#include "rc522_picc_internal.h"
RC522_LOG_DEFINE_BASE();
struct rc522_picc_transaction_context
{
const rc522_picc_transaction_t *transaction;
uint8_t interrupts;
bool completed;
uint8_t error_reg;
};
esp_err_t rc522_picc_send(const rc522_handle_t rc522, const rc522_picc_transaction_t *transaction,
rc522_picc_transaction_context_t *out_context)
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK(transaction == NULL);
RC522_CHECK_BYTES(&transaction->bytes);
RC522_CHECK(
transaction->pcd_command != RC522_PCD_TRANSCEIVE_CMD && transaction->pcd_command != RC522_PCD_MF_AUTH_CMD);
RC522_CHECK(transaction->expected_interrupts == 0);
rc522_picc_transaction_context_t context = {
.transaction = transaction,
};
// Prepare values for bit framing
uint8_t bit_framing = (transaction->rx_align << 4) + transaction->valid_bits;
if (RC522_LOG_LEVEL >= ESP_LOG_DEBUG) {
RC522_LOGD("rx_align=%d,tx_last_bits=%d, bit_framing=%d",
transaction->rx_align,
transaction->valid_bits,
bit_framing);
char debug_buffer[64];
rc522_buffer_to_hex_str(transaction->bytes.ptr, transaction->bytes.length, debug_buffer, sizeof(debug_buffer));
RC522_LOGD("picc << %s", debug_buffer);
}
RC522_RETURN_ON_ERROR(rc522_pcd_stop_active_command(rc522));
RC522_RETURN_ON_ERROR(rc522_pcd_clear_all_com_interrupts(rc522));
RC522_RETURN_ON_ERROR(rc522_pcd_fifo_flush(rc522));
RC522_RETURN_ON_ERROR(rc522_pcd_fifo_write(rc522, &transaction->bytes));
RC522_RETURN_ON_ERROR(rc522_pcd_write(rc522, RC522_PCD_BIT_FRAMING_REG, bit_framing));
RC522_RETURN_ON_ERROR(rc522_pcd_write(rc522, RC522_PCD_COMMAND_REG, transaction->pcd_command));
if (transaction->pcd_command == RC522_PCD_TRANSCEIVE_CMD) {
RC522_RETURN_ON_ERROR(rc522_pcd_start_data_transmission(rc522));
}
// TAuto flag in TModeReg is set.
// This means the timer automatically starts when the PCD stops transmitting.
const uint32_t deadline = rc522_millis() + 36;
do {
RC522_RETURN_ON_ERROR(rc522_pcd_read(rc522, RC522_PCD_COM_INT_REQ_REG, &context.interrupts));
if (context.interrupts & transaction->expected_interrupts) {
context.completed = true;
break;
}
// Timer interrupt - nothing received
if (context.interrupts & RC522_PCD_TIMER_IRQ_BIT) {
RC522_LOGD("timer interrupt (irq=0x%02" RC522_X ")", context.interrupts);
rc522_pcd_stop_active_command(rc522);
return RC522_ERR_RX_TIMER_TIMEOUT;
}
rc522_delay_ms(2);
}
while (rc522_millis() < deadline);
// Deadline reached and nothing happened (normal when no card is present).
if (!context.completed) {
rc522_pcd_stop_active_command(rc522);
return RC522_ERR_RX_TIMEOUT;
}
// Stop now if any errors except collisions were detected.
RC522_RETURN_ON_ERROR(rc522_pcd_read(rc522, RC522_PCD_ERROR_REG, &context.error_reg));
if (context.error_reg & RC522_PCD_BUFFER_OVFL_BIT) {
return RC522_ERR_PCD_FIFO_BUFFER_OVERFLOW;
}
else if (context.error_reg & RC522_PCD_PARITY_ERR_BIT) {
RC522_LOGD("parity error detected");
return RC522_ERR_PCD_PARITY_CHECK_FAILED;
}
else if (context.error_reg & RC522_PCD_PROTOCOL_ERR_BIT) {
RC522_LOGD("protocol error detected");
return RC522_ERR_PCD_PROTOCOL_ERROR;
}
if (out_context) {
memcpy(out_context, &context, sizeof(context));
}
return ESP_OK;
}
static esp_err_t rc522_picc_receive(const rc522_handle_t rc522, const rc522_picc_transaction_context_t *context,
rc522_picc_transaction_result_t *out_result)
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK(context == NULL);
RC522_CHECK(context->transaction == NULL);
RC522_CHECK(out_result == NULL);
RC522_CHECK_BYTES(&context->transaction->bytes);
uint8_t fifo_level = 0;
RC522_RETURN_ON_ERROR(rc522_pcd_read(rc522, RC522_PCD_FIFO_LEVEL_REG, &fifo_level));
if (fifo_level < 1) {
RC522_LOGD("fifo empty (irq=0x%02" RC522_X ")", context->interrupts);
return RC522_ERR_PCD_FIFO_EMPTY;
}
RC522_CHECK(fifo_level > out_result->bytes.length);
rc522_picc_transaction_result_t result = {
.bytes = {
.ptr = out_result->bytes.ptr, // Use buffer provided by caller
.length = fifo_level,
},
};
RC522_RETURN_ON_ERROR(rc522_pcd_fifo_read(rc522, &result.bytes));
if (RC522_LOG_LEVEL >= ESP_LOG_DEBUG) {
char debug_buffer[64];
rc522_buffer_to_hex_str(result.bytes.ptr, result.bytes.length, debug_buffer, sizeof(debug_buffer));
RC522_LOGD("picc >> %s", debug_buffer);
}
if (context->transaction->rx_align) {
RC522_LOGD("applying mask (rx_align=%d)", context->transaction->rx_align);
// Apply mask for rx_align..7 of the first byte
result.bytes.ptr[0] &= (0xFF << context->transaction->rx_align);
}
// RxLastBits[2:0] indicates the number of valid bits in the last received byte.
// If this value is 0, the whole byte is valid.
RC522_RETURN_ON_ERROR(rc522_pcd_read(rc522, RC522_PCD_CONTROL_REG, &result.valid_bits));
result.valid_bits &= 0x07;
if (result.valid_bits) {
RC522_LOGD("not full byte received, valid_bits=%d", result.valid_bits);
}
if (context->error_reg & RC522_PCD_COLL_ERR_BIT) {
return RC522_ERR_COLLISION;
}
// Perform CRC_A validation
if (context->transaction->check_crc) {
// We need at least the CRC_A value
// and all 8 bits of the last byte
RC522_CHECK_AND_RETURN(result.bytes.length < 3, ESP_ERR_INVALID_STATE);
RC522_CHECK_AND_RETURN(result.valid_bits != 0, ESP_ERR_INVALID_STATE);
// Verify CRC_A
rc522_pcd_crc_t crc = { 0 };
RC522_RETURN_ON_ERROR(rc522_pcd_calculate_crc(rc522,
&(rc522_bytes_t) { .ptr = result.bytes.ptr, .length = result.bytes.length - 2 },
&crc));
if (memcmp(result.bytes.ptr + result.bytes.length - 2, &crc, sizeof(crc)) != 0) {
return RC522_ERR_CRC_WRONG;
}
}
memcpy(out_result, &result, sizeof(result));
return ESP_OK;
}
esp_err_t rc522_picc_transceive(const rc522_handle_t rc522, const rc522_picc_transaction_t *transaction,
rc522_picc_transaction_result_t *out_result)
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK(transaction == NULL);
rc522_picc_transaction_t transaction_clone = { 0 };
memcpy(&transaction_clone, transaction, sizeof(transaction_clone));
transaction_clone.pcd_command = RC522_PCD_TRANSCEIVE_CMD;
transaction_clone.expected_interrupts = RC522_PCD_RX_IRQ_BIT | RC522_PCD_IDLE_IRQ_BIT;
rc522_picc_transaction_context_t context = { 0 };
RC522_RETURN_ON_ERROR_SILENTLY(rc522_picc_send(rc522, &transaction_clone, &context));
if (out_result) {
if (!(context.interrupts & RC522_PCD_RX_IRQ_BIT)) {
return RC522_ERR_RX_TIMEOUT;
}
RC522_RETURN_ON_ERROR_SILENTLY(rc522_picc_receive(rc522, &context, out_result));
}
return ESP_OK;
}
inline static esp_err_t rc522_picc_parse_atqa(uint16_t atqa, rc522_picc_atqa_desc_t *out_atqa)
{
RC522_CHECK(out_atqa == NULL);
out_atqa->source = atqa;
out_atqa->rfu4 = (atqa >> 12) & 0x0F;
out_atqa->prop_coding = (atqa >> 8) & 0x0F;
out_atqa->uid_size = (atqa >> 6) & 0x03;
out_atqa->rfu1 = (atqa >> 5) & 0x01;
out_atqa->anticollision = atqa & 0x1F;
return ESP_OK;
}
static esp_err_t rc522_picc_reqa_or_wupa(const rc522_handle_t rc522, uint8_t picc_cmd, rc522_picc_atqa_desc_t *out_atqa)
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK(picc_cmd != RC522_PICC_CMD_REQA && picc_cmd != RC522_PICC_CMD_WUPA);
RC522_CHECK(out_atqa == NULL);
RC522_RETURN_ON_ERROR(rc522_pcd_clear_bits(rc522, RC522_PCD_COLL_REG, RC522_PCD_VALUES_AFTER_COLL_BIT));
uint8_t buffer[2] = { 0 };
rc522_picc_transaction_t transaction = {
.bytes = { .ptr = &picc_cmd, .length = 1 },
.valid_bits = 7, // REQA and WUPA use short frame format
};
rc522_picc_transaction_result_t transaction_result = {
.bytes = { .ptr = buffer, .length = sizeof(buffer) },
};
esp_err_t ret = rc522_picc_transceive(rc522, &transaction, &transaction_result);
if (ret != ESP_OK) {
// Timeouts are expected if no PICC are in the field, log other errors
if (ret != RC522_ERR_RX_TIMER_TIMEOUT && ret != RC522_ERR_RX_TIMEOUT) {
RC522_LOGD("non-timeout error: %04" RC522_X, ret);
}
return ret;
}
if (transaction_result.bytes.length != 2 || transaction_result.valid_bits != 0) {
return RC522_ERR_INVALID_ATQA;
}
uint16_t atqa = (buffer[0] << 8) | buffer[1];
RC522_RETURN_ON_ERROR(rc522_picc_parse_atqa(atqa, out_atqa));
return ESP_OK;
}
inline esp_err_t rc522_picc_reqa(const rc522_handle_t rc522, rc522_picc_atqa_desc_t *out_atqa)
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK(out_atqa == NULL);
RC522_LOGD("REQA");
return rc522_picc_reqa_or_wupa(rc522, RC522_PICC_CMD_REQA, out_atqa);
}
inline esp_err_t rc522_picc_wupa(const rc522_handle_t rc522, rc522_picc_atqa_desc_t *out_atqa)
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK(out_atqa == NULL);
RC522_LOGD("WUPA");
return rc522_picc_reqa_or_wupa(rc522, RC522_PICC_CMD_WUPA, out_atqa);
}
/**
* Resolve collision and SELECT a PICC
*/
esp_err_t rc522_picc_select(const rc522_handle_t rc522, rc522_picc_uid_t *out_uid, uint8_t *out_sak, bool skip_anticoll)
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK(skip_anticoll && (out_uid == NULL || out_uid->length < RC522_PICC_UID_SIZE_MIN));
bool uid_complete;
bool select_done;
bool use_cascade_tag;
uint8_t cascade_level = 1;
esp_err_t ret;
uint8_t count;
uint8_t check_bit;
uint8_t index;
uint8_t uid_index; // The first index in uid->uidByte[] that is used in the current Cascade Level.
int8_t current_level_known_bits; // The number of known UID bits in the current Cascade Level.
uint8_t buffer[9]; // The SELECT/ANTICOLLISION commands uses a 7 byte standard frame + 2 bytes CRC_A
uint8_t buffer_used; // The number of bytes used in the buffer, ie the number of bytes to transfer to the FIFO.
uint8_t rx_align; // Used in BitFramingReg. Defines the bit position for the first bit received.
uint8_t tx_last_bits = 0; // Used in BitFramingReg. The number of valid bits in the last transmitted byte.
uint8_t *response_buffer;
uint8_t response_length;
rc522_picc_uid_t uid;
if (skip_anticoll) {
memcpy(&uid, out_uid, sizeof(rc522_picc_uid_t));
}
else {
memset(&uid, 0, sizeof(uid));
}
uint8_t sak;
// Description of buffer structure:
// Byte 0: SEL Indicates the Cascade Level: PICC_CMD_SEL_CL1, PICC_CMD_SEL_CL2 or PICC_CMD_SEL_CL3
// Byte 1: NVB Number of Valid Bits (in complete command, not just the UID):
// High nibble: complete bytes, Low nibble: Extra bits.
// Byte 2: UID-data or CT See explanation below. CT means Cascade Tag.
// Byte 3: UID-data
// Byte 4: UID-data
// Byte 5: UID-data
// Byte 6: BCC Block Check Character - XOR of bytes 2-5
// Byte 7: CRC_A
// Byte 8: CRC_A
//
// The BCC and CRC_A are only transmitted if we know all the UID bits of the current Cascade Level.
//
// Description of bytes 2-5: (Section 6.5.4 of the ISO/IEC 14443-3 draft: UID contents and cascade levels)
// UID size Cascade level Byte2 Byte3 Byte4 Byte5
// ======== ============= ===== ===== ===== =====
// 4 bytes 1 uid0 uid1 uid2 uid3
// 7 bytes 1 CT uid0 uid1 uid2
// 2 uid3 uid4 uid5 uid6
// 10 bytes 1 CT uid0 uid1 uid2
// 2 CT uid3 uid4 uid5
// 3 uid6 uid7 uid8 uid9
// Prepare MFRC522
RC522_RETURN_ON_ERROR(rc522_pcd_clear_bits(rc522, RC522_PCD_COLL_REG, RC522_PCD_VALUES_AFTER_COLL_BIT));
// Repeat Cascade Level loop until we have a complete UID.
uid_complete = false;
while (!uid_complete) {
RC522_LOGD("cascade_level=%d, uid.length=%d", cascade_level, uid.length);
// Set the Cascade Level in the SEL byte, find out if we need to use the Cascade Tag in byte 2.
switch (cascade_level) {
case 1:
buffer[0] = RC522_PICC_CMD_SEL_CL1;
uid_index = 0;
// When we know that the UID has more than 4 bytes
use_cascade_tag = uid.length > 4;
break;
case 2:
buffer[0] = RC522_PICC_CMD_SEL_CL2;
uid_index = 3;
// When we know that the UID has more than 7 bytes
use_cascade_tag = uid.length > 7;
break;
case 3:
buffer[0] = RC522_PICC_CMD_SEL_CL3;
uid_index = 6;
use_cascade_tag = false; // Never used in CL3.
break;
default:
return ESP_FAIL; // TODO: use custom err
break;
}
RC522_LOGD("cl=%d, uid_index=%d, use_cascade_tag=%d", cascade_level, uid_index, use_cascade_tag);
// How many UID bits are known in this Cascade Level?
current_level_known_bits = skip_anticoll ? (4 * 8) : 0;
// Copy the known bits from uid.uidByte[] to buffer[]
index = 2; // destination index in buffer[]
if (use_cascade_tag) {
buffer[index++] = RC522_PICC_CMD_CT;
}
// The number of bytes needed to represent the known bits for this level.
uint8_t bytes_to_copy = current_level_known_bits / 8 + (current_level_known_bits % 8 ? 1 : 0);
if (bytes_to_copy) {
// Max 4 bytes in each Cascade Level. Only 3 left if we use the Cascade Tag
uint8_t max_bytes = use_cascade_tag ? 3 : 4;
if (bytes_to_copy > max_bytes) {
bytes_to_copy = max_bytes;
}
for (count = 0; count < bytes_to_copy; count++) {
buffer[index++] = uid.value[uid_index + count];
}
}
// Now that the data has been copied we need to include the 8 bits in CT in current_level_known_bits
if (use_cascade_tag) {
current_level_known_bits += 8;
}
// Repeat anti collision loop until we can transmit all UID bits + BCC and receive a SAK - max 32 iterations.
select_done = false;
while (!select_done) {
// Find out how many bits and bytes to send and receive.
if (current_level_known_bits >= 32) { // All UID bits in this Cascade Level are known. This is a SELECT.
RC522_LOGD("SELECT (cl=%d)", cascade_level);
// NVB - Number of Valid Bits: Seven whole bytes
buffer[1] = 0x70;
// Calculate BCC - Block Check Character
buffer[6] = buffer[2] ^ buffer[3] ^ buffer[4] ^ buffer[5];
// Calculate CRC_A
rc522_pcd_crc_t crc = { 0 };
RC522_RETURN_ON_ERROR(
rc522_pcd_calculate_crc(rc522, &(rc522_bytes_t) { .ptr = buffer, .length = 7 }, &crc));
buffer[7] = crc.lsb;
buffer[8] = crc.msb;
tx_last_bits = 0; // 0 => All 8 bits are valid.
buffer_used = 9;
// Store response in the last 3 bytes of buffer (BCC and CRC_A - not needed after tx)
response_buffer = &buffer[6];
response_length = 3;
}
else { // This is an ANTICOLLISION.
RC522_LOGD("ANTICOLLISION (cl=%d)", cascade_level);
tx_last_bits = current_level_known_bits % 8;
count = current_level_known_bits / 8; // Number of whole bytes in the UID part.
index = 2 + count; // Number of whole bytes: SEL + NVB + UIDs
buffer[1] = (index << 4) + tx_last_bits; // NVB - Number of Valid Bits
buffer_used = index + (tx_last_bits ? 1 : 0);
// Store response in the unused part of buffer
response_buffer = &buffer[index];
response_length = sizeof(buffer) - index;
}
// Set bit adjustments
// Having a separate variable is overkill. But it makes the next line easier to read.
rx_align = tx_last_bits;
// RxAlign = BitFramingReg[6..4]. TxLastBits = BitFramingReg[2..0]
RC522_RETURN_ON_ERROR(rc522_pcd_write(rc522, RC522_PCD_BIT_FRAMING_REG, (rx_align << 4) + tx_last_bits));
// Transmit the buffer and receive the response.
rc522_picc_transaction_t transaction = {
.bytes = { .ptr = buffer, .length = buffer_used },
.rx_align = rx_align,
.valid_bits = tx_last_bits,
};
rc522_picc_transaction_result_t transaction_result = {
.bytes = { .ptr = response_buffer, .length = response_length },
};
ret = rc522_picc_transceive(rc522, &transaction, &transaction_result);
if (ret == ESP_OK) {
response_length = transaction_result.bytes.length;
tx_last_bits = transaction_result.valid_bits;
}
if (ret == RC522_ERR_COLLISION) { // More than one PICC in the field => collision.
RC522_LOGD("collision detected (cl=%d, skip_anticoll=%d)", cascade_level, skip_anticoll);
if (skip_anticoll) {
// If we are skipping anticoll, we should not have collisions
RC522_LOGD("unexpected collision detected");
return ret;
}
// CollReg[7..0] bits are: ValuesAfterColl reserved CollPosNotValid CollPos[4:0]
uint8_t value_of_coll_reg;
rc522_pcd_read(rc522, RC522_PCD_COLL_REG, &value_of_coll_reg);
if (value_of_coll_reg & RC522_PCD_COLL_POS_NOT_VALID_BIT) {
// Without a valid collision position we cannot continue
RC522_LOGD("collision position not valid, coll_poss[4:0] out of range");
return RC522_ERR_COLLISION_UNSOLVABLE;
}
uint8_t collision_pos = value_of_coll_reg & 0x1F; // Values 0-31, 0 means bit 32.
if (collision_pos == 0) {
collision_pos = 32;
}
if (collision_pos <= current_level_known_bits) { // No progress - should not happen
RC522_LOGD("collision_pos (%d) <= current_level_known_bits (%d)",
collision_pos,
current_level_known_bits);
return RC522_ERR_COLLISION_UNSOLVABLE;
}
// Choose the PICC with the bit set.
current_level_known_bits = collision_pos;
count = current_level_known_bits % 8; // The bit to modify
check_bit = (current_level_known_bits - 1) % 8;
index = 1 + (current_level_known_bits / 8) + (count ? 1 : 0); // First byte is index 0.
buffer[index] |= (1 << check_bit);
}
else if (ret != ESP_OK) {
RC522_LOGD("transceive failed");
return ret;
}
else { // ESP_OK
if (current_level_known_bits >= 32) { // This was a SELECT.
// No more anticollision
// We continue below outside the while.
select_done = true;
}
else { // This was an ANTICOLLISION.
// We now have all 32 bits of the UID in this Cascade Level
current_level_known_bits = 32;
// Run loop again to do the SELECT.
}
}
} // End of while (!selectDone)
RC522_LOGD("SELECT (cl=%d) done", cascade_level);
// We do not check the CBB - it was constructed by us above.
// Copy the found UID bytes from buffer[] to uid.uidByte[]
index = (buffer[2] == RC522_PICC_CMD_CT) ? 3 : 2; // source index in buffer[]
bytes_to_copy = (buffer[2] == RC522_PICC_CMD_CT) ? 3 : 4;
for (count = 0; count < bytes_to_copy; count++) {
uid.value[uid_index + count] = buffer[index++];
}
// Check response SAK (Select Acknowledge)
if (response_length != 3 || tx_last_bits != 0) { // SAK must be exactly 24 bits (1 byte + CRC_A).
RC522_LOGD("invalid sak");
return RC522_ERR_INVALID_SAK;
}
// Verify CRC_A - do our own calculation and store the control in buffer[2..3] - those bytes are not needed
// anymore.
// compiler complains about uninitialized response_buffer even is
// no chance that response_buffer is NULL here, so ignore warning here
#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
rc522_pcd_crc_t crc = { 0 };
RC522_RETURN_ON_ERROR(
rc522_pcd_calculate_crc(rc522, &(rc522_bytes_t) { .ptr = response_buffer, .length = 1 }, &crc));
if (memcmp(response_buffer + 1, &crc, sizeof(crc)) != 0) {
RC522_LOGD("crc wrong");
return RC522_ERR_CRC_WRONG;
}
buffer[2] = crc.lsb;
buffer[3] = crc.msb;
if (response_buffer[0] & 0x04) { // Cascade bit set - UID not complete yes
cascade_level++;
}
else {
uid_complete = true;
sak = response_buffer[0];
}
#pragma GCC diagnostic pop
} // End of while (!uidComplete)
// Set correct uid.size
uid.length = 3 * cascade_level + 1;
RC522_LOGD("sak=0x%02" RC522_X, sak);
if (out_uid) {
memcpy(out_uid, &uid, sizeof(uid));
}
if (out_sak) {
*out_sak = sak;
}
return ESP_OK;
}
/**
* Checks if PICC is still in the PCD field
*/
esp_err_t rc522_picc_heartbeat(
const rc522_handle_t rc522, const rc522_picc_t *picc, rc522_picc_uid_t *out_uid, uint8_t *out_sak)
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK(picc == NULL);
RC522_CHECK(picc->state != RC522_PICC_STATE_ACTIVE && picc->state != RC522_PICC_STATE_ACTIVE_H);
esp_err_t ret = ESP_OK;
const uint8_t retries = 5;
uint8_t retry = 1;
do {
rc522_picc_atqa_desc_t atqa;
if (retry <= 2) {
if (picc->state == RC522_PICC_STATE_ACTIVE) {
ret = rc522_picc_reqa(rc522, &atqa);
}
else if (picc->state == RC522_PICC_STATE_ACTIVE_H) {
ret = rc522_picc_wupa(rc522, &atqa);
}
}
else {
if ((ret = rc522_picc_reqa(rc522, &atqa)) != ESP_OK) {
ret = rc522_picc_wupa(rc522, &atqa);
}
}
if (ret == ESP_OK) {
break;
}
rc522_delay_ms(5);
}
while (retry++ < retries);
if (ret != ESP_OK) {
return ret;
}
rc522_picc_uid_t uid;
uint8_t sak;
memcpy(&uid, &picc->uid, sizeof(rc522_picc_uid_t));
ret = rc522_picc_select(rc522, &uid, &sak, true);
if (ret != ESP_OK) {
return ret;
}
if (picc->sak != sak) {
return RC522_ERR_PICC_POST_HEARTBEAT_MISSMATCH;
}
for (uint8_t i = 0; i < uid.length; i++) {
if (picc->uid.value[i] != uid.value[i]) {
return RC522_ERR_PICC_POST_HEARTBEAT_MISSMATCH;
}
}
if (out_uid) {
memcpy(out_uid, &uid, sizeof(uid));
}
if (out_sak) {
*out_sak = sak;
}
return ESP_OK;
}
esp_err_t rc522_picc_uid_to_str(const rc522_picc_uid_t *uid, char *buffer, uint8_t buffer_size)
{
RC522_CHECK(uid == NULL);
RC522_CHECK(buffer == NULL);
RC522_CHECK(buffer_size < RC522_PICC_UID_STR_BUFFER_SIZE_MAX);
return rc522_buffer_to_hex_str(uid->value, uid->length, buffer, buffer_size);
}
esp_err_t rc522_picc_halta(const rc522_handle_t rc522, rc522_picc_t *picc)
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK(picc == NULL);
RC522_LOGD("HALTA");
uint8_t buffer[4] = { 0 };
buffer[0] = RC522_PICC_CMD_HLTA;
buffer[1] = 0;
rc522_pcd_crc_t crc = { 0 };
RC522_RETURN_ON_ERROR(rc522_pcd_calculate_crc(rc522, &(rc522_bytes_t) { .ptr = buffer, .length = 2 }, &crc));
buffer[2] = crc.lsb;
buffer[3] = crc.msb;
rc522_picc_transaction_t transaction = {
.bytes = { .ptr = buffer, .length = sizeof(buffer) },
};
esp_err_t ret = rc522_picc_transceive(rc522, &transaction, NULL);
// If the PICC responds with any modulation during a period of 1 ms after the HLTA,
// response shall be interpreted as 'not acknowledge', so timeout is not an error.
if (ret == ESP_OK) {
return RC522_ERR_HLTA_NOT_ACKED;
}
else if (ret == RC522_ERR_RX_TIMER_TIMEOUT || ret == RC522_ERR_RX_TIMEOUT) {
RC522_RETURN_ON_ERROR(rc522_picc_set_state(rc522, picc, RC522_PICC_STATE_HALT, true));
return ESP_OK;
}
return ret;
}
esp_err_t rc522_picc_set_state(
const rc522_handle_t rc522, rc522_picc_t *picc, rc522_picc_state_t new_state, bool fire_event)
{
RC522_CHECK(rc522 == NULL);
RC522_CHECK(picc == NULL);
esp_err_t ret = ESP_OK;
if (picc->state == new_state) {
return ESP_OK;
}
RC522_LOGD("changing state from %d to %d (fire=%d)", picc->state, new_state, fire_event);
rc522_picc_state_t old_state = picc->state;
picc->state = new_state;
if (fire_event) {
rc522_picc_state_changed_event_t event_data = {
.old_state = old_state,
.picc = picc,
};
if ((ret = rc522_dispatch_event(rc522, RC522_EVENT_PICC_STATE_CHANGED, &event_data, sizeof(event_data)))
!= ESP_OK) {
RC522_LOGW("picc_state_changed event dispatch failed (err=%04" RC522_X ")", ret);
}
}
return ret;
}
rc522_picc_type_t rc522_picc_get_type(const rc522_picc_t *picc)
{
RC522_CHECK(picc == NULL);
uint8_t sak = picc->sak;
// http://www.nxp.com/documents/application_note/AN10833.pdf
// Section: Coding of Select Acknowledge (SAK)
// ignore 8th (iso14443 starts with LSBit = bit 1)
// fixes wrong type for manufacturer Infineon (http://nfc-tools.org/index.php?title=ISO14443A)
sak &= 0x7F;
switch (sak) {
case 0x09:
return RC522_PICC_TYPE_MIFARE_MINI;
case 0x08:
return RC522_PICC_TYPE_MIFARE_1K;
case 0x18:
return RC522_PICC_TYPE_MIFARE_4K;
case 0x00:
return RC522_PICC_TYPE_MIFARE_UL;
case 0x10:
case 0x11:
return RC522_PICC_TYPE_MIFARE_PLUS;
case 0x01:
return RC522_PICC_TYPE_TNP3XXX;
case 0x20:
return picc->atqa.source == 0x4400 ? RC522_PICC_TYPE_MIFARE_DESFIRE : RC522_PICC_TYPE_ISO_14443_4;
case 0x40:
return RC522_PICC_TYPE_ISO_18092;
default:
return RC522_PICC_TYPE_UNKNOWN;
}
}
char *rc522_picc_type_name(rc522_picc_type_t type)
{
switch (type) {
/* SAK-determined */
case RC522_PICC_TYPE_ISO_14443_4:
return "PICC compliant with ISO/IEC 14443-4";
case RC522_PICC_TYPE_ISO_18092:
return "PICC compliant with ISO/IEC 18092 (NFC)";
case RC522_PICC_TYPE_MIFARE_MINI:
return "MIFARE Mini, 320 bytes";
case RC522_PICC_TYPE_MIFARE_1K:
return "MIFARE 1K";
case RC522_PICC_TYPE_MIFARE_4K:
return "MIFARE 4K";
case RC522_PICC_TYPE_MIFARE_UL:
// SAK=0x00 for both of these families
return "MIFARE Ultralight or NXP NTAG";
case RC522_PICC_TYPE_MIFARE_PLUS:
return "MIFARE Plus";
case RC522_PICC_TYPE_MIFARE_DESFIRE:
return "MIFARE DESFire";
case RC522_PICC_TYPE_TNP3XXX:
return "MIFARE TNP3XXX";
/* Further ID processing */
case RC522_PICC_TYPE_MIFARE_UL_:
return "MIFARE Ultralight";
case RC522_PICC_TYPE_MIFARE_UL_C:
return "MIFARE Ultralight C";
case RC522_PICC_TYPE_MIFARE_UL_EV1_1:
return "MIFARE Ultralight EV1 (80 bytes)";
case RC522_PICC_TYPE_MIFARE_UL_EV1_2:
return "MIFARE Ultralight EV1 (164 bytes)";
case RC522_PICC_TYPE_MIFARE_UL_NANO:
return "MIFARE Ultralight NANO";
case RC522_PICC_TYPE_MIFARE_UL_AES:
return "MIFARE Ultralight AES";
case RC522_PICC_TYPE_NTAG2xx:
return "NTAG2xx";
case RC522_PICC_TYPE_NTAG213:
return "NTAG213";
case RC522_PICC_TYPE_NTAG215:
return "NTAG215";
case RC522_PICC_TYPE_NTAG216:
return "NTAG216";
case RC522_PICC_TYPE_UNDEFINED:
case RC522_PICC_TYPE_UNKNOWN:
default:
return "unknown";
}
}
esp_err_t rc522_picc_print(const rc522_picc_t *picc)
{
RC522_CHECK(picc == NULL);
char uid_str[RC522_PICC_UID_STR_BUFFER_SIZE_MAX];
RC522_RETURN_ON_ERROR(rc522_picc_uid_to_str(&picc->uid, uid_str, sizeof(uid_str)));
ESP_LOGI(TAG, "");
ESP_LOGI(TAG, "╔══════════════╗");
ESP_LOGI(TAG, "║ ║ Type: %s", rc522_picc_type_name(picc->type));
ESP_LOGI(TAG, "║ RFID ║ UID: %s", uid_str);
ESP_LOGI(TAG, "║ CARD ║ ATQA: 0x%04" RC522_X, picc->atqa.source);
ESP_LOGI(TAG, "║ ║ SAK: 0x%02" RC522_X, picc->sak);
ESP_LOGI(TAG, "╚══════════════╝");
ESP_LOGI(TAG, "");
return ESP_OK;
}

View File

@@ -8,8 +8,8 @@ idf_component_register(SRCS
"scryfall_client.c"
"web_server.c"
"encoder.c"
"led_manager.c"
"buzzer.c"
"app_state.c"
"ota_manager.c"
INCLUDE_DIRS "."
)

View File

@@ -12,4 +12,28 @@ menu "MTG RFID Companion"
bool "Prefetch card data for all mapped UIDs at boot"
default y
config MTG_GITEA_URL
string "Gitea instance base URL (e.g. https://gitea.com)"
default "https://gitea.com"
help
Base HTTPS URL of the Gitea instance hosting releases.
config MTG_GITEA_REPO_OWNER
string "Gitea repository owner / org"
default "user"
help
Repository owner or organization name.
config MTG_GITEA_REPO_NAME
string "Gitea repository name"
default "espMTG"
help
Repository name.
config MTG_OTA_AUTO_CHECK
bool "Automatically check for updates on startup"
default y
help
When connected to Wi-Fi, query Gitea for new releases shortly after boot.
endmenu

View File

@@ -26,6 +26,7 @@ typedef enum {
APP_STATE_STORAGE_MANAGER,
APP_STATE_DEVICE_INFO,
APP_STATE_REGISTRATION,
APP_STATE_OTA_UPDATE,
} app_state_t;
/* Life counter state */

View File

@@ -46,8 +46,5 @@
#define HW_ENC_DT_GPIO (GPIO_NUM_33)
#define HW_ENC_SW_GPIO (GPIO_NUM_25)
/* WS2812B Neopixel */
#define HW_NEOPIXEL_GPIO (GPIO_NUM_27)
/* Piezo buzzer */
#define HW_BUZZER_GPIO (GPIO_NUM_14)

View File

@@ -1,90 +0,0 @@
/*
* MTG RFID Companion - WS2812B Neopixel LED manager (RMT)
*/
#include "esp_log.h"
#include "driver/rmt_tx.h"
#include "driver/rmt_encoder.h"
#include "hw_pins.h"
#include "led_manager.h"
static const char *TAG = "led";
static rmt_channel_handle_t s_tx_chan = NULL;
static rmt_encoder_handle_t s_enc = NULL;
esp_err_t led_manager_init(void)
{
if (s_tx_chan != NULL) {
return ESP_OK;
}
rmt_tx_channel_config_t chan_cfg = {
.gpio_num = HW_NEOPIXEL_GPIO,
.clk_src = RMT_CLK_SRC_DEFAULT,
.resolution_hz = 10 * 1000 * 1000, /* 100 ns per tick */
.mem_block_symbols = 64,
.trans_queue_depth = 4,
};
esp_err_t ret = rmt_new_tx_channel(&chan_cfg, &s_tx_chan);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "rmt_new_tx_channel failed: %s", esp_err_to_name(ret));
return ret;
}
rmt_bytes_encoder_config_t enc_cfg = {
.bit0 = {
.duration0 = 4, /* T0H ~ 400 ns */
.level0 = 1,
.duration1 = 8, /* T0L ~ 800 ns */
.level1 = 0,
},
.bit1 = {
.duration0 = 9, /* T1H ~ 900 ns */
.level0 = 1,
.duration1 = 4, /* T1L ~ 400 ns */
.level1 = 0,
},
};
ret = rmt_new_bytes_encoder(&enc_cfg, &s_enc);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "rmt_new_bytes_encoder failed: %s", esp_err_to_name(ret));
return ret;
}
ret = rmt_enable(s_tx_chan);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "rmt_enable failed: %s", esp_err_to_name(ret));
return ret;
}
ESP_LOGI(TAG, "WS2812B LED ready on GPIO %d", HW_NEOPIXEL_GPIO);
return ESP_OK;
}
esp_err_t led_manager_set_color(uint8_t r, uint8_t g, uint8_t b)
{
if (s_tx_chan == NULL) {
esp_err_t ret = led_manager_init();
if (ret != ESP_OK) {
return ret;
}
}
/* WS2812B expects GRB byte order */
uint8_t txbuf[3] = { g, r, b };
rmt_transmit_config_t tx_cfg = {
.loop_count = 0, /* one-shot; LED latches and holds the level */
};
esp_err_t ret = rmt_transmit(s_tx_chan, s_enc, txbuf, sizeof(txbuf), &tx_cfg);
if (ret != ESP_OK) {
return ret;
}
return rmt_tx_wait_all_done(s_tx_chan, pdMS_TO_TICKS(50));
}
esp_err_t led_manager_off(void)
{
return led_manager_set_color(0, 0, 0);
}

View File

@@ -1,33 +0,0 @@
/*
* MTG RFID Companion - WS2812B Neopixel LED manager (RMT)
*
* Phase 7 Step 7.3: Single WS2812B driver on GPIO 27. LED holds the last
* transmitted color until it is overwritten.
*/
#pragma once
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Initialize RMT TX channel for the Neopixel.
*/
esp_err_t led_manager_init(void);
/**
* @brief Set the LED color immediately (GRB order internally).
*/
esp_err_t led_manager_set_color(uint8_t r, uint8_t g, uint8_t b);
/**
* @brief Turn the LED off.
*/
esp_err_t led_manager_off(void);
#ifdef __cplusplus
}
#endif

View File

@@ -9,6 +9,7 @@
#include "wifi_manager.h"
#include "scryfall_client.h"
#include "web_server.h"
#include "ota_manager.h"
static const char *TAG = "MAIN";
@@ -16,6 +17,9 @@ void app_main(void)
{
ESP_LOGI(TAG, "MTG RFID Companion booting...");
/* Validate OTA partition & cancel rollback if just updated */
ota_manager_init();
ESP_ERROR_CHECK(storage_manager_init());
/* RFID comes BEFORE the display: the reader lives on its own SPI3 bus

444
main/ota_manager.c Normal file
View File

@@ -0,0 +1,444 @@
/*
* MTG RFID Companion - OTA Firmware Update Manager
*
* Checks Gitea release API, downloads firmware updates, and flashes them
* to the passive OTA slot with rollback protection.
*/
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_log.h"
#include "esp_system.h"
#include "esp_ota_ops.h"
#include "esp_https_ota.h"
#include "esp_http_client.h"
#include "esp_crt_bundle.h"
#include "esp_app_desc.h"
#include "cJSON.h"
#include "ota_manager.h"
static const char *TAG = "ota";
#ifndef CONFIG_MTG_GITEA_URL
#define CONFIG_MTG_GITEA_URL "https://gitea.com"
#endif
#ifndef CONFIG_MTG_GITEA_REPO_OWNER
#define CONFIG_MTG_GITEA_REPO_OWNER "user"
#endif
#ifndef CONFIG_MTG_GITEA_REPO_NAME
#define CONFIG_MTG_GITEA_REPO_NAME "espMTG"
#endif
#define GITEA_API_BUF_SIZE (8192)
static SemaphoreHandle_t s_ota_mux = NULL;
static ota_status_t s_status = OTA_STATUS_IDLE;
static char s_message[128] = "Idle";
static ota_release_info_t s_release = {0};
static size_t s_bytes_read = 0;
static size_t s_total_bytes = 0;
static volatile bool s_cancel_requested = false;
static TaskHandle_t s_ota_task_handle = NULL;
static void set_state(ota_status_t st, const char *msg)
{
if (s_ota_mux) xSemaphoreTake(s_ota_mux, portMAX_DELAY);
s_status = st;
if (msg) {
strncpy(s_message, msg, sizeof(s_message) - 1);
s_message[sizeof(s_message) - 1] = '\0';
}
if (s_ota_mux) xSemaphoreGive(s_ota_mux);
}
esp_err_t ota_manager_init(void)
{
if (s_ota_mux == NULL) {
s_ota_mux = xSemaphoreCreateMutex();
}
const esp_partition_t *running = esp_ota_get_running_partition();
esp_ota_img_states_t ota_state;
if (esp_ota_get_state_partition(running, &ota_state) == ESP_OK) {
if (ota_state == ESP_OTA_IMG_PENDING_VERIFY) {
ESP_LOGI(TAG, "First boot of new firmware on '%s'! Validating...", running->label);
esp_err_t err = esp_ota_mark_app_valid_cancel_rollback();
if (err == ESP_OK) {
ESP_LOGI(TAG, "Firmware validated and rollback canceled successfully");
} else {
ESP_LOGE(TAG, "Failed to validate firmware: %s", esp_err_to_name(err));
}
}
}
const esp_app_desc_t *app_desc = esp_app_get_description();
ESP_LOGI(TAG, "Running firmware '%s' version '%s' compiled on %s %s on partition '%s'",
app_desc->project_name, app_desc->version, app_desc->date, app_desc->time,
running ? running->label : "unknown");
return ESP_OK;
}
const char *ota_manager_get_current_version(void)
{
return esp_app_get_description()->version;
}
ota_status_t ota_manager_get_status(void)
{
ota_status_t st;
if (s_ota_mux) xSemaphoreTake(s_ota_mux, portMAX_DELAY);
st = s_status;
if (s_ota_mux) xSemaphoreGive(s_ota_mux);
return st;
}
const char *ota_manager_get_message(void)
{
return s_message;
}
int ota_manager_get_progress(void)
{
if (s_total_bytes == 0) return 0;
int p = (int)((s_bytes_read * 100) / s_total_bytes);
if (p > 100) p = 100;
return p;
}
void ota_manager_get_bytes(size_t *read_bytes, size_t *total_bytes)
{
if (s_ota_mux) xSemaphoreTake(s_ota_mux, portMAX_DELAY);
if (read_bytes) *read_bytes = s_bytes_read;
if (total_bytes) *total_bytes = s_total_bytes;
if (s_ota_mux) xSemaphoreGive(s_ota_mux);
}
const ota_release_info_t *ota_manager_get_release_info(void)
{
return &s_release;
}
void ota_manager_cancel(void)
{
s_cancel_requested = true;
}
void ota_manager_reset(void)
{
if (s_status != OTA_STATUS_CHECKING && s_status != OTA_STATUS_DOWNLOADING) {
set_state(OTA_STATUS_IDLE, "Idle");
}
}
/* Semver parsing: compares "v1.2.3" or "1.2.3" */
static bool parse_semver(const char *ver, int *maj, int *min, int *patch)
{
if (!ver) return false;
while (*ver && !isdigit((unsigned char)*ver)) {
ver++;
}
*maj = 0; *min = 0; *patch = 0;
int n = sscanf(ver, "%d.%d.%d", maj, min, patch);
return (n >= 1);
}
static bool check_is_newer(const char *remote, const char *local)
{
int r_maj = 0, r_min = 0, r_patch = 0;
int l_maj = 0, l_min = 0, l_patch = 0;
bool r_ok = parse_semver(remote, &r_maj, &r_min, &r_patch);
bool l_ok = parse_semver(local, &l_maj, &l_min, &l_patch);
if (r_ok && l_ok) {
if (r_maj > l_maj) return true;
if (r_maj < l_maj) return false;
if (r_min > l_min) return true;
if (r_min < l_min) return false;
return (r_patch > l_patch);
}
/* Fallback to string comparison */
return (strcmp(remote, local) != 0);
}
typedef struct {
char *buf;
int len;
int max;
} http_rx_buf_t;
static esp_err_t http_rx_event_handler(esp_http_client_event_t *evt)
{
http_rx_buf_t *rx = (http_rx_buf_t *)evt->user_data;
if (evt->event_id == HTTP_EVENT_ON_DATA && rx && rx->buf) {
if (rx->len + evt->data_len < rx->max - 1) {
memcpy(rx->buf + rx->len, evt->data, evt->data_len);
rx->len += evt->data_len;
rx->buf[rx->len] = '\0';
}
}
return ESP_OK;
}
static void ota_check_task(void *arg)
{
set_state(OTA_STATUS_CHECKING, "Connecting to Gitea...");
char api_url[256];
snprintf(api_url, sizeof(api_url), "%s/api/v1/repos/%s/%s/releases/latest",
CONFIG_MTG_GITEA_URL, CONFIG_MTG_GITEA_REPO_OWNER, CONFIG_MTG_GITEA_REPO_NAME);
ESP_LOGI(TAG, "Querying Gitea release API: %s", api_url);
http_rx_buf_t rx = {
.buf = malloc(GITEA_API_BUF_SIZE),
.len = 0,
.max = GITEA_API_BUF_SIZE,
};
if (!rx.buf) {
set_state(OTA_STATUS_FAILED, "Out of memory");
s_ota_task_handle = NULL;
vTaskDelete(NULL);
return;
}
rx.buf[0] = '\0';
esp_http_client_config_t cfg = {
.url = api_url,
.transport_type = HTTP_TRANSPORT_OVER_SSL,
.timeout_ms = 10000,
.event_handler = http_rx_event_handler,
.user_data = &rx,
.buffer_size = 2048,
.crt_bundle_attach = esp_crt_bundle_attach,
};
esp_http_client_handle_t client = esp_http_client_init(&cfg);
if (!client) {
free(rx.buf);
set_state(OTA_STATUS_FAILED, "HTTP init failed");
s_ota_task_handle = NULL;
vTaskDelete(NULL);
return;
}
esp_http_client_set_method(client, HTTP_METHOD_GET);
esp_http_client_set_header(client, "User-Agent", "MTGCompanion/1.0");
esp_http_client_set_header(client, "Accept", "application/json");
esp_err_t ret = esp_http_client_perform(client);
int status_code = esp_http_client_get_status_code(client);
esp_http_client_cleanup(client);
if (ret != ESP_OK || status_code != 200) {
char err_msg[64];
if (status_code == 404) {
snprintf(err_msg, sizeof(err_msg), "No releases found (404)");
} else if (ret != ESP_OK) {
snprintf(err_msg, sizeof(err_msg), "Network error: %s", esp_err_to_name(ret));
} else {
snprintf(err_msg, sizeof(err_msg), "HTTP error %d", status_code);
}
ESP_LOGW(TAG, "Gitea check failed: %s (code %d)", esp_err_to_name(ret), status_code);
free(rx.buf);
set_state(OTA_STATUS_FAILED, err_msg);
s_ota_task_handle = NULL;
vTaskDelete(NULL);
return;
}
/* Parse JSON */
cJSON *root = cJSON_Parse(rx.buf);
free(rx.buf);
if (!root) {
set_state(OTA_STATUS_FAILED, "Invalid JSON from server");
s_ota_task_handle = NULL;
vTaskDelete(NULL);
return;
}
cJSON *tag_item = cJSON_GetObjectItem(root, "tag_name");
cJSON *title_item = cJSON_GetObjectItem(root, "name");
const char *tag_str = (tag_item && tag_item->valuestring) ? tag_item->valuestring : "unknown";
const char *title_str = (title_item && title_item->valuestring) ? title_item->valuestring : tag_str;
if (s_ota_mux) xSemaphoreTake(s_ota_mux, portMAX_DELAY);
strncpy(s_release.tag_name, tag_str, sizeof(s_release.tag_name) - 1);
strncpy(s_release.release_title, title_str, sizeof(s_release.release_title) - 1);
s_release.download_url[0] = '\0';
s_release.binary_size = 0;
/* Search assets for mtg-rfid-companion.bin or *.bin */
cJSON *assets = cJSON_GetObjectItem(root, "assets");
if (assets && cJSON_IsArray(assets)) {
int count = cJSON_GetArraySize(assets);
for (int i = 0; i < count; i++) {
cJSON *asset = cJSON_GetArrayItem(assets, i);
cJSON *aname = cJSON_GetObjectItem(asset, "name");
cJSON *aurl = cJSON_GetObjectItem(asset, "browser_download_url");
cJSON *asize = cJSON_GetObjectItem(asset, "size");
if (aname && aname->valuestring && aurl && aurl->valuestring) {
if (strstr(aname->valuestring, ".bin") != NULL) {
const char *url_str = aurl->valuestring;
if (url_str[0] == '/') {
snprintf(s_release.download_url, sizeof(s_release.download_url),
"%s%s", CONFIG_MTG_GITEA_URL, url_str);
} else {
strncpy(s_release.download_url, url_str, sizeof(s_release.download_url) - 1);
}
if (asize && asize->valuedouble > 0) {
s_release.binary_size = (size_t)asize->valuedouble;
}
if (strstr(aname->valuestring, "mtg-rfid-companion") != NULL) {
break; /* Exact match, stop looking */
}
}
}
}
}
/* Fallback URL if not in assets list */
if (s_release.download_url[0] == '\0') {
snprintf(s_release.download_url, sizeof(s_release.download_url),
"%s/%s/%s/releases/download/%s/mtg-rfid-companion.bin",
CONFIG_MTG_GITEA_URL, CONFIG_MTG_GITEA_REPO_OWNER, CONFIG_MTG_GITEA_REPO_NAME,
s_release.tag_name);
}
const char *current_ver = ota_manager_get_current_version();
s_release.is_newer = check_is_newer(s_release.tag_name, current_ver);
if (s_ota_mux) xSemaphoreGive(s_ota_mux);
ESP_LOGI(TAG, "Discovered release '%s' (%s), download: %s, is_newer: %d (current: %s)",
s_release.tag_name, s_release.release_title, s_release.download_url,
s_release.is_newer, current_ver);
cJSON_Delete(root);
if (s_release.is_newer) {
char msg[128];
snprintf(msg, sizeof(msg), "New firmware %s available!", s_release.tag_name);
set_state(OTA_STATUS_UPDATE_AVAILABLE, msg);
} else {
char msg[128];
snprintf(msg, sizeof(msg), "Firmware %s is up to date", current_ver);
set_state(OTA_STATUS_UP_TO_DATE, msg);
}
s_ota_task_handle = NULL;
vTaskDelete(NULL);
}
static void ota_download_task(void *arg)
{
s_cancel_requested = false;
s_bytes_read = 0;
s_total_bytes = s_release.binary_size;
set_state(OTA_STATUS_DOWNLOADING, "Starting download...");
ESP_LOGI(TAG, "Starting HTTPS OTA from: %s", s_release.download_url);
esp_http_client_config_t http_cfg = {
.url = s_release.download_url,
.transport_type = HTTP_TRANSPORT_OVER_SSL,
.timeout_ms = 30000,
.keep_alive_enable = true,
.buffer_size = 2048,
.crt_bundle_attach = esp_crt_bundle_attach,
};
esp_https_ota_config_t ota_cfg = {
.http_config = &http_cfg,
};
esp_https_ota_handle_t ota_handle = NULL;
esp_err_t err = esp_https_ota_begin(&ota_cfg, &ota_handle);
if (err != ESP_OK || ota_handle == NULL) {
ESP_LOGE(TAG, "esp_https_ota_begin failed: %s", esp_err_to_name(err));
set_state(OTA_STATUS_FAILED, "Failed to connect to image");
s_ota_task_handle = NULL;
vTaskDelete(NULL);
return;
}
while (1) {
if (s_cancel_requested) {
ESP_LOGW(TAG, "OTA canceled by user");
esp_https_ota_abort(ota_handle);
set_state(OTA_STATUS_IDLE, "Update canceled");
s_ota_task_handle = NULL;
vTaskDelete(NULL);
return;
}
err = esp_https_ota_perform(ota_handle);
if (err != ESP_ERR_HTTPS_OTA_IN_PROGRESS) {
break;
}
int read = esp_https_ota_get_image_len_read(ota_handle);
int total = esp_https_ota_get_image_size(ota_handle);
if (read > 0) s_bytes_read = (size_t)read;
if (total > 0) s_total_bytes = (size_t)total;
vTaskDelay(pdMS_TO_TICKS(10));
}
if (err == ESP_OK) {
if (esp_https_ota_is_complete_data_received(ota_handle)) {
esp_err_t finish_err = esp_https_ota_finish(ota_handle);
if (finish_err == ESP_OK) {
ESP_LOGI(TAG, "OTA upgrade successful! Rebooting in 2 seconds...");
set_state(OTA_STATUS_SUCCESS_REBOOTING, "Update complete! Rebooting...");
vTaskDelay(pdMS_TO_TICKS(2000));
esp_restart();
} else {
ESP_LOGE(TAG, "esp_https_ota_finish failed: %s", esp_err_to_name(finish_err));
set_state(OTA_STATUS_FAILED, "Validation failed");
}
} else {
esp_https_ota_abort(ota_handle);
set_state(OTA_STATUS_FAILED, "Incomplete download");
}
} else {
ESP_LOGE(TAG, "esp_https_ota_perform failed: %s", esp_err_to_name(err));
esp_https_ota_abort(ota_handle);
set_state(OTA_STATUS_FAILED, "Flash write failed");
}
s_ota_task_handle = NULL;
vTaskDelete(NULL);
}
esp_err_t ota_manager_check_update_async(void)
{
if (s_status == OTA_STATUS_CHECKING || s_status == OTA_STATUS_DOWNLOADING) {
return ESP_ERR_INVALID_STATE;
}
BaseType_t ret = xTaskCreatePinnedToCore(
ota_check_task, "ota_check", 8192, NULL, 5, &s_ota_task_handle, 0);
return (ret == pdPASS) ? ESP_OK : ESP_ERR_NO_MEM;
}
esp_err_t ota_manager_start_update_async(void)
{
if (s_status == OTA_STATUS_DOWNLOADING) {
return ESP_ERR_INVALID_STATE;
}
if (s_release.download_url[0] == '\0') {
return ESP_ERR_NOT_FOUND;
}
BaseType_t ret = xTaskCreatePinnedToCore(
ota_download_task, "ota_update", 8192, NULL, 5, &s_ota_task_handle, 0);
return (ret == pdPASS) ? ESP_OK : ESP_ERR_NO_MEM;
}

95
main/ota_manager.h Normal file
View File

@@ -0,0 +1,95 @@
/*
* MTG RFID Companion - OTA Firmware Update Manager
*
* Checks Gitea release API, downloads firmware updates, and flashes them
* to the passive OTA slot with rollback protection.
*/
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
OTA_STATUS_IDLE,
OTA_STATUS_CHECKING,
OTA_STATUS_UPDATE_AVAILABLE,
OTA_STATUS_UP_TO_DATE,
OTA_STATUS_DOWNLOADING,
OTA_STATUS_SUCCESS_REBOOTING,
OTA_STATUS_FAILED
} ota_status_t;
typedef struct {
char tag_name[32]; /* e.g. "v1.2.0" */
char release_title[64]; /* e.g. "Release 1.2.0" */
char download_url[256]; /* URL to mtg-rfid-companion.bin */
size_t binary_size; /* in bytes, or 0 if unknown */
bool is_newer;
} ota_release_info_t;
/**
* @brief Initialize OTA manager, validates running partition, and cancels rollback.
*/
esp_err_t ota_manager_init(void);
/**
* @brief Current firmware version string (e.g. "1.0.0").
*/
const char *ota_manager_get_current_version(void);
/**
* @brief Get current OTA state.
*/
ota_status_t ota_manager_get_status(void);
/**
* @brief Get human-readable status message or error details.
*/
const char *ota_manager_get_message(void);
/**
* @brief Get percentage of download progress (0-100).
*/
int ota_manager_get_progress(void);
/**
* @brief Get bytes downloaded so far and total bytes.
*/
void ota_manager_get_bytes(size_t *read_bytes, size_t *total_bytes);
/**
* @brief Get the latest discovered release metadata.
*/
const ota_release_info_t *ota_manager_get_release_info(void);
/**
* @brief Kick off an asynchronous release check against the configured Gitea instance.
* @return ESP_OK if started, ESP_ERR_INVALID_STATE if already in progress.
*/
esp_err_t ota_manager_check_update_async(void);
/**
* @brief Kick off asynchronous firmware download and flash.
* @return ESP_OK if started.
*/
esp_err_t ota_manager_start_update_async(void);
/**
* @brief Abort any running OTA operation and reset status to IDLE.
*/
void ota_manager_cancel(void);
/**
* @brief Clear state back to IDLE.
*/
void ota_manager_reset(void);
#ifdef __cplusplus
}
#endif

View File

@@ -18,9 +18,9 @@
#include "wifi_manager.h"
#include "app_state.h"
#include "encoder.h"
#include "led_manager.h"
#include "buzzer.h"
#include "jpeg_decoder.h"
#include "ota_manager.h"
#include "ui_task.h"
static const char *TAG = "ui";
@@ -28,7 +28,6 @@ static const char *TAG = "ui";
#define UI_TASK_STACK (8 * 1024)
#define UI_TASK_PRIO (5)
#define UI_CORE (1)
#define LED_HOLD_MS (2500)
#define COLOR_AMBER (0xFFB300)
#define COLOR_GOLD (0xFFCC00)
@@ -36,13 +35,12 @@ static TaskHandle_t s_ui_task = NULL;
static char s_uid[RFID_UID_STR_MAX_LEN] = {0};
static char s_card_name[128] = {0};
static bool s_waiting_fetch = false;
static uint32_t s_led_off_tick = 0;
static lv_img_dsc_t s_art_img_dsc;
static uint16_t *s_art_pixels = NULL;
/* Menu items */
#define MENU_ITEM_COUNT 7
#define MENU_ITEM_COUNT 8
static int s_menu_idx = 0;
static const char *s_menu_labels[MENU_ITEM_COUNT] = {
"1. CARD SCANNER",
@@ -51,7 +49,8 @@ static const char *s_menu_labels[MENU_ITEM_COUNT] = {
"4. COMMANDER TAX",
"5. TOKEN & COUNTERS",
"6. DECK STORAGE (LFS)",
"7. DEVICE & WIFI INFO"
"7. DEVICE & WIFI INFO",
"8. CHECK FIRMWARE OTA"
};
/* Life & Counters */
@@ -127,6 +126,7 @@ static void view_storage(void);
static void view_device_info(void);
static void view_unmapped(const char *uid);
static void view_fetching(const char *name);
static void view_ota(void);
static void free_art_buffer(void)
{
@@ -280,13 +280,13 @@ static void view_menu(void)
ui_clean_screen();
ui_draw_header("MTG COMPANION", "- MAIN MENU -");
/* 7 Menu Items */
int start_y = 52;
/* 8 Menu Items */
int start_y = 44;
for (int i = 0; i < MENU_ITEM_COUNT; i++) {
bool selected = (i == s_menu_idx);
lv_obj_t *item_box = lv_obj_create(lv_scr_act());
lv_obj_set_size(item_box, DISP_W - 24, 31);
lv_obj_align(item_box, LV_ALIGN_TOP_MID, 0, start_y + i * 34);
lv_obj_set_size(item_box, DISP_W - 24, 28);
lv_obj_align(item_box, LV_ALIGN_TOP_MID, 0, start_y + i * 31);
lv_obj_set_style_radius(item_box, 6, 0);
lv_obj_set_style_pad_all(item_box, 2, 0);
lv_obj_set_scrollbar_mode(item_box, LV_SCROLLBAR_MODE_OFF);
@@ -446,8 +446,6 @@ static void view_life(void)
if (!s_was_lethal) {
s_was_lethal = true;
buzzer_play_sound(BUZZER_SOUND_LOSS);
led_manager_set_color(255, 0, 0);
s_led_off_tick = xTaskGetTickCount() + pdMS_TO_TICKS(LED_HOLD_MS);
}
lv_obj_t *dead_lbl = lv_label_create(lv_scr_act());
lv_label_set_text(dead_lbl, "*** LETHAL / DEFEAT ***");
@@ -595,31 +593,21 @@ static void roll_current_dice(void)
s_dice_result = (int)(esp_random() % 20) + 1;
if (s_dice_result == 20) {
/* Natural 20 - Victory Fanfare! */
led_manager_set_color(0, 255, 0);
buzzer_play_sound(BUZZER_SOUND_VICTORY);
} else if (s_dice_result == 1) {
/* Critical Fail */
led_manager_set_color(255, 0, 0);
buzzer_chirp(220, 150);
} else {
led_manager_set_color(0, 180, 255);
buzzer_chirp(880, 50);
}
} else if (s_dice_mode == DICE_MODE_D6) {
s_dice_result = (int)(esp_random() % 6) + 1;
led_manager_set_color(0, 180, 255);
buzzer_chirp(880, 50);
} else {
/* Coin */
s_dice_result = (int)(esp_random() % 2); // 0 = Tails, 1 = Heads
if (s_dice_result == 1) {
led_manager_set_color(0, 255, 60); // Green for Heads
} else {
led_manager_set_color(255, 140, 0); // Amber for Tails
}
buzzer_chirp(988, 60);
}
s_led_off_tick = xTaskGetTickCount() + pdMS_TO_TICKS(2000);
s_dice_rolled = true;
view_dice();
}
@@ -1005,8 +993,9 @@ static void view_device_info(void)
lv_obj_set_style_text_font(lbl_keys, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(lbl_keys, lv_color_hex(0x00E5FF), 0);
lv_label_set_text(lbl_keys,
"WI-FI STA:\n"
"WEB PAIR:\n"
"VERSION:\n"
"WI-FI IP:\n"
"HOTSPOT:\n"
"STORAGE:\n"
"CARDS:\n"
"FREE RAM:\n"
@@ -1021,6 +1010,7 @@ static void view_device_info(void)
lv_obj_set_style_text_color(lbl_vals, lv_color_hex(COLOR_AMBER), 0);
lv_obj_set_style_text_align(lbl_vals, LV_TEXT_ALIGN_RIGHT, 0);
lv_label_set_text_fmt(lbl_vals,
"%s\n"
"%s\n"
"192.168.4.1\n"
"%d / %d KB\n"
@@ -1029,6 +1019,7 @@ static void view_device_info(void)
"%d steps\n"
"ESP32+ST7789\n"
"MFRC522",
ota_manager_get_current_version(),
wifi_manager_get_ip(),
(int)(used_b / 1024), (int)(total_b / 1024),
json_c,
@@ -1045,6 +1036,237 @@ static void view_device_info(void)
lvgl_port_unlock();
}
/* 8. Firmware OTA Update Screen */
static ota_status_t s_last_ota_rendered_st = OTA_STATUS_IDLE;
static int s_last_ota_percent = -1;
static void view_ota(void)
{
lvgl_port_lock(0);
ui_clean_screen();
ota_status_t st = ota_manager_get_status();
s_last_ota_rendered_st = st;
ui_draw_header("FIRMWARE UPDATE", "GITEA OTA");
switch (st) {
case OTA_STATUS_CHECKING: {
lv_obj_t *box = lv_obj_create(lv_scr_act());
lv_obj_set_size(box, DISP_W - 24, 180);
lv_obj_align(box, LV_ALIGN_CENTER, 0, -10);
lv_obj_set_style_bg_color(box, lv_color_hex(0x1C1C1C), 0);
lv_obj_set_style_border_color(box, lv_color_hex(0x00E5FF), 0);
lv_obj_set_style_border_width(box, 2, 0);
lv_obj_set_style_radius(box, 8, 0);
lv_obj_t *lbl = lv_label_create(box);
lv_label_set_text(lbl, "CHECKING GITEA...\n\nConnecting to\nreleases API");
lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(lbl, lv_color_hex(0x00E5FF), 0);
lv_obj_set_style_text_align(lbl, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_align(lbl, LV_ALIGN_CENTER, 0, 0);
lv_obj_t *hint = lv_label_create(lv_scr_act());
lv_label_set_text(hint, "HOLD BUTTON: MAIN MENU");
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_AMBER), 0);
lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -8);
break;
}
case OTA_STATUS_UPDATE_AVAILABLE: {
const ota_release_info_t *rel = ota_manager_get_release_info();
const char *cur_ver = ota_manager_get_current_version();
lv_obj_t *box = lv_obj_create(lv_scr_act());
lv_obj_set_size(box, DISP_W - 24, 200);
lv_obj_align(box, LV_ALIGN_CENTER, 0, -15);
lv_obj_set_style_bg_color(box, lv_color_hex(0x102818), 0);
lv_obj_set_style_border_color(box, lv_color_hex(0x00E676), 0);
lv_obj_set_style_border_width(box, 2, 0);
lv_obj_set_style_radius(box, 8, 0);
lv_obj_t *t = lv_label_create(box);
lv_label_set_text(t, "NEW VERSION AVAILABLE!");
lv_obj_set_style_text_font(t, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(t, lv_color_hex(0x00E676), 0);
lv_obj_align(t, LV_ALIGN_TOP_MID, 0, 8);
lv_obj_t *v = lv_label_create(box);
lv_label_set_text_fmt(v, "TARGET: %s", rel->tag_name);
lv_obj_set_style_text_font(v, &lv_font_montserrat_20, 0);
lv_obj_set_style_text_color(v, lv_color_hex(0xFFCC00), 0);
lv_obj_align(v, LV_ALIGN_TOP_MID, 0, 38);
lv_obj_t *c = lv_label_create(box);
lv_label_set_text_fmt(c, "Current: %s", cur_ver);
lv_obj_set_style_text_font(c, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(c, lv_color_white(), 0);
lv_obj_align(c, LV_ALIGN_TOP_MID, 0, 70);
lv_obj_t *s = lv_label_create(box);
if (rel->binary_size > 0) {
lv_label_set_text_fmt(s, "Size: %d KB", (int)(rel->binary_size / 1024));
} else {
lv_label_set_text(s, "Ready to stream");
}
lv_obj_set_style_text_font(s, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(s, lv_color_hex(0x888888), 0);
lv_obj_align(s, LV_ALIGN_TOP_MID, 0, 95);
lv_obj_t *act = lv_label_create(box);
lv_label_set_text(act, "> CLICK: INSTALL NOW <");
lv_obj_set_style_text_font(act, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(act, lv_color_hex(0x00E676), 0);
lv_obj_align(act, LV_ALIGN_BOTTOM_MID, 0, -8);
lv_obj_t *hint = lv_label_create(lv_scr_act());
lv_label_set_text(hint, "ROTATE / HOLD: SKIP FOR NOW");
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_AMBER), 0);
lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -8);
break;
}
case OTA_STATUS_UP_TO_DATE: {
const char *cur_ver = ota_manager_get_current_version();
lv_obj_t *box = lv_obj_create(lv_scr_act());
lv_obj_set_size(box, DISP_W - 24, 180);
lv_obj_align(box, LV_ALIGN_CENTER, 0, -10);
lv_obj_set_style_bg_color(box, lv_color_hex(0x1C1C1C), 0);
lv_obj_set_style_border_color(box, lv_color_hex(0x333333), 0);
lv_obj_set_style_border_width(box, 1, 0);
lv_obj_set_style_radius(box, 8, 0);
lv_obj_t *t = lv_label_create(box);
lv_label_set_text(t, "UP TO DATE");
lv_obj_set_style_text_font(t, &lv_font_montserrat_20, 0);
lv_obj_set_style_text_color(t, lv_color_hex(0x00E676), 0);
lv_obj_align(t, LV_ALIGN_TOP_MID, 0, 15);
lv_obj_t *v = lv_label_create(box);
lv_label_set_text_fmt(v, "Version: %s\n\nNo newer releases\nfound on Gitea.", cur_ver);
lv_obj_set_style_text_font(v, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(v, lv_color_white(), 0);
lv_obj_set_style_text_align(v, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_align(v, LV_ALIGN_CENTER, 0, 15);
lv_obj_t *hint = lv_label_create(lv_scr_act());
lv_label_set_text(hint, "CLICK: MAIN MENU");
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_AMBER), 0);
lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -8);
break;
}
case OTA_STATUS_DOWNLOADING: {
int percent = ota_manager_get_progress();
s_last_ota_percent = percent;
size_t rb = 0, tb = 0;
ota_manager_get_bytes(&rb, &tb);
lv_obj_t *box = lv_obj_create(lv_scr_act());
lv_obj_set_size(box, DISP_W - 24, 200);
lv_obj_align(box, LV_ALIGN_CENTER, 0, -10);
lv_obj_set_style_bg_color(box, lv_color_hex(0x181828), 0);
lv_obj_set_style_border_color(box, lv_color_hex(0x00E5FF), 0);
lv_obj_set_style_border_width(box, 2, 0);
lv_obj_set_style_radius(box, 8, 0);
lv_obj_t *t = lv_label_create(box);
lv_label_set_text(t, "FLASHING FIRMWARE...");
lv_obj_set_style_text_font(t, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(t, lv_color_hex(0x00E5FF), 0);
lv_obj_align(t, LV_ALIGN_TOP_MID, 0, 10);
lv_obj_t *p_lbl = lv_label_create(box);
lv_label_set_text_fmt(p_lbl, "%d%%", percent);
lv_obj_set_style_text_font(p_lbl, &lv_font_montserrat_48, 0);
lv_obj_set_style_text_color(p_lbl, lv_color_hex(0xFFCC00), 0);
lv_obj_align(p_lbl, LV_ALIGN_CENTER, 0, -15);
lv_obj_t *bar = lv_bar_create(box);
lv_obj_set_size(bar, DISP_W - 60, 14);
lv_obj_align(bar, LV_ALIGN_CENTER, 0, 35);
lv_bar_set_range(bar, 0, 100);
lv_bar_set_value(bar, percent, LV_ANIM_OFF);
lv_obj_set_style_bg_color(bar, lv_color_hex(0x333333), 0);
lv_obj_set_style_bg_color(bar, lv_color_hex(0x00E676), LV_PART_INDICATOR);
lv_obj_t *b_lbl = lv_label_create(box);
lv_label_set_text_fmt(b_lbl, "%d / %d KB", (int)(rb / 1024), (int)(tb / 1024));
lv_obj_set_style_text_font(b_lbl, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(b_lbl, lv_color_hex(0xAAAAAA), 0);
lv_obj_align(b_lbl, LV_ALIGN_BOTTOM_MID, 0, -8);
lv_obj_t *warn = lv_label_create(lv_scr_act());
lv_label_set_text(warn, "DO NOT TURN OFF POWER");
lv_obj_set_style_text_font(warn, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(warn, lv_color_hex(0xFF1744), 0);
lv_obj_align(warn, LV_ALIGN_BOTTOM_MID, 0, -8);
break;
}
case OTA_STATUS_SUCCESS_REBOOTING: {
lv_obj_t *box = lv_obj_create(lv_scr_act());
lv_obj_set_size(box, DISP_W - 24, 180);
lv_obj_align(box, LV_ALIGN_CENTER, 0, -10);
lv_obj_set_style_bg_color(box, lv_color_hex(0x102818), 0);
lv_obj_set_style_border_color(box, lv_color_hex(0x00E676), 0);
lv_obj_set_style_border_width(box, 2, 0);
lv_obj_set_style_radius(box, 8, 0);
lv_obj_t *t = lv_label_create(box);
lv_label_set_text(t, "UPDATE SUCCESSFUL!");
lv_obj_set_style_text_font(t, &lv_font_montserrat_20, 0);
lv_obj_set_style_text_color(t, lv_color_hex(0x00E676), 0);
lv_obj_align(t, LV_ALIGN_TOP_MID, 0, 20);
lv_obj_t *m = lv_label_create(box);
lv_label_set_text(m, "New firmware verified.\n\nRebooting device...");
lv_obj_set_style_text_font(m, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(m, lv_color_white(), 0);
lv_obj_set_style_text_align(m, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_align(m, LV_ALIGN_CENTER, 0, 25);
break;
}
case OTA_STATUS_FAILED:
default: {
lv_obj_t *box = lv_obj_create(lv_scr_act());
lv_obj_set_size(box, DISP_W - 24, 180);
lv_obj_align(box, LV_ALIGN_CENTER, 0, -10);
lv_obj_set_style_bg_color(box, lv_color_hex(0x281010), 0);
lv_obj_set_style_border_color(box, lv_color_hex(0xFF1744), 0);
lv_obj_set_style_border_width(box, 2, 0);
lv_obj_set_style_radius(box, 8, 0);
lv_obj_t *t = lv_label_create(box);
lv_label_set_text(t, "UPDATE FAILED");
lv_obj_set_style_text_font(t, &lv_font_montserrat_20, 0);
lv_obj_set_style_text_color(t, lv_color_hex(0xFF1744), 0);
lv_obj_align(t, LV_ALIGN_TOP_MID, 0, 15);
lv_obj_t *msg = lv_label_create(box);
lv_label_set_text_fmt(msg, "%s\n\nExisting firmware intact.", ota_manager_get_message());
lv_obj_set_style_text_font(msg, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(msg, lv_color_white(), 0);
lv_obj_set_style_text_align(msg, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_align(msg, LV_ALIGN_CENTER, 0, 20);
lv_obj_t *hint = lv_label_create(lv_scr_act());
lv_label_set_text(hint, "CLICK: MAIN MENU");
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_AMBER), 0);
lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -8);
break;
}
}
lvgl_port_unlock();
}
/* 8. Card View Screen */
static void view_card(void)
{
@@ -1225,22 +1447,39 @@ static void view_unmapped(const char *uid)
lv_obj_align(u_lbl, LV_ALIGN_TOP_MID, 0, 70);
lv_obj_t *sub = lv_label_create(lv_scr_act());
lv_label_set_text(sub, "PAIR IT ON THE WEB:");
lv_obj_set_style_text_font(sub, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(sub, lv_color_white(), 0);
lv_obj_align(sub, LV_ALIGN_TOP_MID, 0, 130);
lv_obj_t *ssid = lv_label_create(lv_scr_act());
lv_label_set_text(ssid, "SSID: MTG-Companion");
lv_obj_set_style_text_font(ssid, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(ssid, lv_color_hex(0xFFCC00), 0);
lv_obj_align(ssid, LV_ALIGN_TOP_MID, 0, 160);
lv_obj_t *ip = lv_label_create(lv_scr_act());
lv_label_set_text(ip, "http://192.168.4.1");
lv_obj_set_style_text_font(ip, &lv_font_montserrat_20, 0);
lv_obj_set_style_text_color(ip, lv_color_hex(0x00E676), 0);
lv_obj_align(ip, LV_ALIGN_TOP_MID, 0, 190);
if (wifi_manager_connected()) {
lv_label_set_text(sub, "PAIR VIA WI-FI:");
lv_obj_align(sub, LV_ALIGN_TOP_MID, 0, 105);
lv_label_set_text_fmt(ip, "http://%s", wifi_manager_get_ip());
lv_obj_align(ip, LV_ALIGN_TOP_MID, 0, 135);
lv_obj_t *alt = lv_label_create(lv_scr_act());
lv_label_set_text(alt, "or Hotspot: MTG-Companion\n(http://192.168.4.1)");
lv_obj_set_style_text_font(alt, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(alt, lv_color_hex(0x888888), 0);
lv_obj_set_style_text_align(alt, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_align(alt, LV_ALIGN_TOP_MID, 0, 175);
} else {
lv_label_set_text(sub, "PAIR ON HOTSPOT:");
lv_obj_align(sub, LV_ALIGN_TOP_MID, 0, 105);
lv_obj_t *ssid = lv_label_create(lv_scr_act());
lv_label_set_text(ssid, "SSID: MTG-Companion");
lv_obj_set_style_text_font(ssid, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(ssid, lv_color_hex(0xFFCC00), 0);
lv_obj_align(ssid, LV_ALIGN_TOP_MID, 0, 135);
lv_label_set_text(ip, "http://192.168.4.1");
lv_obj_align(ip, LV_ALIGN_TOP_MID, 0, 165);
}
lv_obj_t *hint = lv_label_create(lv_scr_act());
lv_label_set_text(hint, "HOLD BUTTON: MAIN MENU");
@@ -1281,40 +1520,6 @@ static void view_fetching(const char *name)
lvgl_port_unlock();
}
/* ------------------------------------------------------------------ */
/* LED helpers */
/* ------------------------------------------------------------------ */
static void led_for_card(const char *mana)
{
uint8_t r = 255, g = 200, b = 0;
if (mana != NULL && mana[0] != '\0') {
if (strstr(mana, "{W}")) { r = 255; g = 255; b = 200; }
else if (strstr(mana, "{U}")) { r = 40; g = 100; b = 255; }
else if (strstr(mana, "{B}")) { r = 90; g = 20; b = 150; }
else if (strstr(mana, "{R}")) { r = 255; g = 60; b = 20; }
else if (strstr(mana, "{G}")) { r = 20; g = 200; b = 60; }
}
led_manager_set_color(r, g, b);
s_led_off_tick = xTaskGetTickCount() + pdMS_TO_TICKS(LED_HOLD_MS);
}
static void read_cached_color(const char *uid)
{
char json_path[72];
snprintf(json_path, sizeof(json_path), STORAGE_CARDS_DIR "/%s.json", uid);
char *buf = NULL;
size_t len = 0;
if (storage_read_text(json_path, &buf, &len) == ESP_OK && buf != NULL) {
cJSON *root = cJSON_Parse(buf);
if (root != NULL) {
led_for_card(get_json_str(root, "mana_cost", ""));
cJSON_Delete(root);
}
free(buf);
}
}
static void handle_new_card(const char *uid)
{
snprintf(s_uid, sizeof(s_uid), "%s", uid);
@@ -1325,8 +1530,6 @@ static void handle_new_card(const char *uid)
if (ret != ESP_OK || name == NULL) {
app_set_view(APP_STATE_REGISTRATION);
view_unmapped(uid);
led_manager_set_color(60, 60, 200);
s_led_off_tick = xTaskGetTickCount() + pdMS_TO_TICKS(LED_HOLD_MS);
buzzer_chirp(392, 80);
free(name);
return;
@@ -1336,7 +1539,6 @@ static void handle_new_card(const char *uid)
app_set_view(APP_STATE_CARD_VIEW);
buzzer_chirp(880, 60);
read_cached_color(uid);
char json_path[72];
snprintf(json_path, sizeof(json_path), STORAGE_CARDS_DIR "/%s.json", uid);
@@ -1375,12 +1577,9 @@ static void handle_fetch_ready(void)
scryfall_consume_result();
s_waiting_fetch = false;
if (success) {
read_cached_color(s_uid);
view_card();
} else {
buzzer_chirp(220, 150);
led_manager_set_color(255, 0, 0);
s_led_off_tick = xTaskGetTickCount() + pdMS_TO_TICKS(LED_HOLD_MS);
lvgl_port_lock(0);
ui_clean_screen();
@@ -1465,11 +1664,31 @@ static void handle_encoder_click(void)
app_set_view(APP_STATE_DEVICE_INFO);
view_device_info();
break;
case 7:
app_set_view(APP_STATE_OTA_UPDATE);
buzzer_chirp(988, 40);
ota_manager_check_update_async();
view_ota();
break;
default:
break;
}
break;
case APP_STATE_OTA_UPDATE: {
ota_status_t st = ota_manager_get_status();
if (st == OTA_STATUS_UPDATE_AVAILABLE) {
buzzer_chirp(1046, 100);
ota_manager_start_update_async();
view_ota();
} else if (st == OTA_STATUS_UP_TO_DATE || st == OTA_STATUS_FAILED) {
ota_manager_reset();
app_set_view(APP_STATE_MENU);
view_menu();
}
break;
}
case APP_STATE_LIFE_COUNTER:
s_life_mode = (s_life_mode + 1) % 3;
buzzer_chirp(880, 30);
@@ -1506,8 +1725,6 @@ static void handle_encoder_click(void)
if (s_storage_option_idx == 0) {
storage_manager_clear_all_cards(true);
buzzer_chirp(1046, 100);
led_manager_set_color(0, 255, 60);
s_led_off_tick = xTaskGetTickCount() + pdMS_TO_TICKS(1500);
s_storage_confirm_dialog = false;
view_storage();
} else {
@@ -1654,6 +1871,16 @@ static void handle_encoder_rotation(int32_t delta)
view_storage();
break;
case APP_STATE_OTA_UPDATE: {
ota_status_t st = ota_manager_get_status();
if (st == OTA_STATUS_UPDATE_AVAILABLE) {
ota_manager_reset();
app_set_view(APP_STATE_MENU);
view_menu();
}
break;
}
default:
break;
}
@@ -1669,13 +1896,17 @@ static void ui_task(void *arg)
ESP_LOGI(TAG, "UI task on core %d", xPortGetCoreID());
encoder_init();
led_manager_init();
buzzer_init();
/* Start on Main Menu at boot */
app_set_view(APP_STATE_MENU);
view_menu();
#if CONFIG_MTG_OTA_AUTO_CHECK
static bool s_boot_ota_checked = false;
static uint32_t s_wifi_up_ms = 0;
#endif
while (1) {
/* 1. New RFID card - scans immediately anywhere */
if (rfid_manager_has_pending_card()) {
@@ -1697,8 +1928,6 @@ static void ui_task(void *arg)
buzzer_chirp(1046, 50);
vTaskDelay(pdMS_TO_TICKS(40));
buzzer_chirp(1318, 70);
led_manager_set_color(180, 0, 255);
s_led_off_tick = xTaskGetTickCount() + pdMS_TO_TICKS(800);
app_set_view(APP_STATE_MENU);
view_menu();
}
@@ -1716,12 +1945,34 @@ static void ui_task(void *arg)
handle_encoder_rotation(delta);
}
/* 5. Auto-dim LED */
if (s_led_off_tick != 0 && xTaskGetTickCount() >= s_led_off_tick) {
s_led_off_tick = 0;
led_manager_off();
/* 5. OTA screen live refresh / status change */
if (app_get_view() == APP_STATE_OTA_UPDATE) {
ota_status_t cur_ota_st = ota_manager_get_status();
int cur_percent = ota_manager_get_progress();
if (cur_ota_st != s_last_ota_rendered_st ||
(cur_ota_st == OTA_STATUS_DOWNLOADING && cur_percent != s_last_ota_percent)) {
view_ota();
}
}
#if CONFIG_MTG_OTA_AUTO_CHECK
/* 6. Automatic background OTA check on startup after Wi-Fi connects */
if (!s_boot_ota_checked && wifi_manager_connected()) {
s_wifi_up_ms += 25;
if (s_wifi_up_ms >= 4000) {
s_boot_ota_checked = true;
ESP_LOGI(TAG, "Triggering startup Gitea OTA check...");
ota_manager_check_update_async();
}
}
if (s_boot_ota_checked && ota_manager_get_status() == OTA_STATUS_UPDATE_AVAILABLE &&
app_get_view() == APP_STATE_MENU) {
app_set_view(APP_STATE_OTA_UPDATE);
buzzer_chirp(1046, 120);
view_ota();
}
#endif
vTaskDelay(pdMS_TO_TICKS(25));
}
}

View File

@@ -13,6 +13,7 @@
#include "rfid_manager.h"
#include "storage_manager.h"
#include "scryfall_client.h"
#include "wifi_manager.h"
#include "web_server.h"
static const char *TAG = "web";
@@ -397,6 +398,7 @@ esp_err_t web_server_start(void)
ESP_LOGE(TAG, "failed to create AP netif");
return ESP_FAIL;
}
esp_netif_set_hostname(ap, "mtg-companion");
}
esp_err_t ret = esp_wifi_stop(); /* safe even if not started */
@@ -471,7 +473,7 @@ esp_err_t web_server_start(void)
ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_ART));
ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_UNBIND));
ESP_LOGI(TAG, "SoftAP '%s' at 192.168.4.1, web server ready", AP_SSID);
ESP_LOGI(TAG, "Web server ready on all interfaces (Hotspot: 192.168.4.1, Wi-Fi: %s)", wifi_manager_get_ip());
return ESP_OK;
}

View File

@@ -73,7 +73,10 @@ esp_err_t wifi_manager_init(void)
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
if (sta_netif != NULL) {
esp_netif_set_hostname(sta_netif, "mtg-companion");
}
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));

View File

@@ -1,10 +1,14 @@
# Name, Type, SubType, Offset, Size
# 4MB ESP32 flash layout:
# nvs 24KB 0x9000 - NVS settings (required by esp_wifi / http / storage)
# phy_init 4KB 0xF000 - PHY init data (optional reserved)
# factory 2MB app - firmware
# storage ~1.9MB littlefs - card database (/spiffs)
nvs, data, nvs, 0x9000, 0x6000
phy_init, data, phy, 0xf000, 0x1000
factory, app, factory, 0x10000, 0x200000
storage, data, littlefs, 0x210000, 0x1F0000
# 4MB ESP32 dual-OTA layout:
# nvs 16KB 0x9000 - NVS settings
# otadata 8KB 0xD000 - OTA selection & rollback data
# phy_init 4KB 0xF000 - PHY calibration data
# ota_0 1.56MB 0x10000 - App slot 0
# ota_1 1.56MB 0x1A0000 - App slot 1
# storage 832KB 0x330000 - LittleFS database (/spiffs)
nvs, data, nvs, 0x9000, 0x4000
otadata, data, ota, 0xd000, 0x2000
phy_init, data, phy, 0xf000, 0x1000
ota_0, app, ota_0, 0x10000, 0x190000
ota_1, app, ota_1, 0x1a0000, 0x190000
storage, data, littlefs, 0x330000, 0xd0000
1 # Name, Type, SubType, Offset, Size
2 # 4MB ESP32 flash layout: # 4MB ESP32 dual-OTA layout:
3 # nvs 24KB 0x9000 - NVS settings (required by esp_wifi / http / storage) # nvs 16KB 0x9000 - NVS settings
4 # phy_init 4KB 0xF000 - PHY init data (optional reserved) # otadata 8KB 0xD000 - OTA selection & rollback data
5 # factory 2MB app - firmware # phy_init 4KB 0xF000 - PHY calibration data
6 # storage ~1.9MB littlefs - card database (/spiffs) # ota_0 1.56MB 0x10000 - App slot 0
7 nvs, data, nvs, 0x9000, 0x6000 # ota_1 1.56MB 0x1A0000 - App slot 1
8 phy_init, data, phy, 0xf000, 0x1000 # storage 832KB 0x330000 - LittleFS database (/spiffs)
9 factory, app, factory, 0x10000, 0x200000 nvs, data, nvs, 0x9000, 0x4000
10 storage, data, littlefs, 0x210000, 0x1F0000 otadata, data, ota, 0xd000, 0x2000
11 phy_init, data, phy, 0xf000, 0x1000
12 ota_0, app, ota_0, 0x10000, 0x190000
13 ota_1, app, ota_1, 0x1a0000, 0x190000
14 storage, data, littlefs, 0x330000, 0xd0000

View File

@@ -437,6 +437,10 @@ CONFIG_PARTITION_TABLE_MD5=y
CONFIG_MTG_WIFI_SSID="WiFimodem-FD58"
CONFIG_MTG_WIFI_PASS="gzhrt5yzm3"
CONFIG_MTG_SCAN_PREFETCH=y
CONFIG_MTG_GITEA_URL="https://gitea.com"
CONFIG_MTG_GITEA_REPO_OWNER="user"
CONFIG_MTG_GITEA_REPO_NAME="espMTG"
CONFIG_MTG_OTA_AUTO_CHECK=y
# end of MTG RFID Companion
#

View File

@@ -14,4 +14,10 @@ CONFIG_JD_FORMAT_RGB565=y
CONFIG_JD_FASTDECODE_32BIT=y
# Some ESP32 boards ship a Boya flash chip; use the optimized driver
CONFIG_SPI_FLASH_SUPPORT_BOYA_CHIP=y
CONFIG_SPI_FLASH_SUPPORT_BOYA_CHIP=y
# Set network hostname for DHCP / router discovery
CONFIG_LWIP_LOCAL_HOSTNAME="mtg-companion"
# Enable OTA rollback protection
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y