Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ Releases up through `v0.4.0` were workspace-wide and tagged `vX.Y.Z`. From `0.4.

## [tinyboot-protocol 0.5.1] - 2026-10-20

### Added
### Changed

- `InfoData`, `EraseData` and `VerifyData` gain `from_bytes` and `to_bytes`, so a host reads and writes them from plain byte slices without going through the `Data` union. `EraseData` and `VerifyData` gain `SIZE` like `InfoData`.
- The crate builds for little-endian targets only. The typed payloads (`InfoData`, `EraseData`, `VerifyData`) are padding-free `repr(C)` structs whose in-memory bytes are their wire bytes, so a host may read them straight from a received buffer. Every tinyboot device and host so far is little-endian.

## [tinyboot-core 0.6.1] - 2026-10-20

Expand Down
157 changes: 0 additions & 157 deletions lib/protocol/src/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,38 +40,6 @@ pub struct InfoData {
impl InfoData {
/// Encoded size in bytes.
pub const SIZE: u16 = core::mem::size_of::<Self>() as u16;

/// Decode from the start of `bytes` (little-endian fields in declaration
/// order); `None` when `bytes` is shorter than [`SIZE`](Self::SIZE).
/// Bytes past `SIZE` are ignored, so a longer reply from a newer device
/// still decodes.
pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
let b = bytes.get(..Self::SIZE as usize)?;
Some(Self {
capacity: u32::from_le_bytes([b[0], b[1], b[2], b[3]]),
erase_size: u16::from_le_bytes([b[4], b[5]]),
boot_version: u16::from_le_bytes([b[6], b[7]]),
app_version: u16::from_le_bytes([b[8], b[9]]),
mode: b[10],
boot_baud: b[11],
compat: u32::from_le_bytes([b[12], b[13], b[14], b[15]]),
max_payload: u16::from_le_bytes([b[16], b[17]]),
})
}

/// Encode as little-endian fields in declaration order.
pub fn to_bytes(&self) -> [u8; Self::SIZE as usize] {
let mut b = [0; Self::SIZE as usize];
b[0..4].copy_from_slice(&{ self.capacity }.to_le_bytes());
b[4..6].copy_from_slice(&{ self.erase_size }.to_le_bytes());
b[6..8].copy_from_slice(&{ self.boot_version }.to_le_bytes());
b[8..10].copy_from_slice(&{ self.app_version }.to_le_bytes());
b[10] = self.mode;
b[11] = self.boot_baud;
b[12..16].copy_from_slice(&{ self.compat }.to_le_bytes());
b[16..18].copy_from_slice(&{ self.max_payload }.to_le_bytes());
b
}
}

/// Typed Erase request data.
Expand All @@ -82,27 +50,6 @@ pub struct EraseData {
pub byte_count: u16,
}

impl EraseData {
/// Encoded size in bytes.
pub const SIZE: u16 = core::mem::size_of::<Self>() as u16;

/// Decode from the start of `bytes` (little-endian fields in declaration
/// order); `None` when `bytes` is shorter than [`SIZE`](Self::SIZE).
/// Bytes past `SIZE` are ignored, so a longer reply from a newer device
/// still decodes.
pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
let b = bytes.get(..Self::SIZE as usize)?;
Some(Self {
byte_count: u16::from_le_bytes([b[0], b[1]]),
})
}

/// Encode as little-endian fields in declaration order.
pub fn to_bytes(&self) -> [u8; Self::SIZE as usize] {
self.byte_count.to_le_bytes()
}
}

/// Typed Verify response data.
#[repr(C)]
#[derive(Clone, Copy)]
Expand All @@ -111,27 +58,6 @@ pub struct VerifyData {
pub crc: u16,
}

impl VerifyData {
/// Encoded size in bytes.
pub const SIZE: u16 = core::mem::size_of::<Self>() as u16;

/// Decode from the start of `bytes` (little-endian fields in declaration
/// order); `None` when `bytes` is shorter than [`SIZE`](Self::SIZE).
/// Bytes past `SIZE` are ignored, so a longer reply from a newer device
/// still decodes.
pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
let b = bytes.get(..Self::SIZE as usize)?;
Some(Self {
crc: u16::from_le_bytes([b[0], b[1]]),
})
}

/// Encode as little-endian fields in declaration order.
pub fn to_bytes(&self) -> [u8; Self::SIZE as usize] {
self.crc.to_le_bytes()
}
}

/// Per-command flags occupying addr byte 3.
///
/// Each command can define its own bitflags type as a union variant.
Expand Down Expand Up @@ -522,89 +448,6 @@ mod tests {
assert_eq!(InfoData::SIZE, 18);
}

fn union_bytes<const L: usize>(d: &Data) -> [u8; L] {
let mut b = [0; L];
b.copy_from_slice(unsafe { &d.raw[..L] });
b
}

fn info() -> InfoData {
InfoData {
capacity: 0x0403_0201,
erase_size: 0x0605,
boot_version: 0x0807,
app_version: 0x0A09,
mode: 0x0B,
boot_baud: 0x0C,
compat: 0x100F_0E0D,
max_payload: 0x1211,
}
}

fn assert_info_eq(a: &InfoData, b: &InfoData) {
assert_eq!({ a.capacity }, { b.capacity });
assert_eq!({ a.erase_size }, { b.erase_size });
assert_eq!({ a.boot_version }, { b.boot_version });
assert_eq!({ a.app_version }, { b.app_version });
assert_eq!(a.mode, b.mode);
assert_eq!(a.boot_baud, b.boot_baud);
assert_eq!({ a.compat }, { b.compat });
assert_eq!({ a.max_payload }, { b.max_payload });
}

#[test]
fn info_bytes_match_union() {
let d = Data { info: info() };
let raw = union_bytes::<{ InfoData::SIZE as usize }>(&d);
assert_info_eq(&InfoData::from_bytes(&raw).unwrap(), &info());
assert_eq!(info().to_bytes(), raw);
}

#[test]
fn info_from_bytes_length() {
let raw = info().to_bytes();
assert!(InfoData::from_bytes(&raw[..raw.len() - 1]).is_none());
let mut long = [0xEE; InfoData::SIZE as usize + 3];
long[..raw.len()].copy_from_slice(&raw);
assert_info_eq(&InfoData::from_bytes(&long).unwrap(), &info());
}

#[test]
fn erase_bytes_match_union() {
let d = Data {
erase: EraseData { byte_count: 0x0201 },
};
let raw = union_bytes::<{ EraseData::SIZE as usize }>(&d);
assert_eq!(EraseData::from_bytes(&raw).unwrap().byte_count, 0x0201);
assert_eq!(EraseData { byte_count: 0x0201 }.to_bytes(), raw);
}

#[test]
fn erase_from_bytes_length() {
assert_eq!(EraseData::SIZE, 2);
assert!(EraseData::from_bytes(&[0x01]).is_none());
let long = [0x01, 0x02, 0xEE, 0xEE, 0xEE];
assert_eq!(EraseData::from_bytes(&long).unwrap().byte_count, 0x0201);
}

#[test]
fn verify_bytes_match_union() {
let d = Data {
verify: VerifyData { crc: 0x0201 },
};
let raw = union_bytes::<{ VerifyData::SIZE as usize }>(&d);
assert_eq!(VerifyData::from_bytes(&raw).unwrap().crc, 0x0201);
assert_eq!(VerifyData { crc: 0x0201 }.to_bytes(), raw);
}

#[test]
fn verify_from_bytes_length() {
assert_eq!(VerifyData::SIZE, 2);
assert!(VerifyData::from_bytes(&[0x01]).is_none());
let long = [0x01, 0x02, 0xEE, 0xEE, 0xEE];
assert_eq!(VerifyData::from_bytes(&long).unwrap().crc, 0x0201);
}

#[test]
fn large_payload_round_trip() {
let data = [0x3C; 240];
Expand Down
5 changes: 5 additions & 0 deletions lib/protocol/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
//! Defines the frame format, commands, status codes, and CRC used for
//! host-device communication over UART / RS-485.

// Typed payloads are padding-free structs whose in-memory bytes are the wire
// bytes; a host may read them straight from a received buffer.
#[cfg(target_endian = "big")]
compile_error!("tinyboot-protocol supports little-endian targets only");

/// CRC16-CCITT implementation.
pub mod crc;
/// Frame encoding, decoding, and typed payload access.
Expand Down
Loading