Skip to content

Commit acc165e

Browse files
committed
Adds per-call ISO-DEP timeout override
Implements Issue #5 proposal 1 (per-call fwt/wtx/retries override on transceiveINF/transceiveAPDU; works for NFC-A and NFC-B via the shared ISO-DEP layer). Part of #5.
2 parents c70f54d + 56caa40 commit acc165e

11 files changed

Lines changed: 464 additions & 25 deletions

File tree

.github/workflows/arduino-esp-v2-build-check.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,14 @@ jobs:
129129
platform-version: 2.0.17
130130
board: m5stack-core-esp32
131131
build-properties: "-DUSING_UNIT_NFC"
132+
- sketch: PolicyOverride
133+
nfc: NFCA
134+
platform-url: https://espressif.github.io/arduino-esp32/package_esp32_index.json
135+
platform: esp32
136+
archi: esp32
137+
platform-version: 2.0.17
138+
board: m5stack-core-esp32
139+
build-properties: "-DUSING_UNIT_NFC"
132140

133141
# NFCB Specific sketches
134142
- sketch: JapanIDCard

.github/workflows/arduino-esp-v3-build-check.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,14 @@ jobs:
149149
platform-version: 3.3.6
150150
board: m5stack_core
151151
build-properties: "-DUSING_UNIT_NFC"
152+
- sketch: PolicyOverride
153+
nfc: NFCA
154+
platform-url: https://espressif.github.io/arduino-esp32/package_esp32_index.json
155+
platform: esp32
156+
archi: esp32
157+
platform-version: 3.3.6
158+
board: m5stack_core
159+
build-properties: "-DUSING_UNIT_NFC"
152160

153161
# NFCB Specific sketches
154162
- sketch: JapanIDCard

.github/workflows/arduino-m5-build-check.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,14 @@ jobs:
160160
platform-version: 3.2.5
161161
board: m5stack_core
162162
build-properties: "-DUSING_UNIT_NFC"
163+
- sketch: PolicyOverride
164+
nfc: NFCA
165+
platform-url: https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json
166+
platform: m5stack
167+
archi: esp32
168+
platform-version: 3.2.5
169+
board: m5stack_core
170+
build-properties: "-DUSING_UNIT_NFC"
163171

164172
# NFCB Specific sketches
165173
- sketch: JapanIDCard

.github/workflows/platformio-build-check.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ jobs:
119119
board: Core
120120
framework: Arduino
121121
espressif32: latest
122+
- example: NFCA_PolicyOverride
123+
unit: UnitNFC
124+
board: Core
125+
framework: Arduino
126+
espressif32: latest
122127

123128
# NFCB Specific sketches
124129
- example: NFCB_JapanIDCard

cap_cc1101_env.ini

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ build_flags = ${Cardputer.build_flags}
6464
${option_release.build_flags}
6565
-D USING_CAP_CC1101
6666

67+
[env:CapCC1101_NFCA_PolicyOverride_Core_Arduino_latest]
68+
extends=Cardputer, option_release, arduino_latest
69+
build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/NFCA/PolicyOverride>
70+
build_flags = ${Cardputer.build_flags}
71+
${option_release.build_flags}
72+
-D USING_CAP_CC1101
73+
74+
6775
[env:-----------------------------------------------separator01]
6876
; ---- NFC-B
6977
[env:CapCC1101_NFCB_Detect_CardputerADV_Arduino_latest]
@@ -154,3 +162,5 @@ build_flags = ${Cardputer.build_flags}
154162
${option_release.build_flags}
155163
-D USING_CAP_CC1101
156164

165+
166+
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 M5Stack Technology CO LTD
3+
*
4+
* SPDX-License-Identifier: MIT
5+
*/
6+
/*
7+
Example using M5UnitUnified for M5Unit-NFC/RFID
8+
Demonstrate per-call ISO-DEP timeout/retry override (policy_t) on an ISO/IEC 14443-4 PICC.
9+
This example is shared with M5Unit-RFID
10+
*/
11+
// *************************************************************
12+
// Choose ONE define symbol to match the unit/board you are using
13+
// *************************************************************
14+
#if !defined(USING_UNIT_NFC) && !defined(USING_CAP_CC1101) && !defined(USING_UNIT_RFID2) && \
15+
!defined(USING_M5DIAL_BUILTIN_WS1850S)
16+
// For UnitNFC (ST25R3916, I2C)
17+
// #define USING_UNIT_NFC
18+
// For CapCC1101NFC (ST25R3916, SPI)
19+
// #define USING_CAP_CC1101
20+
// For UnitRFID2 (WS1850S external, I2C GROVE)
21+
// #define USING_UNIT_RFID2
22+
// For M5Dial Builtin WS1850S (internal I2C)
23+
// #define USING_M5DIAL_BUILTIN_WS1850S
24+
#endif
25+
#include "main/PolicyOverride.cpp"
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 M5Stack Technology CO LTD
3+
*
4+
* SPDX-License-Identifier: MIT
5+
*/
6+
/*
7+
Example using M5UnitUnified for M5Unit-NFC/RFID
8+
Demonstrate per-call ISO-DEP timeout/retry override (policy_t) on an ISO/IEC 14443-4 PICC.
9+
10+
After activating an ISO/IEC 14443-4 PICC, the same APDU is sent three ways:
11+
(a) default policy (timeout/retries from the activation-derived ISO-DEP config)
12+
(b) override with a long fwt_ms and max_retries=0 (e.g. for a slow, non-idempotent command)
13+
(c) override with a deliberately tiny fwt_ms to show that the override actually controls
14+
the per-exchange timeout (this one is expected to time out)
15+
The override is per-call only: it does not change the session config and does not reset the
16+
ISO-DEP block number.
17+
This example is shared with M5Unit-RFID
18+
*/
19+
#include <M5Unified.h>
20+
#include <M5UnitUnified.h>
21+
#include <M5UnitUnifiedNFC.h>
22+
#include <M5Utility.h>
23+
#include <Wire.h>
24+
25+
// *************************************************************
26+
// Choose ONE define symbol to match the unit/board you are using
27+
// *************************************************************
28+
#if !defined(USING_UNIT_NFC) && !defined(USING_CAP_CC1101) && !defined(USING_UNIT_RFID2) && \
29+
!defined(USING_M5DIAL_BUILTIN_WS1850S)
30+
// For UnitNFC (ST25R3916, I2C)
31+
// #define USING_UNIT_NFC
32+
// For CapCC1101NFC (ST25R3916, SPI)
33+
// #define USING_CAP_CC1101
34+
// For UnitRFID2 (WS1850S external, I2C GROVE)
35+
// #define USING_UNIT_RFID2
36+
// For M5Dial Builtin WS1850S (internal I2C)
37+
// #define USING_M5DIAL_BUILTIN_WS1850S
38+
#endif
39+
40+
#if defined(USING_UNIT_RFID2) || defined(USING_M5DIAL_BUILTIN_WS1850S)
41+
#include <M5UnitUnifiedRFID.h>
42+
#endif
43+
44+
using namespace m5::nfc::a;
45+
46+
namespace {
47+
auto& lcd = M5.Display;
48+
m5::unit::UnitUnified Units;
49+
50+
#if defined(USING_UNIT_NFC)
51+
#pragma message "Choose UnitNFC"
52+
m5::unit::UnitNFC unit{}; // I2C
53+
#elif defined(USING_CAP_CC1101)
54+
#pragma message "Choose CapCC1101NFC"
55+
m5::unit::CapCC1101NFC unit{}; // CapCC1101 (SPI)
56+
#elif defined(USING_UNIT_RFID2)
57+
#pragma message "Choose UnitRFID2"
58+
m5::unit::UnitRFID2 unit{}; // UnitRFID2 external (M5Unit-RFID, GROVE)
59+
#elif defined(USING_M5DIAL_BUILTIN_WS1850S)
60+
#pragma message "Choose UnitRFID2 (M5Dial Builtin)"
61+
m5::unit::UnitRFID2 unit{}; // M5Dial builtin WS1850S (internal I2C)
62+
#else
63+
#error Choose ONE: USING_UNIT_NFC / USING_CAP_CC1101 / USING_UNIT_RFID2 / USING_M5DIAL_BUILTIN_WS1850S
64+
#endif
65+
m5::nfc::NFCLayerA nfc_a{unit};
66+
67+
// ISO/IEC 7816-4 SELECT by name (NDEF Tag Application AID). A benign Type-4 command:
68+
// the PICC answers with a status word (e.g. 9000 or 6A82) whose value does not matter here;
69+
// we only observe whether the per-call timeout lets the exchange complete.
70+
constexpr uint8_t SELECT_NDEF_AID[]{0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0x00};
71+
72+
// Send SELECT_NDEF_AID once with the given per-call override (nullptr = use config) and log the result.
73+
void send_select(m5::nfc::isodep::IsoDEP* dep, const char* label, const m5::nfc::isodep::policy_t* override_policy)
74+
{
75+
uint8_t rx[64]{};
76+
uint16_t rx_len = sizeof(rx);
77+
const bool ok = dep->transceiveAPDU(rx, rx_len, SELECT_NDEF_AID, sizeof(SELECT_NDEF_AID), override_policy);
78+
if (ok && rx_len >= 2) {
79+
M5.Log.printf(" [%s] OK SW=%02X%02X (rx_len=%u)\n", label, rx[rx_len - 2], rx[rx_len - 1], rx_len);
80+
} else {
81+
M5.Log.printf(" [%s] FAILED (timeout/no response) ok=%d rx_len=%u\n", label, ok, rx_len);
82+
}
83+
}
84+
} // namespace
85+
86+
void setup()
87+
{
88+
M5.begin();
89+
M5.setTouchButtonHeightByRatio(100);
90+
91+
// The screen shall be in landscape mode
92+
if (lcd.height() > lcd.width()) {
93+
lcd.setRotation(1);
94+
}
95+
96+
bool unit_ready{};
97+
98+
#if defined(USING_M5DIAL_BUILTIN_WS1850S)
99+
// M5Dial builtin WS1850S: small loop antenna; reduce RxGain to 33dB to mitigate
100+
// reflection interference (default 48dB causes unstable WUPA on Builtin).
101+
{
102+
auto cfg = unit.config();
103+
cfg.receiver_gain = m5::unit::mfrc522::ReceiverGain::dB33;
104+
unit.config(cfg);
105+
}
106+
// M5Dial builtin WS1850S on In_I2C (G12/G11, shared with RTC8563)
107+
M5_LOGI("Using M5.In_I2C for builtin WS1850S");
108+
unit_ready = Units.add(unit, M5.In_I2C) && Units.begin();
109+
110+
#elif defined(USING_UNIT_NFC) || defined(USING_UNIT_RFID2)
111+
// External I2C unit (GROVE port).
112+
// NessoN1: Arduino Wire (I2C_NUM_0) cannot be used for GROVE port (used by In_I2C internals).
113+
// Use QWIIC (port_a) with Wire. (Requires QWIIC-GROVE conversion cable)
114+
// NanoC6: Wire.begin() on GROVE pins conflicts with Ex_I2C on I2C_NUM_0; use M5.Ex_I2C directly.
115+
if (M5.getBoard() == m5::board_t::board_M5NanoC6) {
116+
M5_LOGI("Using M5.Ex_I2C");
117+
unit_ready = Units.add(unit, M5.Ex_I2C) && Units.begin();
118+
} else {
119+
auto pin_num_sda = M5.getPin(m5::pin_name_t::port_a_sda);
120+
auto pin_num_scl = M5.getPin(m5::pin_name_t::port_a_scl);
121+
M5_LOGI("getPin: SDA:%u SCL:%u", pin_num_sda, pin_num_scl);
122+
Wire.end();
123+
Wire.begin(pin_num_sda, pin_num_scl, 400 * 1000U);
124+
unit_ready = Units.add(unit, Wire) && Units.begin();
125+
}
126+
127+
#elif defined(USING_CAP_CC1101)
128+
if (!SPI.bus()) {
129+
auto spi_sclk = M5.getPin(m5::pin_name_t::sd_spi_sclk);
130+
auto spi_mosi = M5.getPin(m5::pin_name_t::sd_spi_mosi);
131+
auto spi_miso = M5.getPin(m5::pin_name_t::sd_spi_miso);
132+
M5_LOGI("getPin: %d,%d,%d", spi_sclk, spi_mosi, spi_miso);
133+
SPI.begin(spi_sclk, spi_miso, spi_mosi /* SS is shared SD, CC1101, ST25R3916 */);
134+
}
135+
SPISettings settings = {10000000, MSBFIRST, SPI_MODE1};
136+
unit_ready = Units.add(unit, SPI, settings) && Units.begin();
137+
#endif
138+
139+
if (!unit_ready) {
140+
M5_LOGE("Failed to begin");
141+
lcd.fillScreen(TFT_RED);
142+
while (true) {
143+
m5::utility::delay(10000);
144+
}
145+
}
146+
M5_LOGI("M5UnitUnified initialized");
147+
M5_LOGI("%s", Units.debugInfo().c_str());
148+
149+
lcd.setFont(&fonts::Font0);
150+
lcd.fillScreen(0);
151+
lcd.setCursor(0, lcd.height() / 2);
152+
lcd.printf("Put an ISO14443-4 PICC and click BtnA");
153+
M5.Log.printf("Put an ISO14443-4 PICC and click BtnA\n");
154+
}
155+
156+
void loop()
157+
{
158+
M5.update();
159+
Units.update();
160+
161+
if (!M5.BtnA.wasClicked()) {
162+
return;
163+
}
164+
165+
PICC picc{};
166+
if (!nfc_a.detect(picc) || !nfc_a.identify(picc)) {
167+
M5.Log.printf("No PICC\n");
168+
return;
169+
}
170+
// MIFARE Plus SL1 reports as ISO14443-4 but communicates as MIFARE Classic (no ISO-DEP APDU),
171+
// so skip Classic-compatible PICCs (mirrors the RATS condition in NFCLayerA::activate).
172+
if (!picc.isISO14443_4() || picc.isMifareClassicCompatible()) {
173+
M5.Log.printf("Not an ISO-DEP (14443-4) PICC: %s\n", picc.typeAsString().c_str());
174+
return;
175+
}
176+
// reactivate() runs WUPA + anti-collision + SELECT + RATS, establishing the ISO-DEP session
177+
if (!nfc_a.reactivate(picc)) {
178+
M5_LOGE("Failed to reactivate %s", picc.uidAsString().c_str());
179+
return;
180+
}
181+
auto* dep = nfc_a.isoDEP();
182+
if (!dep) {
183+
M5_LOGE("ISO-DEP not available");
184+
nfc_a.deactivate();
185+
return;
186+
}
187+
188+
M5.Speaker.tone(2000, 30);
189+
M5.Log.printf("=== ISO-DEP per-call policy override: %s %s ===\n", picc.uidAsString().c_str(),
190+
picc.typeAsString().c_str());
191+
192+
// (a) Default policy (timeout/retries derived from the activation-time ISO-DEP config)
193+
send_select(dep, "default", nullptr);
194+
195+
// (b) Override: long fwt and no retry (e.g. a slow, non-idempotent command)
196+
const m5::nfc::isodep::policy_t long_policy{2000, 8000, 0};
197+
send_select(dep, "override fwt=2000ms retries=0", &long_policy);
198+
199+
// (c) Override: deliberately tiny fwt to show the override controls the per-exchange timeout.
200+
// Usually times out, proving the override value reaches the transceive. A very fast PICC
201+
// (e.g. ST25TA) may still answer within 1 ms and succeed.
202+
const m5::nfc::isodep::policy_t tiny_policy{1, 8000, 0};
203+
send_select(dep, "override fwt=1ms (usually times out)", &tiny_policy);
204+
205+
nfc_a.deactivate();
206+
}

0 commit comments

Comments
 (0)