1use alloc::vec::Vec;
4use core::fmt;
5
6use crc::{Crc, CRC_16_IBM_SDLC};
7
8use crate::Frame;
9
10const CRC: Crc<u16> = Crc::<u16>::new(&CRC_16_IBM_SDLC);
11const CRC_LEN: usize = 2;
12
13pub const MAX_FRAME_LEN: usize = 512;
16
17const MAX_ENCODED_LEN: usize = MAX_FRAME_LEN + MAX_FRAME_LEN / 254 + 1;
19
20#[derive(Debug)]
21pub enum EncodeError {
22 Serialize(postcard::Error),
23 TooLarge(usize),
24}
25
26impl fmt::Display for EncodeError {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 match self {
29 EncodeError::Serialize(e) => write!(f, "serialization failed: {e}"),
30 EncodeError::TooLarge(len) => {
31 write!(f, "frame is {len} bytes, max is {MAX_FRAME_LEN}")
32 }
33 }
34 }
35}
36
37pub fn encode(frame: &Frame) -> Result<Vec<u8>, EncodeError> {
44 let mut body = postcard::to_allocvec(frame).map_err(EncodeError::Serialize)?;
45 body.extend_from_slice(&CRC.checksum(&body).to_le_bytes());
46 if body.len() > MAX_FRAME_LEN {
47 return Err(EncodeError::TooLarge(body.len()));
48 }
49
50 let mut out = Vec::with_capacity(body.len() + body.len() / 254 + 3);
51 out.push(0);
52 cobs_encode(&body, &mut out);
53 out.push(0);
54 Ok(out)
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum Decoded {
60 Frame(Frame),
61 Raw(Vec<u8>),
64}
65
66#[derive(Debug, Default)]
68pub struct Decoder {
69 buf: Vec<u8>,
70}
71
72impl Decoder {
73 #[must_use]
74 pub const fn new() -> Self {
75 Self { buf: Vec::new() }
76 }
77
78 pub fn push(&mut self, byte: u8) -> Option<Decoded> {
79 if byte == 0 {
80 return self.take().map(|data| match decode_body(&data) {
81 Some(frame) => Decoded::Frame(frame),
82 None => Decoded::Raw(data),
83 });
84 }
85
86 let overflow = if self.buf.len() >= MAX_ENCODED_LEN {
89 self.take().map(Decoded::Raw)
90 } else {
91 None
92 };
93 self.buf.push(byte);
94 overflow
95 }
96
97 pub fn flush(&mut self) -> Option<Decoded> {
101 self.take().map(Decoded::Raw)
102 }
103
104 fn take(&mut self) -> Option<Vec<u8>> {
105 (!self.buf.is_empty()).then(|| core::mem::take(&mut self.buf))
106 }
107}
108
109fn decode_body(encoded: &[u8]) -> Option<Frame> {
110 let mut body = Vec::with_capacity(encoded.len());
111 cobs_decode(encoded, &mut body)?;
112
113 let split = body.len().checked_sub(CRC_LEN)?;
114 let (payload, crc) = body.split_at(split);
115 if CRC.checksum(payload).to_le_bytes() != crc {
116 return None;
117 }
118
119 match postcard::take_from_bytes::<Frame>(payload) {
120 Ok((frame, [])) => Some(frame),
121 _ => None,
122 }
123}
124
125fn cobs_encode(src: &[u8], out: &mut Vec<u8>) {
126 let mut code_at = out.len();
127 let mut code = 1u8;
128 out.push(0);
129
130 for &byte in src {
131 if byte != 0 {
132 out.push(byte);
133 code += 1;
134 }
135 if byte == 0 || code == 0xFF {
136 out[code_at] = code;
137 code_at = out.len();
138 code = 1;
139 out.push(0);
140 }
141 }
142 out[code_at] = code;
143}
144
145fn cobs_decode(src: &[u8], out: &mut Vec<u8>) -> Option<()> {
146 let mut i = 0;
147 while i < src.len() {
148 let code = usize::from(src[i]);
149 if code == 0 {
150 return None;
151 }
152 let end = i + code;
153 out.extend_from_slice(src.get(i + 1..end)?);
154 i = end;
155 if code != 0xFF && i < src.len() {
156 out.push(0);
157 }
158 }
159 Some(())
160}
161
162#[cfg(test)]
163mod tests {
164 use alloc::string::String;
165 use alloc::vec;
166
167 use super::*;
168 use crate::*;
169
170 fn decode_all(decoder: &mut Decoder, bytes: &[u8]) -> Vec<Decoded> {
171 bytes.iter().filter_map(|&b| decoder.push(b)).collect()
172 }
173
174 fn sample_frames() -> Vec<Frame> {
175 let mapping = Mapping {
176 ir: 0x00FF_02FD,
177 key: HidKey::Home,
178 };
179 let commands = [
180 Command::Ping,
181 Command::GetInfo,
182 Command::GetStatus,
183 Command::ListKeymap { offset: 3 },
184 Command::SetMapping(mapping),
185 Command::RemoveMapping { ir: 0x1234_5678 },
186 Command::ClearKeymap,
187 Command::ResetKeymap,
188 Command::Learn {
189 key: HidKey::VolumeUp,
190 timeout_ms: 10_000,
191 },
192 Command::CancelLearn,
193 Command::SendHid { key: HidKey::Up },
194 Command::SendIr { code: 0x20DF_10EF },
195 Command::SetMonitor { enabled: true },
196 Command::GetBuildInfo,
197 ];
198 let replies = [
199 Ok(Reply::Ok),
200 Ok(Reply::Pong),
201 Ok(Reply::Info(DeviceInfo {
202 protocol_version: PROTOCOL_VERSION,
203 firmware_version: "0.1.0".into(),
204 max_mappings: 64,
205 })),
206 Ok(Reply::Status(Status {
207 uptime_ms: 123_456_789,
208 ble_connected: true,
209 mappings: 12,
210 monitor: false,
211 learning: Some(HidKey::Back),
212 battery: Some(Battery {
213 millivolts: 4_380,
214 percent: Some(85),
215 }),
216 })),
217 Ok(Reply::Keymap(KeymapPage {
218 offset: 0,
219 total: 2,
220 entries: vec![mapping; 2],
221 })),
222 Ok(Reply::BuildInfo(BuildInfo {
223 git: "c7f30e8-dirty 2026-10-27".into(),
224 components: vec![Component {
225 name: "ESP-IDF".into(),
226 version: "v5.5.6".into(),
227 }],
228 })),
229 Err(String::from("nope")),
230 ];
231 let events = [
232 Event::IrReceived {
233 code: 0xFFFF_FFFF,
234 repeat: true,
235 key: None,
236 },
237 Event::Learned(mapping),
238 Event::LearnTimedOut,
239 Event::BleConnection { connected: false },
240 Event::LearnFailed {
241 reason: "keymap full".into(),
242 },
243 ];
244
245 let mut frames: Vec<Frame> = commands
246 .into_iter()
247 .enumerate()
248 .map(|(id, command)| {
249 Frame::Request(Request {
250 id: u32::try_from(id).unwrap(),
251 command,
252 })
253 })
254 .collect();
255 frames.extend(
256 replies
257 .into_iter()
258 .map(|result| Frame::Response(Response { id: 7, result })),
259 );
260 frames.extend(events.into_iter().map(Frame::Event));
261 frames.push(Frame::Log(Log {
262 level: LogLevel::Warn,
263 message: "W (42) ir_bt_bridge: hello\0with nul".into(),
264 }));
265 frames
266 }
267
268 #[test]
269 fn round_trips_every_frame() {
270 let mut decoder = Decoder::new();
271 for frame in sample_frames() {
272 let wire = encode(&frame).unwrap();
273 assert_eq!(wire.first(), Some(&0));
274 assert_eq!(wire.last(), Some(&0));
275 assert!(!wire[1..wire.len() - 1].contains(&0));
276 assert_eq!(decode_all(&mut decoder, &wire), vec![Decoded::Frame(frame)]);
277 }
278 }
279
280 #[test]
281 fn cobs_round_trips_edge_lengths() {
282 for len in [0, 1, 253, 254, 255, 256, 508, 600] {
283 for fill in [0u8, 1, 0xFF] {
284 let src: Vec<u8> = (0..len)
285 .map(|i| {
286 if i % 7 == 3 {
287 fill
288 } else {
289 u8::try_from(i % 251 + 1).unwrap()
290 }
291 })
292 .collect();
293 let mut encoded = Vec::new();
294 cobs_encode(&src, &mut encoded);
295 assert!(!encoded.contains(&0), "len {len} fill {fill}");
296 let mut decoded = Vec::new();
297 cobs_decode(&encoded, &mut decoded).unwrap();
298 assert_eq!(decoded, src, "len {len} fill {fill}");
299 }
300 }
301 }
302
303 #[test]
304 fn text_between_frames_comes_out_raw() {
305 let ping = Frame::Request(Request {
306 id: 1,
307 command: Command::Ping,
308 });
309 let mut stream = b"ESP-ROM:esp32s3-20210327\r\n".to_vec();
310 stream.extend(encode(&ping).unwrap());
311 stream.extend(b"panic text");
312
313 let mut decoder = Decoder::new();
314 assert_eq!(
315 decode_all(&mut decoder, &stream),
316 vec![
317 Decoded::Raw(b"ESP-ROM:esp32s3-20210327\r\n".to_vec()),
318 Decoded::Frame(ping),
319 ]
320 );
321 assert_eq!(decoder.flush(), Some(Decoded::Raw(b"panic text".to_vec())));
322 assert_eq!(decoder.flush(), None);
323 }
324
325 #[test]
326 fn corrupted_frame_is_rejected_and_decoder_resyncs() {
327 let frame = Frame::Event(Event::LearnTimedOut);
328 let mut bad = encode(&frame).unwrap();
329 bad[2] ^= 0x40;
330 let mut stream = bad.clone();
331 stream.extend(encode(&frame).unwrap());
332
333 let mut decoder = Decoder::new();
334 assert_eq!(
335 decode_all(&mut decoder, &stream),
336 vec![
337 Decoded::Raw(bad[1..bad.len() - 1].to_vec()),
338 Decoded::Frame(frame),
339 ]
340 );
341 }
342
343 #[test]
344 fn frame_split_across_reads_decodes() {
345 let frame = Frame::Request(Request {
346 id: 99,
347 command: Command::SendHid {
348 key: HidKey::PlayPause,
349 },
350 });
351 let wire = encode(&frame).unwrap();
352 let (a, b) = wire.split_at(wire.len() / 2);
353
354 let mut decoder = Decoder::new();
355 assert_eq!(decode_all(&mut decoder, a), []);
356 assert_eq!(decode_all(&mut decoder, b), vec![Decoded::Frame(frame)]);
357 }
358
359 #[test]
360 fn endless_text_is_flushed_in_bounded_chunks() {
361 let mut decoder = Decoder::new();
362 let out = decode_all(&mut decoder, &[b'x'; 3 * MAX_ENCODED_LEN]);
363 assert_eq!(out.len(), 2);
364 assert!(out
365 .iter()
366 .all(|d| matches!(d, Decoded::Raw(r) if r.len() == MAX_ENCODED_LEN)));
367 }
368
369 #[test]
370 fn oversized_frame_is_refused() {
371 let frame = Frame::Log(Log {
372 level: LogLevel::Info,
373 message: "x".repeat(MAX_FRAME_LEN),
374 });
375 assert!(matches!(encode(&frame), Err(EncodeError::TooLarge(_))));
376 }
377
378 #[test]
381 fn golden_encodings() {
382 let ping = Frame::Request(Request {
383 id: 1,
384 command: Command::Ping,
385 });
386 assert_eq!(postcard::to_allocvec(&ping).unwrap(), [0, 1, 0]);
387
388 let set = Frame::Request(Request {
389 id: 300,
390 command: Command::SetMapping(Mapping {
391 ir: 0x00FF_02FD,
392 key: HidKey::Home,
393 }),
394 });
395 assert_eq!(
397 postcard::to_allocvec(&set).unwrap(),
398 [0, 0xAC, 0x02, 4, 0xFD, 0x85, 0xFC, 0x07, 6]
399 );
400
401 let err = Frame::Response(Response {
402 id: 2,
403 result: Err("e".into()),
404 });
405 assert_eq!(postcard::to_allocvec(&err).unwrap(), [1, 2, 1, 1, b'e']);
406
407 assert_eq!(
408 postcard::to_allocvec(&Frame::Event(Event::LearnTimedOut)).unwrap(),
409 [2, 2]
410 );
411
412 let build_info = Frame::Request(Request {
413 id: 3,
414 command: Command::GetBuildInfo,
415 });
416 assert_eq!(postcard::to_allocvec(&build_info).unwrap(), [0, 3, 13]);
417
418 let status = Frame::Response(Response {
419 id: 4,
420 result: Ok(Reply::Status(Status {
421 uptime_ms: 1,
422 ble_connected: true,
423 mappings: 2,
424 monitor: false,
425 learning: None,
426 battery: Some(Battery {
427 millivolts: 4_380,
428 percent: Some(85),
429 }),
430 })),
431 });
432 assert_eq!(
435 postcard::to_allocvec(&status).unwrap(),
436 [1, 4, 0, 3, 1, 1, 2, 0, 0, 1, 0x9C, 0x22, 1, 85]
437 );
438 assert_eq!(
440 encode(&ping).unwrap(),
441 [0, 0x01, 0x02, 0x01, 0x03, 0x14, 0xDF, 0]
442 );
443 }
444
445 #[test]
446 fn hid_key_indices_are_stable() {
447 for (index, key) in HidKey::ALL.into_iter().enumerate() {
448 assert_eq!(
449 postcard::to_allocvec(&key).unwrap(),
450 [u8::try_from(index).unwrap()]
451 );
452 }
453 }
454
455 #[test]
456 fn hid_key_serde_names_match_cli_names() {
457 for key in HidKey::ALL {
458 let json = serde_json::to_string(&key).unwrap();
459 assert_eq!(json, alloc::format!("\"{}\"", key.name()));
460 }
461 }
462
463 #[test]
464 fn hid_key_names_parse_back() {
465 for key in HidKey::ALL {
466 assert_eq!(key.name().parse(), Ok(key));
467 }
468 assert_eq!("VOL_UP".parse(), Ok(HidKey::VolumeUp));
469 assert_eq!("volup".parse::<HidKey>(), Err(UnknownKey));
470 }
471}