1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
//! Winsen Infrared CO2 Module MH-Z19 / MH-Z19B / MH-Z14 serial "api" implementation.
//!
//! [MH-Z19 Datasheet](https://www.winsen-sensor.com/d/files/PDF/Infrared%20Gas%20Sensor/NDIR%20CO2%20SENSOR/MH-Z19%20CO2%20Ver1.0.pdf)
//!
//! [MH-Z19B Datasheet](https://www.winsen-sensor.com/d/files/infrared-gas-sensor/mh-z19b-co2-ver1_0.pdf)
//!
//! [MH-Z14 Datasheet](https://www.winsen-sensor.com/d/files/infrared-gas-sensor/mh-z14a_co2-manual-v1_01.pdf)
//!
//! This crates proposes two kinds of functions:
//! - functions for parsing response read from the uart
//! - functions to create command payload to send to the sensor though the uart.
//!

#![cfg_attr(not(feature = "std"), no_std)]

#[cfg(not(feature = "std"))]
use core::fmt;
#[cfg(feature = "std")]
use std::fmt;

/// MH-Z12 Commands
enum Command {
    /// Read the gas concentration
    ReadGasConcentration,
    /// Execute a zero point calibration.
    ///
    /// The sensor must be in a stable gas environment for 20 minutes with a co2 concentration of 400ppm.
    CalibrateZero,
    /// Execute a span point calibration
    CalibrateSpan,
    /// Enable or disable Automatic Baseline Correction (MH-Z19B only)
    SetAutomaticBaselineCorrection,
    /// Set the sensor range detection (2000 or 5000 MH-Z19B only)
    SetSensorDetectionRange,
}

impl Command {
    fn get_command_value(&self) -> u8 {
        use Command::*;
        match self {
            ReadGasConcentration => 0x86,
            CalibrateZero => 0x87,
            CalibrateSpan => 0x88,
            SetAutomaticBaselineCorrection => 0x79,
            SetSensorDetectionRange => 0x99,
        }
    }
}

/// Both input and output packets are 9 bytes long
pub type Packet = [u8; 9];

/// Get the command packet with proper header and checksum.
fn get_command_with_bytes34(command: Command, device_number: u8, byte3: u8, byte4: u8) -> Packet {
    let mut ret: Packet = [
        0xFF,
        device_number,
        command.get_command_value(),
        byte3,
        byte4,
        0x00,
        0x00,
        0x00,
        0x00,
    ];
    ret[8] = checksum(&ret[1..8]);
    ret
}

/// Create a command to read the gas concentration of the sensor.
pub fn read_gas_concentration(device_number: u8) -> Packet {
    get_command_with_bytes34(Command::ReadGasConcentration, device_number, 0x00, 0x00)
}

/// Create a command to enable or disable Automatic Baseline Correction (ABC)
pub fn set_automatic_baseline_correction(device_number: u8, enabled: bool) -> Packet {
    get_command_with_bytes34(
        Command::SetAutomaticBaselineCorrection,
        device_number,
        if enabled { 0xA0 } else { 0x00 },
        0x00,
    )
}

/// Create a command to calibrate the span point.
///
/// Quoting the datasheet: "Note: Pls do ZERO calibration before span calibration
/// Please make sure the sensor worked under a certain level co2 for over 20 minutes.
///
/// Suggest using 2000ppm as span, at least 1000ppm"
pub fn calibrate_span_point(device_number: u8, value: u16) -> Packet {
    get_command_with_bytes34(
        Command::CalibrateSpan,
        device_number,
        ((value & 0xff00) >> 8) as u8,
        (value & 0xff) as u8,
    )
}

/// Create a command to set the sensor detection range (MH-Z19B only).
///
/// Quoting the datasheet: "Detection range is 2000 or 5000ppm"
pub fn set_detection_range(device_number: u8, value: u16) -> Packet {
    get_command_with_bytes34(
        Command::SetSensorDetectionRange,
        device_number,
        ((value & 0xff00) >> 8) as u8,
        (value & 0xff) as u8,
    )
}

/// Create a command to calibrate the zero point.
///
/// Quoting the datasheet: "Note:Zero point is 400ppm, please make sure the sensor has
/// been worked under 400ppm for over 20 minutes"
pub fn calibrate_zero_point(device_number: u8) -> Packet {
    get_command_with_bytes34(Command::CalibrateZero, device_number, 0x00, 0x00)
}

/// 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
fn checksum(payload: &[u8]) -> u8 {
    1u8.wrapping_add(0xff - payload.iter().fold(0u8, |sum, c| sum.wrapping_add(*c)))
}

/// Extract the payload from a packet, validating packet length, checksum & header.
pub fn parse_payload(packet: &[u8]) -> Result<&[u8], MHZ19Error> {
    use MHZ19Error::*;
    if packet.len() != 9 {
        return Err(WrongPacketLength(packet.len()));
    }
    let header = packet[0];
    if header != 0xFF {
        return Err(WrongStartByte(header));
    }
    let payload = &packet[1..8];
    let found_checksum = packet[8];
    let payload_checksum = checksum(payload);
    if found_checksum != payload_checksum {
        return Err(WrongChecksum(payload_checksum, found_checksum));
    }

    Ok(payload)
}

/// Get the CO2 gas concentration in ppm from a response packet.
///
/// Will return an error if the packet is not a "read gas concentration packet"
pub fn parse_gas_concentration_ppm(packet: &[u8]) -> Result<u32, MHZ19Error> {
    let payload = parse_payload(packet)?;
    if payload[0] != Command::ReadGasConcentration.get_command_value() {
        Err(MHZ19Error::WrongPacketType(
            Command::ReadGasConcentration.get_command_value(),
            payload[0],
        ))
    } else {
        Ok(256 * (payload[1] as u32) + (payload[2] as u32))
    }
}

/// Get the CO2 gas concentration in ppm from a response packet.
///
/// Will return an error if the packet is not a "read gas concentration packet"
#[deprecated = "Please use `parse_gas_concentration_ppm` instead"]
pub fn parse_gas_contentration_ppm(packet: &[u8]) -> Result<u32, MHZ19Error> {
    parse_gas_concentration_ppm(packet)
}

#[derive(Debug, PartialEq)]
pub enum MHZ19Error {
    /// Packet of bytes has the wrong size
    WrongPacketLength(usize),
    /// Packet of bytes has the wrong checksum
    WrongChecksum(u8, u8),
    /// Wrong start byte (must be 0xFF)
    WrongStartByte(u8),
    /// The packet type is not the one excepting (eg must be 0x86 when reading gas concentration)
    WrongPacketType(u8, u8),
}

#[cfg(feature = "std")]
impl std::error::Error for MHZ19Error {}

impl fmt::Display for MHZ19Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        use MHZ19Error::*;
        match self {
            WrongChecksum(expected, found) => write!(
                f,
                "Invalid checksum, expected {:X}, found {:X}",
                expected, found
            ),
            WrongPacketLength(found) => {
                write!(f, "Wrong packet length, expected 9, found {}", found)
            }
            WrongStartByte(found) => {
                write!(f, "Wrong start byte, expected 0xFF, found {:X}", found)
            }
            WrongPacketType(expected, found) => write!(
                f,
                "Wrong packet type, expected {}, found {:X}",
                expected, found
            ),
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;

    /// The command payload used to query the co2 cas concentration
    /// from the device number 1 (the default device number)
    ///
    /// It is the same value as the result of
    /// get_command_packet(Command::ReadGasConcentration, 1).
    static READ_GAS_CONCENTRATION_COMMAND_ON_DEV1_PACKET: &'static [u8] =
        &[0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79];

    #[test]
    fn test_checksum() {
        assert_eq!(0x79, checksum(&[0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00]));
        assert_eq!(0xA0, checksum(&[0x01, 0x88, 0x07, 0xD0, 0x00, 0x00, 0x00]));
        assert_eq!(0xD1, checksum(&[0x86, 0x02, 0x60, 0x47, 0x00, 0x00, 0x00]));
    }

    #[test]
    fn test_get_payload() {
        assert_eq!(Err(MHZ19Error::WrongPacketLength(0)), parse_payload(&[]));
        assert_eq!(Err(MHZ19Error::WrongPacketLength(1)), parse_payload(&[12]));
        assert_eq!(
            Err(MHZ19Error::WrongPacketLength(12)),
            parse_payload(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
        );
        assert_eq!(
            Err(MHZ19Error::WrongStartByte(10)),
            parse_payload(&[10, 2, 3, 4, 5, 6, 7, 8, 9])
        );
        assert_eq!(
            Err(MHZ19Error::WrongChecksum(221, 9)),
            parse_payload(&[0xFF, 2, 3, 4, 5, 6, 7, 8, 9])
        );
        assert_eq!(
            Err(MHZ19Error::WrongChecksum(0xD1, 0x10)),
            parse_payload(&[0xFF, 0x86, 0x02, 0x60, 0x47, 0x00, 0x00, 0x00, 0x10])
        );
        assert_eq!(
            Ok(&[0x86, 0x02, 0x60, 0x47, 0x00, 0x00, 0x00][..]),
            parse_payload(&[0xFF, 0x86, 0x02, 0x60, 0x47, 0x00, 0x00, 0x00, 0xD1])
        );
    }

    #[test]
    fn test_get_command_packet() {
        assert_eq!(
            [0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79],
            get_command_with_bytes34(Command::ReadGasConcentration, 1, 0, 0)
        );
        assert_eq!(
            Ok(&[0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00][..]),
            parse_payload(&get_command_with_bytes34(
                Command::ReadGasConcentration,
                1,
                0,
                0
            ))
        );
        assert_eq!(
            READ_GAS_CONCENTRATION_COMMAND_ON_DEV1_PACKET,
            get_command_with_bytes34(Command::ReadGasConcentration, 1, 0, 0)
        );
        assert_eq!(
            READ_GAS_CONCENTRATION_COMMAND_ON_DEV1_PACKET,
            read_gas_concentration(1)
        );

        // Check command values
        assert_eq!(
            super::Command::SetSensorDetectionRange.get_command_value(),
            set_detection_range(1, 1)[2]
        );
        assert_eq!(
            super::Command::CalibrateZero.get_command_value(),
            calibrate_zero_point(1)[2]
        );
        assert_eq!(
            super::Command::CalibrateSpan.get_command_value(),
            calibrate_span_point(1, 1)[2]
        );
        assert_eq!(
            super::Command::ReadGasConcentration.get_command_value(),
            read_gas_concentration(1)[2]
        );
    }

    #[test]
    fn issue_3_op_precedence() {
        let p = set_detection_range(1, 0x07D0);
        assert_eq!(0x07, p[3]);
        assert_eq!(0xD0, p[4]);

        let p = calibrate_span_point(1, 0x07D0);
        assert_eq!(0x07, p[3]);
        assert_eq!(0xD0, p[4]);
    }
}