diff --git a/src/ntag5/mod.rs b/src/ntag5/mod.rs index 0de14d8..e08c096 100644 --- a/src/ntag5/mod.rs +++ b/src/ntag5/mod.rs @@ -9,6 +9,8 @@ /// - READ REGISTER: write [BL_AD1, BL_AD0, REGA], read 1 byte /// - WRITE REGISTER: write [BL_AD1, BL_AD0, REGA, MASK, REGDATA] +pub mod sram; + use embedded_hal::i2c::I2c; pub const DEFAULT_ADDRESS: u8 = 0x54; diff --git a/src/ntag5/sram.rs b/src/ntag5/sram.rs new file mode 100644 index 0000000..496801c --- /dev/null +++ b/src/ntag5/sram.rs @@ -0,0 +1,553 @@ +//! SRAM mailbox command/response protocol for NFC<->MCU communication. +//! +//! The phone writes a command packet to NTAG5Link SRAM via NFC. +//! The MCU reads it over I2C, processes the command, and writes +//! a response packet back to SRAM for the phone to read. + +use crate::ntag5::{self, Ntag5Link, SRAM_SIZE}; +use crate::pattern; +use embedded_hal::i2c::I2c; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +/// Firmware version reported in GET_STATUS response. +pub const FIRMWARE_VERSION: u8 = 0x01; + +// Command IDs (phone -> MCU) +pub const CMD_WRITE_PATTERN: u8 = 0x01; +pub const CMD_GET_STATUS: u8 = 0x02; +pub const CMD_SET_ACTIVE: u8 = 0x03; +pub const CMD_SYNC_START: u8 = 0x05; +pub const CMD_SYNC_END: u8 = 0x06; +pub const CMD_READ_LIBRARY: u8 = 0x07; +pub const CMD_READ_NEXT: u8 = 0x08; + +// Response marker +pub const RESPONSE_MARKER: u8 = 0xFF; + +// Status codes +pub const STATUS_OK: u8 = 0x00; +pub const STATUS_BAD_CRC: u8 = 0x01; +pub const STATUS_BAD_CMD: u8 = 0x02; +pub const STATUS_EEPROM_FAIL: u8 = 0x03; +pub const STATUS_INVALID_INDEX: u8 = 0x04; + +/// Command header size (cmd + seq + payload_len_u16 + crc_u16). +pub const CMD_HEADER_SIZE: usize = 6; + +/// Response header size (marker + seq + status + payload_len + crc_u16). +pub const RSP_HEADER_SIZE: usize = 6; + +// --------------------------------------------------------------------------- +// Mailbox state +// --------------------------------------------------------------------------- + +/// Persistent state for the mailbox protocol across commands. +pub struct MailboxState { + /// True while a SYNC_START..SYNC_END sequence is in progress. + pub syncing: bool, + /// Pattern count supplied by SYNC_START. + pub sync_count: u8, + /// Current (mA setting) supplied by SYNC_START. + pub sync_current: u8, + /// LED mode supplied by SYNC_START. + pub sync_mode: u8, + /// Next pattern index for READ_NEXT. + pub read_index: u8, + /// Total patterns available for READ_NEXT iteration. + pub read_count: u8, +} + +impl MailboxState { + pub fn new() -> Self { + Self { + syncing: false, + sync_count: 0, + sync_current: 0, + sync_mode: 0, + read_index: 0, + read_count: 0, + } + } +} + +// --------------------------------------------------------------------------- +// CRC helpers +// --------------------------------------------------------------------------- + +/// Compute CRC-16/CCITT-FALSE over the given data, starting from an +/// initial CRC value. This allows continuing a CRC computation across +/// multiple buffers. +fn crc16_continue(init: u16, data: &[u8]) -> u16 { + let mut crc = init; + for &b in data { + crc ^= (b as u16) << 8; + for _ in 0..8 { + if crc & 0x8000 != 0 { + crc = (crc << 1) ^ 0x1021; + } else { + crc <<= 1; + } + crc &= 0xFFFF; + } + } + crc +} + +// --------------------------------------------------------------------------- +// Protocol parse / build +// --------------------------------------------------------------------------- + +/// Parse the 6-byte command header. Returns (cmd, seq, payload_len) or None +/// if the buffer is too short or the command ID is out of range. +fn parse_header(buf: &[u8]) -> Option<(u8, u8, u16)> { + if buf.len() < CMD_HEADER_SIZE { + return None; + } + let cmd = buf[0]; + if cmd == 0 || cmd >= 0x80 { + return None; + } + let seq = buf[1]; + let payload_len = (buf[2] as u16) | ((buf[3] as u16) << 8); // LE + Some((cmd, seq, payload_len)) +} + +/// Verify CRC over the command packet. +/// CRC is computed over bytes 0-3 (header minus CRC) + payload, then +/// compared to the big-endian CRC in bytes 4-5. +fn verify_crc(buf: &[u8], payload_len: u16) -> bool { + let total = CMD_HEADER_SIZE + payload_len as usize; + if buf.len() < total { + return false; + } + let expected = ((buf[4] as u16) << 8) | buf[5] as u16; + let crc = crc16_continue(0xFFFF, &buf[0..4]); + let crc = crc16_continue(crc, &buf[CMD_HEADER_SIZE..total]); + crc == expected +} + +/// Build a response packet into `buf`. Returns the total number of bytes +/// written (padded to a multiple of 4 for SRAM block alignment). +fn build_response(buf: &mut [u8], seq: u8, status: u8, payload: &[u8]) -> usize { + let plen = payload.len(); + buf[0] = RESPONSE_MARKER; + buf[1] = seq; + buf[2] = status; + buf[3] = plen as u8; + // CRC placeholder at [4..6], computed below + // Copy payload + buf[RSP_HEADER_SIZE..RSP_HEADER_SIZE + plen].copy_from_slice(payload); + // Compute CRC over bytes 0-3 + payload + let crc = crc16_continue(0xFFFF, &buf[0..4]); + let crc = crc16_continue(crc, payload); + buf[4] = (crc >> 8) as u8; + buf[5] = crc as u8; + // Pad total length to multiple of 4 + let raw_len = RSP_HEADER_SIZE + plen; + let padded = (raw_len + 3) & !3; + // Zero padding bytes + for b in buf[raw_len..padded].iter_mut() { + *b = 0; + } + padded +} + +// --------------------------------------------------------------------------- +// EEPROM helpers +// --------------------------------------------------------------------------- + +/// Block address for pattern N in the EEPROM library. +fn pattern_block(index: u8) -> u16 { + pattern::LIBRARY_BASE_BLOCK + 4 + (index as u16) * 28 +} + +/// Read the XBLK library header from EEPROM. +/// Returns None if magic/version don't match. +fn read_library_header( + ntag: &mut Ntag5Link, +) -> Result, ntag5::Error> +where + I2C: I2c, +{ + let mut hdr_buf = [0u8; pattern::HEADER_SIZE]; + ntag.read_memory(pattern::LIBRARY_BASE_BLOCK, &mut hdr_buf)?; + Ok(pattern::parse_header(&hdr_buf)) +} + +/// Write the 16-byte header to EEPROM (4 blocks starting at LIBRARY_BASE_BLOCK). +fn write_header_to_eeprom( + ntag: &mut Ntag5Link, + hdr: &[u8; pattern::HEADER_SIZE], + delay: &mut impl embedded_hal::delay::DelayNs, +) -> Result<(), ntag5::Error> +where + I2C: I2c, +{ + for i in 0..4u16 { + let mut chunk = [0u8; 4]; + let off = (i as usize) * 4; + chunk.copy_from_slice(&hdr[off..off + 4]); + ntag.write_verify_block(pattern::LIBRARY_BASE_BLOCK + i, &chunk, delay)?; + } + Ok(()) +} + +/// Read all pattern data from EEPROM for the given count (for CRC computation). +/// Returns the number of bytes read into `all_data`. +fn read_all_pattern_data( + ntag: &mut Ntag5Link, + count: u8, + all_data: &mut [u8], +) -> Result> +where + I2C: I2c, +{ + let total = count as usize * pattern::PATTERN_ENTRY_SIZE; + // Read in chunks — read_memory can handle arbitrary lengths + // but we read per-pattern for clarity + for i in 0..count as usize { + let block = pattern_block(i as u8); + let off = i * pattern::PATTERN_ENTRY_SIZE; + ntag.read_memory(block, &mut all_data[off..off + pattern::PATTERN_ENTRY_SIZE])?; + } + Ok(total) +} + +/// Recalculate the header CRC based on current EEPROM pattern data, +/// then write the updated header back. +fn recalculate_header_crc( + ntag: &mut Ntag5Link, + count: u8, + active: u8, + current: u8, + led_mode: u8, + delay: &mut impl embedded_hal::delay::DelayNs, +) -> Result<(), ntag5::Error> +where + I2C: I2c, +{ + // Read all pattern data for CRC + let mut all_data = [0u8; pattern::MAX_PATTERNS * pattern::PATTERN_ENTRY_SIZE]; + let data_len = read_all_pattern_data(ntag, count, &mut all_data)?; + + let mut hdr = [0u8; pattern::HEADER_SIZE]; + pattern::serialize_header(count, active, current, led_mode, &all_data[..data_len], &mut hdr); + write_header_to_eeprom(ntag, &hdr, delay) +} + +/// After a standalone WRITE_PATTERN (not during sync), re-read the header +/// and recalculate its CRC to account for the new pattern data. +fn update_header_after_write( + ntag: &mut Ntag5Link, + delay: &mut impl embedded_hal::delay::DelayNs, +) -> Result<(), ntag5::Error> +where + I2C: I2c, +{ + let hdr = match read_library_header(ntag)? { + Some(h) => h, + None => return Ok(()), // no valid header, nothing to update + }; + recalculate_header_crc( + ntag, + hdr.pattern_count, + hdr.active_index, + hdr.current, + hdr.led_mode, + delay, + ) +} + +// --------------------------------------------------------------------------- +// Command handlers +// --------------------------------------------------------------------------- + +/// GET_STATUS (0x02): Return firmware version + library summary. +fn handle_get_status( + ntag: &mut Ntag5Link, + seq: u8, + rsp_buf: &mut [u8], +) -> Result> +where + I2C: I2c, +{ + let mut payload = [0u8; 5]; + payload[0] = FIRMWARE_VERSION; + match read_library_header(ntag)? { + Some(h) => { + payload[1] = h.pattern_count; + payload[2] = h.active_index; + payload[3] = h.current; + payload[4] = h.led_mode; + } + None => { + // No valid header — return zeros + } + } + Ok(build_response(rsp_buf, seq, STATUS_OK, &payload)) +} + +/// WRITE_PATTERN (0x01): Write a 112-byte pattern entry to EEPROM. +fn handle_write_pattern( + ntag: &mut Ntag5Link, + state: &MailboxState, + seq: u8, + payload: &[u8], + rsp_buf: &mut [u8], + delay: &mut impl embedded_hal::delay::DelayNs, +) -> Result> +where + I2C: I2c, +{ + if payload.len() < 1 + pattern::PATTERN_ENTRY_SIZE { + return Ok(build_response(rsp_buf, seq, STATUS_INVALID_INDEX, &[])); + } + let index = payload[0]; + if index as usize >= pattern::MAX_PATTERNS { + return Ok(build_response(rsp_buf, seq, STATUS_INVALID_INDEX, &[])); + } + + // Write 112 bytes = 28 blocks + let base_block = pattern_block(index); + let pattern_data = &payload[1..1 + pattern::PATTERN_ENTRY_SIZE]; + for i in 0..28u16 { + let off = (i as usize) * 4; + let mut chunk = [0u8; 4]; + chunk.copy_from_slice(&pattern_data[off..off + 4]); + match ntag.write_verify_block(base_block + i, &chunk, delay) { + Ok(()) => {} + Err(ntag5::Error::VerifyFailed) => { + return Ok(build_response(rsp_buf, seq, STATUS_EEPROM_FAIL, &[])); + } + Err(e) => return Err(e), + } + } + + // If not syncing, update header CRC to reflect changed pattern data + if !state.syncing { + match update_header_after_write(ntag, delay) { + Ok(()) => {} + Err(ntag5::Error::VerifyFailed) => { + return Ok(build_response(rsp_buf, seq, STATUS_EEPROM_FAIL, &[])); + } + Err(e) => return Err(e), + } + } + + Ok(build_response(rsp_buf, seq, STATUS_OK, &[])) +} + +/// SET_ACTIVE (0x03): Change the active pattern index in the EEPROM header. +fn handle_set_active( + ntag: &mut Ntag5Link, + seq: u8, + payload: &[u8], + rsp_buf: &mut [u8], + delay: &mut impl embedded_hal::delay::DelayNs, +) -> Result> +where + I2C: I2c, +{ + if payload.is_empty() { + return Ok(build_response(rsp_buf, seq, STATUS_INVALID_INDEX, &[])); + } + let index = payload[0]; + + let hdr = match read_library_header(ntag)? { + Some(h) => h, + None => return Ok(build_response(rsp_buf, seq, STATUS_EEPROM_FAIL, &[])), + }; + + if index >= hdr.pattern_count { + return Ok(build_response(rsp_buf, seq, STATUS_INVALID_INDEX, &[])); + } + + // Recalculate header with new active index + match recalculate_header_crc(ntag, hdr.pattern_count, index, hdr.current, hdr.led_mode, delay) + { + Ok(()) => {} + Err(ntag5::Error::VerifyFailed) => { + return Ok(build_response(rsp_buf, seq, STATUS_EEPROM_FAIL, &[])); + } + Err(e) => return Err(e), + } + + Ok(build_response(rsp_buf, seq, STATUS_OK, &[])) +} + +/// SYNC_START (0x05): Begin a bulk pattern upload session. +fn handle_sync_start( + state: &mut MailboxState, + seq: u8, + payload: &[u8], + rsp_buf: &mut [u8], +) -> usize { + if payload.len() < 3 { + return build_response(rsp_buf, seq, STATUS_BAD_CMD, &[]); + } + let count = payload[0]; + if count == 0 || count as usize > pattern::MAX_PATTERNS { + return build_response(rsp_buf, seq, STATUS_INVALID_INDEX, &[]); + } + state.syncing = true; + state.sync_count = count; + state.sync_current = payload[1]; + state.sync_mode = payload[2]; + build_response(rsp_buf, seq, STATUS_OK, &[]) +} + +/// SYNC_END (0x06): Finalize bulk upload — write the XBLK header with CRC. +fn handle_sync_end( + ntag: &mut Ntag5Link, + state: &mut MailboxState, + seq: u8, + rsp_buf: &mut [u8], + delay: &mut impl embedded_hal::delay::DelayNs, +) -> Result> +where + I2C: I2c, +{ + if !state.syncing { + return Ok(build_response(rsp_buf, seq, STATUS_BAD_CMD, &[])); + } + + // Active index defaults to 0 + match recalculate_header_crc( + ntag, + state.sync_count, + 0, // active_index = 0 + state.sync_current, + state.sync_mode, + delay, + ) { + Ok(()) => {} + Err(ntag5::Error::VerifyFailed) => { + state.syncing = false; + return Ok(build_response(rsp_buf, seq, STATUS_EEPROM_FAIL, &[])); + } + Err(e) => { + state.syncing = false; + return Err(e); + } + } + + state.syncing = false; + Ok(build_response(rsp_buf, seq, STATUS_OK, &[])) +} + +/// READ_LIBRARY (0x07): Return library summary and prepare for READ_NEXT. +fn handle_read_library( + ntag: &mut Ntag5Link, + state: &mut MailboxState, + seq: u8, + rsp_buf: &mut [u8], +) -> Result> +where + I2C: I2c, +{ + let mut payload = [0u8; 4]; + match read_library_header(ntag)? { + Some(h) => { + payload[0] = h.pattern_count; + payload[1] = h.active_index; + payload[2] = h.current; + payload[3] = h.led_mode; + state.read_index = 0; + state.read_count = h.pattern_count; + } + None => { + state.read_index = 0; + state.read_count = 0; + } + } + Ok(build_response(rsp_buf, seq, STATUS_OK, &payload)) +} + +/// READ_NEXT (0x08): Return the next pattern entry (112 bytes). +fn handle_read_next( + ntag: &mut Ntag5Link, + state: &mut MailboxState, + seq: u8, + rsp_buf: &mut [u8], +) -> Result> +where + I2C: I2c, +{ + if state.read_index >= state.read_count { + return Ok(build_response(rsp_buf, seq, STATUS_INVALID_INDEX, &[])); + } + + let mut entry = [0u8; pattern::PATTERN_ENTRY_SIZE]; + let block = pattern_block(state.read_index); + ntag.read_memory(block, &mut entry)?; + state.read_index += 1; + Ok(build_response(rsp_buf, seq, STATUS_OK, &entry)) +} + +// --------------------------------------------------------------------------- +// Main entry point +// --------------------------------------------------------------------------- + +/// Read SRAM, dispatch the command, and write the response back. +/// +/// Returns `Ok(true)` if a command was processed, `Ok(false)` if no valid +/// command was found in SRAM (e.g., empty buffer, bad header, bad CRC). +/// +/// I2C errors propagate as `Err`. EEPROM verify failures are reported via +/// a STATUS_EEPROM_FAIL response (not as Err). +pub fn process_command( + ntag: &mut Ntag5Link, + state: &mut MailboxState, + delay: &mut impl embedded_hal::delay::DelayNs, +) -> Result> +where + I2C: I2c, +{ + // Read full SRAM + let mut sram = [0u8; SRAM_SIZE]; + ntag.read_sram(&mut sram)?; + + // Parse header + let (cmd, seq, payload_len) = match parse_header(&sram) { + Some(h) => h, + None => return Ok(false), + }; + + // Bounds check + let total = CMD_HEADER_SIZE + payload_len as usize; + if total > SRAM_SIZE { + return Ok(false); + } + + // Verify CRC + if !verify_crc(&sram, payload_len) { + // Write BAD_CRC response + let mut rsp = [0u8; SRAM_SIZE]; + let len = build_response(&mut rsp, seq, STATUS_BAD_CRC, &[]); + ntag.write_sram_blocks(0, &rsp[..len])?; + return Ok(true); + } + + let payload = &sram[CMD_HEADER_SIZE..total]; + + // Dispatch + let mut rsp = [0u8; SRAM_SIZE]; + let rsp_len = match cmd { + CMD_GET_STATUS => handle_get_status(ntag, seq, &mut rsp)?, + CMD_WRITE_PATTERN => { + handle_write_pattern(ntag, state, seq, payload, &mut rsp, delay)? + } + CMD_SET_ACTIVE => handle_set_active(ntag, seq, payload, &mut rsp, delay)?, + CMD_SYNC_START => handle_sync_start(state, seq, payload, &mut rsp), + CMD_SYNC_END => handle_sync_end(ntag, state, seq, &mut rsp, delay)?, + CMD_READ_LIBRARY => handle_read_library(ntag, state, seq, &mut rsp)?, + CMD_READ_NEXT => handle_read_next(ntag, state, seq, &mut rsp)?, + _ => build_response(&mut rsp, seq, STATUS_BAD_CMD, &[]), + }; + + ntag.write_sram_blocks(0, &rsp[..rsp_len])?; + Ok(true) +}