mics_vz_89te/
lib.rs

1#![cfg_attr(all(not(test), not(feature = "std")), no_std)]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3
4//! # Driver for MICS-VZ-89TE sensor
5//!
6//! This driver can be used to read CO2 and voc measurements of the MICS-VZ-89TE sensor.
7//! CO2 is measured in ppm in range from 400 to 2000. VOC is measured in ppb in range from 0 to 1000.
8//! At startup the sensor needs around 15 minutes to deliver a valid CO2 value, as noted in the datasheet.
9//!
10//! To use this driver, the I2C bus has to be set to a max baudrate of 100_000.
11//!
12//! ## Feature flags
13//!
14//! - `std`: Enables error handling with `std::error::Error`.
15//! - `time`: Enables compatibility with `time::Date` on struct `RevisionDate`.
16//! - `unproven`: Enables ppm calibration and r0 value retrieving.
17//! (Correct functionality couldn't be verified.)
18//!
19//! # Example Usage
20//! ```ignore
21//! let mut delay = ...; // delay struct from board
22//! let i2c = ...; // I2C bus to use
23//!
24//! let mut device = MicsVz89Te::new(i2c);
25//! let measurements = device.read_measurements(&mut delay).unwrap();
26//!
27//! let co2 = measurements.co2;
28//! let voc = measurements.voc;
29//!
30//! let i2c = device.release(); // destruct driver to use bus with other drivers
31//! ```
32
33pub mod error;
34
35use embedded_hal::blocking::{
36    delay::DelayMs,
37    i2c::{Read, Write},
38};
39use error::PacketParseError;
40
41const MICS_VZ_89TE_ADDR: u8 = 0x70;
42
43const MICS_VZ_89TE_ADDR_CMD_GETSTATUS: u8 = 0x0C;
44const MICS_VZ_89TE_DATE_CODE: u8 = 0x0D;
45#[cfg(any(feature = "unproven", test))]
46const MICS_VZ_89TE_GET_CALIBR_VAL: u8 = 0x10;
47#[cfg(any(feature = "unproven", test))]
48const MICS_VZ_89TE_SET_CALIBR_PPM: u8 = 0x08;
49
50/// Represents the date of revision of the sensor.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub struct RevisionDate {
53    pub year: u16,
54    pub month: u8,
55    pub day: u8,
56}
57
58#[cfg(feature = "time")]
59impl TryFrom<time::Date> for RevisionDate {
60    type Error = time::Error;
61
62    fn try_from(d: time::Date) -> Result<Self, Self::Error> {
63        Ok(RevisionDate {
64            year: u16::try_from(d.year())
65                .map_err(|_| time::Error::ConversionRange(time::error::ConversionRange))?,
66            month: u8::from(d.month()),
67            day: u8::from(d.day()),
68        })
69    }
70}
71
72#[cfg(feature = "time")]
73impl TryFrom<RevisionDate> for time::Date {
74    type Error = time::Error;
75
76    fn try_from(rd: RevisionDate) -> Result<Self, Self::Error> {
77        time::Date::from_calendar_date(i32::from(rd.year), time::Month::try_from(rd.month)?, rd.day)
78            .map_err(|e| time::Error::ComponentRange(e))
79    }
80}
81
82/// Returned measurements by the sensor
83#[derive(Debug, Clone, Copy)]
84pub struct Measurements {
85    pub co2: f32,
86    pub voc: f32,
87}
88
89impl Measurements {
90    fn from_response(response: &[u8; 7]) -> Self {
91        let co2 = f32::from(response[1] - 13) * (1600.0 / 229.0) + 400.0; // ppm: 400 .. 2000
92        let voc = f32::from(response[0] - 13) * (1000.0 / 229.0); // ppb: 0 .. 1000
93        Self { co2, voc }
94    }
95}
96
97/// Driver for MICS-VZ-89TE sensor
98pub struct MicsVz89Te<I2C> {
99    i2c: I2C,
100}
101
102impl<I2C, E> MicsVz89Te<I2C>
103where
104    I2C: Read<Error = E> + Write<Error = E>,
105{
106    /// Time (in millis) to wait until the sensor response should be valid.
107    pub const WAIT_ON_RESPONSE_TIME: u16 = 100;
108
109    /// Create new driver on the supplied i2c bus.
110    pub fn new(i2c: I2C) -> Self {
111        Self { i2c }
112    }
113
114    /// Read measurements from sensor.
115    ///
116    /// This function blocks a minimum time of [MicsVz89Te::WAIT_ON_RESPONSE_TIME].
117    pub fn read_measurements(
118        &mut self,
119        delay: &mut impl DelayMs<u16>,
120    ) -> Result<Measurements, PacketParseError<E>> {
121        let response =
122            self.request_data(&[MICS_VZ_89TE_ADDR_CMD_GETSTATUS, 0, 0, 0, 0, 0xF3], delay)?;
123        Ok(Measurements::from_response(&response))
124    }
125
126    /// This function starts a measurement request and can be used in context where the delay on a response
127    /// has an specific implementation. For example in an async/await manner.
128    ///
129    /// To get a valid measurement result, a delay of [MicsVz89Te::WAIT_ON_RESPONSE_TIME] milliseconds should be implemented,
130    /// after calling this function.
131    ///
132    /// # Example Usage
133    /// implementation with [smol Timer](https://docs.rs/smol/latest/smol/struct.Timer.html)
134    /// ```ignore
135    /// driver.start_measurement().unwrap();
136    /// Timer::after(Duration::from_millis(u64::from(MicsVz89Te::WAIT_ON_RESPONSE_TIME))).await;
137    /// let measurements = driver.get_measurement_result().unwrap();
138    /// ```
139    pub fn start_measurement(&mut self) -> Result<(), PacketParseError<E>> {
140        self.send_request(&[MICS_VZ_89TE_ADDR_CMD_GETSTATUS, 0, 0, 0, 0, 0xF3])
141    }
142
143    /// Get the before requested measurements. To see an example, see [MicsVz89Te::start_measurement()].
144    pub fn get_measurement_result(&mut self) -> Result<Measurements, PacketParseError<E>> {
145        let response = self.receive_response()?;
146        Ok(Measurements::from_response(&response))
147    }
148
149    /// Read revision date of the sensor.
150    ///
151    /// This function blocks a minimum time of [MicsVz89Te::WAIT_ON_RESPONSE_TIME].
152    pub fn read_revision(
153        &mut self,
154        delay: &mut impl DelayMs<u16>,
155    ) -> Result<RevisionDate, PacketParseError<E>> {
156        let response = self.request_data(&[MICS_VZ_89TE_DATE_CODE, 0, 0, 0, 0, 0xF2], delay)?;
157        let date = RevisionDate {
158            year: u16::from(response[0]) + 2000,
159            month: response[1],
160            day: response[2],
161        };
162        Ok(date)
163    }
164
165    #[cfg(any(feature = "unproven", doc, test))]
166    #[cfg_attr(docsrs, doc(cfg(feature = "unproven")))]
167    /// Read the calibration value R0 of the sensor in kOhms.
168    ///
169    /// This function blocks a minimum time of [MicsVz89Te::WAIT_ON_RESPONSE_TIME].
170    pub fn read_calibration_r0(
171        &mut self,
172        delay: &mut impl DelayMs<u16>,
173    ) -> Result<u16, PacketParseError<E>> {
174        let response =
175            self.request_data(&[MICS_VZ_89TE_GET_CALIBR_VAL, 0, 0, 0, 0, 0xEF], delay)?;
176        Ok(u16::from_le_bytes([response[0], response[1]]))
177    }
178
179    #[cfg(any(feature = "unproven", doc, test))]
180    #[cfg_attr(docsrs, doc(cfg(feature = "unproven")))]
181    /// Writes the calibration CO2 value in ppm in range from 400 to 2000 measured by another device.
182    pub fn write_calibration_ppm(&mut self, ppm: f32) -> Result<(), PacketParseError<E>> {
183        debug_assert!(
184            ppm > 400.0 && ppm < 2000.0,
185            "ppm must be in range from 400 to 2000"
186        );
187        let send_ppm = ((ppm - 400.0) / (1600.0 / 229.0) + 13.0) as u8;
188        let mut cmd_array = [MICS_VZ_89TE_SET_CALIBR_PPM, send_ppm, 0, 0, 0, 0];
189        cmd_array[5] = gen_checksum(&cmd_array[..5]);
190        self.i2c
191            .write(MICS_VZ_89TE_ADDR, &cmd_array)
192            .map_err(PacketParseError::from)
193    }
194
195    fn request_data(
196        &mut self,
197        cmd_buffer: &[u8; 6],
198        delay: &mut impl DelayMs<u16>,
199    ) -> Result<[u8; 7], PacketParseError<E>> {
200        self.send_request(cmd_buffer)?;
201        delay.delay_ms(Self::WAIT_ON_RESPONSE_TIME);
202        self.receive_response()
203    }
204
205    fn send_request(&mut self, cmd_buffer: &[u8; 6]) -> Result<(), PacketParseError<E>> {
206        self.i2c
207            .write(MICS_VZ_89TE_ADDR, cmd_buffer)
208            .map_err(PacketParseError::from)
209    }
210
211    fn receive_response(&mut self) -> Result<[u8; 7], PacketParseError<E>> {
212        let mut buffer = [0u8; 7];
213        self.i2c.read(MICS_VZ_89TE_ADDR, &mut buffer)?;
214
215        let check = gen_checksum(&buffer[..5]);
216        if buffer[6].ne(&check) {
217            return Err(PacketParseError::WrongChecksum);
218        }
219
220        Ok(buffer)
221    }
222}
223
224impl<I2C> MicsVz89Te<I2C> {
225    /// Releases the underlying I2C bus and destroys the driver.
226    ///
227    /// # Example Usage
228    /// ```ignore
229    /// let i2c = ...; // I2C bus to use
230    /// let driver = MicsVz89Te::new(i2c);
231    /// ...; // read measurements from sensor
232    /// let i2c = driver.release();
233    /// ```
234    pub fn release(self) -> I2C {
235        self.i2c
236    }
237}
238
239fn gen_checksum(byte_array: &[u8]) -> u8 {
240    let sum = byte_array.iter().fold(0u16, |a, v| a + (*v as u16));
241    0xFF - (sum as u8 + (sum / 0x0100) as u8)
242}
243
244#[cfg(test)]
245mod test {
246
247    use crate::{error::PacketParseError, RevisionDate};
248
249    use super::MicsVz89Te;
250    use assert_matches::assert_matches;
251    use core::assert_eq;
252    use embedded_hal_mock::{
253        delay::MockNoop as DelayMock,
254        i2c::{Mock as I2cMock, Transaction as I2cTransaction},
255    };
256    use std::vec;
257
258    #[test]
259    fn test_read_measurements() {
260        let expectations = [
261            I2cTransaction::write(0x70, vec![0x0C, 0, 0, 0, 0, 0xF3]),
262            I2cTransaction::read(0x70, vec![0x27, 0x3C, 0, 0xBA, 0xBA, 0, 0x27]),
263        ];
264        let i2c = I2cMock::new(&expectations);
265        let mut delay = DelayMock::new();
266
267        let mut device = MicsVz89Te::new(i2c);
268        let measurements = device.read_measurements(&mut delay);
269
270        assert!(measurements.is_ok());
271        let measurements = measurements.unwrap();
272
273        assert_eq!(measurements.co2 as u32, 728);
274        assert_eq!(measurements.voc as u32, 113);
275    }
276
277    #[test]
278    fn test_read_measurements_wrong_checksum() {
279        let expectations = [
280            I2cTransaction::write(0x70, vec![0x0C, 0, 0, 0, 0, 0xF3]),
281            I2cTransaction::read(0x70, vec![0x27, 0x3C, 0, 0xBA, 0xBA, 0, 0x26]),
282        ];
283        let i2c = I2cMock::new(&expectations);
284        let mut delay = DelayMock::new();
285
286        let mut device = MicsVz89Te::new(i2c);
287        let measurements = device.read_measurements(&mut delay);
288
289        assert_matches!(measurements, Err(PacketParseError::WrongChecksum));
290    }
291
292    #[test]
293    fn test_read_revision_date() {
294        let expectations = [
295            I2cTransaction::write(0x70, vec![0x0D, 0, 0, 0, 0, 0xF2]),
296            I2cTransaction::read(0x70, vec![0x10, 0x03, 0x11, 0x48, 00, 0, 0x93]),
297        ];
298        let i2c = I2cMock::new(&expectations);
299        let mut delay = DelayMock::new();
300
301        let mut device = MicsVz89Te::new(i2c);
302        let revision = device.read_revision(&mut delay);
303
304        assert_matches!(
305                revision, Ok(r) if r == RevisionDate { year: 2016, month: 3, day: 17 }
306        );
307    }
308
309    #[test]
310    fn test_write_calibration_ppm() {
311        let expectations = [I2cTransaction::write(0x70, vec![0x08, 0x62, 0, 0, 0, 0x95])];
312        let i2c = I2cMock::new(&expectations);
313
314        let mut device = MicsVz89Te::new(i2c);
315        let res = device.write_calibration_ppm(1000.0);
316
317        assert!(res.is_ok());
318    }
319
320    #[test]
321    fn test_read_calibration_r0() {
322        let expectations = [
323            I2cTransaction::write(0x70, vec![0x10, 0, 0, 0, 0, 0xEF]),
324            I2cTransaction::read(0x70, vec![0xFB, 0x01, 0, 0, 0, 0, 0x03]),
325        ];
326        let i2c = I2cMock::new(&expectations);
327        let mut delay = DelayMock::new();
328
329        let mut device = MicsVz89Te::new(i2c);
330        let value = device.read_calibration_r0(&mut delay);
331
332        assert_matches!(value, Ok(v) if v == 507);
333    }
334}