1use thiserror::Error;
15
16use crate::PixelFormat;
17
18pub const FRAME_MAGIC: u16 = 0x4746;
21
22pub const HEADER_SIZE: usize = 16;
24
25pub const SUPPORTED_VERSION: u8 = 1;
27
28#[derive(Debug, Error, PartialEq, Eq)]
30#[non_exhaustive]
31pub enum FrameHeaderError {
32 #[error("buffer too short: need {HEADER_SIZE} bytes, got {0}")]
34 TooShort(usize),
35
36 #[error("bad magic: expected 0x{:04X}, got 0x{got:04X}", FRAME_MAGIC)]
38 BadMagic { got: u16 },
39
40 #[error("unsupported version {0}; only version {SUPPORTED_VERSION} is supported")]
42 UnsupportedVersion(u8),
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct FrameHeader {
50 pub pixel_format: PixelFormat,
52 pub width: u32,
54 pub height: u32,
56 pub seq: u32,
58}
59
60impl FrameHeader {
61 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 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
107pub 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
140pub 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}