diff --git a/Cargo.lock b/Cargo.lock index a8b363d..8e4f205 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -159,6 +159,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "az" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" + [[package]] name = "backtrace" version = "0.3.71" @@ -215,6 +221,12 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytemuck" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" + [[package]] name = "byteorder" version = "1.5.0" @@ -322,6 +334,12 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "eclss" version = "0.1.0" @@ -331,11 +349,13 @@ dependencies = [ "embedded-hal", "embedded-hal-async", "ens160", + "fixed", "maitake-sync", "pmsa003i", "scd4x", "serde", "sgp30", + "sht4x", "tinymetrics", "tracing", ] @@ -464,6 +484,18 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "fixed" +version = "1.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc715d38bea7b5bf487fcd79bcf8c209f0b58014f3018a7a19c2b855f472048" +dependencies = [ + "az", + "bytemuck", + "half", + "typenum", +] + [[package]] name = "flume" version = "0.11.0" @@ -655,6 +687,16 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hash32" version = "0.3.1" @@ -1656,6 +1698,17 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "sht4x" +version = "0.2.0" +source = "git+https://github.com/hawkw/sht4x?branch=eliza/embedded-hal-async#d5c0e803ef7f4ed60c5dd96f9bbd13a6fe49cbbd" +dependencies = [ + "embedded-hal", + "embedded-hal-async", + "fixed", + "sensirion-i2c", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -2019,6 +2072,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unescaper" version = "0.1.4" diff --git a/Cargo.toml b/Cargo.toml index c5df46d..11eb2b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ eclss-axum = { path = "lib/eclss-axum" } ens160 = { version = "0.6.1", default-features = false } embedded-hal = { version = "1" } embedded-hal-async = { version = "1" } +fixed = "1.20.0" futures = "0.3" heapless = "0.8" humantime = "2" @@ -36,6 +37,7 @@ 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 } spin_sleep = { version = "1.2.0" } @@ -44,4 +46,5 @@ bosch-bme680 = { git = "https://github.com/hawkw/bosch-bme680", branch = "eliza/ 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 5a4df88..4f27d8b 100644 --- a/eclssd/Cargo.toml +++ b/eclssd/Cargo.toml @@ -6,11 +6,12 @@ readme = "README.md" license = "MIT" [features] -default = ["bme680", "scd41", "sgp30", "pmsa003i", "ens160", "mdns"] +default = ["bme680", "scd41", "sgp30", "sht41", "pmsa003i", "ens160", "mdns"] bme680 = ["eclss/bme680"] scd41 = ["eclss/scd41"] scd40 = ["eclss/scd40"] sgp30 = ["eclss/sgp30"] +sht41 = ["eclss/sht41"] pmsa003i = ["eclss/pmsa003i"] ens160 = ["eclss/ens160"] mdns = ["mdns-sd", "hostname", "local-ip-address"] diff --git a/eclssd/src/main.rs b/eclssd/src/main.rs index 5e1b312..db84f25 100644 --- a/eclssd/src/main.rs +++ b/eclssd/src/main.rs @@ -134,6 +134,20 @@ async fn main() -> anyhow::Result<()> { } }); + #[cfg(feature = "sht41")] + sensors.spawn({ + let sensor = sensor::Sht41::new(eclss, GoodDelay::default()); + + let backoff = backoff.clone(); + async move { + tracing::info!("starting SHT41..."); + eclss + .run_sensor(sensor, backoff.clone(), linux_embedded_hal::Delay) + .await + .unwrap() + } + }); + #[cfg(feature = "ens160")] sensors.spawn({ let sensor = sensor::Ens160::new(eclss, GoodDelay::default()); diff --git a/flake.nix b/flake.nix index 7d30c72..1c1900e 100644 --- a/flake.nix +++ b/flake.nix @@ -60,6 +60,7 @@ "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="; "tinymetrics-0.1.0" = "sha256-7y2pq8qBtOpXO6ii/j+NhwsglxRMLvz8hx7a/w6GRBU="; "bosch-bme680-1.0.2" = "sha256-C4l3MLqI9HYt2t459iccyJHjcVnFsXEywM3D+kcC95o="; diff --git a/lib/eclss/Cargo.toml b/lib/eclss/Cargo.toml index adeaca4..b595848 100644 --- a/lib/eclss/Cargo.toml +++ b/lib/eclss/Cargo.toml @@ -11,6 +11,7 @@ serde = ["dep:serde", "tinymetrics/serde"] scd40 = ["dep:scd4x"] scd41 = ["dep:scd4x", "scd4x/scd41"] sgp30 = ["dep:sgp30"] +sht41 = ["dep:sht4x", "dep:fixed"] default = ["pmsa003i", "scd40", "ens160", "sgp30", "bme680"] [dependencies] @@ -19,10 +20,12 @@ eclss-api = { workspace = true, features = ["fmt"] } ens160 = { workspace = true, optional = true, features = ["async"] } embedded-hal-async = { workspace = true } embedded-hal = { workspace = true } +fixed = { workspace = true, optional = true } 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 } tracing = { workspace = true, optional = true, default-features = false, features = ["attributes"] } pmsa003i = { workspace = true, optional = true, features = ["embedded-hal-async"] } diff --git a/lib/eclss/src/sensor.rs b/lib/eclss/src/sensor.rs index d93c8dd..cd50647 100644 --- a/lib/eclss/src/sensor.rs +++ b/lib/eclss/src/sensor.rs @@ -23,6 +23,11 @@ pub mod sgp30; #[cfg(feature = "sgp30")] pub use sgp30::Sgp30; +#[cfg(feature = "sht41")] +pub mod sht41; +#[cfg(feature = "sht41")] +pub use sht41::Sht41; + #[cfg(feature = "ens160")] pub mod ens160; #[cfg(feature = "ens160")] diff --git a/lib/eclss/src/sensor/sht41.rs b/lib/eclss/src/sensor/sht41.rs new file mode 100644 index 0000000..a62d6a6 --- /dev/null +++ b/lib/eclss/src/sensor/sht41.rs @@ -0,0 +1,137 @@ +use crate::{ + error::{Context, EclssError, SensorError}, + metrics::{Gauge, SensorLabel}, + sensor::Sensor, + SharedBus, +}; +use core::{fmt, num::Wrapping, time::Duration}; +use embedded_hal_async::{ + delay::DelayNs, + i2c::{self, I2c}, +}; +use sht4x::AsyncSht4x; +pub use sht4x::Precision; + +#[must_use = "sensors do nothing unless polled"] +pub struct Sht41 { + sensor: AsyncSht4x<&'static SharedBus, D>, + temp: &'static Gauge, + rel_humidity: &'static Gauge, + abs_humidity: &'static Gauge, + abs_humidity_interval: usize, + precision: Precision, + polls: Wrapping, + delay: D, +} + +pub struct Sht4xError(sht4x::Error); + +const NAME: &str = "SHT41"; + +impl Sht41 +where + I: I2c + 'static, + D: DelayNs, +{ + pub fn new( + eclss: &'static crate::Eclss, + delay: D, + ) -> Self { + let metrics = &eclss.metrics; + const LABEL: SensorLabel = SensorLabel(NAME); + // This is the default I2C address of the Adafruit breakout board. + // TODO(eliza): make this configurable + let address = sht4x::Address::Address0x44; + + Self { + sensor: AsyncSht4x::new_with_address(&eclss.i2c, address), + temp: metrics.temp.register(LABEL).unwrap(), + rel_humidity: metrics.rel_humidity.register(LABEL).unwrap(), + abs_humidity: metrics.abs_humidity.register(LABEL).unwrap(), + polls: Wrapping(0), + abs_humidity_interval: 1, + precision: Precision::Medium, + delay, + } + } + + pub fn with_abs_humidity_interval(self, interval: usize) -> Self { + Self { + abs_humidity_interval: interval, + ..self + } + } + + pub fn with_precision(self, precision: Precision) -> Self { + Self { precision, ..self } + } +} + +impl Sensor for Sht41 +where + I: I2c + 'static, + D: DelayNs, +{ + const NAME: &'static str = NAME; + const POLL_INTERVAL: Duration = Duration::from_secs(1); + type Error = EclssError>; + + async fn init(&mut self) -> Result<(), Self::Error> { + let serial = self + .sensor + .serial_number(&mut self.delay) + .await + .context("error reading SHT41 serial number")?; + tracing::info!("Connected to {NAME}, serial number: {serial:#x}"); + Ok(()) + } + + async fn poll(&mut self) -> Result<(), Self::Error> { + let reading = self + .sensor + .measure(self.precision, &mut self.delay) + .await + .context("error reading SHT41 measurement")?; + + let temp = reading.temperature_celsius().to_num::(); + let rel_humidity = reading.humidity_percent().to_num::(); + self.temp.set_value(temp); + self.rel_humidity.set_value(rel_humidity); + tracing::debug!("{NAME}: Temp: {temp}°C, Humidity: {rel_humidity}%"); + + if self.polls.0 % self.abs_humidity_interval == 0 { + let abs_humidity = super::absolute_humidity(temp as f32, rel_humidity as f32); + self.abs_humidity.set_value(abs_humidity.into()); + tracing::debug!("{NAME}: Absolute humidity: {abs_humidity} g/m³"); + } + + self.polls += 1; + + Ok(()) + } +} + +impl From> for Sht4xError { + fn from(value: sht4x::Error) -> Self { + Self(value) + } +} + +impl SensorError for Sht4xError { + fn i2c_error(&self) -> Option { + match self { + Self(sht4x::Error::I2c(i)) => Some(i.kind()), + _ => None, + } + } +} + +impl fmt::Display for Sht4xError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self(sht4x::Error::I2c(i)) => fmt::Display::fmt(i, f), + Self(sht4x::Error::Crc) => write!(f, "{NAME} CRC checksum validation failed"), + Self(_) => write!(f, "unknown {NAME} error"), + } + } +}