Skip to content

Commit

Permalink
Add embedded-hal-async support
Browse files Browse the repository at this point in the history
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]: Sensirion/sensirion-i2c-rs@f7b9f3a
  • Loading branch information
hawkw authored and sirhcel committed Nov 20, 2024
1 parent 5051836 commit 876679c
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 13 deletions.
12 changes: 11 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.0"

[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" }
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,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

Expand Down
165 changes: 165 additions & 0 deletions src/async_sht4x.rs
Original file line number Diff line number Diff line change
@@ -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<I, D> {
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<D>,
}

impl<I, D> AsyncSht4x<I, D>
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<Measurement, Error<I::Error>> {
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<SensorData, Error<I::Error>> {
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<Measurement, Error<I::Error>> {
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<SensorData, Error<I::Error>> {
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<u32, Error<I::Error>> {
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<I::Error>> {
self.write_command_and_delay_for_execution(Command::SoftReset, delay)
.await
}

async fn read_response(&mut self) -> Result<[u8; RESPONSE_LEN], Error<I::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<I::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(())
}
}
3 changes: 1 addition & 2 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use embedded_hal::i2c::I2c;
use sensirion_i2c::i2c;

/// Error conditions from accessing SHT4x sensors.
Expand All @@ -14,7 +13,7 @@ pub enum Error<E> {

impl<I> From<i2c::Error<I>> for Error<I::Error>
where
I: I2c,
I: embedded_hal::i2c::ErrorType,
{
fn from(err: i2c::Error<I>) -> Self {
match err {
Expand Down
6 changes: 6 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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::*;
20 changes: 10 additions & 10 deletions src/sht4x.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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,
Expand All @@ -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]]),
}
}

0 comments on commit 876679c

Please sign in to comment.