From d5c0e803ef7f4ed60c5dd96f9bbd13a6fe49cbbd Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Sun, 9 Jun 2024 14:42:49 -0700 Subject: [PATCH] Add `embedded-hal-async` support This commit adds support for the `embedded-hal-async` crate in addition to `embedded-hal`. I've done this by adding a separate `AsyncSht4x` type, based on the assumption that most projects won't need to use both the blocking `embedded-hal` traits and the `embedded-hal-async` traits at the same time, and providing `async fn` methods on a separate type with the same names as the blocking ones seemed a bit nicer than having one type that has both `fn measure` and `async fn measure_async` and so on. Support for `embedded-hal-async` is gated behind the `embedded-hal-async` feature flag, so the dependency is not enabled by default. Note that this branch depends on my PR #6, which updates this crate to use `embedded-hal` v1.0, and currently contains the commit from that change as well. Once #6 has merged, this branch will need to be rebased onto the main branch. It also depends on my upstream PR adding `embedded-hal-async` support to `sensirion-i2c-rs`, sensirion/sensirion-i2c-rs#30, which has been [merged], but hasn't been published to crates.io yet. Currently, this branch adds a Cargo `[patch]` to use a Git dep on `sensirion-i2c-rs`. So, this change cannot be released to crates.io until upstream publishes a new release of `sensirion-i2c-rs`. Hopefully they do that soon! :) [merged]: https://github.com/Sensirion/sensirion-i2c-rs/commit/f7b9f3a81b777bc6e6b2f0acb4c1ef9c57dfa06d --- Cargo.toml | 12 +++- README.md | 24 +++++++ src/async_sht4x.rs | 165 +++++++++++++++++++++++++++++++++++++++++++++ src/error.rs | 2 +- src/lib.rs | 6 ++ src/sht4x.rs | 20 +++--- 6 files changed, 217 insertions(+), 12 deletions(-) create mode 100644 src/async_sht4x.rs diff --git a/Cargo.toml b/Cargo.toml index 1b5f413..d22f4c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,12 +20,22 @@ exclude = [ ".gitignore", ] - [dependencies] defmt = { version = "0.3.2", optional = true } embedded-hal = "1.0.0" +embedded-hal-async = { version = "1.0.0", optional = true } fixed = "1.20.0" sensirion-i2c = "0.3" [features] defmt = ["dep:defmt"] +embedded-hal-async = ["dep:embedded-hal-async", "sensirion-i2c/embedded-hal-async"] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +# Necessary for `embedded-hal-async` support until a new release of +# `sensirion-i2c-rs` is published. +[patch.crates-io] +sensirion-i2c = { git = "https://github.com/sensirion/sensirion-i2c-rs", rev = "f7b9f3a81b777bc6e6b2f0acb4c1ef9c57dfa06d" } diff --git a/README.md b/README.md index b6e8f32..96064e8 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,30 @@ if let Ok(measurement) = measurement { } ``` +## `embedded-hal-async` support + +This crate has optional support for the [`embedded-hal-async`] crate, which +provides `async` versions of the `I2c` and `DelayNs` traits. Async support +is an off-by-default optional feature, so that projects which aren't using +[`embedded-hal-async`] can avoid the additional dependency. + +To use this crate with `embedded-hal-async`, enable the `embedded-hal-async` +feature flag in your `Cargo.toml`: + +```toml +sht4x = { version = "0.2", features = ["embedded-hal-async"] } +``` + +Once the `embedded-hal-async` feature is enabled, construct an instance of +the `AsyncSht4x` struct, providing types implementing the +[`embedded_hal_async::i2c::I2c`] and [`embedded_hal_async::delay::DelayNs`] +traits. The `AsyncSht4x` struct is identical to the `Sht4x` struct, +except that its methods are `async fn`s. + +[`embedded-hal-async`]: https://crates.io/crates/embedded-hal-async +[`embedded_hal_async::i2c::I2c`]: https://docs.rs/embedded-hal-async/latest/embedded_hal_async/i2c/trait.I2c.html +[`embedded_hal_async::delay::DelayNs`]: https://docs.rs/embedded-hal-async/latest/embedded_hal_async/delay/trait.DelayNs.html + ## Related Work diff --git a/src/async_sht4x.rs b/src/async_sht4x.rs new file mode 100644 index 0000000..c7a48b6 --- /dev/null +++ b/src/async_sht4x.rs @@ -0,0 +1,165 @@ +use crate::{ + commands::Command, + error::Error, + sht4x::RESPONSE_LEN, + types::{Address, HeatingDuration, HeatingPower, Measurement, Precision, SensorData}, +}; +use core::marker::PhantomData; +use embedded_hal_async::{delay::DelayNs, i2c::I2c}; +use sensirion_i2c::i2c_async; + +/// Async driver for STH4x sensors. +/// +/// This type behaves identically to the [`Sht4x`](crate::Sht4x) type, except +/// that it uses the `embedded-hal-async` [`I2c`] and [`DelayNs`] traits instead +/// of the `embedded-hal` traits, and all of its methods are `async fn`s. +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[derive(Debug)] +pub struct AsyncSht4x { + i2c: I, + address: Address, + // If we want to globally define the delay type for this struct, we have to consume the type + // parameter. + _delay: PhantomData, +} + +impl AsyncSht4x +where + I: I2c, + D: DelayNs, +{ + /// Creates a new driver instance using the given I2C bus. It configures the default I2C + /// address 0x44 used by most family members. + /// + /// For operating multiple devices on the same bus, + /// [`shared-bus`](https://github.com/Rahix/shared-bus) might come in handy. + pub fn new(i2c: I) -> Self { + Self::new_with_address(i2c, Address::Address0x44) + } + + /// Crates a new driver instance using the given I2C bus and address. This constructor allows + /// to instantiate the driver for the SHT40-BD1B which uses the non-default I2C address 0x45. + /// + /// For operating multiple devices on the same bus, + /// [`shared-bus`](https://github.com/Rahix/shared-bus) might come in handy. + pub fn new_with_address(i2c: I, address: Address) -> Self { + Self { + i2c, + address, + _delay: PhantomData, + } + } + + /// Destroys the driver and returns the used I2C bus. + pub fn destroy(self) -> I { + self.i2c + } + + /// Activates the heater and performs a measurement returning measurands in SI units. + /// + /// **Note:** The heater is designed to be used up to 10 % of the sensor's lifetime. Please + /// check the + /// [datasheet](https://sensirion.com/media/documents/33FD6951/624C4357/Datasheet_SHT4x.pdf), + /// section 4.9 _Heater Operation_ for details. + pub async fn heat_and_measure( + &mut self, + power: HeatingPower, + duration: HeatingDuration, + delay: &mut D, + ) -> Result> { + let raw = self.heat_and_measure_raw(power, duration, delay).await?; + + Ok(Measurement::from(raw)) + } + + /// Activates the heater and performs a measurement returning raw sensor data. + /// + /// **Note:** The heater is designed to be used up to 10 % of the sensor's lifetime. Please + /// check the + /// [datasheet](https://sensirion.com/media/documents/33FD6951/624C4357/Datasheet_SHT4x.pdf), + /// section 4.9 _Heater Operation_ for details. + pub async fn heat_and_measure_raw( + &mut self, + power: HeatingPower, + duration: HeatingDuration, + delay: &mut D, + ) -> Result> { + let command = Command::from((power, duration)); + + self.write_command_and_delay_for_execution(command, delay) + .await?; + let response = self.read_response().await?; + let raw = crate::sht4x::sensor_data_from_response(&response); + + Ok(raw) + } + + /// Performs a measurement returning measurands in SI units. + pub async fn measure( + &mut self, + precision: Precision, + delay: &mut D, + ) -> Result> { + let raw = self.measure_raw(precision, delay).await?; + Ok(Measurement::from(raw)) + } + + /// Performs a measurement returning raw sensor data. + pub async fn measure_raw( + &mut self, + precision: Precision, + delay: &mut D, + ) -> Result> { + let command = Command::from(precision); + + self.write_command_and_delay_for_execution(command, delay) + .await?; + let response = self.read_response().await?; + let raw = crate::sht4x::sensor_data_from_response(&response); + + Ok(raw) + } + + /// Reads the sensor's serial number. + pub async fn serial_number(&mut self, delay: &mut D) -> Result> { + self.write_command_and_delay_for_execution(Command::SerialNumber, delay) + .await?; + let response = self.read_response().await?; + + Ok(u32::from_be_bytes([ + response[0], + response[1], + response[3], + response[4], + ])) + } + + /// Performs a soft reset of the sensor. + pub async fn soft_reset(&mut self, delay: &mut D) -> Result<(), Error> { + self.write_command_and_delay_for_execution(Command::SoftReset, delay) + .await + } + + async fn read_response(&mut self) -> Result<[u8; RESPONSE_LEN], Error> { + let mut response = [0; RESPONSE_LEN]; + + i2c_async::read_words_with_crc(&mut self.i2c, self.address.into(), &mut response).await?; + + Ok(response) + } + + async fn write_command_and_delay_for_execution( + &mut self, + command: Command, + delay: &mut D, + ) -> Result<(), Error> { + let code = command.code(); + + i2c_async::write_command_u8(&mut self.i2c, self.address.into(), code) + .await + .map_err(Error::I2c)?; + delay.delay_ms(command.duration_ms()).await; + + Ok(()) + } +} diff --git a/src/error.rs b/src/error.rs index 2c188ed..bb5514f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,7 +13,7 @@ pub enum Error { impl From> for Error where - I: embedded_hal::i2c::I2c, + I: embedded_hal::i2c::ErrorType, { fn from(err: i2c::Error) -> Self { match err { diff --git a/src/lib.rs b/src/lib.rs index 51c3c40..bd46055 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,18 @@ #![deny(unsafe_code)] #![no_std] #![doc = include_str!("../README.md")] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] mod commands; mod error; mod sht4x; mod types; +#[cfg(feature = "embedded-hal-async")] +mod async_sht4x; +#[cfg(feature = "embedded-hal-async")] +pub use self::async_sht4x::AsyncSht4x; + pub use crate::error::*; pub use crate::sht4x::*; pub use crate::types::*; diff --git a/src/sht4x.rs b/src/sht4x.rs index da0184d..dafe099 100644 --- a/src/sht4x.rs +++ b/src/sht4x.rs @@ -7,7 +7,7 @@ use core::marker::PhantomData; use embedded_hal::{delay::DelayNs, i2c::I2c}; use sensirion_i2c::i2c; -const RESPONSE_LEN: usize = 6; +pub(crate) const RESPONSE_LEN: usize = 6; /// Driver for STH4x sensors. #[cfg_attr(feature = "defmt", derive(defmt::Format))] @@ -108,7 +108,7 @@ where self.write_command_and_delay_for_execution(command, delay)?; let response = self.read_response()?; - let raw = self.sensor_data_from_response(&response); + let raw = sensor_data_from_response(&response); Ok(raw) } @@ -133,7 +133,7 @@ where self.write_command_and_delay_for_execution(command, delay)?; let response = self.read_response()?; - let raw = self.sensor_data_from_response(&response); + let raw = sensor_data_from_response(&response); Ok(raw) } @@ -164,13 +164,6 @@ where Ok(response) } - fn sensor_data_from_response(&self, response: &[u8; RESPONSE_LEN]) -> SensorData { - SensorData { - temperature: u16::from_be_bytes([response[0], response[1]]), - humidity: u16::from_be_bytes([response[3], response[4]]), - } - } - fn write_command_and_delay_for_execution( &mut self, command: Command, @@ -184,3 +177,10 @@ where Ok(()) } } + +pub(crate) fn sensor_data_from_response(response: &[u8; RESPONSE_LEN]) -> SensorData { + SensorData { + temperature: u16::from_be_bytes([response[0], response[1]]), + humidity: u16::from_be_bytes([response[3], response[4]]), + } +}