diff --git a/CHANGELOG.md b/CHANGELOG.md index 019ccef..3e5c346 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/protocol/src/frame.rs b/lib/protocol/src/frame.rs index 069b195..6ed0a89 100644 --- a/lib/protocol/src/frame.rs +++ b/lib/protocol/src/frame.rs @@ -40,38 +40,6 @@ pub struct InfoData { impl InfoData { /// Encoded size in bytes. pub const SIZE: u16 = core::mem::size_of::() 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 { - 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. @@ -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::() 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 { - 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)] @@ -111,27 +58,6 @@ pub struct VerifyData { pub crc: u16, } -impl VerifyData { - /// Encoded size in bytes. - pub const SIZE: u16 = core::mem::size_of::() 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 { - 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. @@ -522,89 +448,6 @@ mod tests { assert_eq!(InfoData::SIZE, 18); } - fn union_bytes(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]; diff --git a/lib/protocol/src/lib.rs b/lib/protocol/src/lib.rs index 0dc8053..c7a1a63 100644 --- a/lib/protocol/src/lib.rs +++ b/lib/protocol/src/lib.rs @@ -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.