|
| 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