From 1d954287a2ecf5977d98138162129024c09b0f9f Mon Sep 17 00:00:00 2001 From: Rasmus Date: Sat, 12 Sep 2026 10:49:39 +0200 Subject: [PATCH] Add Gitea OTA firmware update, dual-slot partition layout, and Gitea Actions release workflow --- .gitea/workflows/releases.yml | 57 ++ .gitignore | 28 +- AGENTS.md | 5 +- README.md | 342 +++---- components/rc522/.clang-format | 75 ++ components/rc522/.component_hash | 1 + components/rc522/.gitignore | 6 + components/rc522/CHECKSUMS.json | 1 + components/rc522/CMakeLists.txt | 47 + components/rc522/Kconfig | 14 + components/rc522/LICENSE | 202 +++++ components/rc522/README.md | 109 +++ .../rc522/examples/basic/CMakeLists.txt | 8 + .../rc522/examples/basic/main/CMakeLists.txt | 2 + components/rc522/examples/basic/main/basic.c | 55 ++ .../examples/basic/main/idf_component.yml | 3 + .../rc522/examples/basic/sdkconfig.defaults | 1 + .../rc522/examples/basic_i2c/CMakeLists.txt | 8 + .../examples/basic_i2c/main/CMakeLists.txt | 2 + .../rc522/examples/basic_i2c/main/basic_i2c.c | 56 ++ .../examples/basic_i2c/main/idf_component.yml | 3 + .../examples/basic_i2c/sdkconfig.defaults | 1 + .../rc522/examples/memory_dump/CMakeLists.txt | 8 + .../examples/memory_dump/main/CMakeLists.txt | 2 + .../memory_dump/main/idf_component.yml | 3 + .../examples/memory_dump/main/memory_dump.c | 187 ++++ .../examples/memory_dump/sdkconfig.defaults | 1 + .../examples/multiple_scanners/CMakeLists.txt | 8 + .../multiple_scanners/main/CMakeLists.txt | 2 + .../multiple_scanners/main/idf_component.yml | 3 + .../main/multiple_scanners.c | 117 +++ .../multiple_scanners/sdkconfig.defaults | 1 + .../rc522/examples/picc_nxp/CMakeLists.txt | 4 + .../rc522/examples/picc_nxp/example.log | 65 ++ .../examples/picc_nxp/main/CMakeLists.txt | 2 + .../examples/picc_nxp/main/idf_component.yml | 3 + .../rc522/examples/picc_nxp/main/picc_nxp.c | 283 ++++++ .../examples/picc_nxp/sdkconfig.defaults | 1 + .../rc522/examples/read_write/CMakeLists.txt | 8 + .../examples/read_write/main/CMakeLists.txt | 2 + .../read_write/main/idf_component.yml | 3 + .../examples/read_write/main/read_write.c | 163 ++++ .../examples/read_write/sdkconfig.defaults | 1 + components/rc522/include/driver/rc522_i2c.h | 28 + components/rc522/include/driver/rc522_spi.h | 32 + components/rc522/include/picc/rc522_mifare.h | 193 ++++ components/rc522/include/picc/rc522_nxp.h | 377 ++++++++ components/rc522/include/rc522.h | 26 + components/rc522/include/rc522_driver.h | 15 + components/rc522/include/rc522_pcd.h | 33 + components/rc522/include/rc522_picc.h | 170 ++++ components/rc522/include/rc522_types.h | 52 ++ .../rc522/internal/rc522_driver_internal.h | 42 + .../rc522/internal/rc522_helpers_internal.h | 21 + components/rc522/internal/rc522_internal.h | 15 + .../rc522/internal/rc522_pcd_internal.h | 448 +++++++++ .../rc522/internal/rc522_picc_internal.h | 115 +++ .../rc522/internal/rc522_types_internal.h | 97 ++ components/rc522/src/driver/rc522_i2c.c | 114 +++ components/rc522/src/driver/rc522_spi.c | 216 +++++ components/rc522/src/picc/rc522_mifare.c | 604 ++++++++++++ components/rc522/src/picc/rc522_nxp.c | 618 +++++++++++++ components/rc522/src/rc522.c | 342 +++++++ components/rc522/src/rc522_driver.c | 108 +++ components/rc522/src/rc522_helpers.c | 65 ++ components/rc522/src/rc522_pcd.c | 370 ++++++++ components/rc522/src/rc522_picc.c | 858 ++++++++++++++++++ main/CMakeLists.txt | 2 +- main/Kconfig.projbuild | 24 + main/app_state.h | 1 + main/hw_pins.h | 3 - main/led_manager.c | 90 -- main/led_manager.h | 33 - main/main.c | 4 + main/ota_manager.c | 444 +++++++++ main/ota_manager.h | 95 ++ main/ui_task.c | 415 +++++++-- main/web_server.c | 4 +- main/wifi_manager.c | 5 +- partitions.csv | 22 +- sdkconfig | 4 + sdkconfig.defaults | 8 +- 82 files changed, 7611 insertions(+), 395 deletions(-) create mode 100644 .gitea/workflows/releases.yml create mode 100644 components/rc522/.clang-format create mode 100644 components/rc522/.component_hash create mode 100644 components/rc522/.gitignore create mode 100644 components/rc522/CHECKSUMS.json create mode 100644 components/rc522/CMakeLists.txt create mode 100644 components/rc522/Kconfig create mode 100644 components/rc522/LICENSE create mode 100644 components/rc522/README.md create mode 100644 components/rc522/examples/basic/CMakeLists.txt create mode 100644 components/rc522/examples/basic/main/CMakeLists.txt create mode 100644 components/rc522/examples/basic/main/basic.c create mode 100644 components/rc522/examples/basic/main/idf_component.yml create mode 100644 components/rc522/examples/basic/sdkconfig.defaults create mode 100644 components/rc522/examples/basic_i2c/CMakeLists.txt create mode 100644 components/rc522/examples/basic_i2c/main/CMakeLists.txt create mode 100644 components/rc522/examples/basic_i2c/main/basic_i2c.c create mode 100644 components/rc522/examples/basic_i2c/main/idf_component.yml create mode 100644 components/rc522/examples/basic_i2c/sdkconfig.defaults create mode 100644 components/rc522/examples/memory_dump/CMakeLists.txt create mode 100644 components/rc522/examples/memory_dump/main/CMakeLists.txt create mode 100644 components/rc522/examples/memory_dump/main/idf_component.yml create mode 100644 components/rc522/examples/memory_dump/main/memory_dump.c create mode 100644 components/rc522/examples/memory_dump/sdkconfig.defaults create mode 100644 components/rc522/examples/multiple_scanners/CMakeLists.txt create mode 100644 components/rc522/examples/multiple_scanners/main/CMakeLists.txt create mode 100644 components/rc522/examples/multiple_scanners/main/idf_component.yml create mode 100644 components/rc522/examples/multiple_scanners/main/multiple_scanners.c create mode 100644 components/rc522/examples/multiple_scanners/sdkconfig.defaults create mode 100644 components/rc522/examples/picc_nxp/CMakeLists.txt create mode 100644 components/rc522/examples/picc_nxp/example.log create mode 100644 components/rc522/examples/picc_nxp/main/CMakeLists.txt create mode 100644 components/rc522/examples/picc_nxp/main/idf_component.yml create mode 100644 components/rc522/examples/picc_nxp/main/picc_nxp.c create mode 100644 components/rc522/examples/picc_nxp/sdkconfig.defaults create mode 100644 components/rc522/examples/read_write/CMakeLists.txt create mode 100644 components/rc522/examples/read_write/main/CMakeLists.txt create mode 100644 components/rc522/examples/read_write/main/idf_component.yml create mode 100644 components/rc522/examples/read_write/main/read_write.c create mode 100644 components/rc522/examples/read_write/sdkconfig.defaults create mode 100644 components/rc522/include/driver/rc522_i2c.h create mode 100644 components/rc522/include/driver/rc522_spi.h create mode 100644 components/rc522/include/picc/rc522_mifare.h create mode 100644 components/rc522/include/picc/rc522_nxp.h create mode 100644 components/rc522/include/rc522.h create mode 100644 components/rc522/include/rc522_driver.h create mode 100644 components/rc522/include/rc522_pcd.h create mode 100644 components/rc522/include/rc522_picc.h create mode 100644 components/rc522/include/rc522_types.h create mode 100644 components/rc522/internal/rc522_driver_internal.h create mode 100644 components/rc522/internal/rc522_helpers_internal.h create mode 100644 components/rc522/internal/rc522_internal.h create mode 100644 components/rc522/internal/rc522_pcd_internal.h create mode 100644 components/rc522/internal/rc522_picc_internal.h create mode 100644 components/rc522/internal/rc522_types_internal.h create mode 100644 components/rc522/src/driver/rc522_i2c.c create mode 100644 components/rc522/src/driver/rc522_spi.c create mode 100644 components/rc522/src/picc/rc522_mifare.c create mode 100644 components/rc522/src/picc/rc522_nxp.c create mode 100644 components/rc522/src/rc522.c create mode 100644 components/rc522/src/rc522_driver.c create mode 100644 components/rc522/src/rc522_helpers.c create mode 100644 components/rc522/src/rc522_pcd.c create mode 100644 components/rc522/src/rc522_picc.c delete mode 100644 main/led_manager.c delete mode 100644 main/led_manager.h create mode 100644 main/ota_manager.c create mode 100644 main/ota_manager.h diff --git a/.gitea/workflows/releases.yml b/.gitea/workflows/releases.yml new file mode 100644 index 0000000..b4912c5 --- /dev/null +++ b/.gitea/workflows/releases.yml @@ -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/* diff --git a/.gitignore b/.gitignore index 55c4a1a..716485d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,29 @@ +# ESP-IDF build output and temporary files build/ -managed_components/ sdkconfig.old sdkconfig.defaults.bak -dependencies.lock \ No newline at end of file +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 \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 6e84d87..8e18407 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 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 | diff --git a/README.md b/README.md index 7ff4c4c..bb01eec 100644 --- a/README.md +++ b/README.md @@ -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/ - โ”œโ”€โ”€ .json Scryfall attributes (Mana, Type, Oracle, P/T) - โ””โ”€โ”€ .jpg Cached 200x110 Baseline JPEG art crop + โ”œโ”€โ”€ .json # Cached Scryfall card data (Mana, Oracle, Type, P/T) + โ””โ”€โ”€ .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. | \ No newline at end of file +| **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. | \ No newline at end of file diff --git a/components/rc522/.clang-format b/components/rc522/.clang-format new file mode 100644 index 0000000..95e68e2 --- /dev/null +++ b/components/rc522/.clang-format @@ -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 diff --git a/components/rc522/.component_hash b/components/rc522/.component_hash new file mode 100644 index 0000000..a300e00 --- /dev/null +++ b/components/rc522/.component_hash @@ -0,0 +1 @@ +3302c1e7e19ee75c37e3e7284e86d0f624b56b7c5c55929c4ed1a1bdd28b62c5 \ No newline at end of file diff --git a/components/rc522/.gitignore b/components/rc522/.gitignore new file mode 100644 index 0000000..e2d48db --- /dev/null +++ b/components/rc522/.gitignore @@ -0,0 +1,6 @@ +/dist +**/build/ +sdkconfig.old +sdkconfig +.vscode +dependencies.lock diff --git a/components/rc522/CHECKSUMS.json b/components/rc522/CHECKSUMS.json new file mode 100644 index 0000000..684e0d3 --- /dev/null +++ b/components/rc522/CHECKSUMS.json @@ -0,0 +1 @@ +{"version":"1.0","algorithm":"sha256","created_at":"2026-06-25T13:42:00.269426+00:00","files":[{"path":"README.md","size":5007,"hash":"a4176a46a778f69825d0c8d251f01cbbfbe2bbfa617c2910272f393fdb31f061"},{"path":"LICENSE","size":11347,"hash":"d0960ee983b92545cc99cafbfb15a218be0cabfec9edc62abd9b2f58e3996f56"},{"path":"Kconfig","size":527,"hash":"032d62d27ad562293f7af46ef839449283b353b819e831d598751e3ebd6ce283"},{"path":"CMakeLists.txt","size":941,"hash":"159a254915b308142185872f93e1d8fd98e87ed8846815057db9b74f6137747c"},{"path":".gitignore","size":66,"hash":"c564b2ab8c5614173757eea8259e950275855bff07d60f5a0ef16fccb24ff414"},{"path":"idf_component.yml","size":498,"hash":"c85337e75737ed71a37948181283b1c2a172ae169810f7eda16b69bc3dba8170"},{"path":".clang-format","size":2077,"hash":"190e4b178bf3e4b38acd8f5d3ec76e0801e1f26c846f78b622686dc3d308f3eb"},{"path":"include/rc522_types.h","size":1795,"hash":"f5b2a7ae9038d8140c0766a6cada002dcc532453da433d8ad1b525788f0d4be7"},{"path":"include/rc522_driver.h","size":282,"hash":"925db80cad00abe6dc76ccd8521c494dd159f9190b75c636672d610cd6631bf6"},{"path":"include/rc522_picc.h","size":5427,"hash":"988dc84fd70117c3f9ef66fde1b812afb11fdc21f7d1b0bff345baf21a1c27c3"},{"path":"include/rc522.h","size":637,"hash":"abf1ca658704f2ecae54304e9b28f7e0259a4baec08322925ccfbdabcdf72645"},{"path":"include/rc522_pcd.h","size":1139,"hash":"ccd19b8499329967eca386b0422cb055dc965a206999e06fdb8f448fafa16f7c"},{"path":"src/rc522_pcd.c","size":12404,"hash":"9bbb24f881692ad47310cd2da1b2f2c1d9243caf9d27b26843aefc765286c728"},{"path":"src/rc522_picc.c","size":30162,"hash":"7db81f31926e3f78a7e01158707faa73b50f2f3c4592184bfb497696ff08aafe"},{"path":"src/rc522_driver.c","size":2646,"hash":"2c725a259f183c22802a242bf9addef79deabdeb999d95b988391d7934139af7"},{"path":"src/rc522_helpers.c","size":1325,"hash":"01d57be900627eef6ca6cbd8018653011369263c8428883af36bfe5b544753e6"},{"path":"src/rc522.c","size":10574,"hash":"d441654cbf88c7a1d64368e6e1b896b4afb1ae573ee29ca3539cadf9d0cae9b8"},{"path":"internal/rc522_picc_internal.h","size":3059,"hash":"528d334df1c9ab4bb489b19f28d0d393987889f3de7dc18881d00e2565e852ad"},{"path":"internal/rc522_helpers_internal.h","size":387,"hash":"8eff463a8a4a09093552d8bef572af476f290b1fac7221f94997ed3a93a84ff3"},{"path":"internal/rc522_pcd_internal.h","size":14523,"hash":"dbbf2ea169948c4ad4e5ce8ef3124f0f17027acc78b6b550e31c0276b11919e6"},{"path":"internal/rc522_driver_internal.h","size":1589,"hash":"eec7589b248f502eac3b718eab7ad7a63ac970e24dbfde33bc0793114ecd2191"},{"path":"internal/rc522_types_internal.h","size":5166,"hash":"a86e4dbb608c1ccbc381926710031799a03c806a71508d878cf9829b934bb41c"},{"path":"internal/rc522_internal.h","size":249,"hash":"385df03591fd482c913fe7f3e9632b5f7251dda3b10218c56dab2108028daa52"},{"path":"src/picc/rc522_nxp.c","size":20496,"hash":"7d68e156f2cae5842be80a7469d691eee13181f81d71bf7d67e258e26133e5ff"},{"path":"src/picc/rc522_mifare.c","size":20542,"hash":"06ecc2849adcf0580cfd62b837873202d6a51b853fb50dc1688042217773d940"},{"path":"src/driver/rc522_spi.c","size":6753,"hash":"7b446c42283231f0479047b2bb0bb63803b1f93d015cd0dff39eb1b8ca25a435"},{"path":"src/driver/rc522_i2c.c","size":3382,"hash":"673bb42675b7eb41e58d3ab4e35ad07d376927b3029097f2c7bedbd57cdf5e48"},{"path":"examples/basic/CMakeLists.txt","size":366,"hash":"a0113d8c5a9c754d76465cc9a2549aeb9d63f5bc0e12ce5a97ff28843b106016"},{"path":"examples/basic/sdkconfig.defaults","size":20,"hash":"efae87d20cd7e56c3e45281f558cd2861e84d44a1408ebcf5dbc4b37145c3178"},{"path":"examples/memory_dump/CMakeLists.txt","size":372,"hash":"5d478afdc040e98846be4212017becd71248fc9854641d89f86b2cebd04cfb0c"},{"path":"examples/memory_dump/sdkconfig.defaults","size":20,"hash":"efae87d20cd7e56c3e45281f558cd2861e84d44a1408ebcf5dbc4b37145c3178"},{"path":"examples/picc_nxp/CMakeLists.txt","size":106,"hash":"b2c410b166917e42f0beee8f825ae2ada8251591f9537cd3d7ded5be9b79baf6"},{"path":"examples/picc_nxp/sdkconfig.defaults","size":20,"hash":"efae87d20cd7e56c3e45281f558cd2861e84d44a1408ebcf5dbc4b37145c3178"},{"path":"examples/picc_nxp/example.log","size":3642,"hash":"db0453e09a22502937db3e9dcb4b75c7693db2f3aca37b4bf2f4b7e7e0d20203"},{"path":"examples/multiple_scanners/CMakeLists.txt","size":378,"hash":"bd7049718f5e901d5c7cbc7f22d75230f28f2b7289bb3bf477ad424e78655729"},{"path":"examples/multiple_scanners/sdkconfig.defaults","size":20,"hash":"efae87d20cd7e56c3e45281f558cd2861e84d44a1408ebcf5dbc4b37145c3178"},{"path":"examples/basic_i2c/CMakeLists.txt","size":370,"hash":"e32bc3a685d83ab988f9891ffa23524581a58b198a519fdcd534067af255b743"},{"path":"examples/basic_i2c/sdkconfig.defaults","size":20,"hash":"efae87d20cd7e56c3e45281f558cd2861e84d44a1408ebcf5dbc4b37145c3178"},{"path":"examples/read_write/CMakeLists.txt","size":371,"hash":"eed92f19f9c330b98ce5ff2876d4de4dcd9ae95fbbd58b2eb15df35f2c221eb1"},{"path":"examples/read_write/sdkconfig.defaults","size":20,"hash":"efae87d20cd7e56c3e45281f558cd2861e84d44a1408ebcf5dbc4b37145c3178"},{"path":"examples/read_write/main/CMakeLists.txt","size":81,"hash":"6856ee9b9e842610526d9af0faebe10b8e793f94b21ca435b557834100b500e1"},{"path":"examples/read_write/main/idf_component.yml","size":48,"hash":"b9a60a8dfb14d09936affbef72f89fffe15c760042a2b23cb5da6046f6a29958"},{"path":"examples/read_write/main/read_write.c","size":4822,"hash":"7a573b2e74122032c2984d4b50f902bc817da71ef33adf23e5838186c8ca420d"},{"path":"examples/basic_i2c/main/CMakeLists.txt","size":80,"hash":"8da227d6d55e0d31b57c4740e583c1c0fedf52ac92d32ea73cc2bcd373035e03"},{"path":"examples/basic_i2c/main/idf_component.yml","size":48,"hash":"b9a60a8dfb14d09936affbef72f89fffe15c760042a2b23cb5da6046f6a29958"},{"path":"examples/basic_i2c/main/basic_i2c.c","size":1558,"hash":"45924038e91b489cf0e5c7e0c61b6b1cf52b9af2c39ecf411629a747daff0bd5"},{"path":"examples/multiple_scanners/main/multiple_scanners.c","size":2863,"hash":"850fbb68eaa689a458bf1e4970203d082abd2c95fd5dab1e363d93d2a07c5689"},{"path":"examples/multiple_scanners/main/CMakeLists.txt","size":88,"hash":"7f0c66e652522d82ae05339f34b6e57888066d38cf1fd3eb074d25bd0cec1332"},{"path":"examples/multiple_scanners/main/idf_component.yml","size":48,"hash":"b9a60a8dfb14d09936affbef72f89fffe15c760042a2b23cb5da6046f6a29958"},{"path":"examples/picc_nxp/main/CMakeLists.txt","size":79,"hash":"4a34f484ae7b3fb83a6b7d35a0889fb77da7152f7250db44c1427ff42be253d4"},{"path":"examples/picc_nxp/main/idf_component.yml","size":48,"hash":"b9a60a8dfb14d09936affbef72f89fffe15c760042a2b23cb5da6046f6a29958"},{"path":"examples/picc_nxp/main/picc_nxp.c","size":9480,"hash":"ed781f7df9c24f2636e6e546a55067969a0ce7429c6f323eee866ea5aa63d308"},{"path":"examples/memory_dump/main/memory_dump.c","size":5276,"hash":"f026a8890e054aa5fe54d12e5aa98958ccd26bb82adbd497c2314d472fe7e420"},{"path":"examples/memory_dump/main/CMakeLists.txt","size":82,"hash":"dc8924ecb1fd7a4ee55c7518018c8af41345b1803b10c376f6cfab2cced85df2"},{"path":"examples/memory_dump/main/idf_component.yml","size":48,"hash":"b9a60a8dfb14d09936affbef72f89fffe15c760042a2b23cb5da6046f6a29958"},{"path":"examples/basic/main/basic.c","size":1627,"hash":"8e0d8b2d49e6d4d0edbc50c8e58fede24be79ab96b5ddf554f7c348f95b2d1e1"},{"path":"examples/basic/main/CMakeLists.txt","size":76,"hash":"5a5f1de073ad72ba841631ae8c395005878963fe94a0cb184977231df1062b22"},{"path":"examples/basic/main/idf_component.yml","size":48,"hash":"b9a60a8dfb14d09936affbef72f89fffe15c760042a2b23cb5da6046f6a29958"},{"path":"include/picc/rc522_nxp.h","size":11083,"hash":"a3411930b56a0cb2fa7c4559b676e12a48cd861916da5053e8d85513852f0339"},{"path":"include/picc/rc522_mifare.h","size":6675,"hash":"c59773bad83485c397aa0228d85a44b92d36d68e74970a82b0453dc6e7921e06"},{"path":"include/driver/rc522_i2c.h","size":583,"hash":"933741168152c961955d665f75202e82c35dbcd23fc483cbe0a6e80c20b01022"},{"path":"include/driver/rc522_spi.h","size":631,"hash":"a4136d5ca83ef2d6059d3d429e30fbc6a3460f69e4fdd5ad43d312d1162c5c58"}]} \ No newline at end of file diff --git a/components/rc522/CMakeLists.txt b/components/rc522/CMakeLists.txt new file mode 100644 index 0000000..c39628d --- /dev/null +++ b/components/rc522/CMakeLists.txt @@ -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 +) diff --git a/components/rc522/Kconfig b/components/rc522/Kconfig new file mode 100644 index 0000000..1c78f8f --- /dev/null +++ b/components/rc522/Kconfig @@ -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 diff --git a/components/rc522/LICENSE b/components/rc522/LICENSE new file mode 100644 index 0000000..01a2550 --- /dev/null +++ b/components/rc522/LICENSE @@ -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. diff --git a/components/rc522/README.md b/components/rc522/README.md new file mode 100644 index 0000000..48a137f --- /dev/null +++ b/components/rc522/README.md @@ -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)
+Homepage: [abobija.com](https://abobija.com) + +## License + +This component is provided under Apache 2.0 license, see [LICENSE](LICENSE) file for details. diff --git a/components/rc522/examples/basic/CMakeLists.txt b/components/rc522/examples/basic/CMakeLists.txt new file mode 100644 index 0000000..42ebffe --- /dev/null +++ b/components/rc522/examples/basic/CMakeLists.txt @@ -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) diff --git a/components/rc522/examples/basic/main/CMakeLists.txt b/components/rc522/examples/basic/main/CMakeLists.txt new file mode 100644 index 0000000..bb45756 --- /dev/null +++ b/components/rc522/examples/basic/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "basic.c" + INCLUDE_DIRS ".") diff --git a/components/rc522/examples/basic/main/basic.c b/components/rc522/examples/basic/main/basic.c new file mode 100644 index 0000000..50c391e --- /dev/null +++ b/components/rc522/examples/basic/main/basic.c @@ -0,0 +1,55 @@ +#include +#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); +} diff --git a/components/rc522/examples/basic/main/idf_component.yml b/components/rc522/examples/basic/main/idf_component.yml new file mode 100644 index 0000000..77a3269 --- /dev/null +++ b/components/rc522/examples/basic/main/idf_component.yml @@ -0,0 +1,3 @@ +dependencies: + abobija/rc522: + version: '*' diff --git a/components/rc522/examples/basic/sdkconfig.defaults b/components/rc522/examples/basic/sdkconfig.defaults new file mode 100644 index 0000000..21057aa --- /dev/null +++ b/components/rc522/examples/basic/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_LOG_COLORS=y diff --git a/components/rc522/examples/basic_i2c/CMakeLists.txt b/components/rc522/examples/basic_i2c/CMakeLists.txt new file mode 100644 index 0000000..c7af1c4 --- /dev/null +++ b/components/rc522/examples/basic_i2c/CMakeLists.txt @@ -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) diff --git a/components/rc522/examples/basic_i2c/main/CMakeLists.txt b/components/rc522/examples/basic_i2c/main/CMakeLists.txt new file mode 100644 index 0000000..0524fa1 --- /dev/null +++ b/components/rc522/examples/basic_i2c/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "basic_i2c.c" + INCLUDE_DIRS ".") diff --git a/components/rc522/examples/basic_i2c/main/basic_i2c.c b/components/rc522/examples/basic_i2c/main/basic_i2c.c new file mode 100644 index 0000000..f8bcd07 --- /dev/null +++ b/components/rc522/examples/basic_i2c/main/basic_i2c.c @@ -0,0 +1,56 @@ +#include +#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); +} diff --git a/components/rc522/examples/basic_i2c/main/idf_component.yml b/components/rc522/examples/basic_i2c/main/idf_component.yml new file mode 100644 index 0000000..77a3269 --- /dev/null +++ b/components/rc522/examples/basic_i2c/main/idf_component.yml @@ -0,0 +1,3 @@ +dependencies: + abobija/rc522: + version: '*' diff --git a/components/rc522/examples/basic_i2c/sdkconfig.defaults b/components/rc522/examples/basic_i2c/sdkconfig.defaults new file mode 100644 index 0000000..21057aa --- /dev/null +++ b/components/rc522/examples/basic_i2c/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_LOG_COLORS=y diff --git a/components/rc522/examples/memory_dump/CMakeLists.txt b/components/rc522/examples/memory_dump/CMakeLists.txt new file mode 100644 index 0000000..3ac52f8 --- /dev/null +++ b/components/rc522/examples/memory_dump/CMakeLists.txt @@ -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) diff --git a/components/rc522/examples/memory_dump/main/CMakeLists.txt b/components/rc522/examples/memory_dump/main/CMakeLists.txt new file mode 100644 index 0000000..6fcebf8 --- /dev/null +++ b/components/rc522/examples/memory_dump/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "memory_dump.c" + INCLUDE_DIRS ".") diff --git a/components/rc522/examples/memory_dump/main/idf_component.yml b/components/rc522/examples/memory_dump/main/idf_component.yml new file mode 100644 index 0000000..77a3269 --- /dev/null +++ b/components/rc522/examples/memory_dump/main/idf_component.yml @@ -0,0 +1,3 @@ +dependencies: + abobija/rc522: + version: '*' diff --git a/components/rc522/examples/memory_dump/main/memory_dump.c b/components/rc522/examples/memory_dump/main/memory_dump.c new file mode 100644 index 0000000..63bea2d --- /dev/null +++ b/components/rc522/examples/memory_dump/main/memory_dump.c @@ -0,0 +1,187 @@ +#include +#include +#include +#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, §or), TAG, ""); + ESP_RETURN_ON_ERROR(rc522_mifare_auth_sector(scanner, picc, §or, &key), TAG, ""); + + rc522_mifare_sector_block_t trailer; + ESP_RETURN_ON_ERROR(rc522_mifare_read_sector_trailer_block(scanner, picc, §or, &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, §or, &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); +} diff --git a/components/rc522/examples/memory_dump/sdkconfig.defaults b/components/rc522/examples/memory_dump/sdkconfig.defaults new file mode 100644 index 0000000..21057aa --- /dev/null +++ b/components/rc522/examples/memory_dump/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_LOG_COLORS=y diff --git a/components/rc522/examples/multiple_scanners/CMakeLists.txt b/components/rc522/examples/multiple_scanners/CMakeLists.txt new file mode 100644 index 0000000..c651a50 --- /dev/null +++ b/components/rc522/examples/multiple_scanners/CMakeLists.txt @@ -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) diff --git a/components/rc522/examples/multiple_scanners/main/CMakeLists.txt b/components/rc522/examples/multiple_scanners/main/CMakeLists.txt new file mode 100644 index 0000000..5b20d2d --- /dev/null +++ b/components/rc522/examples/multiple_scanners/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "multiple_scanners.c" + INCLUDE_DIRS ".") diff --git a/components/rc522/examples/multiple_scanners/main/idf_component.yml b/components/rc522/examples/multiple_scanners/main/idf_component.yml new file mode 100644 index 0000000..77a3269 --- /dev/null +++ b/components/rc522/examples/multiple_scanners/main/idf_component.yml @@ -0,0 +1,3 @@ +dependencies: + abobija/rc522: + version: '*' diff --git a/components/rc522/examples/multiple_scanners/main/multiple_scanners.c b/components/rc522/examples/multiple_scanners/main/multiple_scanners.c new file mode 100644 index 0000000..853dda3 --- /dev/null +++ b/components/rc522/examples/multiple_scanners/main/multiple_scanners.c @@ -0,0 +1,117 @@ +#include +#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); +} diff --git a/components/rc522/examples/multiple_scanners/sdkconfig.defaults b/components/rc522/examples/multiple_scanners/sdkconfig.defaults new file mode 100644 index 0000000..21057aa --- /dev/null +++ b/components/rc522/examples/multiple_scanners/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_LOG_COLORS=y diff --git a/components/rc522/examples/picc_nxp/CMakeLists.txt b/components/rc522/examples/picc_nxp/CMakeLists.txt new file mode 100644 index 0000000..f6f4e03 --- /dev/null +++ b/components/rc522/examples/picc_nxp/CMakeLists.txt @@ -0,0 +1,4 @@ +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(picc_nxp) diff --git a/components/rc522/examples/picc_nxp/example.log b/components/rc522/examples/picc_nxp/example.log new file mode 100644 index 0000000..70070fe --- /dev/null +++ b/components/rc522/examples/picc_nxp/example.log @@ -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 diff --git a/components/rc522/examples/picc_nxp/main/CMakeLists.txt b/components/rc522/examples/picc_nxp/main/CMakeLists.txt new file mode 100644 index 0000000..c7d6d30 --- /dev/null +++ b/components/rc522/examples/picc_nxp/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "picc_nxp.c" + INCLUDE_DIRS ".") diff --git a/components/rc522/examples/picc_nxp/main/idf_component.yml b/components/rc522/examples/picc_nxp/main/idf_component.yml new file mode 100644 index 0000000..77a3269 --- /dev/null +++ b/components/rc522/examples/picc_nxp/main/idf_component.yml @@ -0,0 +1,3 @@ +dependencies: + abobija/rc522: + version: '*' diff --git a/components/rc522/examples/picc_nxp/main/picc_nxp.c b/components/rc522/examples/picc_nxp/main/picc_nxp.c new file mode 100644 index 0000000..10303c8 --- /dev/null +++ b/components/rc522/examples/picc_nxp/main/picc_nxp.c @@ -0,0 +1,283 @@ +#include +#include +#include +#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); +} diff --git a/components/rc522/examples/picc_nxp/sdkconfig.defaults b/components/rc522/examples/picc_nxp/sdkconfig.defaults new file mode 100644 index 0000000..21057aa --- /dev/null +++ b/components/rc522/examples/picc_nxp/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_LOG_COLORS=y diff --git a/components/rc522/examples/read_write/CMakeLists.txt b/components/rc522/examples/read_write/CMakeLists.txt new file mode 100644 index 0000000..cb345eb --- /dev/null +++ b/components/rc522/examples/read_write/CMakeLists.txt @@ -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) diff --git a/components/rc522/examples/read_write/main/CMakeLists.txt b/components/rc522/examples/read_write/main/CMakeLists.txt new file mode 100644 index 0000000..7e41e85 --- /dev/null +++ b/components/rc522/examples/read_write/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "read_write.c" + INCLUDE_DIRS ".") diff --git a/components/rc522/examples/read_write/main/idf_component.yml b/components/rc522/examples/read_write/main/idf_component.yml new file mode 100644 index 0000000..77a3269 --- /dev/null +++ b/components/rc522/examples/read_write/main/idf_component.yml @@ -0,0 +1,3 @@ +dependencies: + abobija/rc522: + version: '*' diff --git a/components/rc522/examples/read_write/main/read_write.c b/components/rc522/examples/read_write/main/read_write.c new file mode 100644 index 0000000..62adc3a --- /dev/null +++ b/components/rc522/examples/read_write/main/read_write.c @@ -0,0 +1,163 @@ +#include +#include +#include +#include +#include +#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); +} diff --git a/components/rc522/examples/read_write/sdkconfig.defaults b/components/rc522/examples/read_write/sdkconfig.defaults new file mode 100644 index 0000000..21057aa --- /dev/null +++ b/components/rc522/examples/read_write/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_LOG_COLORS=y diff --git a/components/rc522/include/driver/rc522_i2c.h b/components/rc522/include/driver/rc522_i2c.h new file mode 100644 index 0000000..3fcff11 --- /dev/null +++ b/components/rc522/include/driver/rc522_i2c.h @@ -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 diff --git a/components/rc522/include/driver/rc522_spi.h b/components/rc522/include/driver/rc522_spi.h new file mode 100644 index 0000000..fcac169 --- /dev/null +++ b/components/rc522/include/driver/rc522_spi.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#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 diff --git a/components/rc522/include/picc/rc522_mifare.h b/components/rc522/include/picc/rc522_mifare.h new file mode 100644 index 0000000..b51a5be --- /dev/null +++ b/components/rc522/include/picc/rc522_mifare.h @@ -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 diff --git a/components/rc522/include/picc/rc522_nxp.h b/components/rc522/include/picc/rc522_nxp.h new file mode 100644 index 0000000..46b90a2 --- /dev/null +++ b/components/rc522/include/picc/rc522_nxp.h @@ -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 diff --git a/components/rc522/include/rc522.h b/components/rc522/include/rc522.h new file mode 100644 index 0000000..b88a73b --- /dev/null +++ b/components/rc522/include/rc522.h @@ -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 diff --git a/components/rc522/include/rc522_driver.h b/components/rc522/include/rc522_driver.h new file mode 100644 index 0000000..8dadfb8 --- /dev/null +++ b/components/rc522/include/rc522_driver.h @@ -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 diff --git a/components/rc522/include/rc522_pcd.h b/components/rc522/include/rc522_pcd.h new file mode 100644 index 0000000..0eb9080 --- /dev/null +++ b/components/rc522/include/rc522_pcd.h @@ -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 diff --git a/components/rc522/include/rc522_picc.h b/components/rc522/include/rc522_picc.h new file mode 100644 index 0000000..7ddfb1b --- /dev/null +++ b/components/rc522/include/rc522_picc.h @@ -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 diff --git a/components/rc522/include/rc522_types.h b/components/rc522/include/rc522_types.h new file mode 100644 index 0000000..cee9dcd --- /dev/null +++ b/components/rc522/include/rc522_types.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include +#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; /* +#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; /* +#include + +#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 diff --git a/components/rc522/internal/rc522_internal.h b/components/rc522/internal/rc522_internal.h new file mode 100644 index 0000000..2edea31 --- /dev/null +++ b/components/rc522/internal/rc522_internal.h @@ -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 diff --git a/components/rc522/internal/rc522_pcd_internal.h b/components/rc522/internal/rc522_pcd_internal.h new file mode 100644 index 0000000..91b8300 --- /dev/null +++ b/components/rc522/internal/rc522_pcd_internal.h @@ -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 diff --git a/components/rc522/internal/rc522_picc_internal.h b/components/rc522/internal/rc522_picc_internal.h new file mode 100644 index 0000000..ec6f54f --- /dev/null +++ b/components/rc522/internal/rc522_picc_internal.h @@ -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 diff --git a/components/rc522/internal/rc522_types_internal.h b/components/rc522/internal/rc522_types_internal.h new file mode 100644 index 0000000..2b57c3f --- /dev/null +++ b/components/rc522/internal/rc522_types_internal.h @@ -0,0 +1,97 @@ +#pragma once + +#include +#include +#include +#include +#include +#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, /*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 diff --git a/components/rc522/src/driver/rc522_i2c.c b/components/rc522/src/driver/rc522_i2c.c new file mode 100644 index 0000000..854c732 --- /dev/null +++ b/components/rc522/src/driver/rc522_i2c.c @@ -0,0 +1,114 @@ +#include +#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; +} diff --git a/components/rc522/src/driver/rc522_spi.c b/components/rc522/src/driver/rc522_spi.c new file mode 100644 index 0000000..f3bb413 --- /dev/null +++ b/components/rc522/src/driver/rc522_spi.c @@ -0,0 +1,216 @@ +#include +#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)); +} diff --git a/components/rc522/src/picc/rc522_mifare.c b/components/rc522/src/picc/rc522_mifare.c new file mode 100644 index 0000000..ca9bd90 --- /dev/null +++ b/components/rc522/src/picc/rc522_mifare.c @@ -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 +#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, §or)); + + *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; +} diff --git a/components/rc522/src/picc/rc522_nxp.c b/components/rc522/src/picc/rc522_nxp.c new file mode 100644 index 0000000..5387d00 --- /dev/null +++ b/components/rc522/src/picc/rc522_nxp.c @@ -0,0 +1,618 @@ +#include +#include +#include + +#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; +} diff --git a/components/rc522/src/rc522.c b/components/rc522/src/rc522.c new file mode 100644 index 0000000..c5de2bd --- /dev/null +++ b/components/rc522/src/rc522.c @@ -0,0 +1,342 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#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 +} diff --git a/components/rc522/src/rc522_driver.c b/components/rc522/src/rc522_driver.c new file mode 100644 index 0000000..9772838 --- /dev/null +++ b/components/rc522/src/rc522_driver.c @@ -0,0 +1,108 @@ +#include +#include +#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; +} diff --git a/components/rc522/src/rc522_helpers.c b/components/rc522/src/rc522_helpers.c new file mode 100644 index 0000000..d4732a4 --- /dev/null +++ b/components/rc522/src/rc522_helpers.c @@ -0,0 +1,65 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "rc522_types_internal.h" +#include "rc522_helpers_internal.h" + +#include + +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; +} diff --git a/components/rc522/src/rc522_pcd.c b/components/rc522/src/rc522_pcd.c new file mode 100644 index 0000000..5435634 --- /dev/null +++ b/components/rc522/src/rc522_pcd.c @@ -0,0 +1,370 @@ +#include +#include +#include + +#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)); +} diff --git a/components/rc522/src/rc522_picc.c b/components/rc522/src/rc522_picc.c new file mode 100644 index 0000000..6358a59 --- /dev/null +++ b/components/rc522/src/rc522_picc.c @@ -0,0 +1,858 @@ +#include +#include +#include + +#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; +} diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index eec1560..321c42e 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -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 "." ) \ No newline at end of file diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 2f827a9..cc57527 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -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 \ No newline at end of file diff --git a/main/app_state.h b/main/app_state.h index ac2f4b1..d4a9da2 100644 --- a/main/app_state.h +++ b/main/app_state.h @@ -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 */ diff --git a/main/hw_pins.h b/main/hw_pins.h index 6e1a23f..54d7a6c 100644 --- a/main/hw_pins.h +++ b/main/hw_pins.h @@ -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) \ No newline at end of file diff --git a/main/led_manager.c b/main/led_manager.c deleted file mode 100644 index 1e90a40..0000000 --- a/main/led_manager.c +++ /dev/null @@ -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); -} \ No newline at end of file diff --git a/main/led_manager.h b/main/led_manager.h deleted file mode 100644 index 0d4bcf1..0000000 --- a/main/led_manager.h +++ /dev/null @@ -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 \ No newline at end of file diff --git a/main/main.c b/main/main.c index a87a550..1519924 100644 --- a/main/main.c +++ b/main/main.c @@ -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 diff --git a/main/ota_manager.c b/main/ota_manager.c new file mode 100644 index 0000000..dc32630 --- /dev/null +++ b/main/ota_manager.c @@ -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 +#include +#include +#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; +} diff --git a/main/ota_manager.h b/main/ota_manager.h new file mode 100644 index 0000000..9008a4f --- /dev/null +++ b/main/ota_manager.h @@ -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 +#include +#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 diff --git a/main/ui_task.c b/main/ui_task.c index 306b9fa..d9f184e 100644 --- a/main/ui_task.c +++ b/main/ui_task.c @@ -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)); } } diff --git a/main/web_server.c b/main/web_server.c index 0b6065a..473136f 100644 --- a/main/web_server.c +++ b/main/web_server.c @@ -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; } diff --git a/main/wifi_manager.c b/main/wifi_manager.c index fe3a37e..628ff06 100644 --- a/main/wifi_manager.c +++ b/main/wifi_manager.c @@ -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)); diff --git a/partitions.csv b/partitions.csv index d4aa813..6b66f46 100644 --- a/partitions.csv +++ b/partitions.csv @@ -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 \ No newline at end of file +# 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 \ No newline at end of file diff --git a/sdkconfig b/sdkconfig index 6132358..3a5b93e 100644 --- a/sdkconfig +++ b/sdkconfig @@ -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 # diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 26238af..f992710 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -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 \ No newline at end of file +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 \ No newline at end of file