Skip to content

Commit

Permalink
feat(eclss): switch to libscd, add scd30 support
Browse files Browse the repository at this point in the history
  • Loading branch information
hawkw committed Jun 11, 2024
1 parent f98bf77 commit 048a962
Show file tree
Hide file tree
Showing 13 changed files with 505 additions and 177 deletions.
22 changes: 10 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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" }
5 changes: 3 additions & 2 deletions eclssd/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
36 changes: 32 additions & 4 deletions eclssd/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
1 change: 0 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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=";
Expand Down
9 changes: 5 additions & 4 deletions lib/eclss/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand All @@ -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 }
Expand Down
4 changes: 2 additions & 2 deletions lib/eclss/src/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
12 changes: 8 additions & 4 deletions lib/eclss/src/sensor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
123 changes: 123 additions & 0 deletions lib/eclss/src/sensor/scd.rs
Original file line number Diff line number Diff line change
@@ -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<E> {
Libscd(libscd::error::Error<E>),
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<usize>,
name: &'static str,
}

impl Shared {
fn new<I, const SENSORS: usize>(
eclss: &'static crate::Eclss<I, { SENSORS }>,
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<u32> {
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<u32> = 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<E: i2c::Error> SensorError for ScdError<E> {
fn i2c_error(&self) -> Option<i2c::ErrorKind> {
match self {
Self::Libscd(libscd::error::Error::I2C(ref e)) => Some(e.kind()),
_ => None,
}
}
}

impl<E: fmt::Display> fmt::Display for ScdError<E> {
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<E> From<libscd::error::Error<E>> for ScdError<E> {
fn from(e: libscd::error::Error<E>) -> Self {
Self::Libscd(e)
}
}
Loading

0 comments on commit 048a962

Please sign in to comment.