Add SRAM mailbox protocol with all 7 command handlers

Implements the NFC-to-MCU SRAM mailbox protocol in src/ntag5/sram.rs:
- Command parsing with CRC-16/CCITT-FALSE verification
- Response building with 4-byte-aligned SRAM writes
- WRITE_PATTERN: stores 112-byte pattern entries to EEPROM
- GET_STATUS: returns firmware version + library summary
- SET_ACTIVE: changes the active pattern index in EEPROM header
- SYNC_START/SYNC_END: bulk upload with deferred header CRC
- READ_LIBRARY/READ_NEXT: iterative pattern readback
- MailboxState struct for cross-command state tracking

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
michael
2026-03-05 16:16:12 -08:00
parent a6f8c70051
commit 8dfe9117af
2 changed files with 555 additions and 0 deletions

View File

@@ -9,6 +9,8 @@
/// - READ REGISTER: write [BL_AD1, BL_AD0, REGA], read 1 byte /// - READ REGISTER: write [BL_AD1, BL_AD0, REGA], read 1 byte
/// - WRITE REGISTER: write [BL_AD1, BL_AD0, REGA, MASK, REGDATA] /// - WRITE REGISTER: write [BL_AD1, BL_AD0, REGA, MASK, REGDATA]
pub mod sram;
use embedded_hal::i2c::I2c; use embedded_hal::i2c::I2c;
pub const DEFAULT_ADDRESS: u8 = 0x54; pub const DEFAULT_ADDRESS: u8 = 0x54;

553
src/ntag5/sram.rs Normal file
View File

@@ -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<I2C, E>(
ntag: &mut Ntag5Link<I2C>,
) -> Result<Option<pattern::LibraryHeader>, ntag5::Error<E>>
where
I2C: I2c<Error = E>,
{
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<I2C, E>(
ntag: &mut Ntag5Link<I2C>,
hdr: &[u8; pattern::HEADER_SIZE],
delay: &mut impl embedded_hal::delay::DelayNs,
) -> Result<(), ntag5::Error<E>>
where
I2C: I2c<Error = E>,
{
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<I2C, E>(
ntag: &mut Ntag5Link<I2C>,
count: u8,
all_data: &mut [u8],
) -> Result<usize, ntag5::Error<E>>
where
I2C: I2c<Error = E>,
{
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<I2C, E>(
ntag: &mut Ntag5Link<I2C>,
count: u8,
active: u8,
current: u8,
led_mode: u8,
delay: &mut impl embedded_hal::delay::DelayNs,
) -> Result<(), ntag5::Error<E>>
where
I2C: I2c<Error = E>,
{
// 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<I2C, E>(
ntag: &mut Ntag5Link<I2C>,
delay: &mut impl embedded_hal::delay::DelayNs,
) -> Result<(), ntag5::Error<E>>
where
I2C: I2c<Error = E>,
{
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<I2C, E>(
ntag: &mut Ntag5Link<I2C>,
seq: u8,
rsp_buf: &mut [u8],
) -> Result<usize, ntag5::Error<E>>
where
I2C: I2c<Error = E>,
{
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<I2C, E>(
ntag: &mut Ntag5Link<I2C>,
state: &MailboxState,
seq: u8,
payload: &[u8],
rsp_buf: &mut [u8],
delay: &mut impl embedded_hal::delay::DelayNs,
) -> Result<usize, ntag5::Error<E>>
where
I2C: I2c<Error = E>,
{
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<I2C, E>(
ntag: &mut Ntag5Link<I2C>,
seq: u8,
payload: &[u8],
rsp_buf: &mut [u8],
delay: &mut impl embedded_hal::delay::DelayNs,
) -> Result<usize, ntag5::Error<E>>
where
I2C: I2c<Error = E>,
{
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<I2C, E>(
ntag: &mut Ntag5Link<I2C>,
state: &mut MailboxState,
seq: u8,
rsp_buf: &mut [u8],
delay: &mut impl embedded_hal::delay::DelayNs,
) -> Result<usize, ntag5::Error<E>>
where
I2C: I2c<Error = E>,
{
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<I2C, E>(
ntag: &mut Ntag5Link<I2C>,
state: &mut MailboxState,
seq: u8,
rsp_buf: &mut [u8],
) -> Result<usize, ntag5::Error<E>>
where
I2C: I2c<Error = E>,
{
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<I2C, E>(
ntag: &mut Ntag5Link<I2C>,
state: &mut MailboxState,
seq: u8,
rsp_buf: &mut [u8],
) -> Result<usize, ntag5::Error<E>>
where
I2C: I2c<Error = E>,
{
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<I2C, E>(
ntag: &mut Ntag5Link<I2C>,
state: &mut MailboxState,
delay: &mut impl embedded_hal::delay::DelayNs,
) -> Result<bool, ntag5::Error<E>>
where
I2C: I2c<Error = E>,
{
// 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)
}