mh_zx_driver/
lib.rs

1/*
2 Copyright 2020 Constantine Verutin
3
4 Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17#![no_std]
18
19//! # MH-Z* CO2 sensor driver
20//!
21//! MH-Z* family CO2 sensor driver built on top of `embedded-hal` primitives.
22//! This is a `no_std` crate suitable for use on bare-metal.
23//!
24//! ## Usage
25//! The [`Sensor`](struct.Sensor.html) struct exposes methods to
26//! send commands ([`write_packet_op`](struct.Sensor.html#method.write_packet_op))
27//! to the sensor and to read the response([`read_packet_op`](struct.Sensor.html#method.read_packet_op)).
28//!
29//! ## Example
30//! ```
31//! use mh_zx_driver::{commands, Sensor, Measurement};
32//! use nb::block;
33//! use core::convert::TryInto;
34//! # use embedded_hal_mock::serial::{Mock, Transaction};
35//!
36//! # let mut uart = Mock::new(&[
37//! #   Transaction::write_many(commands::READ_CO2.as_slice()),
38//! #   Transaction::flush(),
39//! #   Transaction::read_many(&[
40//! #     0xFF, 0x86, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79,
41//! #   ]),
42//! # ]);
43//! let mut sensor = Sensor::new(uart);
44//! // Send command to the sensor.
45//! {
46//!   let mut op = sensor.write_packet_op(commands::READ_CO2);
47//!   block!(op()).unwrap();
48//! };
49//! // Read the response.
50//! let mut packet = Default::default();
51//! {
52//!   let mut op = sensor.read_packet_op(&mut packet);
53//!   block!(op()).unwrap();
54//! }
55//! let meas: Measurement = packet.try_into().unwrap();
56//! println!("CO2 concentration: {}", meas.co2);
57//! ```
58//!
59//! ## [`Future`](../futures/future/trait.Future.html) support
60//! The experimental support for `async`/`await` can be activated by enabling the `async` feature in `Cargo.toml`.
61//! It adds async methods to the [`Sensor`](struct.Sensor.html) struct.
62
63use embedded_hal::serial::{Read, Write};
64use nb::Result;
65
66use core::convert::TryFrom;
67
68#[cfg(feature = "async")]
69pub mod futures_compat;
70#[cfg(feature = "async")]
71use futures_compat::nb_fn;
72
73const PAYLOAD_SIZE: usize = 9;
74
75/// A wrapper for payload (9 bytes) sent/received by sensor hardware with some utility methods.
76#[derive(Debug, Default)]
77pub struct Packet([u8; PAYLOAD_SIZE]);
78
79impl Packet {
80    /// Returns packet checksum.
81    fn checksum(&self) -> u8 {
82        (!self
83            .0
84            .iter()
85            .skip(1)
86            .take(7)
87            .fold(0u8, |s, i| s.wrapping_add(*i)))
88        .wrapping_add(1)
89    }
90
91    /// Verifies packet checksum.
92    fn checksum_valid(&self) -> bool {
93        self.checksum() == self.0[8]
94    }
95
96    /// Returns underlying byte payload as slice.
97    pub fn as_slice(&self) -> &[u8] {
98        &self.0
99    }
100}
101
102/// A struct representing measurement data returned by sensor as a response to
103/// the [`READ_CO2`](commands/constant.READ_CO2.html) command.
104pub struct Measurement {
105    /// CO2 concentration, PPM.
106    pub co2: u16,
107    /// Temperature, degrees Celsius plus 40.
108    pub temp: u8,
109    /// If ABC is turned on - counter in "ticks" within a calibration cycle.
110    pub calib_ticks: u8,
111    /// If ABC is turned on - the nuumber of performed calibration cycles.
112    pub calib_cycles: u8,
113}
114
115/// A struct representing raw CO2 data returned by sensor as a response to
116/// the [`READ_RAW_CO2`](commands/constant.READ_RAW_CO2.html) command.
117pub struct RawMeasurement {
118    // Smoothed temperature ADC value.
119    pub adc_temp: u16,
120    // CO2 level before clamping
121    pub co2: u16,
122    // Minimum light ADC value.
123    pub adc_min_light: u16,
124}
125
126#[derive(Debug)]
127pub struct InvalidResponse(Packet);
128
129impl TryFrom<Packet> for Measurement {
130    type Error = InvalidResponse;
131
132    fn try_from(p: Packet) -> core::result::Result<Self, Self::Error> {
133        if p.0[0] != 0xFF || p.0[1] != 0x86 || !p.checksum_valid() {
134            return Err(InvalidResponse(p));
135        }
136
137        let Packet([_, _, ch, cl, temp, calib_ticks, calib_cycles, _, _]) = p;
138        Ok(Measurement {
139            co2: u16::from_be_bytes([ch, cl]),
140            temp,
141            calib_ticks,
142            calib_cycles,
143        })
144    }
145}
146
147impl TryFrom<Packet> for RawMeasurement {
148    type Error = InvalidResponse;
149
150    fn try_from(p: Packet) -> core::result::Result<Self, Self::Error> {
151        if p.0[0] != 0xFF || p.0[1] != 0x85 || !p.checksum_valid() {
152            return Err(InvalidResponse(p));
153        }
154
155        let Packet([_, _, th, tl, ch, cl, lh, ll, _]) = p;
156        Ok(RawMeasurement {
157            adc_temp: u16::from_be_bytes([th, tl]),
158            co2: u16::from_be_bytes([ch, cl]),
159            adc_min_light: u16::from_be_bytes([lh, ll]),
160        })
161    }
162}
163
164pub mod commands {
165    use super::Packet;
166
167    /// Read "final" CO2 concentration.
168    pub const READ_CO2: &Packet = &Packet([0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79]);
169    /// Read raw CO2 concentration.
170    pub const READ_RAW_CO2: &Packet =
171        &Packet([0xFF, 0x01, 0x85, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7a]);
172}
173
174pub struct UartWrapper<R, W>(R, W);
175
176impl<R, W> Read<u8> for UartWrapper<R, W>
177where
178    R: Read<u8>,
179{
180    type Error = R::Error;
181    fn read(&mut self) -> Result<u8, R::Error> {
182        self.0.read()
183    }
184}
185
186impl<R, W> Write<u8> for UartWrapper<R, W>
187where
188    W: Write<u8>,
189{
190    type Error = W::Error;
191    fn write(&mut self, word: u8) -> Result<(), W::Error> {
192        self.1.write(word)
193    }
194    fn flush(&mut self) -> Result<(), W::Error> {
195        self.1.flush()
196    }
197}
198
199/// A struct representing sensor interface.
200pub struct Sensor<U> {
201    uart: U,
202}
203
204impl<R, W> Sensor<UartWrapper<R, W>>
205where
206    R: Read<u8>,
207    W: Write<u8>,
208{
209    //! Constructs the [`Sensor`](struct.Sensor.html) interface from 2 'halves' of UART.
210    pub fn from_rx_tx(read: R, write: W) -> Sensor<UartWrapper<R, W>> {
211        Sensor {
212            uart: UartWrapper(read, write),
213        }
214    }
215}
216
217impl<U> Sensor<U>
218where
219    U: Read<u8> + Write<u8>,
220{
221    pub fn new(uart: U) -> Sensor<U> {
222        Sensor { uart }
223    }
224
225    /// Write a packet to the device.
226    ///
227    /// Returns a closure that sends a packet to the sensor.
228    /// The result of this function can be used with the
229    /// [`block!()`](../nb/macro.block.html) macro from
230    /// [`nb`](../nb/index.html) crate, e.g.:
231    /// ```
232    /// # use embedded_hal_mock::serial::{Mock, Transaction};
233    /// # use mh_zx_driver::{commands, Sensor};
234    /// # use nb::block;
235    ///
236    /// # let mut uart = Mock::new(&[
237    /// #   Transaction::write_many(commands::READ_CO2.as_slice()),
238    /// #   Transaction::flush(),
239    /// # ]);
240    /// # let mut sensor = Sensor::new(uart.clone());
241    /// let mut op = sensor.write_packet_op(commands::READ_CO2);
242    /// block!(op()).unwrap();
243    /// # uart.done()
244    /// ```
245    pub fn write_packet_op<'a>(
246        &'a mut self,
247        packet: &'a Packet,
248    ) -> impl FnMut() -> Result<(), <U as Write<u8>>::Error> + 'a {
249        let mut i = 0usize;
250        move || {
251            while i < PAYLOAD_SIZE {
252                self.uart.write(packet.0[i])?;
253                i += 1;
254            }
255            self.uart.flush()
256        }
257    }
258
259    #[cfg(feature = "async")]
260    /// Asynchronously write a packet to the device.
261    ///
262    /// Returns a [`Future`](../futures/future/trait.Future.html) that can be
263    /// used in `async` code, e.g.:
264    /// ```
265    /// # use embedded_hal_mock::serial::{Mock, Transaction};
266    /// # use mh_zx_driver::{commands, Sensor, Packet};
267    /// use futures::executor::block_on;
268    ///
269    /// # let mut uart = Mock::new(&[
270    /// #   Transaction::write_many(commands::READ_CO2.as_slice()),
271    /// #   Transaction::flush(),
272    /// # ]);
273    /// # let mut sensor = Sensor::new(uart.clone());
274    /// block_on(async {
275    ///     sensor.write_packet(commands::READ_CO2).await.unwrap()
276    /// });
277    /// # uart.done()
278    /// ```
279    pub async fn write_packet<'a>(
280        &'a mut self,
281        packet: &'a Packet,
282    ) -> core::result::Result<(), <U as Write<u8>>::Error>
283    {
284        nb_fn(self.write_packet_op(packet)).await
285    }
286
287    /// Read a packet from the device.
288    ///
289    /// Returns a closure that reads a response packet from the sensor.
290    /// The result of this function can be used with the
291    /// [`block!()`](../nb/macro.block.html) macro from
292    /// [`nb`](../nb/index.html) crate, e.g.:
293    /// ```
294    /// # use embedded_hal_mock::serial::{Mock, Transaction};
295    /// # use mh_zx_driver::{commands, Sensor, Packet};
296    /// # use nb::block;
297    ///
298    /// # let mut uart = Mock::new(&[
299    /// #   Transaction::read_many(&[
300    /// #     0xFF, 0x86, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79,
301    /// #   ]),
302    /// # ]);
303    /// # let mut sensor = Sensor::new(uart.clone());
304    /// let mut packet = Default::default();
305    /// let mut op = sensor.read_packet_op(&mut packet);
306    /// block!(op()).unwrap();
307    /// # uart.done()
308    /// ```
309    pub fn read_packet_op<'a>(
310        &'a mut self,
311        packet: &'a mut Packet,
312    ) -> impl FnMut() -> Result<(), <U as Read<u8>>::Error> + 'a {
313        let mut i = 0usize;
314        move || {
315            while i < PAYLOAD_SIZE {
316                packet.0[i] = self.uart.read()?;
317                i += 1;
318            }
319            Ok(())
320        }
321    }
322
323    #[cfg(feature = "async")]
324    /// Asynchronously read a packet from the device.
325    ///
326    /// Returns a [`Future`](../futures/future/trait.Future.html) that can be
327    /// used in `async` code, e.g.:
328    /// ```
329    /// # use embedded_hal_mock::serial::{Mock, Transaction};
330    /// # use mh_zx_driver::{commands, Sensor, Packet};
331    /// use futures::executor::block_on;
332    ///
333    /// # let mut uart = Mock::new(&[
334    /// #   Transaction::read_many(&[
335    /// #     0xFF, 0x86, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79,
336    /// #   ]),
337    /// # ]);
338    /// # let mut sensor = Sensor::new(uart.clone());
339    /// let mut packet = Default::default();
340    /// block_on(async {
341    ///     sensor.read_packet(&mut packet).await.unwrap()
342    /// });
343    /// # uart.done()
344    /// ```
345    pub async fn read_packet<'a>(
346        &'a mut self,
347        packet: &'a mut Packet,
348    ) -> core::result::Result<(), <U as Read<u8>>::Error>
349    {
350        nb_fn(self.read_packet_op(packet)).await
351    }
352}
353
354#[cfg(test)]
355mod tests {
356    use embedded_hal_mock::serial::{Mock, Transaction};
357    use nb::block;
358
359    use super::*;
360    use core::convert::TryInto;
361
362    #[cfg(feature = "async")]
363    use futures::executor::block_on;
364
365    #[test]
366    fn sensor_rx_tx() {
367        let mut rx = Mock::new(&[
368            Transaction::read_error(nb::Error::WouldBlock),
369            Transaction::read(0xFF),
370            Transaction::read_error(nb::Error::WouldBlock),
371            Transaction::read_error(nb::Error::WouldBlock),
372            Transaction::read_many(&[0x86, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79]),
373        ]);
374        let mut tx = Mock::new(&[
375            Transaction::write_error(0xFF, nb::Error::WouldBlock),
376            Transaction::write_error(0xFF, nb::Error::WouldBlock),
377            Transaction::write_error(0xFF, nb::Error::WouldBlock),
378            Transaction::write_many(commands::READ_CO2.0),
379            Transaction::flush(),
380        ]);
381
382        let mut s = Sensor::from_rx_tx(rx.clone(), tx.clone());
383
384        {
385            let mut op = s.write_packet_op(commands::READ_CO2);
386            block!(op()).unwrap()
387        }
388        let mut p = Default::default();
389        {
390            let mut op = s.read_packet_op(&mut p);
391            block!(op()).unwrap()
392        }
393
394        let _m: Measurement = p.try_into().unwrap();
395        rx.done();
396        tx.done();
397    }
398
399    #[cfg(feature = "async")]
400    #[test]
401    fn async_rx_tx() {
402        let mut rx = Mock::new(&[
403            Transaction::read_error(nb::Error::WouldBlock),
404            Transaction::read(0xFF),
405            Transaction::read_error(nb::Error::WouldBlock),
406            Transaction::read_error(nb::Error::WouldBlock),
407            Transaction::read_many(&[0x86, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79]),
408        ]);
409        let mut tx = Mock::new(&[
410            Transaction::write_error(0xFF, nb::Error::WouldBlock),
411            Transaction::write_error(0xFF, nb::Error::WouldBlock),
412            Transaction::write_error(0xFF, nb::Error::WouldBlock),
413            Transaction::write_many(commands::READ_CO2.0),
414            Transaction::flush(),
415        ]);
416
417        let mut s = Sensor::from_rx_tx(rx.clone(), tx.clone());
418
419        let f = async {
420            s.write_packet(commands::READ_CO2).await.unwrap();
421            let mut p = Default::default();
422            s.read_packet(&mut p).await.unwrap();
423            p
424        };
425
426        let p = block_on(f);
427
428        let _m: Measurement = p.try_into().unwrap();
429        rx.done();
430        tx.done();
431    }
432
433    #[test]
434    fn parse_measurement() {
435        let p: Packet = Packet([0xFF, 0x86, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79]);
436
437        let _m: Measurement = p.try_into().unwrap();
438
439        // checksum mismatch
440        let p: Packet = Packet([0xFF, 0x86, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78]);
441        assert!(!p.checksum_valid());
442        let res: core::result::Result<Measurement, _> = p.try_into();
443        assert!(res.is_err());
444
445        // invalid command field
446        let p: Packet = Packet([0xFF, 0x87, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78]);
447        assert!(p.checksum_valid());
448        let res: core::result::Result<Measurement, _> = p.try_into();
449        assert!(res.is_err());
450
451        // byte0 is not 0xFF
452        let p: Packet = Packet([0xFE, 0x86, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79]);
453        assert!(p.checksum_valid());
454        let res: core::result::Result<Measurement, _> = p.try_into();
455        assert!(res.is_err());
456    }
457
458    #[test]
459    fn packet_checksum() {
460        let mut p: Packet = Packet([0xFF, 0x86, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79]);
461
462        assert!(p.checksum_valid());
463
464        for i in 1..PAYLOAD_SIZE - 1 {
465            let b = p.0[i];
466            p.0[i] = b.wrapping_add(1);
467            assert!(!p.checksum_valid());
468            p.0[i] = b;
469        }
470
471        assert!(commands::READ_CO2.checksum_valid());
472        assert!(commands::READ_RAW_CO2.checksum_valid());
473    }
474}