/* * SPDX-FileCopyrightText: 2025 M5Stack Technology CO LTD * * SPDX-License-Identifier: MIT */ /* Example using M5UnitUnified for M5Unit-NFC/RFID Read/write NDEF NFC-A PICC This example is shared with M5Unit-RFID */ #include #include #include #include #include #include #include // ************************************************************* // Choose ONE define symbol to match the unit/board you are using // ************************************************************* #if !defined(USING_UNIT_NFC) && !defined(USING_CAP_CC1101) && !defined(USING_UNIT_RFID2) && \ !defined(USING_M5DIAL_BUILTIN_WS1850S) // For UnitNFC (ST25R3916, I2C) // #define USING_UNIT_NFC // For CapCC1101NFC (ST25R3916, SPI) // #define USING_CAP_CC1101 // For UnitRFID2 (WS1850S external, I2C GROVE) // #define USING_UNIT_RFID2 // For M5Dial Builtin WS1850S (internal I2C) // #define USING_M5DIAL_BUILTIN_WS1850S #endif #if defined(USING_UNIT_RFID2) || defined(USING_M5DIAL_BUILTIN_WS1850S) #include #endif using namespace m5::nfc; using namespace m5::nfc::a; using namespace m5::nfc::a::mifare; using namespace m5::nfc::ndef; namespace { auto& lcd = M5.Display; m5::unit::UnitUnified Units; #if defined(USING_UNIT_NFC) #pragma message "Choose UnitNFC" m5::unit::UnitNFC unit{}; // I2C #elif defined(USING_CAP_CC1101) #pragma message "Choose CapCC1101NFC" m5::unit::CapCC1101NFC unit{}; // CapCC1101 (SPI) #elif defined(USING_UNIT_RFID2) #pragma message "Choose UnitRFID2" m5::unit::UnitRFID2 unit{}; // UnitRFID2 external (M5Unit-RFID, GROVE) #elif defined(USING_M5DIAL_BUILTIN_WS1850S) #pragma message "Choose UnitRFID2 (M5Dial Builtin)" m5::unit::UnitRFID2 unit{}; // M5Dial builtin WS1850S (internal I2C) #else #error Choose ONE: USING_UNIT_NFC / USING_CAP_CC1101 / USING_UNIT_RFID2 / USING_M5DIAL_BUILTIN_WS1850S #endif m5::nfc::NFCLayerA nfc_a{unit}; // PNG image binary constexpr uint8_t poji_64_png[] = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0x00, 0x00, 0x82, 0x12, 0x4c, 0x73, 0x00, 0x00, 0x00, 0x02, 0x62, 0x4b, 0x47, 0x44, 0x00, 0x01, 0xdd, 0x8a, 0x13, 0xa4, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x48, 0x00, 0x46, 0xc9, 0x6b, 0x3e, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4d, 0x45, 0x07, 0xe8, 0x0b, 0x16, 0x08, 0x12, 0x36, 0x8d, 0x3c, 0xbe, 0xef, 0x00, 0x00, 0x00, 0x77, 0x74, 0x45, 0x58, 0x74, 0x52, 0x61, 0x77, 0x20, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x20, 0x74, 0x79, 0x70, 0x65, 0x20, 0x38, 0x62, 0x69, 0x6d, 0x00, 0x0a, 0x38, 0x62, 0x69, 0x6d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x34, 0x30, 0x0a, 0x33, 0x38, 0x34, 0x32, 0x34, 0x39, 0x34, 0x64, 0x30, 0x34, 0x30, 0x34, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x33, 0x38, 0x34, 0x32, 0x34, 0x39, 0x34, 0x64, 0x30, 0x34, 0x32, 0x35, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x31, 0x30, 0x64, 0x34, 0x31, 0x64, 0x38, 0x63, 0x64, 0x39, 0x38, 0x66, 0x30, 0x30, 0x62, 0x32, 0x30, 0x34, 0x65, 0x39, 0x38, 0x30, 0x30, 0x39, 0x39, 0x38, 0x0a, 0x65, 0x63, 0x66, 0x38, 0x34, 0x32, 0x37, 0x65, 0x0a, 0xa6, 0x53, 0xc3, 0x8e, 0x00, 0x00, 0x00, 0x01, 0x6f, 0x72, 0x4e, 0x54, 0x01, 0xcf, 0xa2, 0x77, 0x9a, 0x00, 0x00, 0x00, 0x6e, 0x49, 0x44, 0x41, 0x54, 0x28, 0xcf, 0x63, 0xf8, 0x0f, 0x05, 0x0c, 0xc3, 0x98, 0xf1, 0x43, 0x1e, 0xcc, 0xf8, 0xbc, 0xf7, 0xf3, 0xf9, 0xbd, 0xe7, 0x81, 0x8c, 0xef, 0x36, 0xef, 0x81, 0x08, 0xc8, 0x78, 0xc2, 0x71, 0xfe, 0xb3, 0x80, 0x3a, 0x90, 0xf1, 0x4e, 0x22, 0xfe, 0x97, 0x44, 0x39, 0x90, 0xf1, 0x5e, 0x28, 0xfe, 0x97, 0xc7, 0x67, 0x20, 0xe3, 0x5c, 0xfc, 0xfc, 0x9f, 0xbf, 0x8a, 0x81, 0x8c, 0xf3, 0xff, 0xef, 0xff, 0xfe, 0xff, 0x19, 0x99, 0xf1, 0xfe, 0xff, 0xfb, 0xef, 0xff, 0xbf, 0x03, 0x19, 0xcf, 0xff, 0x7f, 0x7f, 0x0f, 0x24, 0x40, 0x0c, 0xa0, 0x15, 0x20, 0xc6, 0x67, 0x90, 0x95, 0x20, 0x2b, 0x7e, 0x83, 0x18, 0xf7, 0x07, 0x81, 0xdf, 0x69, 0xcc, 0x00, 0x00, 0x17, 0xc5, 0xed, 0x7a, 0x25, 0x80, 0xdc, 0xb3, 0x00, 0x00, 0x00, 0x50, 0x65, 0x58, 0x49, 0x66, 0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x01, 0x12, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x87, 0x69, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xa0, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xa0, 0x02, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0xa0, 0x03, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0x25, 0x9b, 0x9b, 0x00, 0x00, 0x00, 0x25, 0x74, 0x45, 0x58, 0x74, 0x64, 0x61, 0x74, 0x65, 0x3a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x00, 0x32, 0x30, 0x32, 0x34, 0x2d, 0x31, 0x31, 0x2d, 0x32, 0x32, 0x54, 0x30, 0x38, 0x3a, 0x31, 0x35, 0x3a, 0x32, 0x31, 0x2b, 0x30, 0x30, 0x3a, 0x30, 0x30, 0x28, 0xd2, 0x30, 0x68, 0x00, 0x00, 0x00, 0x25, 0x74, 0x45, 0x58, 0x74, 0x64, 0x61, 0x74, 0x65, 0x3a, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x79, 0x00, 0x32, 0x30, 0x32, 0x33, 0x2d, 0x30, 0x34, 0x2d, 0x32, 0x38, 0x54, 0x30, 0x36, 0x3a, 0x35, 0x32, 0x3a, 0x32, 0x35, 0x2b, 0x30, 0x30, 0x3a, 0x30, 0x30, 0xcf, 0xa4, 0xfa, 0x1c, 0x00, 0x00, 0x00, 0x28, 0x74, 0x45, 0x58, 0x74, 0x64, 0x61, 0x74, 0x65, 0x3a, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x00, 0x32, 0x30, 0x32, 0x34, 0x2d, 0x31, 0x31, 0x2d, 0x32, 0x32, 0x54, 0x30, 0x38, 0x3a, 0x31, 0x38, 0x3a, 0x35, 0x34, 0x2b, 0x30, 0x30, 0x3a, 0x30, 0x30, 0xa3, 0x99, 0x04, 0x05, 0x00, 0x00, 0x00, 0x11, 0x74, 0x45, 0x58, 0x74, 0x65, 0x78, 0x69, 0x66, 0x3a, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x53, 0x70, 0x61, 0x63, 0x65, 0x00, 0x31, 0x0f, 0x9b, 0x02, 0x49, 0x00, 0x00, 0x00, 0x12, 0x74, 0x45, 0x58, 0x74, 0x65, 0x78, 0x69, 0x66, 0x3a, 0x45, 0x78, 0x69, 0x66, 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x00, 0x33, 0x38, 0xad, 0xb8, 0xbe, 0x23, 0x00, 0x00, 0x00, 0x18, 0x74, 0x45, 0x58, 0x74, 0x65, 0x78, 0x69, 0x66, 0x3a, 0x50, 0x69, 0x78, 0x65, 0x6c, 0x58, 0x44, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x35, 0x31, 0x32, 0xb6, 0x2e, 0xb8, 0xdc, 0x00, 0x00, 0x00, 0x18, 0x74, 0x45, 0x58, 0x74, 0x65, 0x78, 0x69, 0x66, 0x3a, 0x50, 0x69, 0x78, 0x65, 0x6c, 0x59, 0x44, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x35, 0x31, 0x32, 0x2b, 0x21, 0x59, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82}; constexpr uint32_t poji_64_png_len = 738; void format_desfire() { auto& picc = nfc_a.activatedPICC(); if (picc.isMifareDESFire()) { desfire::DESFireFileSystem dfs(nfc_a); if (picc.type == Type::MIFARE_DESFire_Light) { M5_LOGW("DESFire light can NOT format"); return; } else { if (!dfs.formatPICC(desfire::DESFIRE_DEFAULT_KEY)) { M5_LOGE("Failed to formatPICC"); return; } uint32_t free_size{}; if (dfs.selectApplication() && dfs.getFreeMemory(free_size)) { M5_LOGI("free(picc):%u", free_size); } } } } void read_ndef() { // auto& picc = nfc_a.activatedPICC(); // Disable non-test NDEF read path; keep only the current debug logging. bool valid{}; if (!nfc_a.ndefIsValidFormat(valid)) { M5_LOGE("Failed to ndefIsValidFormat"); lcd.fillScreen(TFT_RED); return; } if (!valid) { M5.Log.printf("Data format is NOT NDEF\n"); return; } TLV msg{}; // Read NDEF message TLV if (!nfc_a.ndefRead(msg)) { M5_LOGE("Failed to read"); lcd.fillScreen(TFT_RED); return; } // If it does not exist, a Null TLV is returned if (msg.isMessageTLV()) { lcd.setCursor(0, lcd.fontHeight()); for (auto&& r : msg.records()) { switch (r.tnf()) { case TNF::Wellknown: { const auto payload = r.payloadAsString(); M5.Log.printf("SZ:%3u TNF:%u T:%s [%s]\n", r.payloadSize(), r.tnf(), r.type(), payload.c_str()); lcd.printf("T:%s [%s]\n", r.type(), payload.c_str()); } break; default: M5.Log.printf("SZ:%3u TNF:%u T:%s\n", r.payloadSize(), r.tnf(), r.type()); lcd.printf("T:%s\n", r.type()); if (strcmp(r.type(), "image/png") == 0) { lcd.drawPng(r.payload(), r.payloadSize(), lcd.width() >> 1, lcd.height() >> 1); } break; } } } else { M5.Log.printf("NDEF Message TLV is NOT exists\n"); } } void write_ndef() { auto& picc = nfc_a.activatedPICC(); /* **** MIFARE Ultralight NOTICE *************************** Change the Ultralight series to NDEF format Note: This change cannot be undone ********************************************************* */ if (picc.isMifareUltralight()) { if (!nfc_a.mifareUltralightChangeFormatToNDEF()) { M5_LOGE("Failed to mifareUltralightChangeFormatToNDEF"); lcd.fillScreen(TFT_RED); return; } M5_LOGI("Changed NDEF format"); } /* **** MIFARE DESFire NOTICE ****************************** If the DESFire card is not in NDEF format, the PICC will be formatted This means all existing files and applications will be deleted! For DESFire light, the file structure is changed to comply with the NDEF specification, and the data is overwritten. ********************************************************* */ if (picc.isMifareDESFire() && picc.type != Type::MIFARE_DESFire_Light) { bool valid{}; if (!nfc_a.ndefIsValidFormat(valid)) { lcd.fillScreen(TFT_RED); return; } M5_LOGI("NDEF format valid?:%u", valid); if (!valid) { format_desfire(); if (!nfc_a.ndefPrepareDesfire(picc.userAreaSize())) { M5_LOGE("Failed to prepare NDEF files"); lcd.fillScreen(TFT_RED); return; } M5_LOGI("Prepare for NDEF OK"); } } // Build message and write TLV msg{Tag::Message}; Record r[5] = {}; // Wellknown as default // URI record r[0].setURIPayload("m5stack.com/", URIProtocol::HTTPS); // Text record with language type const char* zh_data = "你好 M5Stack"; r[1].setTextPayload(zh_data, "zh"); const char* en_data = "Hello M5Stack"; r[2].setTextPayload(en_data, "en"); const char* ja_data = "こんにちは M5Stack"; r[3].setTextPayload(ja_data, "ja"); // MIME record Record png{TNF::MIMEMedia}; png.setType("image/png"); png.setPayload(poji_64_png, poji_64_png_len); r[4] = png; uint32_t max_user_size = nfc_a.activatedPICC().userAreaSize() - 1 /* terminator TLV */; for (auto&& rr : r) { msg.push_back(rr); if (msg.required() > max_user_size) { msg.pop_back(); break; } } if (!nfc_a.ndefWrite(msg)) { M5_LOGE("Failed to write"); lcd.fillScreen(TFT_RED); return; } M5.Log.printf("Write NDEF OK!\n"); } } // namespace void setup() { M5.begin(); M5.setTouchButtonHeightByRatio(100); // The screen shall be in landscape mode if (lcd.height() > lcd.width()) { lcd.setRotation(1); } bool unit_ready{}; #if defined(USING_M5DIAL_BUILTIN_WS1850S) // M5Dial builtin WS1850S: small loop antenna; reduce RxGain to 33dB to mitigate // reflection interference (default 48dB causes unstable WUPA on Builtin). { auto cfg = unit.config(); cfg.receiver_gain = m5::unit::mfrc522::ReceiverGain::dB33; unit.config(cfg); } // M5Dial builtin WS1850S on In_I2C (G12/G11, shared with RTC8563) M5_LOGI("Using M5.In_I2C for builtin WS1850S"); unit_ready = Units.add(unit, M5.In_I2C) && Units.begin(); #elif defined(USING_UNIT_NFC) || defined(USING_UNIT_RFID2) // External I2C unit (GROVE port). // NessoN1: Arduino Wire (I2C_NUM_0) cannot be used for GROVE port (used by In_I2C internals). // Use QWIIC (port_a) with Wire. (Requires QWIIC-GROVE conversion cable) // NanoC6: Wire.begin() on GROVE pins conflicts with Ex_I2C on I2C_NUM_0; use M5.Ex_I2C directly. if (M5.getBoard() == m5::board_t::board_M5NanoC6) { M5_LOGI("Using M5.Ex_I2C"); unit_ready = Units.add(unit, M5.Ex_I2C) && Units.begin(); } else { auto pin_num_sda = M5.getPin(m5::pin_name_t::port_a_sda); auto pin_num_scl = M5.getPin(m5::pin_name_t::port_a_scl); M5_LOGI("getPin: SDA:%u SCL:%u", pin_num_sda, pin_num_scl); Wire.end(); Wire.begin(pin_num_sda, pin_num_scl, 400 * 1000U); unit_ready = Units.add(unit, Wire) && Units.begin(); } #elif defined(USING_CAP_CC1101) if (!SPI.bus()) { auto spi_sclk = M5.getPin(m5::pin_name_t::sd_spi_sclk); auto spi_mosi = M5.getPin(m5::pin_name_t::sd_spi_mosi); auto spi_miso = M5.getPin(m5::pin_name_t::sd_spi_miso); M5_LOGI("getPin: %d,%d,%d", spi_sclk, spi_mosi, spi_miso); SPI.begin(spi_sclk, spi_miso, spi_mosi /* SS is shared SD, CC1101, ST25R3916 */); } SPISettings settings = {10000000, MSBFIRST, SPI_MODE1}; unit_ready = Units.add(unit, SPI, settings) && Units.begin(); #endif if (!unit_ready) { M5_LOGE("Failed to begin"); lcd.fillScreen(TFT_RED); while (true) { m5::utility::delay(10000); } } M5_LOGI("M5UnitUnified initialized"); M5_LOGI("%s", Units.debugInfo().c_str()); lcd.setCursor(0, lcd.height() / 2); lcd.printf("Please put the PICC and click/hold BtnA"); M5.Log.printf("Please put the PICC and click/hold BtnA\n"); } void loop() { M5.update(); Units.update(); bool clicked = M5.BtnA.wasClicked(); // For read bool held = M5.BtnA.wasHold(); // For write if (clicked || held) { PICC picc{}; if (nfc_a.detect(picc)) { if (nfc_a.identify(picc) && nfc_a.reactivate(picc)) { M5.Log.printf("PICC:%s %s %u/%u\n", picc.uidAsString().c_str(), picc.typeAsString().c_str(), picc.userAreaSize(), picc.totalSize()); if (picc.supportsNDEF()) { if (clicked) { M5.Speaker.tone(2000, 30); lcd.fillScreen(TFT_BLUE); // nfc_a.dump(); read_ndef(); } else if (held) { M5.Speaker.tone(4000, 30); lcd.fillScreen(TFT_YELLOW); write_ndef(); lcd.fillScreen(0); } M5.Log.printf("Please remove the PICC from the reader\n"); } else { M5.Speaker.tone(1000, 50); M5.Log.printf("Not support the NDEF\n"); } } else { M5_LOGE("Failed to identify/activate %s", picc.uidAsString().c_str()); } nfc_a.deactivate(); lcd.setCursor(0, lcd.height() / 2); lcd.printf("Please put the PICC and click/hold BtnA"); M5.Log.printf("Please put the PICC and click/hold BtnA\n"); } else { M5.Log.printf("PICC NOT exists\n"); } } }