Skip to main content

tinyboot_protocol/
frame.rs

1use core::mem::MaybeUninit;
2
3use crate::crc::{CRC_INIT, crc16};
4use crate::sync::Sync;
5use crate::{Cmd, ReadError, Status};
6
7/// Maximum data payload size per frame.
8pub const MAX_PAYLOAD: usize = 64;
9
10/// Typed Info response data.
11///
12/// Packed to keep alignment ≤ 2 so the `Data` union doesn't force padding
13/// inside `Frame` (data starts at offset 10, not 4-byte aligned).
14#[repr(C, packed)]
15#[derive(Clone, Copy)]
16pub struct InfoData {
17    /// App region capacity in bytes.
18    pub capacity: u32,
19    /// Erase page size in bytes.
20    pub erase_size: u16,
21    /// Boot version (packed 5.5.6, `0xFFFF` = none).
22    pub boot_version: u16,
23    /// App version (packed 5.5.6, `0xFFFF` = none).
24    pub app_version: u16,
25    /// 0 = bootloader, 1 = app.
26    pub mode: u16,
27}
28
29/// Typed Erase request data.
30#[repr(C)]
31#[derive(Clone, Copy)]
32pub struct EraseData {
33    /// Number of bytes to erase (must be aligned to erase size).
34    pub byte_count: u16,
35}
36
37/// Typed Verify response data.
38#[repr(C)]
39#[derive(Clone, Copy)]
40pub struct VerifyData {
41    /// CRC16 of the app region.
42    pub crc: u16,
43}
44
45/// Union-typed data payload.
46///
47/// Provides zero-cost typed access to frame data. Reading fields is unsafe
48/// because the caller must know which variant is active.
49#[repr(C)]
50pub union Data {
51    /// Raw byte access.
52    pub raw: [u8; MAX_PAYLOAD],
53    /// Info response fields.
54    pub info: InfoData,
55    /// Erase request fields.
56    pub erase: EraseData,
57    /// Verify response fields.
58    pub verify: VerifyData,
59}
60
61/// Wire frame: SYNC(2) + CMD(1) + STATUS(1) + ADDR(4) + LEN(2) + DATA(len) + CRC(2)
62///
63/// Used for both requests and responses. For requests, `status` is
64/// [`Status::Request`]. For responses, `cmd` and `addr` echo the request.
65///
66/// Single instance, reused each iteration of the protocol loop.
67#[repr(C)]
68pub struct Frame {
69    sync: Sync,
70    /// Command code.
71    pub cmd: Cmd,
72    /// Response status (always [`Status::Request`] for requests).
73    pub status: Status,
74    /// Flash address (for Write/Erase) or mode selector (for Reset).
75    pub addr: u32,
76    /// Data payload length in bytes (0..64).
77    pub len: u16,
78    /// Payload data (union-typed).
79    pub data: Data,
80    /// CRC16 over the frame body (little-endian).
81    pub crc: [u8; 2],
82}
83
84impl Default for Frame {
85    /// Create a default frame. Data buffer is uninitialized — `read()` or
86    /// caller writes populate it before use.
87    fn default() -> Self {
88        let frame: MaybeUninit<Self> = MaybeUninit::uninit();
89        let mut frame = unsafe { frame.assume_init() };
90        frame.sync = Sync::default();
91        frame.cmd = Cmd::Info;
92        frame.status = Status::Request;
93        frame.addr = 0;
94        frame.len = 0;
95        frame.crc = [0; 2];
96        frame
97    }
98}
99
100impl Frame {
101    fn as_bytes(&self, offset: usize, len: usize) -> &[u8] {
102        debug_assert!(offset + len <= core::mem::size_of::<Self>());
103        unsafe {
104            let ptr = (self as *const Self as *const u8).add(offset);
105            core::slice::from_raw_parts(ptr, len)
106        }
107    }
108
109    fn as_bytes_mut(&mut self, offset: usize, len: usize) -> &mut [u8] {
110        debug_assert!(offset + len <= core::mem::size_of::<Self>());
111        unsafe {
112            let ptr = (self as *mut Self as *mut u8).add(offset);
113            core::slice::from_raw_parts_mut(ptr, len)
114        }
115    }
116
117    /// Send the frame. CRC is placed inline after data for a single write.
118    pub fn send<W: embedded_io::Write>(&mut self, w: &mut W) -> Result<(), W::Error> {
119        self.sync = Sync::valid();
120        let body_len = 10 + self.len as usize;
121        let crc = crc16(CRC_INIT, self.as_bytes(0, body_len)).to_le_bytes();
122        // Place CRC immediately after payload for a contiguous write.
123        // SAFETY: Frame is #[repr(C)] so offset 10+len is within the struct
124        // (inside `data` when len < MAX_PAYLOAD, or at `crc` field when len == MAX_PAYLOAD).
125        unsafe {
126            let base = self as *mut Self as *mut u8;
127            *base.add(10 + self.len as usize) = crc[0];
128            *base.add(10 + self.len as usize + 1) = crc[1];
129        }
130        w.write_all(self.as_bytes(0, body_len + 2))
131    }
132
133    /// Read one frame from the transport.
134    ///
135    /// Syncs on preamble, reads header + payload, validates CRC.
136    /// Returns `Ok(Status::Ok)` on success, `Ok(Status::*)` for protocol
137    /// errors (CRC, invalid frame, overflow), `Err` only for transport IO.
138    pub fn read<R: embedded_io::Read>(&mut self, r: &mut R) -> Result<Status, ReadError> {
139        self.sync.read(r)?;
140
141        // Read header fields: cmd(1) + status(1) + addr(4) + len(2) = 8 bytes at offset 2
142        r.read_exact(self.as_bytes_mut(2, 8))
143            .map_err(|_| ReadError)?;
144
145        if !Cmd::is_valid(self.as_bytes(2, 1)[0]) || !Status::is_valid(self.as_bytes(3, 1)[0]) {
146            // Remaining payload + CRC bytes are left in the transport. The sync
147            // scanner will skip them as garbage on the next read(). Draining here
148            // is not feasible: the len field is untrusted and the payload could be
149            // up to 64 KB, which exceeds our buffer and time budget on small MCUs.
150            return Ok(Status::Unsupported);
151        }
152
153        let data_len = self.len as usize;
154
155        if data_len > MAX_PAYLOAD {
156            // Same as above — we cannot drain a payload larger than our buffer.
157            // The sync scanner recovers on the next frame.
158            return Ok(Status::PayloadOverflow);
159        }
160
161        // Read data directly into buffer
162        if data_len > 0 {
163            r.read_exact(unsafe { &mut self.data.raw[..data_len] })
164                .map_err(|_| ReadError)?;
165        }
166
167        // Read CRC directly — [u8; 2] has no alignment constraint
168        r.read_exact(&mut self.crc).map_err(|_| ReadError)?;
169
170        // Validate CRC over body
171        if self.crc != crc16(CRC_INIT, self.as_bytes(0, 10 + data_len)).to_le_bytes() {
172            return Ok(Status::CrcMismatch);
173        }
174
175        Ok(Status::Ok)
176    }
177
178    /// Async version of [`send`](Self::send).
179    pub async fn send_async<W: embedded_io_async::Write>(
180        &mut self,
181        w: &mut W,
182    ) -> Result<(), W::Error> {
183        self.sync = Sync::valid();
184        let body_len = 10 + self.len as usize;
185        self.crc = crc16(CRC_INIT, self.as_bytes(0, body_len)).to_le_bytes();
186        w.write_all(self.as_bytes(0, body_len)).await?;
187        w.write_all(&self.crc).await
188    }
189
190    /// Async version of [`read`](Self::read).
191    pub async fn read_async<R: embedded_io_async::Read>(
192        &mut self,
193        r: &mut R,
194    ) -> Result<Status, ReadError> {
195        self.sync.read_async(r).await?;
196
197        r.read_exact(self.as_bytes_mut(2, 8))
198            .await
199            .map_err(|_| ReadError)?;
200
201        if !Cmd::is_valid(self.as_bytes(2, 1)[0]) || !Status::is_valid(self.as_bytes(3, 1)[0]) {
202            // See sync read() for rationale on not draining here.
203            return Ok(Status::Unsupported);
204        }
205
206        let data_len = self.len as usize;
207
208        if data_len > MAX_PAYLOAD {
209            // See sync read() for rationale on not draining here.
210            return Ok(Status::PayloadOverflow);
211        }
212
213        if data_len > 0 {
214            r.read_exact(unsafe { &mut self.data.raw[..data_len] })
215                .await
216                .map_err(|_| ReadError)?;
217        }
218
219        r.read_exact(&mut self.crc).await.map_err(|_| ReadError)?;
220
221        if self.crc != crc16(CRC_INIT, self.as_bytes(0, 10 + data_len)).to_le_bytes() {
222            return Ok(Status::CrcMismatch);
223        }
224
225        Ok(Status::Ok)
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    struct MockReader<'a> {
234        data: &'a [u8],
235        pos: usize,
236    }
237
238    impl<'a> MockReader<'a> {
239        fn new(data: &'a [u8]) -> Self {
240            Self { data, pos: 0 }
241        }
242    }
243
244    impl embedded_io::ErrorType for MockReader<'_> {
245        type Error = core::convert::Infallible;
246    }
247
248    impl embedded_io::Read for MockReader<'_> {
249        fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
250            let n = buf.len().min(self.data.len() - self.pos);
251            buf[..n].copy_from_slice(&self.data[self.pos..self.pos + n]);
252            self.pos += n;
253            Ok(n)
254        }
255    }
256
257    struct Sink {
258        buf: [u8; 512],
259        pos: usize,
260    }
261
262    impl Sink {
263        fn new() -> Self {
264            Self {
265                buf: [0; 512],
266                pos: 0,
267            }
268        }
269        fn written(&self) -> &[u8] {
270            &self.buf[..self.pos]
271        }
272    }
273
274    impl embedded_io::ErrorType for Sink {
275        type Error = core::convert::Infallible;
276    }
277
278    impl embedded_io::Write for Sink {
279        fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
280            let n = buf.len().min(self.buf.len() - self.pos);
281            self.buf[self.pos..self.pos + n].copy_from_slice(&buf[..n]);
282            self.pos += n;
283            Ok(n)
284        }
285        fn flush(&mut self) -> Result<(), Self::Error> {
286            Ok(())
287        }
288    }
289
290    fn frame(cmd: Cmd, status: Status, addr: u32, data: &[u8]) -> Frame {
291        let mut f = Frame {
292            cmd,
293            status,
294            addr,
295            len: data.len() as u16,
296            ..Default::default()
297        };
298        unsafe { f.data.raw[..data.len()].copy_from_slice(data) };
299        f
300    }
301
302    #[test]
303    fn request_round_trip() {
304        let mut frame = frame(Cmd::Write, Status::Request, 0x0800, &[0xDE, 0xAD]);
305
306        let mut sink = Sink::new();
307        frame.send(&mut sink).unwrap();
308
309        let mut frame2 = Frame::default();
310        frame2.read(&mut MockReader::new(sink.written())).unwrap();
311        assert_eq!(frame2.cmd, Cmd::Write);
312        assert_eq!(frame2.len, 2);
313        assert_eq!(frame2.addr, 0x0800);
314        assert_eq!(frame2.status, Status::Request);
315        assert_eq!(unsafe { &frame2.data.raw[..2] }, &[0xDE, 0xAD]);
316    }
317
318    #[test]
319    fn response_round_trip() {
320        let mut frame = frame(Cmd::Verify, Status::Ok, 0, &[0x12, 0x34]);
321
322        let mut sink = Sink::new();
323        frame.send(&mut sink).unwrap();
324
325        let mut frame2 = Frame::default();
326        frame2.read(&mut MockReader::new(sink.written())).unwrap();
327        assert_eq!(frame2.cmd, Cmd::Verify);
328        assert_eq!(frame2.status, Status::Ok);
329        assert_eq!(unsafe { &frame2.data.raw[..2] }, &[0x12, 0x34]);
330    }
331
332    #[test]
333    fn request_no_data() {
334        let mut frame = frame(Cmd::Erase, Status::Request, 0, &[]);
335
336        let mut sink = Sink::new();
337        frame.send(&mut sink).unwrap();
338
339        // Frame: SYNC(2) + CMD(1) + STATUS(1) + ADDR(4) + LEN(2) + CRC(2) = 12
340        assert_eq!(sink.written().len(), 12);
341
342        let mut frame2 = Frame::default();
343        frame2.read(&mut MockReader::new(sink.written())).unwrap();
344        assert_eq!(frame2.cmd, Cmd::Erase);
345        assert_eq!(frame2.len, 0);
346    }
347
348    #[test]
349    fn large_addr_round_trip() {
350        let mut frame = frame(Cmd::Write, Status::Request, 0x0001_0800, &[0xAB]);
351
352        let mut sink = Sink::new();
353        frame.send(&mut sink).unwrap();
354
355        let mut frame2 = Frame::default();
356        frame2.read(&mut MockReader::new(sink.written())).unwrap();
357        assert_eq!(frame2.addr, 0x0001_0800);
358    }
359
360    #[test]
361    fn cmd_addr_carry_over() {
362        let mut frame = frame(Cmd::Write, Status::Request, 0x0400, &[0xAB, 0xCD]);
363
364        let mut sink = Sink::new();
365        frame.send(&mut sink).unwrap();
366
367        // "Device" reads the request
368        let mut dev = Frame::default();
369        dev.read(&mut MockReader::new(sink.written())).unwrap();
370
371        // Device sends response — cmd and addr carry over
372        dev.status = Status::Ok;
373        dev.len = 0;
374        let mut resp_sink = Sink::new();
375        dev.send(&mut resp_sink).unwrap();
376
377        // Host reads response
378        let mut host = Frame::default();
379        host.read(&mut MockReader::new(resp_sink.written()))
380            .unwrap();
381        assert_eq!(host.cmd, Cmd::Write);
382        assert_eq!(host.addr, 0x0400);
383        assert_eq!(host.status, Status::Ok);
384    }
385
386    #[test]
387    fn read_bad_cmd() {
388        let mut frame = frame(Cmd::Info, Status::Request, 0, &[]);
389
390        let mut sink = Sink::new();
391        frame.send(&mut sink).unwrap();
392        sink.buf[2] ^= 0xFF; // corrupt CMD byte
393
394        let mut frame2 = Frame::default();
395        assert_eq!(
396            frame2.read(&mut MockReader::new(sink.written())),
397            Ok(Status::Unsupported)
398        );
399    }
400
401    #[test]
402    fn read_after_garbage() {
403        let mut frame = frame(Cmd::Verify, Status::Request, 0, &[]);
404
405        let mut sink = Sink::new();
406        frame.send(&mut sink).unwrap();
407        let frame_len = sink.pos;
408
409        let mut input = [0u8; 4 + 512];
410        input[..4].copy_from_slice(&[0xFF, 0x00, 0xAA, 0x42]);
411        input[4..4 + frame_len].copy_from_slice(&sink.buf[..frame_len]);
412
413        let mut frame2 = Frame::default();
414        assert_eq!(
415            frame2.read(&mut MockReader::new(&input[..4 + frame_len])),
416            Ok(Status::Ok)
417        );
418        assert_eq!(frame2.cmd, Cmd::Verify);
419    }
420
421    #[test]
422    fn read_overflow() {
423        // Build a valid frame, then patch the LEN field to exceed MAX_PAYLOAD.
424        let mut f = frame(Cmd::Write, Status::Request, 0, &[]);
425
426        let mut sink = Sink::new();
427        f.send(&mut sink).unwrap();
428
429        // LEN is at offset 8..10 (little-endian u16). Set to MAX_PAYLOAD + 1.
430        let overflow_len = (MAX_PAYLOAD as u16 + 1).to_le_bytes();
431        sink.buf[8] = overflow_len[0];
432        sink.buf[9] = overflow_len[1];
433
434        let mut frame2 = Frame::default();
435        assert_eq!(
436            frame2.read(&mut MockReader::new(sink.written())),
437            Ok(Status::PayloadOverflow)
438        );
439    }
440}