mh_z19/
lib.rs

1//! Winsen Infrared CO2 Module MH-Z19 / MH-Z19B / MH-Z14 serial "api" implementation.
2//!
3//! [MH-Z19 Datasheet](https://www.winsen-sensor.com/d/files/PDF/Infrared%20Gas%20Sensor/NDIR%20CO2%20SENSOR/MH-Z19%20CO2%20Ver1.0.pdf)
4//!
5//! [MH-Z19B Datasheet](https://www.winsen-sensor.com/d/files/infrared-gas-sensor/mh-z19b-co2-ver1_0.pdf)
6//!
7//! [MH-Z14 Datasheet](https://www.winsen-sensor.com/d/files/infrared-gas-sensor/mh-z14a_co2-manual-v1_01.pdf)
8//!
9//! This crates proposes two kinds of functions:
10//! - functions for parsing response read from the uart
11//! - functions to create command payload to send to the sensor though the uart.
12//!
13
14#![cfg_attr(not(feature = "std"), no_std)]
15
16#[cfg(not(feature = "std"))]
17use core::fmt;
18#[cfg(feature = "std")]
19use std::fmt;
20
21/// MH-Z12 Commands
22enum Command {
23    /// Read the gas concentration
24    ReadGasConcentration,
25    /// Execute a zero point calibration.
26    ///
27    /// The sensor must be in a stable gas environment for 20 minutes with a co2 concentration of 400ppm.
28    CalibrateZero,
29    /// Execute a span point calibration
30    CalibrateSpan,
31    /// Enable or disable Automatic Baseline Correction (MH-Z19B only)
32    SetAutomaticBaselineCorrection,
33    /// Set the sensor range detection (2000 or 5000 MH-Z19B only)
34    SetSensorDetectionRange,
35}
36
37impl Command {
38    fn get_command_value(&self) -> u8 {
39        use Command::*;
40        match self {
41            ReadGasConcentration => 0x86,
42            CalibrateZero => 0x87,
43            CalibrateSpan => 0x88,
44            SetAutomaticBaselineCorrection => 0x79,
45            SetSensorDetectionRange => 0x99,
46        }
47    }
48}
49
50/// Both input and output packets are 9 bytes long
51pub type Packet = [u8; 9];
52
53/// Get the command packet with proper header and checksum.
54fn get_command_with_bytes34(command: Command, device_number: u8, byte3: u8, byte4: u8) -> Packet {
55    let mut ret: Packet = [
56        0xFF,
57        device_number,
58        command.get_command_value(),
59        byte3,
60        byte4,
61        0x00,
62        0x00,
63        0x00,
64        0x00,
65    ];
66    ret[8] = checksum(&ret[1..8]);
67    ret
68}
69
70/// Create a command to read the gas concentration of the sensor.
71pub fn read_gas_concentration(device_number: u8) -> Packet {
72    get_command_with_bytes34(Command::ReadGasConcentration, device_number, 0x00, 0x00)
73}
74
75/// Create a command to enable or disable Automatic Baseline Correction (ABC)
76pub fn set_automatic_baseline_correction(device_number: u8, enabled: bool) -> Packet {
77    get_command_with_bytes34(
78        Command::SetAutomaticBaselineCorrection,
79        device_number,
80        if enabled { 0xA0 } else { 0x00 },
81        0x00,
82    )
83}
84
85/// Create a command to calibrate the span point.
86///
87/// Quoting the datasheet: "Note: Pls do ZERO calibration before span calibration
88/// Please make sure the sensor worked under a certain level co2 for over 20 minutes.
89///
90/// Suggest using 2000ppm as span, at least 1000ppm"
91pub fn calibrate_span_point(device_number: u8, value: u16) -> Packet {
92    get_command_with_bytes34(
93        Command::CalibrateSpan,
94        device_number,
95        ((value & 0xff00) >> 8) as u8,
96        (value & 0xff) as u8,
97    )
98}
99
100/// Create a command to set the sensor detection range (MH-Z19B only).
101///
102/// Quoting the datasheet: "Detection range is 2000 or 5000ppm"
103pub fn set_detection_range(device_number: u8, value: u16) -> Packet {
104    get_command_with_bytes34(
105        Command::SetSensorDetectionRange,
106        device_number,
107        ((value & 0xff00) >> 8) as u8,
108        (value & 0xff) as u8,
109    )
110}
111
112/// Create a command to calibrate the zero point.
113///
114/// Quoting the datasheet: "Note:Zero point is 400ppm, please make sure the sensor has
115/// been worked under 400ppm for over 20 minutes"
116pub fn calibrate_zero_point(device_number: u8) -> Packet {
117    get_command_with_bytes34(Command::CalibrateZero, device_number, 0x00, 0x00)
118}
119
120/// Implementation of the checksum as defined in https://www.winsen-sensor.com/d/files/PDF/Infrared%20Gas%20Sensor/NDIR%20CO2%20SENSOR/MH-Z19%20CO2%20Ver1.0.pdf
121fn checksum(payload: &[u8]) -> u8 {
122    1u8.wrapping_add(0xff - payload.iter().fold(0u8, |sum, c| sum.wrapping_add(*c)))
123}
124
125/// Extract the payload from a packet, validating packet length, checksum & header.
126pub fn parse_payload(packet: &[u8]) -> Result<&[u8], MHZ19Error> {
127    use MHZ19Error::*;
128    if packet.len() != 9 {
129        return Err(WrongPacketLength(packet.len()));
130    }
131    let header = packet[0];
132    if header != 0xFF {
133        return Err(WrongStartByte(header));
134    }
135    let payload = &packet[1..8];
136    let found_checksum = packet[8];
137    let payload_checksum = checksum(payload);
138    if found_checksum != payload_checksum {
139        return Err(WrongChecksum(payload_checksum, found_checksum));
140    }
141
142    Ok(payload)
143}
144
145/// Get the CO2 gas concentration in ppm from a response packet.
146///
147/// Will return an error if the packet is not a "read gas concentration packet"
148pub fn parse_gas_concentration_ppm(packet: &[u8]) -> Result<u32, MHZ19Error> {
149    let payload = parse_payload(packet)?;
150    if payload[0] != Command::ReadGasConcentration.get_command_value() {
151        Err(MHZ19Error::WrongPacketType(
152            Command::ReadGasConcentration.get_command_value(),
153            payload[0],
154        ))
155    } else {
156        Ok(256 * (payload[1] as u32) + (payload[2] as u32))
157    }
158}
159
160/// Get the CO2 gas concentration in ppm from a response packet.
161///
162/// Will return an error if the packet is not a "read gas concentration packet"
163#[deprecated = "Please use `parse_gas_concentration_ppm` instead"]
164pub fn parse_gas_contentration_ppm(packet: &[u8]) -> Result<u32, MHZ19Error> {
165    parse_gas_concentration_ppm(packet)
166}
167
168#[derive(Debug, PartialEq)]
169pub enum MHZ19Error {
170    /// Packet of bytes has the wrong size
171    WrongPacketLength(usize),
172    /// Packet of bytes has the wrong checksum
173    WrongChecksum(u8, u8),
174    /// Wrong start byte (must be 0xFF)
175    WrongStartByte(u8),
176    /// The packet type is not the one excepting (eg must be 0x86 when reading gas concentration)
177    WrongPacketType(u8, u8),
178}
179
180#[cfg(feature = "std")]
181impl std::error::Error for MHZ19Error {}
182
183impl fmt::Display for MHZ19Error {
184    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
185        use MHZ19Error::*;
186        match self {
187            WrongChecksum(expected, found) => write!(
188                f,
189                "Invalid checksum, expected {:X}, found {:X}",
190                expected, found
191            ),
192            WrongPacketLength(found) => {
193                write!(f, "Wrong packet length, expected 9, found {}", found)
194            }
195            WrongStartByte(found) => {
196                write!(f, "Wrong start byte, expected 0xFF, found {:X}", found)
197            }
198            WrongPacketType(expected, found) => write!(
199                f,
200                "Wrong packet type, expected {}, found {:X}",
201                expected, found
202            ),
203        }
204    }
205}
206
207#[cfg(test)]
208mod test {
209    use super::*;
210
211    /// The command payload used to query the co2 cas concentration
212    /// from the device number 1 (the default device number)
213    ///
214    /// It is the same value as the result of
215    /// get_command_packet(Command::ReadGasConcentration, 1).
216    static READ_GAS_CONCENTRATION_COMMAND_ON_DEV1_PACKET: &'static [u8] =
217        &[0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79];
218
219    #[test]
220    fn test_checksum() {
221        assert_eq!(0x79, checksum(&[0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00]));
222        assert_eq!(0xA0, checksum(&[0x01, 0x88, 0x07, 0xD0, 0x00, 0x00, 0x00]));
223        assert_eq!(0xD1, checksum(&[0x86, 0x02, 0x60, 0x47, 0x00, 0x00, 0x00]));
224    }
225
226    #[test]
227    fn test_get_payload() {
228        assert_eq!(Err(MHZ19Error::WrongPacketLength(0)), parse_payload(&[]));
229        assert_eq!(Err(MHZ19Error::WrongPacketLength(1)), parse_payload(&[12]));
230        assert_eq!(
231            Err(MHZ19Error::WrongPacketLength(12)),
232            parse_payload(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
233        );
234        assert_eq!(
235            Err(MHZ19Error::WrongStartByte(10)),
236            parse_payload(&[10, 2, 3, 4, 5, 6, 7, 8, 9])
237        );
238        assert_eq!(
239            Err(MHZ19Error::WrongChecksum(221, 9)),
240            parse_payload(&[0xFF, 2, 3, 4, 5, 6, 7, 8, 9])
241        );
242        assert_eq!(
243            Err(MHZ19Error::WrongChecksum(0xD1, 0x10)),
244            parse_payload(&[0xFF, 0x86, 0x02, 0x60, 0x47, 0x00, 0x00, 0x00, 0x10])
245        );
246        assert_eq!(
247            Ok(&[0x86, 0x02, 0x60, 0x47, 0x00, 0x00, 0x00][..]),
248            parse_payload(&[0xFF, 0x86, 0x02, 0x60, 0x47, 0x00, 0x00, 0x00, 0xD1])
249        );
250    }
251
252    #[test]
253    fn test_get_command_packet() {
254        assert_eq!(
255            [0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79],
256            get_command_with_bytes34(Command::ReadGasConcentration, 1, 0, 0)
257        );
258        assert_eq!(
259            Ok(&[0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00][..]),
260            parse_payload(&get_command_with_bytes34(
261                Command::ReadGasConcentration,
262                1,
263                0,
264                0
265            ))
266        );
267        assert_eq!(
268            READ_GAS_CONCENTRATION_COMMAND_ON_DEV1_PACKET,
269            get_command_with_bytes34(Command::ReadGasConcentration, 1, 0, 0)
270        );
271        assert_eq!(
272            READ_GAS_CONCENTRATION_COMMAND_ON_DEV1_PACKET,
273            read_gas_concentration(1)
274        );
275
276        // Check command values
277        assert_eq!(
278            super::Command::SetSensorDetectionRange.get_command_value(),
279            set_detection_range(1, 1)[2]
280        );
281        assert_eq!(
282            super::Command::CalibrateZero.get_command_value(),
283            calibrate_zero_point(1)[2]
284        );
285        assert_eq!(
286            super::Command::CalibrateSpan.get_command_value(),
287            calibrate_span_point(1, 1)[2]
288        );
289        assert_eq!(
290            super::Command::ReadGasConcentration.get_command_value(),
291            read_gas_concentration(1)[2]
292        );
293    }
294
295    #[test]
296    fn issue_3_op_precedence() {
297        let p = set_detection_range(1, 0x07D0);
298        assert_eq!(0x07, p[3]);
299        assert_eq!(0xD0, p[4]);
300
301        let p = calibrate_span_point(1, 0x07D0);
302        assert_eq!(0x07, p[3]);
303        assert_eq!(0xD0, p[4]);
304    }
305}