# Bring up X-NUCLEO-NFC10A1 as an Aliro Reader on NUCLEO-U545RE-Q Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Produce a second independent end-to-end validation of the Aliro applet by running ST's X-CUBE-ALIRO reader firmware on NUCLEO-U545RE-Q + X-NUCLEO-NFC10A1, hitting AUTH0 + AUTH1 green against our personalized J3R180. The PC/SC bench reader (`aliro-bench-test`) already proves the applet — this adds an MCU-driven reader path.

**Architecture:** X-NUCLEO-NFC10A1 and X-NUCLEO-NFC09A1 both carry the **same** ST25R200 NFC reader IC (ST UM3414 covers both boards). X-CUBE-ALIRO already ships a working NFC09A1 project at `Projects/nucleo-u5/Applications/Aliro/nfc-only/` that uses `Drivers/BSP/Components/st25r200/` — the chip driver is reusable verbatim. The port is board-level only: clone the `nfc-only/` project, remap pin/LED assignments from NFC09A1's Arduino header layout to NFC10A1's (plus add an explicit reset pin that NFC10A1 exposes), wire `aliro-trustgen`'s generated header into the build so the reader's long-term keys match what the J3R180 was personalized with, and add instrumentation. **No new chip driver fetch is required** (an earlier draft of this plan wrongly claimed NFC10A1 was ST25R3916 and would need X-CUBE-NFC7/NFC6 ported in — memory [j3r180_ec_curve_seeding.md](.claude/projects/-home-work-VSCodeProjects-aliro-project/memory/x_cube_aliro_supported_boards.md) now captures the correct chip pairings).

**Tech Stack:** NUCLEO-U545RE-Q (STM32U545RE MCU), X-NUCLEO-NFC10A1 (ST25R200 reader IC), CMake + `arm-none-eabi-gcc` 13.2+, STM32CubeProgrammer (or `st-flash`/OpenOCD) for flashing, ST-LINK onboard the Nucleo, RFAL + ST25R200 driver (both already in the X-CUBE-ALIRO vendor tree).

---

## Pre-flight toolchain gaps

Before Task 4 (build) the host needs:
```bash
sudo apt install ninja-build
```
Before Task 5 (flash) the host needs one of:
- **STM32CubeProgrammer** (preferred — ST-LINK out of the box): [st.com/en/development-tools/stm32cubeprog.html](https://www.st.com/en/development-tools/stm32cubeprog.html)
- `sudo apt install stlink-tools` (provides `st-flash`)
- `sudo apt install openocd`

`arm-none-eabi-gcc` 13.2.1, `cmake` 3.28 are already present (verified).

---

## Critical files and locations

**Template project (DO NOT MODIFY — clone from here):**
- [reader/STM32CubeExpansion_ALIRO_V1_0_0/Projects/nucleo-u5/Applications/Aliro/nfc-only/](reader/STM32CubeExpansion_ALIRO_V1_0_0/Projects/nucleo-u5/Applications/Aliro/nfc-only/)

**Shared code (DO NOT MODIFY — inherited as-is by new project):**
- `reader/STM32CubeExpansion_ALIRO_V1_0_0/Drivers/BSP/Components/st25r200/` — chip driver
- `reader/STM32CubeExpansion_ALIRO_V1_0_0/Drivers/BSP/NFC09A1/` — BSP shim (the current project uses NFC09A1's macros; we'll add an NFC10A1 one)
- `reader/STM32CubeExpansion_ALIRO_V1_0_0/Middlewares/ST/rfal/` — RFAL middleware
- `reader/STM32CubeExpansion_ALIRO_V1_0_0/Projects/Common/Aliro/` — Aliro protocol layer (where trust is injected in Task 4)

**New dirs we will create:**
- `reader/STM32CubeExpansion_ALIRO_V1_0_0/Drivers/BSP/NFC10A1/` — BSP shim copied + renamed from NFC09A1
- `reader/STM32CubeExpansion_ALIRO_V1_0_0/Projects/nucleo-u5/Applications/Aliro/nfc10-only/` — project root cloned from `nfc-only/`

**NFC10A1 pin-map reference (stm32duino):**
- `/tmp/x-nucleo-nfc10a1-ref/examples/X_NUCLEO_NFC10A1_HelloWorld/X_NUCLEO_NFC10A1_HelloWorld.ino` (already cloned this session)
- The `#define` block in that file is the authoritative Arduino-header pin map for NFC10A1.

**Trust bundle:** `~/aliro-trust/` (from `aliro-trustgen init`, same bundle the J3R180 was personalized with).

**Harness-side comparison point:** `aliro-bench-test --trust-dir ~/aliro-trust` → `RESULT: OK` is the known-good reference.

---

## Task 1: Clone the BSP shim for NFC10A1

**Files:**
- Create: `reader/STM32CubeExpansion_ALIRO_V1_0_0/Drivers/BSP/NFC10A1/nfc10a1.h`
- Create: `reader/STM32CubeExpansion_ALIRO_V1_0_0/Drivers/BSP/NFC10A1/nfc10a1.c`
- Reference (read-only): `reader/STM32CubeExpansion_ALIRO_V1_0_0/Drivers/BSP/NFC09A1/nfc09a1.{h,c}`

**Step 1: Copy the NFC09A1 shim**

```bash
cd reader/STM32CubeExpansion_ALIRO_V1_0_0/Drivers/BSP
cp -r NFC09A1 NFC10A1
cd NFC10A1
mv nfc09a1.h nfc10a1.h
mv nfc09a1.c nfc10a1.c
```

**Step 2: Rename identifiers inside the files**

```bash
sed -i 's/NFC09A1/NFC10A1/g; s/nfc09a1/nfc10a1/g' nfc10a1.h nfc10a1.c
```

The chip driver include (`st25r200.h`) must STAY unchanged — same chip. Grep to confirm:
```bash
grep -c st25r200 nfc10a1.{h,c}  # should be ≥1 in each file
grep -c st25r500 nfc10a1.{h,c}  # must be 0
```

**Step 3: Verify BSP API surface is unchanged**

The top-level `Projects/Common/Aliro/Src/app_nfc.c` calls e.g. `BSP_NFC0XCOMM_SendRecv(...)` — those symbol names are generic (NFC0X prefix) and don't change between NFC09A1 and NFC10A1. Confirm by grepping `nfc10a1.c` for `BSP_NFC0XCOMM_` — symbols must match the NFC09A1 original.

**Step 4: Commit (if reader/ is under git; currently the project isn't a git repo — create a note file instead)**

Since [/home/work/VSCodeProjects/aliro_project](.) isn't a git repository, track changes in a plain ledger file at `reader/bench-notes/nfc10a1-port-log.md`: append a line noting Task 1 completion with a timestamp.

---

## Task 2: Clone `nfc-only/` → `nfc10-only/` project

**Files:**
- Create: `reader/STM32CubeExpansion_ALIRO_V1_0_0/Projects/nucleo-u5/Applications/Aliro/nfc10-only/` (whole subtree)

**Step 1: Recursive copy**

```bash
cd reader/STM32CubeExpansion_ALIRO_V1_0_0/Projects/nucleo-u5/Applications/Aliro
cp -r nfc-only nfc10-only
```

**Step 2: Rename the per-board Target directory**

```bash
cd nfc10-only
mv X-CUBE-NFC9 X-CUBE-NFC10
```

**Step 3: Update nfc_conf.h for NFC10A1 layout**

Edit `nfc10-only/X-CUBE-NFC10/Target/nfc_conf.h`:

- Change include guard: `__NFC09A1_CONF_H__` → `__NFC10A1_CONF_H__`
- Rename every `NFC09A1_*` macro to `NFC10A1_*` (LED1-LED6 RCC clock enables + pin definitions)
- **Pin remap per NFC10A1 Arduino header:**

  | Function | NFC09A1 (current) | NFC10A1 (new) | Nucleo-U545RE-Q pin |
  |----------|-------------------|---------------|---------------------|
  | SPI CS (`ST25R_SS`) | PC9 (line 87-88) | D10 → PB6 (verify against `stm32u5xx_nucleo_bus.h`) | **update** |
  | SPI IRQ (`ST25R_INT`) | PA0 (line 89-90) | A0 → PA0 (same) | unchanged |
  | **Reset** (NEW — NFC10A1 exposes) | — | D8 → PA9 (typical U5 mapping; verify) | **add new** |
  | LED A | PB6 | A2 → PA4 (verify) | **update** |
  | LED B | PB0 | A3 → PB0 (may be same) | **verify** |
  | LED F | PB10 | D5 → PB4 (verify) | **update** |
  | LED V | PA8 | D7 → PA8 (may be same) | **verify** |
  | LED AP2P | PB10 | D6 → PB10 (may be same) | **verify** |
  | LED FIELD | PA1 | A1 → PA1 (may be same) | **verify** |

  **Authoritative source for Arduino→MCU mapping:** `reader/STM32CubeExpansion_ALIRO_V1_0_0/Drivers/BSP/NUCLEO-U545RE-Q/stm32u5xx_nucleo_bus.h` — open it and grep for `ARDUINO_D10_PIN`, `ARDUINO_A0_PIN`, etc. If the header uses macros like `ARDUINO_Dx_Pin` / `ARDUINO_Ax_Pin`, reference those directly (avoids hardcoding PAx literals that could drift).

- Add reset pin definitions (NFC10A1-specific, absent from NFC09A1):
  ```c
  #define NFC10A1_RESET_PIN         GPIO_PIN_9   /* or whatever D8 resolves to */
  #define NFC10A1_RESET_PIN_PORT    GPIOA
  #define NFC10A1_RESET_PIN_CLK_ENABLE()  __HAL_RCC_GPIOA_CLK_ENABLE()
  ```
  Plumb the reset line through to the `st25r200` driver if the driver has a reset hook (check `st25r200.c` for a function like `st25r200_Reset()` or similar; the NFC12 template uses `BUS_SPI1_RST_GPIO_PIN` per the explorer's diff).

- Include path update: `#include "rfal_defConfig.h"` stays the same (shared); the board-specific bits should now reference `nfc10a1.h` (not `nfc09a1.h`) if included.

**Step 4: Update rfal_platform.h**

Open `nfc10-only/X-CUBE-NFC10/Target/rfal_platform.h`. The platform macros (`platformSpiSelect`, `platformSpiTxRx`, `platformProtectST25RComm`) bind to `ST25R_SS_PIN/PORT` etc. — those come from nfc_conf.h so if the pins there are right, this file just needs the header guard rename (and any `NFC09A1`/`NFC9` references → `NFC10A1`/`NFC10`).

**Step 5: Update CMake**

`nfc10-only/cmake/stm32cubemx/CMakeLists.txt`:
- Change project/target name: any `nfc-only` or `nfc_only` → `nfc10-only` / `nfc10_only`
- Swap include dir: `Drivers/BSP/NFC09A1` → `Drivers/BSP/NFC10A1`
- Update Target include: `X-CUBE-NFC9/Target` → `X-CUBE-NFC10/Target`

The `ST25R200` CMake define stays — same chip.

`nfc10-only/CMakeLists.txt` (top-level): update project name.

**Step 6: Update main.c init**

In `nfc10-only/Core/Src/main.c`, search for `NFC09A1_` or `BSP_NFC09A1_` and replace with `NFC10A1_` / `BSP_NFC10A1_` equivalents. The `BSP_NFC0XCOMM_*` generic names stay.

**Step 7: Append to the port-log ledger**

```
reader/bench-notes/nfc10a1-port-log.md
```

---

## Task 3: First build — iterate on errors

**Files:** no new files; fix whatever the build surfaces.

**Step 1: Configure**

```bash
cd reader/STM32CubeExpansion_ALIRO_V1_0_0/Projects/nucleo-u5/Applications/Aliro/nfc10-only
cmake -B build -G Ninja \
  -DCMAKE_TOOLCHAIN_FILE=cmake/stm32cubemx/gcc-arm-none-eabi.cmake \
  -DCMAKE_BUILD_TYPE=Debug
```

(If ninja wasn't installed, fall back to `-G "Unix Makefiles"` and use `cmake --build build` / `make -j`.)

**Step 2: Build and triage**

```bash
cmake --build build 2>&1 | tee build.log
```

Expected first-pass errors (iterate):
- **Undefined reference to `BSP_NFC10A1_Init`** or similar → Task 1 rename incomplete; grep for stale `NFC09A1` in the new shim.
- **`nfc09a1.h: No such file`** → stale include somewhere in nfc10-only; replace with `nfc10a1.h`.
- **`st25r200.h: No such file`** should NOT appear — if it does, the Components include path was accidentally dropped in Task 2 Step 5.
- **`ARDUINO_Dx_Pin undefined`** → the pin macro doesn't exist on this board's `stm32u5xx_nucleo_bus.h`; fall back to hardcoded PAx literals per NUCLEO-U545RE-Q schematic (UM3414 App A for pinout tables).
- **Reset pin symbol unused** → you defined `NFC10A1_RESET_*` but didn't wire it into init. Either remove the def if the driver doesn't need it, or add a `HAL_GPIO_WritePin` pulse in `nfc10a1.c::BSP_NFC10A1_Init()`.

Fix one class of error at a time.

**Step 3: Confirm artifacts produced**

```bash
ls -la build/nfc10-only.elf build/nfc10-only.bin
```

Expected: both exist, `.bin` ~100-200 KB.

**Step 4: Record build success**

Append to `reader/bench-notes/nfc10a1-port-log.md`.

---

## Task 4: Wire `aliro-trustgen` output into the firmware build

**Files:**
- Possibly modify: `reader/STM32CubeExpansion_ALIRO_V1_0_0/Projects/Common/Aliro/Src/provisioning.c` (or equivalent — find the hardcoded trust first)
- Create: `reader/STM32CubeExpansion_ALIRO_V1_0_0/Projects/nucleo-u5/Applications/Aliro/nfc10-only/Core/Inc/aliro_trust.h` (gitignored, generated)
- Possibly modify: `harness/src/aliro_harness/trustgen/header.py` (to add an `emit-header` CLI subcommand if missing)
- Modify: `nfc10-only/cmake/stm32cubemx/CMakeLists.txt` (add custom_command)

**Step 1: Find the hardcoded trust**

```bash
grep -rln "reader_priv\|reader_pub\|issuer_pub\|credential_issuer" \
  reader/STM32CubeExpansion_ALIRO_V1_0_0/Projects/Common/Aliro/
```

Expected: one or two files in `Src/` with `static const uint8_t` byte arrays. Note extern names — the generated header must match.

**Step 2: Verify `aliro-trustgen` can emit the expected header**

```bash
aliro-trustgen --help
aliro-trustgen init --out-dir /tmp/trust-test
ls /tmp/trust-test/
```

If there's already an `emit-header` subcommand (or `init` writes `aliro_trust.h`), use it. If not, add a one-liner subcommand that reads `reader.pem` + `access_credential.pem` + `reader_group_id.bin` + `reader_group_sub_id.bin` and writes a C header matching the vendor's extern names.

A bare-minimum template at `harness/src/aliro_harness/trustgen/header.py::emit_c_header(trust_dir, out_path)`:
```python
def emit_c_header(trust_dir: Path, out_path: Path) -> None:
    rp = load_priv(trust_dir / "reader.pem")          # 32 bytes
    rP = load_pubk(trust_dir / "reader.pem")          # 64 bytes x||y
    iP = load_pubk(trust_dir / "access_credential.pem")  # 64 bytes x||y
    gid = (trust_dir / "reader_group_id.bin").read_bytes()     # 16 bytes
    sid = (trust_dir / "reader_group_sub_id.bin").read_bytes() # 16 bytes
    with out_path.open("w") as f:
        f.write("/* Auto-generated by aliro-trustgen — do not edit */\n")
        f.write("#pragma once\n#include <stdint.h>\n\n")
        f.write(f"static const uint8_t reader_priv_key[32] = {{ {fmt(rp)} }};\n")
        f.write(f"static const uint8_t reader_pub_key[64] = {{ {fmt(rP)} }};\n")
        f.write(f"static const uint8_t issuer_pub_key[64] = {{ {fmt(iP)} }};\n")
        f.write(f"static const uint8_t reader_group_id[16] = {{ {fmt(gid)} }};\n")
        f.write(f"static const uint8_t reader_group_sub_id[16] = {{ {fmt(sid)} }};\n")
```

Plus a Click command that calls it. Write tests in `harness/tests/test_trustgen_header.py`.

**Step 3: Replace vendor hardcoded bytes with the generated header**

In the file found in Step 1:
```c
// was: static const uint8_t reader_priv_key[32] = { 0xAA, 0xBB, ... };
#include "aliro_trust.h"
```

(If vendor declared its trust bytes as `static const` and consumed them locally, the header defining the same names with `static const` linkage is a drop-in. If vendor declared them `extern`, swap the header to match.)

**Step 4: CMake custom_command to regenerate on trust change**

In `nfc10-only/cmake/stm32cubemx/CMakeLists.txt`:

```cmake
set(ALIRO_TRUST_DIR "$ENV{HOME}/aliro-trust" CACHE PATH "Trust bundle directory")
set(ALIRO_TRUST_HEADER "${CMAKE_CURRENT_SOURCE_DIR}/../../Core/Inc/aliro_trust.h")

add_custom_command(
  OUTPUT "${ALIRO_TRUST_HEADER}"
  COMMAND aliro-trustgen emit-header --trust-dir "${ALIRO_TRUST_DIR}" --out "${ALIRO_TRUST_HEADER}"
  DEPENDS "${ALIRO_TRUST_DIR}/reader.pem" "${ALIRO_TRUST_DIR}/access_credential.pem"
  COMMENT "Regenerating aliro_trust.h from ${ALIRO_TRUST_DIR}"
  VERBATIM
)
add_custom_target(aliro_trust_header DEPENDS "${ALIRO_TRUST_HEADER}")
add_dependencies(${CMAKE_PROJECT_NAME}.elf aliro_trust_header)
```

**Step 5: Ignore the generated header**

```bash
# Since project isn't git, create a LOCAL .gitignore-equivalent ledger:
echo "Projects/nucleo-u5/Applications/Aliro/nfc10-only/Core/Inc/aliro_trust.h" \
  >> reader/STM32CubeExpansion_ALIRO_V1_0_0/.distignore
```

(Documentational — mostly a reminder.)

**Step 6: Rebuild + verify the bytes made it into the ELF**

```bash
rm -rf build && cmake -B build -G Ninja ...
cmake --build build
arm-none-eabi-objdump -s -j .rodata build/nfc10-only.elf | grep -A2 reader_priv
```

Expected: 32 bytes matching the first 32 bytes of the PEM-decoded reader private scalar.

---

## Task 5: Flash + first-boot verification (HARDWARE REQUIRED)

**Files:** none — on-device only.

**Step 1: Connect hardware**

- NUCLEO-U545RE-Q to host USB (enumerates as `/dev/ttyACM0` for VCP + ST-LINK DFU)
- X-NUCLEO-NFC10A1 plugged onto Arduino header (ensure correct orientation — D0 pin aligned)

**Step 2: Flash**

```bash
# With STM32CubeProgrammer:
STM32_Programmer_CLI -c port=SWD -w build/nfc10-only.elf -v -rst
# Or with st-flash:
arm-none-eabi-objcopy -O binary build/nfc10-only.elf build/nfc10-only.bin
st-flash write build/nfc10-only.bin 0x08000000
```

**Step 3: Open UART console**

```bash
minicom -D /dev/ttyACM0 -b 115200
# or: picocom -b 115200 /dev/ttyACM0
```

Expected: within a second of reset, banner text like `Aliro Reader — NFC10A1 / ST25R200` followed by init messages and a polling loop.

**Step 4: Verify NFC field activates**

Within a few seconds of boot the ST25R200 should drive the antenna. NFC10A1 has a FIELD LED — expect it to light solid (or blink during active polling).

**Step 5: If the FIELD LED doesn't light**

Common causes — debug in order:
1. Chip-ID read returns 0xFF → SPI isn't talking (check CS pin resolves to the right MCU GPIO; probe with logic analyzer if available)
2. Reset pin held low → the reset wiring added in Task 2 isn't pulsed correctly
3. Antenna tuning failed → check UART log for specific RFAL init error code

**Step 6: Stop here if field isn't coming up — no point chasing the protocol layer yet.**

---

## Task 6: Empty-field + random-card discovery (HARDWARE REQUIRED)

**Step 1: With no card present** — UART should log periodic `polling, no target` (or equivalent). Every ~500ms.

**Step 2: Present a random NFC-A card (transit, hotel keycard)** — UART should log `target detected: NFC-A, UID=...`. Proves polling + anti-collision.

**Step 3: Present the J3R180** — UART should log target detected and attempt a SELECT of the Aliro AID. Expected either success (proceed to Task 7) or an SW that points at which APDU exchange failed.

**Step 4: Capture UART log to `reader/bench-notes/nfc10a1-discovery.log`.**

---

## Task 7: Full AUTH0+AUTH1 against J3R180 (HARDWARE REQUIRED)

**Step 1: Confirm baseline still green**

```bash
aliro-bench-test --trust-dir ~/aliro-trust
```
Expected: `RESULT: OK`. If not, stop — something regressed in the applet.

**Step 2: Ensure card personalized from same trust bundle**

(No action if card hasn't been re-personalized since Task 4 wired the same bundle into the firmware. If uncertain, re-run `aliro-personalize --trust-dir ~/aliro-trust` on a fresh card.)

**Step 3: Present J3R180 to the Nucleo reader**

Expected UART log sequence:
```
target detected: NFC-A
SELECT Aliro AID → SW=9000
AUTH0 (lc=N) → response N bytes, SW=9000
AUTH1 (lc=N) → response N bytes, SW=9000
AUTH1 decrypt OK
UD signature verify OK
GRANT — signaling_bitmap=0x0005
```

**Step 4: Failure triage**

- **`SW=6A82` on SELECT** — AID mismatch. Vendor firmware's SELECT target must be `A0000009 09 ACCE 55 01`.
- **`SW=6F00` on AUTH0** — uncaught applet exception; correlate with bench-test behavior, should NOT appear if bench-test is green.
- **AUTH1 decrypt fail** — ExpeditedSKDevice mismatch. Either firmware's trust bundle differs from the card's, or Table 8-12 bytes are assembled differently. Compare Table 8-12 hex from firmware UART log vs bench-test `--debug` output.
- **Signature verify fail after decrypt** — firmware's `issuer_pub_key` / `credential_pub_key` extern doesn't match the trust bundle used to personalize the card.

**Step 5: On success**

Capture UART log to `reader/bench-notes/nfc10a1-auth1-green.log`. Update memory [pcsc_bench_reader.md](.claude/projects/-home-work-VSCodeProjects-aliro-project/memory/pcsc_bench_reader.md): append a note that Nucleo-NFC10A1 is now a second validation path.

---

## Task 8: Instrumentation — UART APDU trace + LED state machine

**Files:**
- Create: `reader/STM32CubeExpansion_ALIRO_V1_0_0/Projects/Common/Aliro/Inc/aliro_log.h`
- Modify: `reader/STM32CubeExpansion_ALIRO_V1_0_0/Projects/Common/Aliro/Src/app_nfc.c` — add APDU log points around each transceive
- Modify: `nfc10-only/Core/Src/main.c` — LED helpers

**Step 1: aliro_log.h**

```c
#pragma once
#include <stdio.h>
#include <stdint.h>

#define ALIRO_LOG_INFO(fmt, ...)   printf("[INFO] " fmt "\n", ##__VA_ARGS__)
#define ALIRO_LOG_ERROR(fmt, ...)  printf("[ERROR] " fmt "\n", ##__VA_ARGS__)

static inline void aliro_log_hex(const char *dir, const uint8_t *buf, uint16_t len) {
    printf("[APDU %s] ", dir);
    for (uint16_t i = 0; i < len; i++) printf("%02x", buf[i]);
    printf("\n");
}
```

**Step 2: Instrument the APDU pump**

In `app_nfc.c` (or wherever ISO-DEP TX/RX happens), wrap:
```c
aliro_log_hex(">", txBuf, txLen);
// ... transceive
aliro_log_hex("<", rxBuf, rxLen);
```

Match the format the bench-test logs emit (see [harness/src/aliro_harness/reader/transaction.py](harness/src/aliro_harness/reader/transaction.py)).

**Step 3: LED helpers**

In `nfc10-only/Core/Src/main.c`:
```c
static void aliro_led_grant(void) { HAL_GPIO_WritePin(PLATFORM_LED_A_PORT, PLATFORM_LED_A_PIN, GPIO_PIN_SET); }
static void aliro_led_deny(void)  { HAL_GPIO_WritePin(PLATFORM_LED_B_PORT, PLATFORM_LED_B_PIN, GPIO_PIN_SET); }
static void aliro_led_active(bool on) { HAL_GPIO_WritePin(PLATFORM_LED_FIELD_PORT, PLATFORM_LED_FIELD_PIN, on ? GPIO_PIN_SET : GPIO_PIN_RESET); }
```

Call from the Aliro state machine: `aliro_led_active(true)` at AUTH0 start, grant/deny at AUTH1 finish.

**Step 4: Verify visually**

J3R180 → FIELD LED (A1) lights during transaction, LED A (A2) lights for ~1s on grant, clears.
Random card → LED B (A3) lights on AUTH0 failure.

---

## Task 9: A/B compare — Nucleo vs PC/SC bench reader

**Files:**
- Create: `reader/bench-notes/ab-compare-2026-04-XX.md`
- Possibly modify: [harness/src/aliro_harness/reader/cli.py](harness/src/aliro_harness/reader/cli.py) — add `--debug` flag that dumps APDU TX/RX to stderr

**Step 1: Bench-test transcript with APDU trace**

If `--debug` doesn't exist, add it (one or two lines in the transmit wrapper at cli.py to `click.echo(f"[APDU >] {apdu.hex()}", err=True)` + same for response).

```bash
aliro-bench-test --trust-dir ~/aliro-trust --debug 2> /tmp/bench-trace.log
```

**Step 2: Nucleo transcript**

```bash
# Present card DURING this capture:
(timeout 10 cat /dev/ttyACM0) > /tmp/nucleo-trace.log
```

**Step 3: Diff APDU streams**

```bash
diff <(grep '^\[APDU' /tmp/bench-trace.log) <(grep '^\[APDU' /tmp/nucleo-trace.log)
```

Expected: differences only in randomized fields (reader ephemeral pubkey, transaction_identifier, encrypted payload ciphertext). Fixed fields (SELECT AID, command_parameters, protocol_version, reader_identifier, response TLV tags) match byte-for-byte.

**Step 4: Document findings**

`reader/bench-notes/ab-compare-2026-04-XX.md` captures:
- Same card + same trust bundle → both GRANT
- APDU diff: expected-only deltas
- Any surprise (extra TLV, unexpected field order, SW difference)

---

## Follow-up work NOT in this plan

- Step-up phase on the Nucleo (SELECT STEP_UP + CBOR ENVELOPE — large, separate plan)
- Antenna tuning against DT's implantable form factor (different tuning than external cards)
- CI build: compile the ELF in CI to catch regressions (no probe needed, just compile)
- Structured binary telemetry log format (replace `printf` — nice-to-have, not needed for bench)

---

## Verification checkpoint after all tasks complete

- [ ] `nfc10-only.elf` builds cleanly from the vendor tree
- [ ] Flasher writes and reader boots with NFC field active
- [ ] `aliro-bench-test --trust-dir ~/aliro-trust` still green (no applet regressions)
- [ ] Nucleo reader → J3R180 → GRANT + full APDU log + LED state
- [ ] A/B diff shows only expected randomness
- [ ] Memory entry updated: Nucleo-NFC10A1 is a second validation path alongside PC/SC
