Fixes isoDEP chain RX R-ACK block number and NFC-B ATTRIB PARAM2

This commit is contained in:
GOB
2026-05-11 18:47:06 +09:00
parent 114f1d07ce
commit 5d995e9bd7
5 changed files with 81 additions and 39 deletions
+1 -1
View File
@@ -186,7 +186,7 @@ enum class Command : uint8_t {
///@name Timeout ///@name Timeout
///@{ ///@{
constexpr uint32_t TIMEOUT_REQ_WUP_B{5}; constexpr uint32_t TIMEOUT_REQ_WUP_B{5};
constexpr uint32_t TIMEOUT_ATTRIB{5}; constexpr uint32_t TIMEOUT_ATTRIB{50};
constexpr uint32_t TIMEOUT_HLTB{5}; constexpr uint32_t TIMEOUT_HLTB{5};
constexpr uint32_t TIMEOUT_DESELECT{5}; constexpr uint32_t TIMEOUT_DESELECT{5};
///@} ///@}
+1 -1
View File
@@ -276,7 +276,7 @@ bool FileSystem::readBinary(std::vector<uint8_t>& out, const uint16_t offset,
// m5::utility::log::dump(cmd.data(), cmd.size(), false); // m5::utility::log::dump(cmd.data(), cmd.size(), false);
std::vector<uint8_t> rx; std::vector<uint8_t> rx;
rx.resize(le + 2 + 16); rx.resize(le * 2 + 64); // generous margin for PICC chain overshoot
uint16_t rx_len = clamp_u16_size(rx.size()); uint16_t rx_len = clamp_u16_size(rx.size());
if (!_isoDEP.transceiveAPDU(rx.data(), rx_len, cmd.data(), static_cast<uint16_t>(cmd.size())) || rx_len < 2) { if (!_isoDEP.transceiveAPDU(rx.data(), rx_len, cmd.data(), static_cast<uint16_t>(cmd.size())) || rx_len < 2) {
+12 -6
View File
@@ -169,7 +169,7 @@ bool IsoDEP::transceiveINF(uint8_t* rx_inf, uint16_t& rx_inf_len, const uint8_t*
const uint32_t timeout_ms = _cfg.fwt_ms; const uint32_t timeout_ms = _cfg.fwt_ms;
// Send I-Block and receive first frame // Send I-Block and receive first frame
// M5_LIB_LOGE("I-Block TX: %u bytes, timeout=%u", tpos, timeout_ms); M5_LIB_LOGV("isoDEP TX I-block[%u] timeout=%u max_rx=%u", tpos, timeout_ms, max_frame_size_rx);
if (!_layer.transceive(rx_buf, rlen, tx_buf, tpos, timeout_ms)) { if (!_layer.transceive(rx_buf, rlen, tx_buf, tpos, timeout_ms)) {
M5_LIB_LOGE("transceive failed, rlen=%u", rlen); M5_LIB_LOGE("transceive failed, rlen=%u", rlen);
if (rlen > 0) { if (rlen > 0) {
@@ -181,6 +181,7 @@ bool IsoDEP::transceiveINF(uint8_t* rx_inf, uint16_t& rx_inf_len, const uint8_t*
PRINT_ERROR(">>>>ERROR 1 %u %02X", rlen, rx_buf[0]); PRINT_ERROR(">>>>ERROR 1 %u %02X", rlen, rx_buf[0]);
return false; return false;
} }
M5_LIB_LOGV("isoDEP RX[%u] PCB=%02X", rlen, rx_buf[0]);
// Parse loop: WTX can replace rx_buf/rlen and we continue parsing without re-sending I-Block. // Parse loop: WTX can replace rx_buf/rlen and we continue parsing without re-sending I-Block.
for (;;) { for (;;) {
@@ -206,7 +207,7 @@ bool IsoDEP::transceiveINF(uint8_t* rx_inf, uint16_t& rx_inf_len, const uint8_t*
return false; return false;
} }
const uint8_t pcb = rx_buf[0]; uint8_t pcb = rx_buf[0];
// --- S-Block (WTX) --- // --- S-Block (WTX) ---
if (is_s_wtx(pcb)) { if (is_s_wtx(pcb)) {
@@ -314,15 +315,19 @@ bool IsoDEP::transceiveINF(uint8_t* rx_inf, uint16_t& rx_inf_len, const uint8_t*
// Response chaining: send R-ACK and receive next I-Block (WTX may appear) // Response chaining: send R-ACK and receive next I-Block (WTX may appear)
while (resp_more) { while (resp_more) {
uint8_t r_ack[2]{}; uint8_t r_ack[2]{};
uint16_t rp = 0; uint16_t rp = 0;
r_ack[rp++] = make_r_ack(i_bn(pcb), _cfg.use_cid); const uint8_t ack_bn = i_bn(pcb) ^ 0x01;
r_ack[rp++] = make_r_ack(ack_bn, _cfg.use_cid);
if (_cfg.use_cid) r_ack[rp++] = (uint8_t)(_cfg.cid & 0x0F); if (_cfg.use_cid) r_ack[rp++] = (uint8_t)(_cfg.cid & 0x0F);
M5_LIB_LOGV("isoDEP chain TX R-ACK bn=%u (collected=%u)", ack_bn, rx_written);
uint16_t rlen2 = sizeof(rx_buf); uint16_t rlen2 = sizeof(rx_buf);
if (!_layer.transceive(rx_buf, rlen2, r_ack, rp, _cfg.fwt_ms)) { if (!_layer.transceive(rx_buf, rlen2, r_ack, rp, _cfg.fwt_ms)) {
M5_LIB_LOGE("isoDEP chain RX failed, rlen=%u", rlen2);
PRINT_ERROR(">>>>ERROR 12"); PRINT_ERROR(">>>>ERROR 12");
return false; return false;
} }
M5_LIB_LOGV("isoDEP chain RX[%u] PCB=%02X", rlen2, rx_buf[0]);
for (;;) { for (;;) {
if (_cfg.rx_crc && rlen2 >= 3) rlen2 -= 2; if (_cfg.rx_crc && rlen2 >= 3) rlen2 -= 2;
@@ -381,8 +386,8 @@ bool IsoDEP::transceiveINF(uint8_t* rx_inf, uint16_t& rx_inf_len, const uint8_t*
const uint16_t inf_len2 = (uint16_t)(rlen2 - idx2); const uint16_t inf_len2 = (uint16_t)(rlen2 - idx2);
if (rx_written + inf_len2 > rx_inf_len_org) { if (rx_written + inf_len2 > rx_inf_len_org) {
PRINT_ERROR("rx_written %u inf_len2 %u rx_inf_len %u", rx_written, inf_len2, PRINT_ERROR("rx_written %u inf_len2 %u rx_inf_len_org %u", rx_written, inf_len2,
rx_inf_len); rx_inf_len_org);
// m5::utility::log::dump(rx_inf, rx_written, false); // m5::utility::log::dump(rx_inf, rx_written, false);
return false; return false;
@@ -391,6 +396,7 @@ bool IsoDEP::transceiveINF(uint8_t* rx_inf, uint16_t& rx_inf_len, const uint8_t*
memcpy(rx_inf + rx_written, rx_buf + idx2, inf_len2); memcpy(rx_inf + rx_written, rx_buf + idx2, inf_len2);
rx_written = (uint16_t)(rx_written + inf_len2); rx_written = (uint16_t)(rx_written + inf_len2);
pcb = pcb2;
resp_more = i_has_more(pcb2); resp_more = i_has_more(pcb2);
break; break;
} }
+51 -21
View File
@@ -43,6 +43,20 @@ constexpr uint8_t required_slots(const Require slots)
{ {
return 1U << m5::stl::to_underlying(slots); return 1U << m5::stl::to_underlying(slots);
} }
// ISO/IEC 14443-4 FSDI table (Frame Size for Device Index, bit[7:4] of ATTRIB PARAM2)
constexpr uint8_t fsdi_for_size(const uint16_t bytes)
{
return (bytes >= 256) ? 8
: (bytes >= 128) ? 7
: (bytes >= 96) ? 6
: (bytes >= 64) ? 5
: (bytes >= 48) ? 4
: (bytes >= 40) ? 3
: (bytes >= 32) ? 2
: (bytes >= 24) ? 1
: 0;
}
} // namespace } // namespace
namespace m5 { namespace m5 {
@@ -69,10 +83,11 @@ bool NFCLayerB::receive(uint8_t* rx, uint16_t& rx_len, const uint32_t timeout_ms
return _impl->receive(rx, rx_len, timeout_ms); return _impl->receive(rx, rx_len, timeout_ms);
} }
bool NFCLayerB::detect(m5::nfc::b::PICC& picc, const uint8_t afi, const uint32_t timeout_ms) bool NFCLayerB::detect(m5::nfc::b::PICC& picc, const uint8_t afi, const uint32_t timeout_ms,
const uint32_t req_timeout_ms)
{ {
std::vector<PICC> piccs{}; std::vector<PICC> piccs{};
if (detect(piccs, afi, 1, timeout_ms)) { if (detect(piccs, afi, 1, timeout_ms, req_timeout_ms)) {
picc = piccs.front(); picc = piccs.front();
return true; return true;
} }
@@ -80,7 +95,7 @@ bool NFCLayerB::detect(m5::nfc::b::PICC& picc, const uint8_t afi, const uint32_t
} }
bool NFCLayerB::detect(std::vector<m5::nfc::b::PICC>& piccs, const uint8_t afi, const uint8_t max_piccs, bool NFCLayerB::detect(std::vector<m5::nfc::b::PICC>& piccs, const uint8_t afi, const uint8_t max_piccs,
const uint32_t timeout_ms) const uint32_t timeout_ms, const uint32_t req_timeout_ms)
{ {
piccs.clear(); piccs.clear();
@@ -88,7 +103,7 @@ bool NFCLayerB::detect(std::vector<m5::nfc::b::PICC>& piccs, const uint8_t afi,
do { do {
uint8_t rx[ATQB_LENGTH]{}; uint8_t rx[ATQB_LENGTH]{};
uint16_t rx_len = sizeof(rx); uint16_t rx_len = sizeof(rx);
if (!request(rx, rx_len, afi, Require::Slot1)) { if (!request(rx, rx_len, afi, Require::Slot1, req_timeout_ms)) {
continue; continue;
} }
@@ -110,7 +125,7 @@ bool NFCLayerB::detect(std::vector<m5::nfc::b::PICC>& piccs, const uint8_t afi,
return !piccs.empty(); return !piccs.empty();
} }
bool NFCLayerB::select(m5::nfc::b::PICC& picc) bool NFCLayerB::select(m5::nfc::b::PICC& picc, const uint32_t timeout_ms)
{ {
// Wakeup for READY // Wakeup for READY
uint16_t len = ATQB_LENGTH; uint16_t len = ATQB_LENGTH;
@@ -118,21 +133,35 @@ bool NFCLayerB::select(m5::nfc::b::PICC& picc)
return false; return false;
} }
M5_LIB_LOGI("ATQB protocol: %02X %02X %02X (FSCI=%u, ISO14443-4=%d, FWI=%u)", picc.protocol[0], picc.protocol[1],
picc.protocol[2], (picc.protocol[1] >> 4) & 0x0F, (picc.protocol[1] & 0x01),
(picc.protocol[2] >> 4) & 0x0F);
// ATTRIB PARAM2 (per ISO/IEC 14443-3 §7.10.2 with b1 = LSB):
// bit[3:0] = FSDI (PCD's max receive frame size index)
// bit[5:4] = PCD->PICC bit rate divisor
// bit[7:6] = PICC->PCD bit rate divisor
// Take min(PCD FIFO, PICC FSCI) so the PICC chains I-blocks within what we can receive.
const uint16_t pcd_rx_cap = maximum_fifo_depth() - 2 /*CRC*/;
const uint8_t pcd_fsdi = fsdi_for_size(pcd_rx_cap);
const uint8_t picc_fsci = picc.maximumFrameLengthBits();
const uint8_t fsdi = std::min<uint8_t>(pcd_fsdi, picc_fsci);
// ATTRIB // ATTRIB
uint8_t cmd[1 + 4 + 1 + 1 + 1 + 1] = {m5::stl::to_underlying(Command::ATTRIB)}; // without option uint8_t cmd[1 + 4 + 1 + 1 + 1 + 1] = {m5::stl::to_underlying(Command::ATTRIB)}; // without option
memcpy(cmd + 1, picc.pupi, 4); memcpy(cmd + 1, picc.pupi, 4);
cmd[5] = 0x00; // PARAM1 cmd[5] = 0x00; // PARAM1
cmd[6] = picc.maximumFrameLengthBits(); // PARAM2 | com speed cmd[6] = (uint8_t)(fsdi & 0x0F); // PARAM2: FSDI(PCD cap) bit[3:0] | rates=0 (106k both)
cmd[7] = picc.protocol[1] & 0x0F; // PARAM 3protocol type cmd[7] = picc.protocol[1] & 0x0F; // PARAM3 protocol type
cmd[8] = 0x00; // PARAM 4 cmd[8] = 0x00; // PARAM 4
std::vector<uint8_t> frame; std::vector<uint8_t> frame;
frame.assign(cmd, cmd + sizeof(cmd)); frame.assign(cmd, cmd + sizeof(cmd));
uint8_t rx[128]{}; uint8_t rx[128]{};
uint16_t rx_len = sizeof(rx); uint16_t rx_len = sizeof(rx);
if (!transceive(rx, rx_len, frame.data(), frame.size(), TIMEOUT_ATTRIB) || !rx_len) { if (!transceive(rx, rx_len, frame.data(), frame.size(), timeout_ms) || !rx_len) {
M5_LIB_LOGE("Failed to select"); M5_LIB_LOGE("Failed to select: rx_len=%u", rx_len);
return false; return false;
} }
@@ -140,14 +169,14 @@ bool NFCLayerB::select(m5::nfc::b::PICC& picc)
return true; return true;
} }
bool NFCLayerB::hlt(const uint8_t pupi[4]) bool NFCLayerB::hlt(const uint8_t pupi[4], const uint32_t timeout_ms)
{ {
if (pupi) { if (pupi) {
uint8_t cmd[1 + 4] = {m5::stl::to_underlying(Command::HLTB)}; uint8_t cmd[1 + 4] = {m5::stl::to_underlying(Command::HLTB)};
memcpy(cmd + 1, pupi, 4); memcpy(cmd + 1, pupi, 4);
uint8_t rx[1]{}; uint8_t rx[1 + 2]{}; // 1 byte payload + 2 byte CRC_B
uint16_t rx_len = sizeof(rx); uint16_t rx_len = sizeof(rx);
if (!transceive(rx, rx_len, cmd, sizeof(cmd), TIMEOUT_HLTB) || rx_len < 1) { if (!transceive(rx, rx_len, cmd, sizeof(cmd), timeout_ms) || rx_len < 1) {
M5_LIB_LOGE("Failed to hlt %02X%02X%02X%02X", cmd[1], cmd[2], cmd[3], cmd[4]); M5_LIB_LOGE("Failed to hlt %02X%02X%02X%02X", cmd[1], cmd[2], cmd[3], cmd[4]);
return false; return false;
} }
@@ -156,17 +185,18 @@ bool NFCLayerB::hlt(const uint8_t pupi[4])
return false; return false;
} }
bool NFCLayerB::deselect(const uint8_t pupi[4], const uint8_t cid) bool NFCLayerB::deselect(const uint8_t pupi[4], const uint8_t cid, const uint32_t timeout_ms)
{ {
uint8_t cmd[2] = {m5::stl::to_underlying(cid != 0xFF ? Command::DESELECT_WITH_CID : Command::DESELECT)}; uint8_t cmd[2] = {m5::stl::to_underlying(cid != 0xFF ? Command::DESELECT_WITH_CID : Command::DESELECT)};
uint16_t cmd_len = 1 + (cid != 0xFF); uint16_t cmd_len = 1 + (cid != 0xFF);
if (cid != 0xFF) { if (cid != 0xFF) {
cmd[1] = cid; cmd[1] = cid;
} }
uint8_t rx[2]{}; uint8_t rx[2 + 2]{}; // payload (1 or 2) + 2 byte CRC_B
uint16_t rx_len = cmd_len; uint16_t rx_len =
cmd_len + 2; // Match actual response size to keep wait_for_FIFO fallback equivalent to the old behavior
if (!transceive(rx, rx_len, cmd, cmd_len, TIMEOUT_DESELECT) || rx_len < cmd_len) { if (!transceive(rx, rx_len, cmd, cmd_len, timeout_ms) || rx_len < cmd_len) {
M5_LIB_LOGE("Failed to deselecte %02X:%02X", cmd[0], cmd[1]); M5_LIB_LOGE("Failed to deselecte %02X:%02X", cmd[0], cmd[1]);
return false; return false;
} }
@@ -188,7 +218,7 @@ bool NFCLayerB::deactivate()
// //
bool NFCLayerB::request_wakeup(uint8_t* atqb, uint16_t& atqb_len, const uint8_t afi, const Require slots, bool NFCLayerB::request_wakeup(uint8_t* atqb, uint16_t& atqb_len, const uint8_t afi, const Require slots,
const bool wakeup) const bool wakeup, const uint32_t timeout_ms)
{ {
if (!atqb || atqb_len < ATQB_LENGTH) { if (!atqb || atqb_len < ATQB_LENGTH) {
return false; return false;
@@ -204,7 +234,7 @@ bool NFCLayerB::request_wakeup(uint8_t* atqb, uint16_t& atqb_len, const uint8_t
atqb_len = 0; atqb_len = 0;
// Ignore non-responsive slots and proceed to the next one. // Ignore non-responsive slots and proceed to the next one.
if (transceive(rx, rx_len, cmd, sizeof(cmd), TIMEOUT_REQ_WUP_B) && rx_len == sizeof(rx) && rx[0] == 0x50) { if (transceive(rx, rx_len, cmd, sizeof(cmd), timeout_ms) && rx_len == sizeof(rx) && rx[0] == 0x50) {
// Occur collision if CRC error // Occur collision if CRC error
const uint16_t crc = crc16.range(rx, ATQB_LENGTH + 1); const uint16_t crc = crc16.range(rx, ATQB_LENGTH + 1);
if (crc == ((uint16_t)rx[13] << 8 | rx[12])) { if (crc == ((uint16_t)rx[13] << 8 | rx[12])) {
@@ -219,7 +249,7 @@ bool NFCLayerB::request_wakeup(uint8_t* atqb, uint16_t& atqb_len, const uint8_t
rx_len = sizeof(rx); rx_len = sizeof(rx);
slot_marker[0] = ((uint8_t)i << 4) | 0x05; slot_marker[0] = ((uint8_t)i << 4) | 0x05;
// Ignore non-responsive slots and proceed to the next one. // Ignore non-responsive slots and proceed to the next one.
if (!transceive(rx, rx_len, slot_marker, sizeof(slot_marker), TIMEOUT_REQ_WUP_B) || rx[0] != 0x50 || if (!transceive(rx, rx_len, slot_marker, sizeof(slot_marker), timeout_ms) || rx[0] != 0x50 ||
rx_len < sizeof(rx)) { rx_len < sizeof(rx)) {
continue; continue;
} }
+16 -10
View File
@@ -28,6 +28,7 @@ namespace m5 {
namespace unit { namespace unit {
class UnitST25R3916; class UnitST25R3916;
class CapST25R3916; class CapST25R3916;
class UnitWS1850S;
} // namespace unit } // namespace unit
namespace nfc { namespace nfc {
@@ -40,6 +41,7 @@ public:
struct Adapter; struct Adapter;
explicit NFCLayerB(m5::unit::UnitST25R3916& u); explicit NFCLayerB(m5::unit::UnitST25R3916& u);
explicit NFCLayerB(m5::unit::CapST25R3916& u); explicit NFCLayerB(m5::unit::CapST25R3916& u);
explicit NFCLayerB(m5::unit::UnitWS1850S& u);
virtual bool transceive(uint8_t* rx, uint16_t& rx_len, const uint8_t* tx, const uint16_t tx_len, virtual bool transceive(uint8_t* rx, uint16_t& rx_len, const uint8_t* tx, const uint16_t tx_len,
const uint32_t timeout_ms) override; const uint32_t timeout_ms) override;
@@ -83,9 +85,10 @@ public:
@post PICC transitions: IDLE -> READY on successful response @post PICC transitions: IDLE -> READY on successful response
*/ */
inline bool request(uint8_t* atqb, uint16_t& atqb_len, const uint8_t afi = 0x00, inline bool request(uint8_t* atqb, uint16_t& atqb_len, const uint8_t afi = 0x00,
const m5::nfc::b::Require slots = m5::nfc::b::Require::Slot1) const m5::nfc::b::Require slots = m5::nfc::b::Require::Slot1,
const uint32_t timeout_ms = m5::nfc::b::TIMEOUT_REQ_WUP_B)
{ {
return request_wakeup(atqb, atqb_len, afi, slots, false); return request_wakeup(atqb, atqb_len, afi, slots, false, timeout_ms);
} }
/*! /*!
@brief Send WUPB to wake a PICC from IDLE or HALT @brief Send WUPB to wake a PICC from IDLE or HALT
@@ -98,9 +101,10 @@ public:
@post PICC transitions: IDLE/HALT -> READY on successful response @post PICC transitions: IDLE/HALT -> READY on successful response
*/ */
inline bool wakeup(uint8_t* atqb, uint16_t& atqb_len, const uint8_t afi = 0x00, inline bool wakeup(uint8_t* atqb, uint16_t& atqb_len, const uint8_t afi = 0x00,
const m5::nfc::b::Require slots = m5::nfc::b::Require::Slot1) const m5::nfc::b::Require slots = m5::nfc::b::Require::Slot1,
const uint32_t timeout_ms = m5::nfc::b::TIMEOUT_REQ_WUP_B)
{ {
return request_wakeup(atqb, atqb_len, afi, slots, true); return request_wakeup(atqb, atqb_len, afi, slots, true, timeout_ms);
} }
/*! /*!
@@ -111,7 +115,8 @@ public:
@return True if detected @return True if detected
@note The detected PICC is typically put into HALT during enumeration to allow discovering others @note The detected PICC is typically put into HALT during enumeration to allow discovering others
*/ */
bool detect(m5::nfc::b::PICC& picc, const uint8_t afi = 0x00, const uint32_t timeout_ms = 100U); bool detect(m5::nfc::b::PICC& picc, const uint8_t afi = 0x00, const uint32_t timeout_ms = 50U,
const uint32_t req_timeout_ms = m5::nfc::b::TIMEOUT_REQ_WUP_B);
/*! /*!
@brief Detect idle PICCs @brief Detect idle PICCs
@param[out] piccs Detected PICC PICCs (one per activated PICC candidate) @param[out] piccs Detected PICC PICCs (one per activated PICC candidate)
@@ -122,11 +127,11 @@ public:
@note The detected PICC is typically put into HALT during enumeration to allow discovering others @note The detected PICC is typically put into HALT during enumeration to allow discovering others
*/ */
bool detect(std::vector<m5::nfc::b::PICC>& piccs, const uint8_t afi = 0x00, const uint8_t max_piccs = 4, bool detect(std::vector<m5::nfc::b::PICC>& piccs, const uint8_t afi = 0x00, const uint8_t max_piccs = 4,
const uint32_t timeout_ms = 1000U); const uint32_t timeout_ms = 1000U, const uint32_t req_timeout_ms = m5::nfc::b::TIMEOUT_REQ_WUP_B);
/*! /*!
*/ */
bool select(m5::nfc::b::PICC& picc); bool select(m5::nfc::b::PICC& picc, const uint32_t timeout_ms = m5::nfc::b::TIMEOUT_ATTRIB);
#if 0 #if 0
bool activate(m5::nfc::b::PICC& picc); bool activate(m5::nfc::b::PICC& picc);
bool reactivate(const m5::nfc::b::PICC& picc); bool reactivate(const m5::nfc::b::PICC& picc);
@@ -140,14 +145,15 @@ public:
///@name For activated PICC ///@name For activated PICC
///@{ ///@{
bool hlt(const uint8_t pupi[4]); bool hlt(const uint8_t pupi[4], const uint32_t timeout_ms = m5::nfc::b::TIMEOUT_HLTB);
bool deselect(const uint8_t pupi[4], const uint8_t cid = 0xFF); bool deselect(const uint8_t pupi[4], const uint8_t cid = 0xFF,
const uint32_t timeout_ms = m5::nfc::b::TIMEOUT_DESELECT);
bool deactivate(); bool deactivate();
///@} ///@}
protected: protected:
bool request_wakeup(uint8_t* atqb, uint16_t& atqb_len, const uint8_t afi, const m5::nfc::b::Require slots, bool request_wakeup(uint8_t* atqb, uint16_t& atqb_len, const uint8_t afi, const m5::nfc::b::Require slots,
const bool wakeup); const bool wakeup, const uint32_t timeout_ms = m5::nfc::b::TIMEOUT_REQ_WUP_B);
virtual bool read(uint8_t* rx, uint16_t& rx_len, const uint16_t saddr) override virtual bool read(uint8_t* rx, uint16_t& rx_len, const uint16_t saddr) override
{ {