From 5591e5e5cd45cb4de5646972b90725e99bd91bc2 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Sun, 9 Jun 2024 11:14:47 -0700 Subject: [PATCH] feat(eclss): add Bosch BME680 sensor support --- Cargo.lock | 12 +++ Cargo.toml | 2 + eclssd/Cargo.toml | 3 +- eclssd/src/main.rs | 13 +++ flake.nix | 1 + lib/eclss/Cargo.toml | 4 +- lib/eclss/src/sensor.rs | 4 + lib/eclss/src/sensor/bme680.rs | 159 +++++++++++++++++++++++++++++++++ 8 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 lib/eclss/src/sensor/bme680.rs diff --git a/Cargo.lock b/Cargo.lock index 020317a..4cdb3dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -198,6 +198,17 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +[[package]] +name = "bosch-bme680" +version = "1.0.2" +source = "git+https://github.com/hawkw/bosch-bme680?branch=eliza/async#69178cdeb1ad96013a6cb0638f68f475cef379f2" +dependencies = [ + "bitfield", + "embedded-hal", + "embedded-hal-async", + "log", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -315,6 +326,7 @@ checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" name = "eclss" version = "0.1.0" dependencies = [ + "bosch-bme680", "eclss-api", "embedded-hal", "embedded-hal-async", diff --git a/Cargo.toml b/Cargo.toml index f07e1fb..878482a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ resolver = "2" [workspace.dependencies] anyhow = "1.0" axum = "0.7.5" +bosch-bme680 = "1.0.2" clap = "4.0" eclss = { path = "lib/eclss" } eclss-api = { path = "lib/eclss-api" } @@ -38,6 +39,7 @@ sgp30 = { version = "0.4.0", default-features = false } serde = { version = "1.0", default-features = false } [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" } diff --git a/eclssd/Cargo.toml b/eclssd/Cargo.toml index 4bba552..9501468 100644 --- a/eclssd/Cargo.toml +++ b/eclssd/Cargo.toml @@ -6,7 +6,8 @@ readme = "README.md" license = "MIT" [features] -default = ["scd41", "sgp30", "pmsa003i", "ens160", "mdns"] +default = ["bme680", "scd41", "sgp30", "pmsa003i", "ens160", "mdns"] +bme680 = ["eclss/bme680"] scd41 = ["eclss/scd41"] scd40 = ["eclss/scd40"] sgp30 = ["eclss/sgp30"] diff --git a/eclssd/src/main.rs b/eclssd/src/main.rs index c850cf5..0e411a8 100644 --- a/eclssd/src/main.rs +++ b/eclssd/src/main.rs @@ -137,6 +137,19 @@ async fn main() -> anyhow::Result<()> { } }); + #[cfg(feature = "bme680")] + sensors.spawn({ + let sensor = sensor::Bme680::new(eclss, linux_embedded_hal::Delay); + let backoff = backoff.clone(); + async move { + tracing::info!("starting BME680..."); + eclss + .run_sensor(sensor, backoff, linux_embedded_hal::Delay) + .await + .unwrap() + } + }); + while let Some(join) = sensors.join_next().await { join.unwrap(); } diff --git a/flake.nix b/flake.nix index 272e68b..7d30c72 100644 --- a/flake.nix +++ b/flake.nix @@ -62,6 +62,7 @@ "sgp30-0.4.0" = "sha256-5LRVMFMTxpgQpxrN0sMTiedL4Sw6NBRd86+ZvRP8CVk="; "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 229b23d..adeaca4 100644 --- a/lib/eclss/Cargo.toml +++ b/lib/eclss/Cargo.toml @@ -6,13 +6,15 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] +bme680 = ["dep:bosch-bme680"] serde = ["dep:serde", "tinymetrics/serde"] scd40 = ["dep:scd4x"] scd41 = ["dep:scd4x", "scd4x/scd41"] sgp30 = ["dep:sgp30"] -default = ["pmsa003i", "scd40", "ens160", "sgp30"] +default = ["pmsa003i", "scd40", "ens160", "sgp30", "bme680"] [dependencies] +bosch-bme680 = { workspace = true, optional = true, features = ["embedded-hal-async"] } eclss-api = { workspace = true, features = ["fmt"] } ens160 = { workspace = true, optional = true, features = ["async"] } embedded-hal-async = { workspace = true } diff --git a/lib/eclss/src/sensor.rs b/lib/eclss/src/sensor.rs index e4703ea..d93c8dd 100644 --- a/lib/eclss/src/sensor.rs +++ b/lib/eclss/src/sensor.rs @@ -4,6 +4,10 @@ use core::time::Duration; use embedded_hal_async::delay::DelayNs; mod status; +#[cfg(feature = "bme680")] +pub mod bme680; +pub use bme680::Bme680; + #[cfg(feature = "pmsa003i")] pub mod pmsa003i; #[cfg(feature = "pmsa003i")] diff --git a/lib/eclss/src/sensor/bme680.rs b/lib/eclss/src/sensor/bme680.rs new file mode 100644 index 0000000..e6cc728 --- /dev/null +++ b/lib/eclss/src/sensor/bme680.rs @@ -0,0 +1,159 @@ +use crate::{ + error::{Context, EclssError, SensorError}, + metrics::{Gauge, SensorLabel}, + sensor::Sensor, + SharedBus, +}; +use bosch_bme680::{AsyncBme680, BmeError}; +use core::fmt; +use core::num::Wrapping; +use embedded_hal_async::{ + delay::DelayNs, + i2c::{self, Error as _, I2c}, +}; +pub struct Bme680 { + sensor: AsyncBme680<&'static SharedBus, D>, + temp: &'static Gauge, + rel_humidity: &'static Gauge, + abs_humidity: &'static Gauge, + pressure: &'static Gauge, + gas_resistance: &'static Gauge, + abs_humidity_interval: usize, + polls: Wrapping, +} + +impl Bme680 +where + I: I2c, + D: DelayNs, +{ + pub fn new( + eclss: &'static crate::Eclss, + delay: D, + ) -> Self { + let metrics = &eclss.metrics; + const LABEL: SensorLabel = SensorLabel(NAME); + + // the default I2C address of the Adafruit BME680 breakout board + // is the "secondary" address, 0x77. + let address = bosch_bme680::DeviceAddress::Secondary; + // TODO(eliza): get this from an ambient measurement... + let ambient_temp = 20; + Self { + sensor: AsyncBme680::new(&eclss.i2c, address, delay, ambient_temp), + temp: metrics.temp.register(LABEL).unwrap(), + pressure: metrics.pressure.register(LABEL).unwrap(), + rel_humidity: metrics.rel_humidity.register(LABEL).unwrap(), + abs_humidity: metrics.abs_humidity.register(LABEL).unwrap(), + gas_resistance: metrics.gas_resistance.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(BmeError); + +const NAME: &str = "BME680"; + +impl Sensor for Bme680 +where + I: I2c + 'static, + I::Error: core::fmt::Display, + 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> { + let config = bosch_bme680::Configuration::default(); + self.sensor + .initialize(&config) + .await + .context("error initializing BME680")?; + tracing::info!("initialized BME680 with config: {config:?}"); + Ok(()) + } + + async fn poll(&mut self) -> Result<(), Self::Error> { + let bosch_bme680::MeasurmentData { + temperature, + humidity, + pressure, + gas_resistance, + } = self + .sensor + .measure() + .await + .context("error reading BME680 measurements")?; + self.polls += 1; + + // pretty sure the `bosch-bme680` library is off by a factor of 100 when + // representing pressures as hectopascals... + let pressure = pressure / 100f32; + self.pressure.set_value(pressure.into()); + self.temp.set_value(temperature.into()); + self.rel_humidity.set_value(humidity.into()); + tracing::debug!("Temp: {temperature}°C, Humidity: {humidity}%, Pressure: {pressure} hPa"); + + if let Some(gas_resistance) = gas_resistance { + self.gas_resistance.set_value(gas_resistance.into()); + tracing::debug!("Gas resistance: {gas_resistance} Ohms"); + } + + 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 +where + E: embedded_hal::i2c::ErrorType, +{ + fn i2c_error(&self) -> Option { + match self.0 { + BmeError::WriteError(ref e) => Some(e.kind()), + BmeError::WriteReadError(ref e) => Some(e.kind()), + _ => None, + } + } +} + +impl fmt::Display for Error +where + E: embedded_hal::i2c::ErrorType, + E::Error: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + BmeError::WriteError(ref e) => write!(f, "I2C write error: {e}"), + BmeError::WriteReadError(ref e) => write!(f, "I2C write-read error: {e}"), + BmeError::MeasuringTimeOut => f.write_str("BME680 measurement timed out"), + BmeError::UnexpectedChipId(id) => { + write!(f, "unexpected BME680 chip ID {id:#04x} (expected 0x61)") + } + BmeError::Uninitialized => f.write_str("BME680 sensor hasn't been initialized yet"), + } + } +} + +impl From> for Error +where + E: embedded_hal::i2c::ErrorType, +{ + fn from(e: BmeError) -> Self { + Self(e) + } +}