# 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 \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 #include #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