flipdot_core/frame.rs
1use std::borrow::Cow;
2use std::fmt::{self, Display, Formatter};
3use std::io::{BufRead, BufReader, Read, Write};
4use std::str;
5
6use derive_more::{Display, LowerHex, UpperHex};
7use lazy_static::lazy_static;
8use num_traits::Num;
9use regex::bytes::Regex;
10use thiserror::Error;
11
12/// Errors related to reading/writing [`Frame`]s of data.
13#[derive(Error, Debug)]
14#[non_exhaustive]
15pub enum FrameError {
16 /// [`Data`] length exceeded the maximum of 255 bytes.
17 #[error("Maximum data length is {} bytes, got {}", max, actual)]
18 DataTooLong {
19 /// The maximum data length.
20 max: u8,
21
22 /// The actual length of the data that was provided.
23 actual: usize,
24 },
25
26 /// Failed reading/writing a [`Frame`] of data.
27 #[error("Failed reading/writing a frame of data")]
28 Io {
29 /// The underlying I/O error.
30 #[from]
31 source: std::io::Error,
32 },
33
34 /// Failed to parse data into a [`Frame`].
35 #[error("Failed to parse invalid Intel HEX [{}] into a Frame", string_for_error(data))]
36 InvalidFrame {
37 /// The invalid frame data.
38 data: Vec<u8>,
39 },
40
41 /// [`Frame`] data didn't match declared length.
42 #[error(
43 "Frame data [{}] didn't match declared length: Expected {}, got {}",
44 string_for_error(data),
45 expected,
46 actual
47 )]
48 FrameDataMismatch {
49 /// The invalid frame data.
50 data: Vec<u8>,
51
52 /// The expected data length.
53 expected: usize,
54
55 /// The actual value of the data that was provided.
56 actual: usize,
57 },
58
59 /// [`Frame`] checksum didn't match declared checksum.
60 #[error(
61 "Frame checksum for [{}] didn't match declared checksum: Expected 0x{:X}, got 0x{:X}",
62 string_for_error(data),
63 expected,
64 actual
65 )]
66 BadChecksum {
67 /// The invalid frame data.
68 data: Vec<u8>,
69
70 /// The expected checksum.
71 expected: u8,
72
73 /// The actual checksum of the data.
74 actual: u8,
75 },
76}
77
78/// A low-level representation of an Intel HEX data frame.
79///
80/// The Luminator protocol uses the [Intel HEX] format but not its semantics.
81/// This struct handles parsing the raw bytes into a form we can reason about,
82/// dealing with checksums, and so forth. It makes no attempt to ascribe meaning
83/// to the address, message type, and data (that's [`Message`](crate::Message)'s job).
84///
85/// Both owned and borrowed data are supported.
86///
87/// # Examples
88///
89/// ```
90/// use flipdot_core::{Address, Data, Frame, MsgType};
91///
92/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
93/// #
94/// let frame = Frame::new(Address(2), MsgType(1), Data::try_new(vec![3, 31])?);
95/// println!("Parsed frame is {}", frame);
96///
97/// let bytes = frame.to_bytes();
98/// assert_eq!(b":02000201031FD9", bytes.as_slice());
99///
100/// let parsed = Frame::from_bytes(&bytes)?;
101/// assert_eq!(parsed, frame);
102/// #
103/// # Ok(()) }
104/// ```
105///
106/// # Format Details
107///
108/// The format consists of a leading colon, several numeric fields (two-character ASCII representations
109/// of hex bytes), and a final carriage return/linefeed terminator. Note that for convenience,
110/// `Frame` allows omitting the final CRLF sequence.
111///
112/// ```text
113/// ┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬ ┄ ┬────┬────┬────┬────┬────┬────┐
114/// │ : │ DataLen │ Address │ MsgType │ Data 0 │...│ Data N │ Chksum │ \r │ \n │
115/// └────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴ ┄ ┴────┴────┴────┴────┴────┴────┘
116/// └╌╌╌╌╌╌╌╌╌╌╌╌╌ # of ╌╌╌╌╌╌╌╌╌╌╌╌╌> ┆ Data bytes ┆
117/// ```
118///
119/// The `DataLen` field describes how many two-character data byte sequences are present.
120/// Note that since it is represented as a single byte, the data length cannot exceed 255 (`0xFF`).
121/// If `DataLen` is 0, there are no data bytes, and `MsgType` is followed directly by `Chksum`.
122/// The checksum is a [longitudinal redundancy check] calculated on all numeric fields.
123///
124/// [Intel HEX]: https://en.wikipedia.org/wiki/Intel_HEX
125/// [longitudinal redundancy check]: https://en.wikipedia.org/wiki/Longitudinal_redundancy_check
126#[derive(Debug, Clone, PartialEq, Eq, Hash)]
127pub struct Frame<'a> {
128 address: Address,
129 message_type: MsgType,
130 data: Data<'a>,
131}
132
133/// A [`Frame`]'s message type.
134///
135/// Carries no implicit meaning, but is interpreted by [`Message`](crate::Message).
136///
137/// # Examples
138///
139/// ```
140/// use flipdot_core::{Address, Data, Frame, MsgType};
141///
142/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
143/// #
144/// // Create a frame with message type 1.
145/// let frame = Frame::new(Address(2), MsgType(1), Data::try_new(vec![1, 2])?);
146/// #
147/// # Ok(()) }
148/// ```
149#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Display, LowerHex, UpperHex)]
150pub struct MsgType(pub u8);
151
152/// The address of a sign, used to identify it on the bus.
153///
154/// # Examples
155///
156/// ```
157/// use flipdot_core::{Address, Data, Frame, MsgType};
158///
159/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
160/// #
161/// // Create a frame addressed to sign 2.
162/// let frame = Frame::new(Address(2), MsgType(1), Data::try_new(vec![1, 2])?);
163/// #
164/// # Ok(()) }
165/// ```
166#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Display, LowerHex, UpperHex)]
167pub struct Address(pub u16);
168
169impl<'a> Frame<'a> {
170 /// Constructs a new `Frame` with the specified address, message type, and data.
171 ///
172 /// # Examples
173 ///
174 /// ```
175 /// # use flipdot_core::{Address, Data, Frame, MsgType};
176 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
177 /// #
178 /// // some_data is moved into owning_frame.
179 /// let some_data = vec![1, 2, 3];
180 /// let owning_frame = Frame::new(Address(0xB), MsgType(0xA), Data::try_new(some_data)?);
181 ///
182 /// // other_data is borrowed.
183 /// let other_data = vec![1, 2, 3];
184 /// let borrowing_frame = Frame::new(Address(0xD), MsgType(0xC), Data::try_new(other_data.as_slice())?);
185 /// #
186 /// # Ok(()) }
187 /// ```
188 pub fn new(address: Address, message_type: MsgType, data: Data<'a>) -> Self {
189 Frame {
190 address,
191 message_type,
192 data,
193 }
194 }
195
196 /// Returns the message type of the frame.
197 ///
198 /// # Examples
199 ///
200 /// ```
201 /// # use flipdot_core::{Address, Data, Frame, MsgType};
202 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
203 /// #
204 /// let frame = Frame::new(Address(1), MsgType(1), Data::try_new(vec![])?);
205 /// match frame.message_type() {
206 /// MsgType(1) => println!("Message 1"),
207 /// _ => println!("Something else"),
208 /// }
209 /// #
210 /// # Ok(()) }
211 /// ```
212 pub fn message_type(&self) -> MsgType {
213 self.message_type
214 }
215
216 /// Returns the address of the frame.
217 ///
218 /// # Examples
219 ///
220 /// ```
221 /// # use flipdot_core::{Address, Data, Frame, MsgType};
222 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
223 /// #
224 /// let frame = Frame::new(Address(1), MsgType(1), Data::try_new(vec![])?);
225 /// if frame.address() == Address(3) {
226 /// println!("This frame is addressed to me!");
227 /// }
228 /// #
229 /// # Ok(()) }
230 /// ```
231 pub fn address(&self) -> Address {
232 self.address
233 }
234
235 /// Returns a reference to the frame's data.
236 ///
237 /// # Examples
238 ///
239 /// ```
240 /// # use flipdot_core::{Address, Data, Frame, MsgType};
241 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
242 /// #
243 /// let frame = Frame::new(Address(1), MsgType(1), Data::try_new(vec![10, 20])?);
244 /// if (frame.data().as_ref() == &[10, 20]) {
245 /// println!("Found the expected data!");
246 /// }
247 /// #
248 /// # Ok(()) }
249 /// ```
250 pub fn data(&self) -> &Cow<'a, [u8]> {
251 &self.data.0
252 }
253
254 /// Consumes the frame and returns ownership of its data.
255 ///
256 /// # Examples
257 ///
258 /// ```
259 /// # use flipdot_core::{Address, Data, Frame, MsgType};
260 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
261 /// #
262 /// let frame = Frame::new(Address(1), MsgType(1), Data::try_new(vec![6, 7])?);
263 /// let frame2 = Frame::new(Address(2), MsgType(2), frame.into_data());
264 /// #
265 /// # Ok(()) }
266 /// ```
267 pub fn into_data(self) -> Data<'a> {
268 self.data
269 }
270
271 /// Converts the frame to its wire format, *without* trailing carriage return/linefeed.
272 ///
273 /// # Examples
274 ///
275 /// ```
276 /// # use flipdot_core::{Address, Data, Frame, MsgType};
277 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
278 /// #
279 /// let frame = Frame::new(Address(2), MsgType(1), Data::try_new(vec![3, 31])?);
280 /// let bytes = frame.to_bytes();
281 /// assert_eq!(b":02000201031FD9", bytes.as_slice());
282 /// #
283 /// # Ok(()) }
284 /// ```
285 pub fn to_bytes(&self) -> Vec<u8> {
286 const HEX_DIGITS: &[u8] = b"0123456789ABCDEF";
287
288 let mut payload = self.payload();
289 let checksum = checksum(&payload);
290 payload.push(checksum);
291 let payload = payload;
292
293 // Colon, 2 ASCII digits for each byte, and 2 bytes for optional CRLF sequence
294 let mut output = Vec::<u8>::with_capacity(1 + 2 * payload.len() + 2);
295 output.push(b':');
296 for byte in &payload {
297 output.push(HEX_DIGITS[(byte >> 4) as usize]);
298 output.push(HEX_DIGITS[(byte & 0x0F) as usize]);
299 }
300 assert_eq!(output.len(), output.capacity() - 2);
301 output
302 }
303
304 /// Converts the frame to its wire format, including trailing carriage return/linefeed.
305 ///
306 /// # Examples
307 ///
308 /// ```
309 /// # use flipdot_core::{Address, Data, Frame, MsgType};
310 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
311 /// #
312 /// let frame = Frame::new(Address(2), MsgType(1), Data::try_new(vec![3, 31])?);
313 /// let bytes = frame.to_bytes_with_newline();
314 /// assert_eq!(b":02000201031FD9\r\n", bytes.as_slice());
315 /// #
316 /// # Ok(()) }
317 /// ```
318 pub fn to_bytes_with_newline(&self) -> Vec<u8> {
319 let mut output = self.to_bytes();
320 output.extend_from_slice(b"\r\n");
321 assert_eq!(output.len(), output.capacity());
322 output
323 }
324
325 /// Parses the Intel HEX wire format into a new `Frame`.
326 ///
327 /// # Errors
328 ///
329 /// Returns:
330 /// * [`FrameError::InvalidFrame`] if the data does not conform to the Intel HEX format.
331 /// * [`FrameError::FrameDataMismatch`] if the actual number of data bytes does not match the specified amount.
332 /// * [`FrameError::BadChecksum`] if the computed checksum on the data does not match the specified one.
333 ///
334 /// # Examples
335 ///
336 /// ```
337 /// # use flipdot_core::{Address, Data, Frame, MsgType};
338 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
339 /// #
340 /// let bytes = b":02000201031FD9\r\n";
341 /// let frame = Frame::from_bytes(&bytes[..])?;
342 /// assert_eq!(Frame::new(Address(2), MsgType(1), Data::try_new(vec![3, 31])?), frame);
343 /// #
344 /// # Ok(()) }
345 /// ```
346 pub fn from_bytes(bytes: &[u8]) -> Result<Self, FrameError> {
347 lazy_static! {
348 static ref RE: Regex = Regex::new(r"(?x)
349 ^: # Colon marks beginning of frame
350 (?P<data_len>[[:xdigit:]]{2}) # 2 hex digits for data length
351 (?P<address>[[:xdigit:]]{4}) # 4 hex digits for address
352 (?P<message_type>[[:xdigit:]]{2}) # 2 hex digits for message type
353 (?P<data>(?:[[:xdigit:]]{2})*) # Zero or more groups of 2 hex digits for data
354 (?P<checksum>[[:xdigit:]]{2}) # 2 hex digits for checksum
355 (?:\r\n)?$ # Optional newline sequence
356 ").unwrap(); // Regex is valid so safe to unwrap.
357 }
358 let captures = RE
359 .captures(bytes)
360 .ok_or_else(|| FrameError::InvalidFrame { data: bytes.into() })?;
361
362 // Regex always matches all capture groups so safe to unwrap.
363 let data_len = parse_hex::<u8>(captures.name("data_len").unwrap().as_bytes());
364 let address = parse_hex::<u16>(captures.name("address").unwrap().as_bytes());
365 let message_type = parse_hex::<u8>(captures.name("message_type").unwrap().as_bytes());
366 let data_bytes = captures.name("data").unwrap().as_bytes();
367 let provided_checksum = parse_hex::<u8>(captures.name("checksum").unwrap().as_bytes());
368
369 let data = data_bytes.chunks(2).map(parse_hex::<u8>).collect::<Vec<_>>();
370 if data.len() != data_len as usize {
371 return Err(FrameError::FrameDataMismatch {
372 data: bytes.into(),
373 expected: data_len as usize,
374 actual: data.len(),
375 });
376 }
377
378 let frame = Frame::new(Address(address), MsgType(message_type), Data::try_new(data)?);
379 let payload = frame.payload();
380 let computed_checksum = checksum(&payload);
381 if computed_checksum != provided_checksum {
382 return Err(FrameError::BadChecksum {
383 data: bytes.into(),
384 expected: provided_checksum,
385 actual: computed_checksum,
386 });
387 }
388
389 Ok(frame)
390 }
391
392 /// Writes the byte representation (including CRLF) of the frame to a writer.
393 ///
394 /// # Errors
395 ///
396 /// Returns [`FrameError::Io`] if the write fails.
397 ///
398 /// # Examples
399 ///
400 /// ```no_run
401 /// # use flipdot_core::{Address, Data, Frame, MsgType};
402 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
403 /// #
404 /// let mut port = serial::open("COM3")?;
405 /// let frame = Frame::new(Address(2), MsgType(1), Data::try_new(vec![3, 31])?);
406 /// frame.write(&mut port)?;
407 /// #
408 /// # Ok(()) }
409 /// ```
410 pub fn write<W: Write>(&self, writer: &mut W) -> Result<(), FrameError> {
411 writer.write_all(&self.to_bytes_with_newline())?;
412 Ok(())
413 }
414
415 /// Reads the next line (up to `\n`) from the reader and converts the result
416 /// into a new `Frame`.
417 ///
418 /// # Errors
419 ///
420 /// Returns:
421 /// * [`FrameError::Io`] if the read fails.
422 /// * [`FrameError::InvalidFrame`] if the data does not conform to the Intel HEX format.
423 /// * [`FrameError::FrameDataMismatch`] if the actual number of data bytes does not match the specified amount.
424 /// * [`FrameError::BadChecksum`] if the computed checksum on the data does not match the specified one.
425 ///
426 /// # Examples
427 ///
428 /// ```no_run
429 /// # use flipdot_core::{Address, Data, Frame, MsgType};
430 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
431 /// #
432 /// let mut port = serial::open("COM3")?;
433 /// let frame = Frame::read(&mut port)?;
434 /// #
435 /// # Ok(()) }
436 /// ```
437 pub fn read<R: Read>(mut reader: &mut R) -> Result<Self, FrameError> {
438 // One-byte buffer seems to work best with such small payloads
439 let mut buf_reader = BufReader::with_capacity(1, &mut reader);
440 let mut data = Vec::<u8>::new();
441 let _ = buf_reader.read_until(b'\n', &mut data)?;
442 let frame = Frame::from_bytes(&data)?;
443 Ok(frame)
444 }
445
446 /// Returns the payload portion of the wire format.
447 ///
448 /// These are the numeric fields other than the checksum, upon which the checksum is computed.
449 fn payload(&self) -> Vec<u8> {
450 // Reserving an extra byte here so the checksum can be appended in to_bytes.
451 let mut payload = Vec::<u8>::with_capacity(5 + self.data.0.len());
452 payload.push(self.data.0.len() as u8);
453 payload.push((self.address.0 >> 8) as u8);
454 payload.push(self.address.0 as u8);
455 payload.push(self.message_type.0);
456 payload.extend_from_slice(&self.data.0);
457 assert_eq!(payload.len(), payload.capacity() - 1);
458 payload
459 }
460}
461
462impl Display for Frame<'_> {
463 /// Formats the frame in a human-readable way.
464 ///
465 /// Useful for viewing traffic on a bus. All numbers are in hex.
466 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
467 write!(f, "Type {:02X} | Addr {:04X}", self.message_type.0, self.address.0)?;
468 if self.data.0.len() > 0 {
469 write!(f, " | Data ")?;
470 for byte in self.data.0.iter() {
471 write!(f, "{:02X} ", byte)?;
472 }
473 }
474 Ok(())
475 }
476}
477
478/// Parses a byte slice representing ASCII text into a hex digit.
479///
480/// Assumes that the data has already been validated and panics if it is invalid.
481fn parse_hex<T: Num>(bytes: &[u8]) -> T
482where
483 <T as Num>::FromStrRadixErr: 'static + ::std::error::Error,
484{
485 // Regex already determined these are valid hex digits, so we can just unwrap.
486 let string = str::from_utf8(bytes).unwrap();
487 T::from_str_radix(string, 16).unwrap()
488}
489
490/// Formats a supposed Intel HEX byte string for display as part of an error message.
491///
492/// Does a lossy UTF-8 conversion (invalid characters represented as `?`) and removes whitespace.
493fn string_for_error(bytes: &[u8]) -> String {
494 String::from_utf8_lossy(bytes).trim().to_string()
495}
496
497/// Computes the LRC of the given byte slice.
498///
499/// The canonical implementation is a wrapping add followed by the two's
500/// complement (negation). Instead, we can just do a wrapping subtract
501/// from zero.
502fn checksum(bytes: &[u8]) -> u8 {
503 bytes.iter().fold(0, |acc, &b| acc.wrapping_sub(b))
504}
505
506/// Owned or borrowed data to be placed in a [`Frame`].
507///
508/// Since the data length in the [`Frame`] will be represented as a single byte,
509/// that length cannot exceed 255 (`0xFF`). `Data` is responsible for maintaining
510/// this invariant.
511///
512/// # Examples
513///
514/// ```
515/// use flipdot_core::{Address, Data, Frame, MsgType};
516/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
517/// #
518/// let data = Data::try_new(vec![1, 2, 3])?; // Ok since length under 255
519/// let frame = Frame::new(Address(2), MsgType(1), data);
520/// #
521/// # Ok(()) }
522/// ```
523#[derive(Debug, Clone, PartialEq, Eq, Hash)]
524pub struct Data<'a>(Cow<'a, [u8]>);
525
526impl<'a> Data<'a> {
527 /// Creates a new `Data` containing owned or borrowed data.
528 ///
529 /// Since the data length in the [`Frame`] will be represented as a single byte,
530 /// that length cannot exceed 255 (`0xFF`).
531 ///
532 /// # Errors
533 ///
534 /// Returns [`FrameError::DataTooLong`] if the data length is greater than 255 (`0xFF`).
535 ///
536 /// # Examples
537 ///
538 /// ```
539 /// use flipdot_core::Data;
540 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
541 /// #
542 /// let data = Data::try_new(vec![1, 2, 3])?;
543 /// assert_eq!(vec![1, 2, 3], data.get().as_ref());
544 /// #
545 /// # Ok(()) }
546 /// ```
547 ///
548 /// Borrowed data can also be used:
549 ///
550 /// ```
551 /// # use flipdot_core::Data;
552 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
553 /// #
554 /// let bytes = vec![1, 2, 3];
555 /// let data = Data::try_new(&bytes)?;
556 /// assert_eq!(vec![1, 2, 3], data.get().as_ref());
557 /// #
558 /// # Ok(()) }
559 /// ```
560 ///
561 /// This will fail since the passed-in vector is too large:
562 ///
563 /// ```
564 /// # use flipdot_core::Data;
565 /// let result = Data::try_new(vec![0; 1000]);
566 /// assert!(result.is_err());
567 /// ```
568 pub fn try_new<T: Into<Cow<'a, [u8]>>>(data: T) -> Result<Self, FrameError> {
569 let data: Cow<'a, [u8]> = data.into();
570 if data.len() > 0xFF {
571 return Err(FrameError::DataTooLong {
572 max: 0xFF,
573 actual: data.len(),
574 });
575 }
576 Ok(Data(data))
577 }
578
579 /// Returns a reference to the inner [`Cow`]`<[u8]>`.
580 ///
581 /// # Examples
582 ///
583 /// ```
584 /// # use flipdot_core::Data;
585 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
586 /// #
587 /// let data = Data::try_new(vec![])?;
588 /// assert!(data.get().is_empty());
589 /// #
590 /// # Ok(()) }
591 /// ```
592 pub fn get(&self) -> &Cow<'a, [u8]> {
593 &self.0
594 }
595}
596
597// Data is mostly used with small static arrays that obviously fit in the 255-byte limit,
598// so create some From impls that make that case simple. We unfortunately can't be generic
599// over integers yet, so use a macro to implement for common array lengths.
600macro_rules! impl_from_array_ref_with_length {
601 ($length:expr) => {
602 impl From<&'static [u8; $length]> for Data<'static> {
603 fn from(value: &'static [u8; $length]) -> Data<'static> {
604 Data::try_new(&value[..]).unwrap()
605 }
606 }
607 };
608}
609
610impl_from_array_ref_with_length!(0);
611impl_from_array_ref_with_length!(1);
612impl_from_array_ref_with_length!(2);
613impl_from_array_ref_with_length!(3);
614impl_from_array_ref_with_length!(4);
615
616#[cfg(test)]
617mod tests {
618 use super::*;
619 use std::error::Error;
620
621 #[test]
622 fn roundtrip_simple_frame() -> Result<(), Box<dyn Error>> {
623 let frame = Frame::new(Address(0x7F), MsgType(0x02), Data::from(&[0xFF]));
624
625 let encoded = frame.to_bytes();
626 let decoded = Frame::from_bytes(&encoded)?;
627
628 assert_eq!(b":01007F02FF7F", encoded.as_slice());
629 assert_eq!(frame, decoded);
630
631 Ok(())
632 }
633
634 #[test]
635 fn roundtrip_complex_frame() -> Result<(), Box<dyn Error>> {
636 let data = Data::try_new(vec![
637 0x01, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x7F, 0x06, 0x0C, 0x18, 0x7F, 0x7F, 0x00,
638 ])?;
639 let frame = Frame::new(Address(0x00), MsgType(0x00), data);
640
641 let encoded = frame.to_bytes();
642 let decoded = Frame::from_bytes(&encoded)?;
643
644 assert_eq!(&b":1000000001100000000000007F7F060C187F7F00B9"[..], encoded.as_slice());
645 assert_eq!(frame, decoded);
646
647 Ok(())
648 }
649
650 #[test]
651 fn roundtrip_complex_frame_newline() -> Result<(), Box<dyn Error>> {
652 let data = Data::try_new(vec![
653 0x01, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x7F, 0x06, 0x0C, 0x18, 0x7F, 0x7F, 0x00,
654 ])?;
655 let frame = Frame::new(Address(0x00), MsgType(0x00), data);
656
657 let encoded = frame.to_bytes_with_newline();
658 let decoded = Frame::from_bytes(&encoded)?;
659
660 assert_eq!(&b":1000000001100000000000007F7F060C187F7F00B9\r\n"[..], encoded.as_slice());
661 assert_eq!(frame, decoded);
662
663 Ok(())
664 }
665
666 #[test]
667 fn roundtrip_empty_data() -> Result<(), Box<dyn Error>> {
668 let frame = Frame::new(Address(0x2B), MsgType(0xA9), Data::from(&[]));
669
670 let encoded = frame.to_bytes();
671 let decoded = Frame::from_bytes(&encoded)?;
672
673 assert_eq!(b":00002BA92C", encoded.as_slice());
674 assert_eq!(frame, decoded);
675
676 Ok(())
677 }
678
679 #[test]
680 fn data_length_over_255_rejected() {
681 let error = Data::try_new(vec![0; 256]).unwrap_err();
682 assert!(matches!(
683 error,
684 FrameError::DataTooLong {
685 max: 255,
686 actual: 256,
687 ..
688 }
689 ));
690 }
691
692 #[test]
693 fn newline_accepted() -> Result<(), Box<dyn Error>> {
694 let decoded = Frame::from_bytes(b":01007F02FF7F\r\n")?;
695 assert_eq!(Frame::new(Address(0x7F), MsgType(0x02), Data::from(&[0xFF])), decoded);
696 Ok(())
697 }
698
699 #[test]
700 fn bad_checksum_detected() {
701 let error = Frame::from_bytes(b":01007F02FF7E").unwrap_err();
702 assert!(matches!(
703 error,
704 FrameError::BadChecksum {
705 expected: 0x7E,
706 actual: 0x7F,
707 ..
708 }
709 ));
710 }
711
712 #[test]
713 fn extra_data_detected() {
714 let error = Frame::from_bytes(b":00007F02007F").unwrap_err();
715 assert!(matches!(
716 error,
717 FrameError::FrameDataMismatch {
718 expected: 0,
719 actual: 1,
720 ..
721 }
722 ));
723 }
724
725 #[test]
726 fn missing_data_detected() {
727 let error = Frame::from_bytes(b":01007F027E").unwrap_err();
728 assert!(matches!(
729 error,
730 FrameError::FrameDataMismatch {
731 expected: 1,
732 actual: 0,
733 ..
734 }
735 ));
736 }
737
738 #[test]
739 fn invalid_format_detected() {
740 let error = Frame::from_bytes(b":01").unwrap_err();
741 assert!(matches!(error, FrameError::InvalidFrame { .. }));
742 }
743
744 #[test]
745 fn garbage_detected() {
746 let error = Frame::from_bytes(b"asdgdfg").unwrap_err();
747 assert!(matches!(error, FrameError::InvalidFrame { .. }));
748 }
749
750 #[test]
751 fn bad_char_detected() {
752 let error = Frame::from_bytes(b":01007F020z7E").unwrap_err();
753 assert!(matches!(error, FrameError::InvalidFrame { .. }));
754 }
755
756 #[test]
757 fn missing_char_detected() {
758 let error = Frame::from_bytes(b":01007F0207E").unwrap_err();
759 assert!(matches!(error, FrameError::InvalidFrame { .. }));
760 }
761
762 #[test]
763 fn leading_chars_detected() {
764 let error = Frame::from_bytes(b"abc:01007F02FF7Fa").unwrap_err();
765 assert!(matches!(error, FrameError::InvalidFrame { .. }));
766 }
767
768 #[test]
769 fn trailing_chars_detected() {
770 let error = Frame::from_bytes(b":01007F02FF7Fabc").unwrap_err();
771 assert!(matches!(error, FrameError::InvalidFrame { .. }));
772 }
773
774 #[test]
775 fn parsed_lifetime_independent() -> Result<(), Box<dyn Error>> {
776 let decoded = {
777 let string = b":01007F02FF7F".to_owned();
778 Frame::from_bytes(&string)?
779 };
780 assert_eq!(Frame::new(Address(0x7F), MsgType(0x02), Data::from(&[0xFF])), decoded);
781 Ok(())
782 }
783
784 #[test]
785 fn getters() {
786 let frame = Frame::new(Address(0x7F), MsgType(0x02), Data::from(&[0xFF]));
787 assert_eq!(frame.message_type(), MsgType(0x02));
788 assert_eq!(frame.address(), Address(0x7F));
789 assert_eq!(frame.data(), &vec![0xFFu8]);
790 }
791
792 #[test]
793 fn write() -> Result<(), Box<dyn Error>> {
794 let frame = Frame::new(Address(0x7F), MsgType(0x02), Data::from(&[0xFF]));
795 let mut output = Vec::new();
796 frame.write(&mut output)?;
797 assert_eq!(b":01007F02FF7F\r\n", output.as_slice());
798 Ok(())
799 }
800
801 #[test]
802 fn read() -> Result<(), Box<dyn Error>> {
803 let mut buffer = &b":01007F02FF7F\r\n"[..];
804 let frame = Frame::read(&mut buffer)?;
805 assert_eq!(Frame::new(Address(0x7F), MsgType(0x02), Data::from(&[0xFF])), frame);
806 Ok(())
807 }
808
809 #[test]
810 fn display() {
811 let frame = Frame::new(Address(0x7F), MsgType(0x02), Data::from(&[0xFF, 0xCB]));
812 let display = format!("{}", frame);
813 assert_eq!("Type 02 | Addr 007F | Data FF CB", display.trim());
814 }
815}