Skip to main content

ddp_rs/protocol/
frame.rs

1//! Allocation-free DDP frame construction.
2//!
3//! [`FrameBuilder`] contains the chunking and sequence-numbering logic for sending pixel
4//! data, decoupled from any transport. It writes each ready-to-send frame into a
5//! caller-provided scratch buffer and hands it to a closure, so it works identically on
6//! `std` (backing [`crate::connection::DDPConnection`]) and on bare-metal `no_std` targets
7//! where you supply your own UDP stack.
8
9use super::{Header, PixelConfig, ID};
10
11/// Maximum pixel data size per DDP packet (480 pixels × 3 bytes RGB = 1440 bytes).
12///
13/// A frame's scratch buffer must hold a header plus this many payload bytes; 1500 (one MTU)
14/// is always sufficient.
15pub const MAX_DATA_LENGTH: usize = 480 * 3;
16
17/// Builds DDP frames from a pixel buffer, splitting large buffers across multiple packets
18/// and managing the rolling sequence number.
19///
20/// # Examples
21///
22/// ```
23/// use ddp_rs::protocol::{FrameBuilder, PixelConfig, ID};
24///
25/// let mut builder = FrameBuilder::new(PixelConfig::default(), ID::Default);
26/// let mut scratch = [0u8; 1500];
27///
28/// // 2 RGB pixels. The closure receives each fully-assembled frame ready to send.
29/// builder
30///     .for_each_frame(&[255, 0, 0, 0, 0, 255], 0, &mut scratch, |frame| {
31///         // e.g. socket.send(frame) on your platform
32///         assert_eq!(frame.len(), 10 + 6);
33///         Ok::<(), ()>(())
34///     })
35///     .unwrap();
36/// ```
37#[derive(Debug, Clone)]
38pub struct FrameBuilder {
39    /// Pixel format configuration written into each frame header.
40    pub pixel_config: PixelConfig,
41    /// Protocol ID written into each frame header.
42    pub id: ID,
43    sequence_number: u8,
44}
45
46impl FrameBuilder {
47    /// Creates a new frame builder. The sequence number starts at 1.
48    pub fn new(pixel_config: PixelConfig, id: ID) -> Self {
49        Self {
50            pixel_config,
51            id,
52            sequence_number: 1,
53        }
54    }
55
56    /// The sequence number that will be used for the next frame.
57    pub fn sequence_number(&self) -> u8 {
58        self.sequence_number
59    }
60
61    /// Splits `data` into DDP frames and invokes `f` with each fully-assembled frame.
62    ///
63    /// Each frame is written into `scratch` (which must be at least
64    /// `header_len + MAX_DATA_LENGTH`, i.e. ≥ 1500 bytes) and the resulting slice is passed
65    /// to `f`, which performs the actual transmission. `offset` is the starting byte offset
66    /// into the display buffer (not a pixel index); subsequent chunks advance it
67    /// automatically. The push flag is set on the final frame.
68    ///
69    /// Uses this builder's configured [`pixel_config`](Self::pixel_config) and
70    /// [`id`](Self::id). For control messages with a custom id, use [`Self::frames_with`].
71    pub fn for_each_frame<F, E>(
72        &mut self,
73        data: &[u8],
74        offset: u32,
75        scratch: &mut [u8],
76        f: F,
77    ) -> Result<(), E>
78    where
79        F: FnMut(&[u8]) -> Result<(), E>,
80    {
81        let header = Header {
82            pixel_config: self.pixel_config,
83            id: self.id,
84            ..Default::default()
85        };
86        self.frames_with(header, data, offset, scratch, f)
87    }
88
89    /// Like [`Self::for_each_frame`] but using a caller-supplied header template.
90    ///
91    /// The template's `packet_type`, `pixel_config`, `id` and `time_code` are used as-is;
92    /// the `offset`, `length`, `sequence_number` and push flag are managed per chunk.
93    pub fn frames_with<F, E>(
94        &mut self,
95        mut header: Header,
96        data: &[u8],
97        offset: u32,
98        scratch: &mut [u8],
99        mut f: F,
100    ) -> Result<(), E>
101    where
102        F: FnMut(&[u8]) -> Result<(), E>,
103    {
104        header.packet_type.push(false);
105
106        let total = data.len();
107        let num_iterations = total.div_ceil(MAX_DATA_LENGTH);
108        let mut chunk_index = 0usize;
109        let mut data_offset = 0usize;
110
111        while data_offset < total {
112            chunk_index += 1;
113
114            // Mark the final chunk with the push flag.
115            if chunk_index == num_iterations {
116                header.packet_type.push(true);
117            }
118
119            header.sequence_number = self.sequence_number;
120
121            let chunk_end = core::cmp::min(data_offset + MAX_DATA_LENGTH, total);
122            let chunk = &data[data_offset..chunk_end];
123            header.length = chunk.len() as u16;
124            header.offset = offset + data_offset as u32;
125
126            let header_len = header.write_into(scratch);
127            scratch[header_len..header_len + chunk.len()].copy_from_slice(chunk);
128            f(&scratch[..header_len + chunk.len()])?;
129
130            // Sequence number wraps around back to 1.
131            if self.sequence_number > 15 {
132                self.sequence_number = 1;
133            } else {
134                self.sequence_number += 1;
135            }
136
137            data_offset += MAX_DATA_LENGTH;
138        }
139
140        Ok(())
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use crate::protocol::PixelConfig;
148
149    #[test]
150    fn single_frame_matches_expected_bytes() {
151        let mut builder = FrameBuilder::new(PixelConfig::default(), ID::Default);
152        let mut scratch = [0u8; 1500];
153        let mut frames: Vec<Vec<u8>> = Vec::new();
154
155        builder
156            .for_each_frame(&[255, 0, 0, 255, 0, 0, 255, 0, 0], 0, &mut scratch, |frame| {
157                frames.push(frame.to_vec());
158                Ok::<(), ()>(())
159            })
160            .unwrap();
161
162        assert_eq!(frames.len(), 1);
163        // Byte-for-byte identical to what DDPConnection emits (see connection.rs test_conn).
164        assert_eq!(
165            frames[0],
166            vec![
167                0x41, 0x01, 0x0D, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0xFF, 0x00, 0x00,
168                0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00
169            ]
170        );
171    }
172
173    #[test]
174    fn chunks_large_data_and_sets_push_on_last() {
175        let mut builder = FrameBuilder::new(PixelConfig::default(), ID::Default);
176        let mut scratch = [0u8; 1500];
177        let data = vec![7u8; MAX_DATA_LENGTH * 2 + 30];
178        let mut pushes: Vec<bool> = Vec::new();
179        let mut seqs: Vec<u8> = Vec::new();
180
181        builder
182            .for_each_frame(&data, 0, &mut scratch, |frame| {
183                let h = Header::from(frame);
184                pushes.push(h.packet_type.push);
185                seqs.push(h.sequence_number);
186                Ok::<(), ()>(())
187            })
188            .unwrap();
189
190        assert_eq!(pushes, vec![false, false, true]);
191        assert_eq!(seqs, vec![1, 2, 3]);
192    }
193
194    #[test]
195    fn frame_roundtrips_through_packet_ref() {
196        use crate::packet::PacketRef;
197
198        let mut builder = FrameBuilder::new(PixelConfig::default(), ID::Custom(42));
199        let mut scratch = [0u8; 1500];
200        let payload: Vec<u8> = (0..90u8).collect();
201
202        builder
203            .for_each_frame(&payload, 30, &mut scratch, |frame| {
204                let parsed = PacketRef::from_bytes(frame).unwrap();
205                assert_eq!(parsed.header.offset, 30);
206                assert_eq!(parsed.header.id, ID::Custom(42));
207                assert_eq!(parsed.header.length as usize, payload.len());
208                assert_eq!(parsed.data, &payload[..]);
209                Ok::<(), ()>(())
210            })
211            .unwrap();
212    }
213
214    #[test]
215    fn empty_data_produces_no_frames() {
216        let mut builder = FrameBuilder::new(PixelConfig::default(), ID::Default);
217        let mut scratch = [0u8; 1500];
218        let mut count = 0;
219        builder
220            .for_each_frame(&[], 0, &mut scratch, |_| {
221                count += 1;
222                Ok::<(), ()>(())
223            })
224            .unwrap();
225        assert_eq!(count, 0);
226    }
227}