Skip to main content

viva_zenoh_api/
frame_header.rs

1//! Binary frame header prepended to every raw pixel buffer on the Zenoh image key.
2//!
3//! ## Layout (16 bytes, all fields little-endian)
4//!
5//! | Offset | Size | Field   | Value / Notes                          |
6//! |--------|------|---------|----------------------------------------|
7//! | 0      | 2    | magic   | `0x4746` LE (`[0x46, 0x47]`)           |
8//! | 2      | 1    | version | `1`                                    |
9//! | 3      | 1    | format  | pixel format discriminant (see table)  |
10//! | 4      | 4    | width   | image width in pixels, u32 LE          |
11//! | 8      | 4    | height  | image height in pixels, u32 LE         |
12//! | 12     | 4    | seq     | monotonically increasing frame counter |
13
14use thiserror::Error;
15
16use crate::PixelFormat;
17
18/// Magic bytes identifying a GenICam frame header.
19/// Little-endian encoding of `0x4746` -> bytes `[0x46, 0x47]`.
20pub const FRAME_MAGIC: u16 = 0x4746;
21
22/// Fixed size of the binary frame header in bytes.
23pub const HEADER_SIZE: usize = 16;
24
25/// The only supported header version. Bumped when the layout changes.
26pub const SUPPORTED_VERSION: u8 = 1;
27
28/// Errors returned by [`FrameHeader::decode`].
29#[derive(Debug, Error, PartialEq, Eq)]
30#[non_exhaustive]
31pub enum FrameHeaderError {
32    /// Buffer is shorter than [`HEADER_SIZE`] bytes.
33    #[error("buffer too short: need {HEADER_SIZE} bytes, got {0}")]
34    TooShort(usize),
35
36    /// First two bytes do not match [`FRAME_MAGIC`].
37    #[error("bad magic: expected 0x{:04X}, got 0x{got:04X}", FRAME_MAGIC)]
38    BadMagic { got: u16 },
39
40    /// Version byte is not [`SUPPORTED_VERSION`].
41    #[error("unsupported version {0}; only version {SUPPORTED_VERSION} is supported")]
42    UnsupportedVersion(u8),
43}
44
45/// 16-byte binary frame header prepended to every image payload.
46///
47/// See module-level documentation for the byte layout and format discriminant table.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct FrameHeader {
50    /// Pixel format of the following pixel data.
51    pub pixel_format: PixelFormat,
52    /// Image width in pixels.
53    pub width: u32,
54    /// Image height in pixels.
55    pub height: u32,
56    /// Monotonically increasing frame sequence number (wraps at u32::MAX).
57    pub seq: u32,
58}
59
60impl FrameHeader {
61    /// Encode this header into a 16-byte `Vec<u8>`.
62    pub fn encode(&self) -> Vec<u8> {
63        let mut buf = Vec::with_capacity(HEADER_SIZE);
64        buf.extend_from_slice(&FRAME_MAGIC.to_le_bytes());
65        buf.push(SUPPORTED_VERSION);
66        buf.push(pixel_format_to_u8(&self.pixel_format));
67        buf.extend_from_slice(&self.width.to_le_bytes());
68        buf.extend_from_slice(&self.height.to_le_bytes());
69        buf.extend_from_slice(&self.seq.to_le_bytes());
70        buf
71    }
72
73    /// Decode a header from the front of `buf`.
74    ///
75    /// On success returns `(header, pixel_data_slice)` where `pixel_data_slice`
76    /// is the remaining bytes after the 16-byte header.
77    pub fn decode(buf: &[u8]) -> Result<(FrameHeader, &[u8]), FrameHeaderError> {
78        if buf.len() < HEADER_SIZE {
79            return Err(FrameHeaderError::TooShort(buf.len()));
80        }
81
82        let magic = u16::from_le_bytes([buf[0], buf[1]]);
83        if magic != FRAME_MAGIC {
84            return Err(FrameHeaderError::BadMagic { got: magic });
85        }
86
87        let version = buf[2];
88        if version != SUPPORTED_VERSION {
89            return Err(FrameHeaderError::UnsupportedVersion(version));
90        }
91
92        let pixel_format = u8_to_pixel_format(buf[3]);
93        let width = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
94        let height = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
95        let seq = u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]);
96
97        let header = FrameHeader {
98            pixel_format,
99            width,
100            height,
101            seq,
102        };
103        Ok((header, &buf[HEADER_SIZE..]))
104    }
105}
106
107/// Map a [`PixelFormat`] variant to its stable 1-byte discriminant code.
108pub fn pixel_format_to_u8(pf: &PixelFormat) -> u8 {
109    match pf {
110        PixelFormat::Unknown => 0,
111        PixelFormat::Mono8 => 1,
112        PixelFormat::Mono10 => 2,
113        PixelFormat::Mono12 => 3,
114        PixelFormat::Mono16 => 4,
115        PixelFormat::BayerRG8 => 5,
116        PixelFormat::BayerGR8 => 6,
117        PixelFormat::BayerBG8 => 7,
118        PixelFormat::BayerGB8 => 8,
119        PixelFormat::BayerRG10 => 9,
120        PixelFormat::BayerGR10 => 10,
121        PixelFormat::BayerBG10 => 11,
122        PixelFormat::BayerGB10 => 12,
123        PixelFormat::BayerRG12 => 13,
124        PixelFormat::BayerGR12 => 14,
125        PixelFormat::BayerBG12 => 15,
126        PixelFormat::BayerGB12 => 16,
127        PixelFormat::BayerRG16 => 17,
128        PixelFormat::BayerGR16 => 18,
129        PixelFormat::BayerBG16 => 19,
130        PixelFormat::BayerGB16 => 20,
131        PixelFormat::RGB8 => 21,
132        PixelFormat::BGR8 => 22,
133        PixelFormat::RGBa8 => 23,
134        PixelFormat::YCbCr422_8 => 24,
135        PixelFormat::YCbCr8 => 25,
136        PixelFormat::Coord3dC16 => 26,
137    }
138}
139
140/// Map a 1-byte discriminant code back to the corresponding [`PixelFormat`].
141pub fn u8_to_pixel_format(code: u8) -> PixelFormat {
142    match code {
143        0 => PixelFormat::Unknown,
144        1 => PixelFormat::Mono8,
145        2 => PixelFormat::Mono10,
146        3 => PixelFormat::Mono12,
147        4 => PixelFormat::Mono16,
148        5 => PixelFormat::BayerRG8,
149        6 => PixelFormat::BayerGR8,
150        7 => PixelFormat::BayerBG8,
151        8 => PixelFormat::BayerGB8,
152        9 => PixelFormat::BayerRG10,
153        10 => PixelFormat::BayerGR10,
154        11 => PixelFormat::BayerBG10,
155        12 => PixelFormat::BayerGB10,
156        13 => PixelFormat::BayerRG12,
157        14 => PixelFormat::BayerGR12,
158        15 => PixelFormat::BayerBG12,
159        16 => PixelFormat::BayerGB12,
160        17 => PixelFormat::BayerRG16,
161        18 => PixelFormat::BayerGR16,
162        19 => PixelFormat::BayerBG16,
163        20 => PixelFormat::BayerGB16,
164        21 => PixelFormat::RGB8,
165        22 => PixelFormat::BGR8,
166        23 => PixelFormat::RGBa8,
167        24 => PixelFormat::YCbCr422_8,
168        25 => PixelFormat::YCbCr8,
169        26 => PixelFormat::Coord3dC16,
170        _ => PixelFormat::Unknown,
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    fn make_header(pf: PixelFormat) -> FrameHeader {
179        FrameHeader {
180            pixel_format: pf,
181            width: 640,
182            height: 480,
183            seq: 7,
184        }
185    }
186
187    #[test]
188    fn test_encode_decode_roundtrip_mono8() {
189        let hdr = make_header(PixelFormat::Mono8);
190        let encoded = hdr.encode();
191        let (decoded, remaining) = FrameHeader::decode(&encoded).expect("decode should succeed");
192        assert_eq!(decoded, hdr);
193        assert!(remaining.is_empty());
194    }
195
196    #[test]
197    fn test_encode_produces_correct_magic() {
198        let hdr = make_header(PixelFormat::Mono8);
199        let encoded = hdr.encode();
200        assert_eq!(encoded[0], 0x46);
201        assert_eq!(encoded[1], 0x47);
202    }
203
204    #[test]
205    fn test_encode_length() {
206        let hdr = make_header(PixelFormat::Mono8);
207        let encoded = hdr.encode();
208        assert_eq!(encoded.len(), HEADER_SIZE);
209    }
210
211    #[test]
212    fn test_decode_buffer_too_short() {
213        let buf = [0u8; 15];
214        let err = FrameHeader::decode(&buf).unwrap_err();
215        assert_eq!(err, FrameHeaderError::TooShort(15));
216    }
217
218    #[test]
219    fn test_decode_bad_magic() {
220        let mut buf = make_header(PixelFormat::Mono8).encode();
221        buf[0] = 0x00;
222        buf[1] = 0x00;
223        let err = FrameHeader::decode(&buf).unwrap_err();
224        assert!(matches!(err, FrameHeaderError::BadMagic { got: 0x0000 }));
225    }
226
227    #[test]
228    fn test_decode_unsupported_version() {
229        let mut buf = make_header(PixelFormat::Mono8).encode();
230        buf[2] = 2;
231        let err = FrameHeader::decode(&buf).unwrap_err();
232        assert_eq!(err, FrameHeaderError::UnsupportedVersion(2));
233    }
234
235    #[test]
236    fn test_pixel_format_roundtrip_all_known() {
237        let variants = [
238            PixelFormat::Mono8,
239            PixelFormat::Mono10,
240            PixelFormat::Mono12,
241            PixelFormat::Mono16,
242            PixelFormat::BayerRG8,
243            PixelFormat::BayerGR8,
244            PixelFormat::BayerBG8,
245            PixelFormat::BayerGB8,
246            PixelFormat::BayerRG10,
247            PixelFormat::BayerGR10,
248            PixelFormat::BayerBG10,
249            PixelFormat::BayerGB10,
250            PixelFormat::BayerRG12,
251            PixelFormat::BayerGR12,
252            PixelFormat::BayerBG12,
253            PixelFormat::BayerGB12,
254            PixelFormat::BayerRG16,
255            PixelFormat::BayerGR16,
256            PixelFormat::BayerBG16,
257            PixelFormat::BayerGB16,
258            PixelFormat::RGB8,
259            PixelFormat::BGR8,
260            PixelFormat::RGBa8,
261            PixelFormat::YCbCr422_8,
262            PixelFormat::YCbCr8,
263            PixelFormat::Coord3dC16,
264        ];
265        for pf in &variants {
266            let code = pixel_format_to_u8(pf);
267            let roundtripped = u8_to_pixel_format(code);
268            assert_eq!(
269                &roundtripped, pf,
270                "roundtrip failed for {pf:?}: code={code}"
271            );
272        }
273    }
274
275    #[test]
276    fn test_pixel_format_unknown_code() {
277        assert_eq!(u8_to_pixel_format(0xFF), PixelFormat::Unknown);
278    }
279}