Skip to main content

mics_6814/
lib.rs

1#![no_std]
2#![doc = include_str!("../README.md")]
3
4pub mod calibration;
5pub mod error;
6pub mod gas;
7
8pub use calibration::rs_r0_to_ppm;
9pub use error::Error;
10pub use gas::{Channel, Gas};
11
12use calibration::rs_r0_to_ppm as convert;
13
14/// Raw resistance readings from the three sensor channels.
15#[derive(Debug, Clone, Copy)]
16#[cfg_attr(feature = "defmt-03", derive(defmt::Format))]
17pub struct ChannelReading {
18    /// Rs/R0 ratio from the RED (reducing) sensor channel.
19    pub red: f32,
20    /// Rs/R0 ratio from the OX (oxidising) sensor channel.
21    pub ox: f32,
22    /// Rs/R0 ratio from the NH3 sensor channel.
23    pub nh3: f32,
24}
25
26/// Gas concentration reading in parts per million.
27#[derive(Debug, Clone, Copy)]
28#[cfg_attr(feature = "defmt-03", derive(defmt::Format))]
29pub struct GasReading {
30    /// The gas that was measured.
31    pub gas: Gas,
32    /// Estimated concentration in ppm.
33    pub ppm: f32,
34}
35
36/// Convert an ADC voltage reading to an Rs/R0 ratio.
37///
38/// Uses the voltage divider equation: `Rs = Rload * (Vcc - Vadc) / Vadc`
39///
40/// # Arguments
41/// - `v_adc` — measured voltage at the ADC pin (volts)
42/// - `v_cc` — supply voltage (volts), typically 3.3 or 5.0
43/// - `r_load` — load resistor value in ohms
44/// - `r0` — baseline resistance in clean air (ohms), obtained during calibration
45pub fn voltage_to_rs_r0(v_adc: f32, v_cc: f32, r_load: f32, r0: f32) -> Result<f32, Error> {
46    if v_adc <= 0.0 || v_adc > v_cc {
47        return Err(Error::InvalidVoltage);
48    }
49    if r_load <= 0.0 || r0 <= 0.0 {
50        return Err(Error::InvalidLoadResistance);
51    }
52    let rs = r_load * (v_cc - v_adc) / v_adc;
53    Ok(rs / r0)
54}
55
56/// Estimate the concentration of a specific gas from a channel reading.
57///
58/// The `reading` must contain the Rs/R0 ratio for the correct channel
59/// (determined by `gas.channel()`).
60pub fn measure(reading: &ChannelReading, gas: Gas) -> Result<GasReading, Error> {
61    let rs_r0 = match gas.channel() {
62        Channel::Red => reading.red,
63        Channel::Ox => reading.ox,
64        Channel::Nh3 => reading.nh3,
65    };
66    let ppm = convert(gas, rs_r0)?;
67    Ok(GasReading { gas, ppm })
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn test_voltage_to_rs_r0() {
76        let ratio = voltage_to_rs_r0(1.65, 3.3, 10_000.0, 10_000.0).unwrap();
77        assert!((ratio - 1.0).abs() < 0.01, "expected 1.0, got {ratio}");
78    }
79
80    #[test]
81    fn test_voltage_to_rs_r0_zero_voltage() {
82        let result = voltage_to_rs_r0(0.0, 3.3, 10_000.0, 10_000.0);
83        assert_eq!(result, Err(Error::InvalidVoltage));
84    }
85
86    #[test]
87    fn test_voltage_exceeds_vcc() {
88        let result = voltage_to_rs_r0(3.5, 3.3, 10_000.0, 10_000.0);
89        assert_eq!(result, Err(Error::InvalidVoltage));
90    }
91
92    #[test]
93    fn test_full_pipeline_co() {
94        // Simulate: Vcc=3.3V, Rload=56kΩ, R0_red=500kΩ
95        // Vadc such that Rs/R0 = 0.25 → ~100 ppm CO
96        let v_adc = 3.3 * 56_000.0 / (125_000.0 + 56_000.0);
97        let ratio = voltage_to_rs_r0(v_adc, 3.3, 56_000.0, 500_000.0).unwrap();
98        let reading = ChannelReading {
99            red: ratio,
100            ox: 1.0,
101            nh3: 1.0,
102        };
103        let result = measure(&reading, Gas::CarbonMonoxide).unwrap();
104        assert!(
105            (result.ppm - 100.0).abs() < 20.0,
106            "expected ~100 ppm CO, got {}",
107            result.ppm
108        );
109    }
110}