From 048a9622b8de675c179e6b8e85b6891bb782bb6e Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Tue, 11 Jun 2024 12:23:40 -0700 Subject: [PATCH] feat(eclss): switch to `libscd`, add scd30 support --- Cargo.lock | 22 ++--- Cargo.toml | 3 +- eclssd/Cargo.toml | 5 +- eclssd/src/main.rs | 36 +++++++- flake.nix | 1 - lib/eclss/Cargo.toml | 9 +- lib/eclss/src/metrics.rs | 4 +- lib/eclss/src/sensor.rs | 12 ++- lib/eclss/src/sensor/scd.rs | 123 +++++++++++++++++++++++++ lib/eclss/src/sensor/scd/scd30.rs | 98 ++++++++++++++++++++ lib/eclss/src/sensor/scd/scd40.rs | 106 ++++++++++++++++++++++ lib/eclss/src/sensor/scd/scd41.rs | 117 ++++++++++++++++++++++++ lib/eclss/src/sensor/scd40.rs | 146 ------------------------------ 13 files changed, 505 insertions(+), 177 deletions(-) create mode 100644 lib/eclss/src/sensor/scd.rs create mode 100644 lib/eclss/src/sensor/scd/scd30.rs create mode 100644 lib/eclss/src/sensor/scd/scd40.rs create mode 100644 lib/eclss/src/sensor/scd/scd41.rs delete mode 100644 lib/eclss/src/sensor/scd40.rs diff --git a/Cargo.lock b/Cargo.lock index 82c8270..4f215bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -350,9 +350,9 @@ dependencies = [ "embedded-hal-async", "ens160", "fixed", + "libscd", "maitake-sync", "pmsa003i", - "scd4x", "serde", "sgp30", "sht4x", @@ -947,6 +947,15 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libscd" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd8930c85111530e419f2f9c067c4639bebb1638029b6cbc37b4fa43ba7e918" +dependencies = [ + "embedded-hal-async", +] + [[package]] name = "linux-embedded-hal" version = "0.4.0" @@ -1574,17 +1583,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" -[[package]] -name = "scd4x" -version = "0.3.0" -source = "git+https://github.com/hawkw/scd4x-rs?branch=eliza/async#36205f3f627c1edc54618f7d03a59509e340413e" -dependencies = [ - "embedded-hal", - "embedded-hal-async", - "log", - "sensirion-i2c", -] - [[package]] name = "scoped-tls" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 11eb2b0..3061beb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,11 +31,11 @@ tinymetrics = { git = "https://github.com/hawkw/tinymetrics", default-features = tracing = { version = "0.1.40", default-features = false } tracing-subscriber = { version = "0.3.18", default-features = false } tracing-journald = { version = "0.3" } +libscd = { version = "0.3", default-features = false } linux-embedded-hal = "0.4.0" local-ip-address = "0.6.1" pmsa003i = { path = "lib/pmsa003i" } reqwest = { version = "0.12.4", default-features = false } -scd4x = { version = "0.3.0", default-features = false } sgp30 = { version = "0.4.0", default-features = false } sht4x = { version = "0.2.0", default-features = false } serde = { version = "1.0", default-features = false } @@ -44,7 +44,6 @@ spin_sleep = { version = "1.2.0" } [patch.crates-io] bosch-bme680 = { git = "https://github.com/hawkw/bosch-bme680", branch = "eliza/async" } linux-embedded-hal = { git = "https://github.com/rust-embedded/linux-embedded-hal/" } -scd4x = { git = "https://github.com/hawkw/scd4x-rs", branch = "eliza/async" } sgp30 = { git = "https://github.com/hawkw/sgp30-rs", branch = "eliza/embedded-hal-async" } sht4x = { git = "https://github.com/hawkw/sht4x", branch = "eliza/embedded-hal-async" } sensirion-i2c = { git = "https://github.com/sensirion/sensirion-i2c-rs", rev = "f7b9f3a81b777bc6e6b2f0acb4c1ef9c57dfa06d" } diff --git a/eclssd/Cargo.toml b/eclssd/Cargo.toml index 4f27d8b..90e23d4 100644 --- a/eclssd/Cargo.toml +++ b/eclssd/Cargo.toml @@ -6,10 +6,11 @@ readme = "README.md" license = "MIT" [features] -default = ["bme680", "scd41", "sgp30", "sht41", "pmsa003i", "ens160", "mdns"] +default = ["bme680", "scd30", "scd40", "scd41", "sgp30", "sht41", "pmsa003i", "ens160", "mdns"] bme680 = ["eclss/bme680"] -scd41 = ["eclss/scd41"] +scd30 = ["eclss/scd30"] scd40 = ["eclss/scd40"] +scd41 = ["eclss/scd41"] sgp30 = ["eclss/sgp30"] sht41 = ["eclss/sht41"] pmsa003i = ["eclss/pmsa003i"] diff --git a/eclssd/src/main.rs b/eclssd/src/main.rs index 59949ae..b83d91a 100644 --- a/eclssd/src/main.rs +++ b/eclssd/src/main.rs @@ -119,13 +119,41 @@ async fn main() -> anyhow::Result<()> { } }); - #[cfg(any(feature = "scd41", feature = "scd40"))] + #[cfg(feature = "scd41")] sensors.spawn({ - let sensor = sensor::Scd4x::new(eclss, GoodDelay::default()); + let sensor = sensor::Scd41::new(eclss, GoodDelay::default()); let backoff = backoff.clone(); async move { - tracing::info!("starting SCD4x..."); + tracing::info!("starting SCD41..."); + eclss + .run_sensor(sensor, backoff.clone(), linux_embedded_hal::Delay) + .await + .unwrap() + } + }); + + #[cfg(feature = "scd40")] + sensors.spawn({ + let sensor = sensor::Scd40::new(eclss, GoodDelay::default()); + + let backoff = backoff.clone(); + async move { + tracing::info!("starting SCD40..."); + eclss + .run_sensor(sensor, backoff.clone(), linux_embedded_hal::Delay) + .await + .unwrap() + } + }); + + #[cfg(feature = "scd30")] + sensors.spawn({ + let sensor = sensor::Scd30::new(eclss, GoodDelay::default()); + + let backoff = backoff.clone(); + async move { + tracing::info!("starting SCD30..."); eclss .run_sensor(sensor, backoff.clone(), linux_embedded_hal::Delay) .await @@ -238,7 +266,7 @@ where /// type is not very precise. Use blocking delays for short sleeps in timing /// critical sensor wire protocols, and use the async delay for longer sleeps /// like in the poll loop.ca -#[derive(Default)] +#[derive(Default, Copy, Clone)] struct GoodDelay(spin_sleep::SpinSleeper); impl GoodDelay { const ONE_MS_NANOS: u32 = Duration::from_millis(1).as_nanos() as u32; diff --git a/flake.nix b/flake.nix index 95ec799..5de3532 100644 --- a/flake.nix +++ b/flake.nix @@ -58,7 +58,6 @@ lockFile = ./Cargo.lock; outputHashes = { "linux-embedded-hal-0.4.0" = "sha256-2CxZcBMWaGP0DTiyQoDkwVFoNbEBojGySirSb2z+40U="; - "scd4x-0.3.0" = "sha256-2pbYEDX2454lP2701eyhtRWu1sSW8RSStVt6iOF0fmI="; "sgp30-0.4.0" = "sha256-5LRVMFMTxpgQpxrN0sMTiedL4Sw6NBRd86+ZvRP8CVk="; "sht4x-0.2.0" = "sha256-LrOvkvNXFNL4EM9aAZfSDFM7zs6M54BGeixtDN5pFCo="; "sensirion-i2c-0.3.0" = "sha256-HS6anAmUBBrhlP/kBSv243ArnK3ULK8+0JA8kpe6LAk="; diff --git a/lib/eclss/Cargo.toml b/lib/eclss/Cargo.toml index b595848..3d0ec50 100644 --- a/lib/eclss/Cargo.toml +++ b/lib/eclss/Cargo.toml @@ -8,11 +8,12 @@ edition = "2021" [features] bme680 = ["dep:bosch-bme680"] serde = ["dep:serde", "tinymetrics/serde"] -scd40 = ["dep:scd4x"] -scd41 = ["dep:scd4x", "scd4x/scd41"] +scd30 = ["dep:libscd", "libscd/scd30"] +scd40 = ["dep:libscd", "libscd/scd40"] +scd41 = ["dep:libscd", "libscd/scd41"] sgp30 = ["dep:sgp30"] sht41 = ["dep:sht4x", "dep:fixed"] -default = ["pmsa003i", "scd40", "ens160", "sgp30", "bme680"] +default = ["pmsa003i", "scd40", "scd41", "scd30", "ens160", "sgp30", "bme680"] [dependencies] bosch-bme680 = { workspace = true, optional = true, features = ["embedded-hal-async"] } @@ -21,9 +22,9 @@ ens160 = { workspace = true, optional = true, features = ["async"] } embedded-hal-async = { workspace = true } embedded-hal = { workspace = true } fixed = { workspace = true, optional = true } +libscd = { workspace = true, optional = true, features = ["async"] } maitake-sync = { workspace = true } tinymetrics = { workspace = true, default-features = false } -scd4x = { workspace = true, optional = true, default-features = false, features = ["embedded-hal-async"] } sgp30 = { workspace = true, optional = true, default-features = false, features = ["embedded-hal-async"] } sht4x = { workspace = true, optional = true, default-features = false, features = ["embedded-hal-async"] } serde = { workspace = true, optional = true } diff --git a/lib/eclss/src/metrics.rs b/lib/eclss/src/metrics.rs index 837ea07..04b8bdf 100644 --- a/lib/eclss/src/metrics.rs +++ b/lib/eclss/src/metrics.rs @@ -39,8 +39,8 @@ macro_rules! count_features { }} } -pub const TEMP_METRICS: usize = count_features!("scd40", "scd41", "bme680", "sht41"); -pub const CO2_METRICS: usize = count_features!("scd40", "scd41", "scd30"); +pub const TEMP_METRICS: usize = count_features!("scd30", "scd40", "scd41", "bme680", "sht41"); +pub const CO2_METRICS: usize = count_features!("scd30", "scd40", "scd41"); pub const ECO2_METRICS: usize = count_features!("sgp30", "bme680", "ens160"); pub const HUMIDITY_METRICS: usize = count_features!("bme680", "scd40", "scd41", "scd30", "sht41"); pub const PRESSURE_METRICS: usize = count_features!("bme680"); diff --git a/lib/eclss/src/sensor.rs b/lib/eclss/src/sensor.rs index cd50647..4b1fc36 100644 --- a/lib/eclss/src/sensor.rs +++ b/lib/eclss/src/sensor.rs @@ -13,10 +13,14 @@ pub mod pmsa003i; #[cfg(feature = "pmsa003i")] pub use pmsa003i::Pmsa003i; -#[cfg(any(feature = "scd40", feature = "scd41"))] -pub mod scd40; -#[cfg(any(feature = "scd40", feature = "scd41"))] -pub use scd40::Scd4x; +#[cfg(any(feature = "scd40", feature = "scd41", feature = "scd30"))] +pub mod scd; +#[cfg(feature = "scd30")] +pub use scd::Scd30; +#[cfg(feature = "scd40")] +pub use scd::Scd40; +#[cfg(feature = "scd41")] +pub use scd::Scd41; #[cfg(feature = "sgp30")] pub mod sgp30; diff --git a/lib/eclss/src/sensor/scd.rs b/lib/eclss/src/sensor/scd.rs new file mode 100644 index 0000000..87d0765 --- /dev/null +++ b/lib/eclss/src/sensor/scd.rs @@ -0,0 +1,123 @@ +use crate::{ + error::SensorError, + metrics::{Gauge, SensorLabel, PRESSURE_METRICS}, +}; +use core::fmt; +use core::num::Wrapping; + +use embedded_hal::i2c; + +#[cfg(feature = "scd30")] +mod scd30; +#[cfg(feature = "scd41")] +pub use self::scd30::Scd30; +#[cfg(feature = "scd40")] +mod scd40; +#[cfg(feature = "scd40")] +pub use self::scd40::Scd40; +#[cfg(feature = "scd41")] +mod scd41; +#[cfg(feature = "scd41")] +pub use self::scd41::Scd41; + +#[derive(Debug)] +pub enum ScdError { + Libscd(libscd::error::Error), + SelfTest, +} + +struct Shared { + temp_c: &'static Gauge, + rel_humidity: &'static Gauge, + abs_humidity: &'static Gauge, + co2_ppm: &'static Gauge, + abs_humidity_interval: usize, + pressure: &'static tinymetrics::GaugeFamily<'static, PRESSURE_METRICS, SensorLabel>, + polls: Wrapping, + name: &'static str, +} + +impl Shared { + fn new( + eclss: &'static crate::Eclss, + name: &'static str, + ) -> Self { + let metrics = &eclss.metrics; + Self { + temp_c: metrics.temp.register(SensorLabel(name)).unwrap(), + rel_humidity: metrics.rel_humidity.register(SensorLabel(name)).unwrap(), + abs_humidity: metrics.abs_humidity.register(SensorLabel(name)).unwrap(), + co2_ppm: metrics.co2.register(SensorLabel(name)).unwrap(), + pressure: &metrics.pressure, + polls: Wrapping(0), + abs_humidity_interval: 1, + name, + } + } + + fn with_abs_humidity_interval(mut self, interval: usize) -> Self { + self.abs_humidity_interval = interval; + self + } + + fn pressure_pascals(&self) -> Option { + let pressure_hpa = self.pressure.mean()?; + let pressure_pascals = (pressure_hpa * 100.0) as u32; + // Valid pressure compensation values per the SCDxx datasheet. + const VALID_PRESSURES: core::ops::Range = 70_000..120_000; + if VALID_PRESSURES.contains(&pressure_pascals) { + Some(pressure_pascals) + } else { + None + } + } + + fn record_measurement(&mut self, co2: u16, temperature: f32, humidity: f32) { + debug!( + "{}: CO2: {co2} ppm, Temp: {temperature}°C, Humidity: {humidity}%", + self.name + ); + self.co2_ppm.set_value(co2.into()); + self.temp_c.set_value(temperature.into()); + self.rel_humidity.set_value(humidity.into()); + + if self.polls.0 % self.abs_humidity_interval == 0 { + let abs_humidity = super::absolute_humidity(temperature, humidity); + self.abs_humidity.set_value(abs_humidity.into()); + debug!("{}: Absolute humidity: {abs_humidity} g/m³", self.name); + } + + self.polls += 1; + } +} + +impl SensorError for ScdError { + fn i2c_error(&self) -> Option { + match self { + Self::Libscd(libscd::error::Error::I2C(ref e)) => Some(e.kind()), + _ => None, + } + } +} + +impl fmt::Display for ScdError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Libscd(libscd::error::Error::I2C(ref e)) => write!(f, "I2C error: {e}"), + Self::Libscd(libscd::error::Error::CRC) => { + f.write_str("CRC checksum validation failed") + } + Self::Libscd(libscd::error::Error::InvalidInput) => f.write_str("invalid input"), + Self::Libscd(libscd::error::Error::NotAllowed) => { + f.write_str("not allowed when periodic measurement is running") + } + Self::SelfTest => f.write_str("self-test validation failed"), + } + } +} + +impl From> for ScdError { + fn from(e: libscd::error::Error) -> Self { + Self::Libscd(e) + } +} diff --git a/lib/eclss/src/sensor/scd/scd30.rs b/lib/eclss/src/sensor/scd/scd30.rs new file mode 100644 index 0000000..820b8bd --- /dev/null +++ b/lib/eclss/src/sensor/scd/scd30.rs @@ -0,0 +1,98 @@ +use super::{ScdError, Shared}; +use crate::{ + error::{Context, EclssError}, + sensor::Sensor, + SharedBus, +}; +use embedded_hal::i2c; +use embedded_hal_async::{delay::DelayNs, i2c::I2c}; +use libscd::asynchronous::scd30; + +pub struct Scd30 { + sensor: scd30::Scd30<&'static SharedBus, D>, + delay: D, + state: Shared, +} + +impl Scd30 +where + I: I2c, + D: DelayNs + Clone, +{ + pub fn new( + eclss: &'static crate::Eclss, + delay: D, + ) -> Self { + Self { + sensor: scd30::Scd30::new(&eclss.i2c, delay.clone()), + state: Shared::new(eclss, NAME), + delay, + } + } + + pub fn with_abs_humidity_interval(mut self, interval: usize) -> Self { + self.state = self.state.with_abs_humidity_interval(interval); + self + } +} + +const NAME: &str = "SCD30"; + +impl Sensor for Scd30 +where + I: I2c + 'static, + I::Error: i2c::Error, + D: DelayNs, +{ + const NAME: &'static str = NAME; + const POLL_INTERVAL: core::time::Duration = core::time::Duration::from_secs(2); + type Error = EclssError>; + + async fn init(&mut self) -> Result<(), Self::Error> { + self.sensor + .soft_reset() + .await + .context("error sending SCD30 soft reset")?; + + let (major, minor) = self + .sensor + .read_firmware_version() + .await + .context("error reading SCD30 firmware version")?; + info!("Connected to SCD30 sensor, firmware v{major}.{minor}"); + self.sensor + .set_measurement_interval(Self::POLL_INTERVAL.as_secs() as u16) + .await + .context("error setting SCD30 measurement interval")?; + + self.sensor + // TODO(calculate ambient pressure hPa here + .start_continuous_measurement(1001) + .await + .context("error starting SCD30 continuous measurement")?; + + Ok(()) + } + + async fn poll(&mut self) -> Result<(), Self::Error> { + while !self + .sensor + .data_ready() + .await + .context("error seeing if SCD30 data is ready")? + { + self.delay.delay_ms(1).await; + } + let scd30::Measurement { + co2, + temperature, + humidity, + } = self + .sensor + .measurement() + .await + .context("error reading SCD30 measurement")?; + self.state.record_measurement(co2, temperature, humidity); + Ok(()) + } +} diff --git a/lib/eclss/src/sensor/scd/scd40.rs b/lib/eclss/src/sensor/scd/scd40.rs new file mode 100644 index 0000000..62aa10d --- /dev/null +++ b/lib/eclss/src/sensor/scd/scd40.rs @@ -0,0 +1,106 @@ +use super::{ScdError, Shared}; +use crate::{ + error::{Context, EclssError}, + sensor::Sensor, + SharedBus, +}; + +use embedded_hal::i2c; +use embedded_hal_async::{delay::DelayNs, i2c::I2c}; +use libscd::asynchronous::scd4x; + +pub struct Scd40 { + sensor: scd4x::Scd40<&'static SharedBus, D>, + state: Shared, + delay: D, +} + +impl Scd40 +where + I: I2c, + D: DelayNs + Clone, +{ + pub fn new( + eclss: &'static crate::Eclss, + delay: D, + ) -> Self { + Self { + sensor: scd4x::Scd40::new(&eclss.i2c, delay.clone()), + state: Shared::new(eclss, NAME), + delay, + } + } + + pub fn with_abs_humidity_interval(mut self, interval: usize) -> Self { + self.state = self.state.with_abs_humidity_interval(interval); + self + } +} + +const NAME: &str = "SCD40"; + +impl Sensor for Scd40 +where + I: I2c + 'static, + I::Error: i2c::Error, + D: DelayNs, +{ + const NAME: &'static str = NAME; + const POLL_INTERVAL: core::time::Duration = core::time::Duration::from_secs(5); + type Error = EclssError>; + + async fn init(&mut self) -> Result<(), Self::Error> { + self.sensor + .stop_periodic_measurement() + .await + .context("error stopping SCD40 periodic measurement")?; + self.sensor + .reinit() + .await + .context("error starting SCD40 periodic measurement")?; + + let serial = self + .sensor + .serial_number() + .await + .context("error reading SCD40 serial number")?; + info!(serial, "Connected to SCD40 sensor"); + if !self + .sensor + .perform_self_test() + .await + .context("error performing SCD40 self test")? + { + Err(ScdError::SelfTest).context("SCD40 self test failed")?; + } + + self.sensor + .start_periodic_measurement() + .await + .context("error starting SCD40 periodic measurement")?; + + Ok(()) + } + + async fn poll(&mut self) -> Result<(), Self::Error> { + while !self + .sensor + .data_ready() + .await + .context("error seeing if SCD40 data is ready")? + { + self.delay.delay_ms(1).await; + } + let scd4x::Measurement { + co2, + temperature, + humidity, + } = self + .sensor + .read_measurement() + .await + .context("error reading SCD40 measurement")?; + self.state.record_measurement(co2, temperature, humidity); + Ok(()) + } +} diff --git a/lib/eclss/src/sensor/scd/scd41.rs b/lib/eclss/src/sensor/scd/scd41.rs new file mode 100644 index 0000000..3f25cfd --- /dev/null +++ b/lib/eclss/src/sensor/scd/scd41.rs @@ -0,0 +1,117 @@ +use super::{ScdError, Shared}; +use crate::{ + error::{Context, EclssError}, + sensor::Sensor, + SharedBus, +}; + +use embedded_hal::i2c; +use embedded_hal_async::{delay::DelayNs, i2c::I2c}; +use libscd::asynchronous::scd4x; + +pub struct Scd41 { + sensor: scd4x::Scd41<&'static SharedBus, D>, + state: Shared, + delay: D, +} + +impl Scd41 +where + I: I2c, + D: DelayNs + Clone, +{ + pub fn new( + eclss: &'static crate::Eclss, + delay: D, + ) -> Self { + Self { + sensor: scd4x::Scd41::new(&eclss.i2c, delay.clone()), + state: Shared::new(eclss, NAME), + delay, + } + } + + pub fn with_abs_humidity_interval(mut self, interval: usize) -> Self { + self.state = self.state.with_abs_humidity_interval(interval); + self + } +} + +const NAME: &str = "SCD41"; + +impl Sensor for Scd41 +where + I: I2c + 'static, + I::Error: i2c::Error, + D: DelayNs, +{ + const NAME: &'static str = NAME; + const POLL_INTERVAL: core::time::Duration = core::time::Duration::from_secs(5); + type Error = EclssError>; + + async fn init(&mut self) -> Result<(), Self::Error> { + self.sensor + .wake_up() + .await + .context("error waking up SCD41")?; + + self.sensor + .stop_periodic_measurement() + .await + .context("error stopping SCD41 periodic measurement")?; + self.sensor + .reinit() + .await + .context("error starting SCD41 periodic measurement")?; + + let serial = self + .sensor + .serial_number() + .await + .context("error reading SCD41 serial number")?; + info!(serial, "Connected to SCD41 sensor"); + if !self + .sensor + .perform_self_test() + .await + .context("error performing SCD41 self test")? + { + Err(ScdError::SelfTest).context("SCD41 self test failed")?; + } + + self.sensor + .start_periodic_measurement() + .await + .context("error starting SCD41 periodic measurement")?; + + Ok(()) + } + + async fn poll(&mut self) -> Result<(), Self::Error> { + while !self + .sensor + .data_ready() + .await + .context("error seeing if SCD41 data is ready")? + { + self.delay.delay_ms(1).await; + } + let scd4x::Measurement { + co2, + temperature, + humidity, + } = self + .sensor + .read_measurement() + .await + .context("error reading SCD41 measurement")?; + self.state.record_measurement(co2, temperature, humidity); + if let Some(pressure) = self.state.pressure_pascals() { + self.sensor + .set_ambient_pressure(pressure) + .await + .context("error setting SCD41 ambient pressure")?; + } + Ok(()) + } +} diff --git a/lib/eclss/src/sensor/scd40.rs b/lib/eclss/src/sensor/scd40.rs deleted file mode 100644 index 4731137..0000000 --- a/lib/eclss/src/sensor/scd40.rs +++ /dev/null @@ -1,146 +0,0 @@ -use crate::{ - error::{Context, EclssError, SensorError}, - metrics::{self, Gauge}, - sensor::Sensor, - SharedBus, -}; -use core::fmt; -use core::num::Wrapping; -use scd4x::AsyncScd4x; - -use embedded_hal::i2c; -use embedded_hal_async::{delay::DelayNs, i2c::I2c}; - -pub struct Scd4x { - sensor: AsyncScd4x<&'static SharedBus, D>, - temp_c: &'static Gauge, - rel_humidity: &'static Gauge, - abs_humidity: &'static Gauge, - co2_ppm: &'static Gauge, - abs_humidity_interval: usize, - polls: Wrapping, -} - -impl Scd4x -where - I: I2c, - D: DelayNs, -{ - pub fn new( - eclss: &'static crate::Eclss, - delay: D, - ) -> Self { - let metrics = &eclss.metrics; - const LABEL: metrics::SensorLabel = metrics::SensorLabel(NAME); - Self { - sensor: AsyncScd4x::new(&eclss.i2c, delay), - temp_c: metrics.temp.register(LABEL).unwrap(), - rel_humidity: metrics.rel_humidity.register(LABEL).unwrap(), - abs_humidity: metrics.abs_humidity.register(LABEL).unwrap(), - co2_ppm: metrics.co2.register(LABEL).unwrap(), - polls: Wrapping(0), - abs_humidity_interval: 1, - } - } - - pub fn with_abs_humidity_interval(mut self, interval: usize) -> Self { - self.abs_humidity_interval = interval; - self - } -} - -#[derive(Debug)] -pub struct Error(scd4x::Error); - -#[cfg(feature = "scd41")] -const NAME: &str = "SCD41"; -#[cfg(not(feature = "scd41"))] -const NAME: &str = "SCD40"; - -impl Sensor for Scd4x -where - I: I2c + 'static, - I::Error: i2c::Error, - D: DelayNs, -{ - const NAME: &'static str = NAME; - const POLL_INTERVAL: core::time::Duration = core::time::Duration::from_secs(5); - type Error = EclssError>; - - async fn init(&mut self) -> Result<(), Self::Error> { - #[cfg(feature = "scd41")] - self.sensor.wake_up().await; - - self.sensor - .stop_periodic_measurement() - .await - .context("error stopping SCD4x periodic measurement")?; - self.sensor - .reinit() - .await - .context("error starting SCD4x periodic measurement")?; - - let serial = self - .sensor - .serial_number() - .await - .context("error reading SCD4x serial number")?; - info!(serial, "Connected to {NAME} sensor"); - - self.sensor - .start_periodic_measurement() - .await - .context("error starting SCD4x periodic measurement")?; - - Ok(()) - } - - async fn poll(&mut self) -> Result<(), Self::Error> { - let scd4x::types::SensorData { - co2, - temperature, - humidity, - } = self.sensor.measurement().await.map_err(Error)?; - self.polls += 1; - debug!("CO2: {co2} ppm, Temp: {temperature}°C, Humidity: {humidity}%"); - self.co2_ppm.set_value(co2.into()); - self.temp_c.set_value(temperature.into()); - self.rel_humidity.set_value(humidity.into()); - - if self.polls.0 % self.abs_humidity_interval == 0 { - let abs_humidity = super::absolute_humidity(temperature, humidity); - self.abs_humidity.set_value(abs_humidity.into()); - debug!("Absolute humidity: {abs_humidity} g/m³"); - } - Ok(()) - } -} - -impl SensorError for Error { - fn i2c_error(&self) -> Option { - match self.0 { - scd4x::Error::I2c(ref e) => Some(e.kind()), - _ => None, - } - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.0 { - scd4x::Error::I2c(ref e) => write!(f, "I2C error: {e}"), - scd4x::Error::Crc => f.write_str("CRC checksum validation failed"), - scd4x::Error::SelfTest => f.write_str("self-test measure failure"), - scd4x::Error::NotAllowed => { - f.write_str("not allowed when periodic measurement is running") - } - scd4x::Error::Internal => f.write_str("internal error"), - } - } -} - -impl From> for Error { - fn from(e: scd4x::Error) -> Self { - Self(e) - } -}