Skip to main content

mh_z19c/
frame.rs

1//! Serial communication frame handling for the MH-Z19C sensor.
2
3use crate::command::Command;
4use core::convert::From;
5use core::fmt::{self, Display, Formatter};
6
7/// Calculates the checksum of `buf`.
8pub fn checksum(buf: &[u8]) -> u8 {
9    buf.iter()
10        .fold(0x00, |acc: u8, &x: &u8| acc.overflowing_sub(x).0)
11}
12
13const START_BYTE: u8 = 0xff;
14const COMMAND_MAGIC_BYTE: u8 = 0x01;
15
16/// Represents a frame for the serial communication.
17#[derive(Clone, Debug, Eq, Hash, PartialEq)]
18pub struct Frame([u8; 9]);
19
20impl From<Command> for Frame {
21    fn from(command: Command) -> Self {
22        let mut buf = [START_BYTE, COMMAND_MAGIC_BYTE, 0, 0, 0, 0, 0, 0, 0];
23        buf[2..8].copy_from_slice(&command.serialize());
24        buf[8] = checksum(&buf[1..8]);
25        Self(buf)
26    }
27}
28
29impl AsRef<[u8]> for Frame {
30    fn as_ref(&self) -> &[u8] {
31        &self.0
32    }
33}
34
35impl Frame {
36    /// Return a new initialized [`Frame`] struct.
37    pub fn new(data: [u8; 9]) -> Self {
38        Self(data)
39    }
40
41    /// Unwrap the frame data.
42    pub fn into_inner(self) -> [u8; 9] {
43        self.0
44    }
45
46    fn start_byte(&self) -> u8 {
47        self.0[0]
48    }
49
50    /// Returns `true` if the frame has a valid start byte.
51    pub fn has_valid_start_byte(&self) -> bool {
52        self.start_byte() == START_BYTE
53    }
54
55    /// Returns `true` if the frame represents the response to a command.
56    pub fn is_response(&self) -> bool {
57        self.0[1] != COMMAND_MAGIC_BYTE
58    }
59
60    /// Returns the op code byte of the command of a request or response.
61    pub fn op_code(&self) -> u8 {
62        if self.is_response() {
63            self.0[1]
64        } else {
65            self.0[2]
66        }
67    }
68
69    fn checksum(&self) -> u8 {
70        self.0[8]
71    }
72
73    /// Returns the command arguments or response data (without op code).
74    pub fn data(&self) -> &[u8] {
75        if self.is_response() {
76            &self.0[2..8]
77        } else {
78            &self.0[3..8]
79        }
80    }
81
82    /// Returns `true` if the frame's checksum is valid.
83    pub fn has_valid_checksum(&self) -> bool {
84        checksum(&self.0[1..8]) == self.checksum()
85    }
86
87    /// Validates the correctness of the frame.
88    ///
89    /// * Checks that the start byte is valid.
90    /// * Checks that the checksum is valid.
91    pub fn validate(&self) -> Result<(), ValidateFrameError> {
92        if !self.has_valid_start_byte() {
93            Err(ValidateFrameError::InvalidStartByte(self.start_byte()))
94        } else if !self.has_valid_checksum() {
95            Err(ValidateFrameError::InvalidChecksum {
96                expected: checksum(&self.0[1..8]),
97                actual: self.checksum(),
98            })
99        } else {
100            Ok(())
101        }
102    }
103}
104
105#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106pub enum ValidateFrameError {
107    /// Indicates that the start byte is invalid and provides that invalid byte.
108    InvalidStartByte(u8),
109    /// Indicates that the checksum is invalid.
110    InvalidChecksum { expected: u8, actual: u8 },
111}
112
113impl Display for ValidateFrameError {
114    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
115        use ValidateFrameError::*;
116        match self {
117            InvalidStartByte(got) => {
118                write!(f, "expected start byte 0xff, but got 0x{got:x}")
119            }
120            InvalidChecksum { expected, actual } => {
121                write!(f, "invalid checksum (got {actual} instead of {expected})",)
122            }
123        }
124    }
125}
126
127#[cfg(std)]
128impl std::error::Error for Error {}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use crate::command::Command;
134
135    #[test]
136    fn test_frame_read_co2_command() {
137        let frame = Frame::new([0xff, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79]);
138        assert!(frame.has_valid_start_byte());
139        assert!(!frame.is_response());
140        assert_eq!(frame.op_code(), 0x86);
141        assert_eq!(frame.data(), [0x00; 5]);
142        assert!(frame.has_valid_checksum());
143        assert!(frame.validate().is_ok());
144    }
145
146    #[test]
147    fn test_frame_read_co2_response() {
148        let frame = Frame::new([0xff, 0x86, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x77]);
149        assert!(frame.has_valid_start_byte());
150        assert!(frame.is_response());
151        assert_eq!(frame.op_code(), 0x86);
152        assert_eq!(frame.data(), [0x01, 0x02, 0x00, 0x00, 0x00, 0x00]);
153        assert!(frame.has_valid_checksum());
154        assert!(frame.validate().is_ok());
155    }
156
157    #[test]
158    fn test_frame_invalid_start_byte() {
159        let frame = Frame::new([0x00; 9]);
160        assert!(!frame.has_valid_start_byte());
161        assert_eq!(
162            frame.validate(),
163            Err(ValidateFrameError::InvalidStartByte(0x00))
164        );
165    }
166
167    #[test]
168    fn test_frame_invalid_checksum() {
169        let frame = Frame::new([0xff; 9]);
170        assert!(!frame.has_valid_checksum());
171        assert_eq!(
172            frame.validate(),
173            Err(ValidateFrameError::InvalidChecksum {
174                expected: 0x07,
175                actual: 0xff,
176            })
177        );
178    }
179
180    #[test]
181    fn test_frame_from_command() {
182        let frame: Frame = Command::SetSelfCalibrate(true).into();
183        assert!(frame.has_valid_start_byte());
184        assert!(!frame.is_response());
185        assert_eq!(frame.op_code(), Command::SetSelfCalibrate(true).op_code());
186        assert_eq!(
187            frame.data(),
188            &Command::SetSelfCalibrate(true).serialize()[1..]
189        );
190        assert!(frame.has_valid_checksum());
191        assert!(frame.validate().is_ok());
192    }
193
194    #[test]
195    fn test_checksum() {
196        assert_eq!(checksum(&[0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00]), 0x79);
197    }
198}