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}