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 `AsyncSgp30`
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. I've also factored out some of the no-IO code for packing and
unpacking Rust to/from bytes, so that it can be shared by both the async
and blocking driver types.

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 #18, which updates this crate to
use `embedded-hal` v1.0.

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 dbrgn committed Jun 30, 2024
1 parent 24450a0 commit 6bdf2e7
Show file tree
Hide file tree
Showing 5 changed files with 427 additions and 39 deletions.
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ edition = "2018"

[features]
default = []
embedded-hal-async = ["dep:embedded-hal-async", "sensirion-i2c/embedded-hal-async"]

[dependencies]
byteorder = { version = "1", default-features = false }
embedded-hal = "1"
embedded-hal-async = { version = "1", optional = true }
num-traits = { version = "0.2", default-features = false }
sensirion-i2c = "0.3"

Expand All @@ -34,3 +36,12 @@ embedded-hal-mock = { version = "0.11.1", features = ["eh1"] }

[profile.release]
lto = true

[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" }
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
![No Std][no-std-badge]

This is a platform agnostic Rust driver for the Sensirion SGP30 gas sensor,
based on the [`embedded-hal`](https://github.com/japaric/embedded-hal) traits.
based on the [`embedded-hal`](https://github.com/japaric/embedded-hal) or
[`embedded-hal-async`] traits.

Docs: https://docs.rs/sgp30

Expand All @@ -29,6 +30,7 @@ Datasheet: https://www.sensirion.com/file/datasheet_sgp30
- [x] Support on-chip self-test
- [x] CRC checks
- [x] Docs
- [x] [`embedded-hal-async] support

## License

Expand All @@ -45,7 +47,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you, as defined in the Apache-2.0 license, shall
be dual licensed as above, without any additional terms or conditions.


[`embedded-hal-async`]: https://crates.io/crates/embedded-hal-async
<!-- Badges -->
[workflow]: https://github.com/dbrgn/sgp30-rs/actions?query=workflow%3ACI
[workflow-badge]: https://github.com/dbrgn/sgp30-rs/actions/workflows/ci.yml/badge.svg
Expand Down
326 changes: 326 additions & 0 deletions src/async_impl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
use super::{types::*, Command, Error, SELFTEST_SUCCESS};
use byteorder::{BigEndian, ByteOrder};
use embedded_hal_async::{delay::DelayNs, i2c::I2c};
use sensirion_i2c::i2c_async;

/// Async driver for the SGP30.
///
/// This type is identical to the [`Sgp30`](crate::Sgp30) type, but using the
/// [`embedded_hal_async`] versions of the [`I2c`] and [`DelayNs`] traits.
#[derive(Debug, Default)]
pub struct AsyncSgp30<I2C, D> {
/// The concrete I²C device implementation.
i2c: I2C,
/// The I²C device address.
address: u8,
/// The concrete Delay implementation.
delay: D,
/// Whether the air quality measurement was initialized.
initialized: bool,
}

impl<I2C, D> AsyncSgp30<I2C, D>
where
I2C: I2c,
D: DelayNs,
{
/// Create a new instance of the SGP30 driver.
pub fn new(i2c: I2C, address: u8, delay: D) -> Self {
Self {
i2c,
address,
delay,
initialized: false,
}
}

/// Destroy driver instance, return I²C bus instance.
pub fn destroy(self) -> I2C {
self.i2c
}

/// Write an I²C command to the sensor.
async fn send_command(&mut self, command: Command) -> Result<(), Error<I2C::Error>> {
self.i2c
.write(self.address, &command.as_bytes())
.await
.map_err(Error::I2cWrite)
}

/// Write an I²C command and data to the sensor.
///
/// The data slice must have a length of 2 or 4.
///
/// CRC checksums will automatically be added to the data.
async fn send_command_and_data(
&mut self,
command: Command,
data: &[u8],
) -> Result<(), Error<I2C::Error>> {
let mut buf = [0; 2 /* command */ + 6 /* max length of data + crc */];
let payload = command.as_bytes_with_data(&mut buf, data);
self.i2c
.write(self.address, payload)
.await
.map_err(Error::I2cWrite)
}

/// Return the 48 bit serial number of the SGP30.
pub async fn serial(&mut self) -> Result<[u8; 6], Error<I2C::Error>> {
// Request serial number
self.send_command(Command::GetSerial).await?;

// Recommended wait time according to datasheet (6.5)
self.delay.delay_us(500).await;

// Read serial number
let mut buf = [0; 9];
i2c_async::read_words_with_crc(&mut self.i2c, self.address, &mut buf).await?;

Ok([buf[0], buf[1], buf[3], buf[4], buf[6], buf[7]])
}

/// Run an on-chip self-test. Return a boolean indicating whether the test succeeded.
pub async fn selftest(&mut self) -> Result<bool, Error<I2C::Error>> {
// Start self test
self.send_command(Command::SelfTest).await?;

// Max duration according to datasheet (Table 10)
self.delay.delay_ms(220).await;

// Read result
let mut buf = [0; 3];
i2c_async::read_words_with_crc(&mut self.i2c, self.address, &mut buf).await?;

// Compare with self-test success pattern
Ok(&buf[0..2] == SELFTEST_SUCCESS)
}

/// Initialize the air quality measurement.
///
/// The SGP30 uses a dynamic baseline compensation algorithm and on-chip
/// calibration parameters to provide two complementary air quality
/// signals.
///
/// Calling this method starts the air quality measurement. After
/// initializing the measurement, the `measure()` method must be called in
/// regular intervals of 1 s to ensure proper operation of the dynamic
/// baseline compensation algorithm. It is the responsibility of the user
/// of this driver to ensure that these periodic measurements are being
/// done.
///
/// For the first 15 s after initializing the air quality measurement, the
/// sensor is in an initialization phase during which it returns fixed
/// values of 400 ppm CO₂eq and 0 ppb TVOC. After 15 s (15 measurements)
/// the values should start to change.
///
/// A new init command has to be sent after every power-up or soft reset.
pub async fn init(&mut self) -> Result<(), Error<I2C::Error>> {
if self.initialized {
// Already initialized
return Ok(());
}
self.force_init().await
}

/// Like [`init()`](Self::init), but without checking
/// whether the sensor is already initialized.
///
/// This might be necessary after a sensor soft or hard reset.
pub async fn force_init(&mut self) -> Result<(), Error<I2C::Error>> {
// Send command to sensor
self.send_command(Command::InitAirQuality).await?;

// Max duration according to datasheet (Table 10)
self.delay.delay_ms(10).await;

self.initialized = true;
Ok(())
}

/// Get an air quality measurement.
///
/// Before calling this method, the air quality measurements must have been
/// initialized using the [`init()`](Self::init) method.
/// Otherwise an [`Error::NotInitialized`] will be returned.
///
/// Once the measurements have been initialized, the
/// [`measure()`](Self::measure) method must be called
/// in regular intervals of 1 s to ensure proper operation of the dynamic
/// baseline compensation algorithm. It is the responsibility of the user
/// of this driver to ensure that these periodic measurements are being
/// done.
///
/// For the first 15 s after initializing the air quality measurement, the
/// sensor is in an initialization phase during which it returns fixed
/// values of 400 ppm CO₂eq and 0 ppb TVOC. After 15 s (15 measurements)
/// the values should start to change.
pub async fn measure(&mut self) -> Result<Measurement, Error<I2C::Error>> {
if !self.initialized {
// Measurements weren't initialized
return Err(Error::NotInitialized);
}

// Send command to sensor
self.send_command(Command::MeasureAirQuality).await?;

// Max duration according to datasheet (Table 10)
self.delay.delay_ms(12).await;

// Read result
let mut buf = [0; 6];
i2c_async::read_words_with_crc(&mut self.i2c, self.address, &mut buf).await?;
Ok(Measurement::from_bytes(&buf))
}

/// Return sensor raw signals.
///
/// This command is intended for part verification and testing purposes. It
/// returns the raw signals which are used as inputs for the on-chip
/// calibration and baseline compensation algorithm. The command performs a
/// measurement to which the sensor responds with the two signals for H2
/// and Ethanol.
pub async fn measure_raw_signals(&mut self) -> Result<RawSignals, Error<I2C::Error>> {
if !self.initialized {
// Measurements weren't initialized
return Err(Error::NotInitialized);
}

// Send command to sensor
self.send_command(Command::MeasureRawSignals).await?;

// Max duration according to datasheet (Table 10)
self.delay.delay_ms(25).await;

// Read result
let mut buf = [0; 6];
i2c_async::read_words_with_crc(&mut self.i2c, self.address, &mut buf).await?;
Ok(RawSignals::from_bytes(&buf))
}

/// Return the baseline values of the baseline correction algorithm.
///
/// The SGP30 provides the possibility to read and write the baseline
/// values of the baseline correction algorithm. This feature is used to
/// save the baseline in regular intervals on an external non-volatile
/// memory and restore it after a new power-up or soft reset of the sensor.
///
/// This function returns the baseline values for the two air quality
/// signals. These two values should be stored on an external memory. After
/// a power-up or soft reset, the baseline of the baseline correction
/// algorithm can be restored by calling [`init()`](Self::init) followed by
/// [`set_baseline()`](Self::set_baseline).
pub async fn get_baseline(&mut self) -> Result<Baseline, Error<I2C::Error>> {
// Send command to sensor
self.send_command(Command::GetBaseline).await?;

// Max duration according to datasheet (Table 10)
self.delay.delay_ms(10).await;

// Read result
let mut buf = [0; 6];
i2c_async::read_words_with_crc(&mut self.i2c, self.address, &mut buf).await?;
Ok(Baseline::from_bytes(&buf))
}

/// Set the baseline values for the baseline correction algorithm.
///
/// Before calling this method, the air quality measurements must have been
/// initialized using the [`init()`](Self::init) method.
/// Otherwise an [`Error::NotInitialized`] will be returned.
///
/// The SGP30 provides the possibility to read and write the baseline
/// values of the baseline correction algorithm. This feature is used to
/// save the baseline in regular intervals on an external non-volatile
/// memory and restore it after a new power-up or soft reset of the sensor.
///
/// This function sets the baseline values for the two air quality
/// signals.
pub async fn set_baseline(&mut self, baseline: &Baseline) -> Result<(), Error<I2C::Error>> {
if !self.initialized {
// Measurements weren't initialized
return Err(Error::NotInitialized);
}

// Send command and data to sensor
// Note that the order of the two parameters is inverted when writing
// compared to when reading.
let mut buf = [0; 4];
BigEndian::write_u16(&mut buf[0..2], baseline.tvoc);
BigEndian::write_u16(&mut buf[2..4], baseline.co2eq);
self.send_command_and_data(Command::SetBaseline, &buf)
.await?;

// Max duration according to datasheet (Table 10)
self.delay.delay_ms(10).await;

Ok(())
}

/// Set the humidity value for the baseline correction algorithm.
///
/// The SGP30 features an on-chip humidity compensation for the air quality
/// signals (CO₂eq and TVOC) and sensor raw signals (H2 and Ethanol). To
/// use the on-chip humidity compensation, an absolute humidity value from
/// an external humidity sensor is required.
///
/// After setting a new humidity value, this value will be used by the
/// on-chip humidity compensation algorithm until a new humidity value is
/// set. Restarting the sensor (power-on or soft reset) or calling the
/// function with a `None` value sets the humidity value used for
/// compensation to its default value (11.57 g/m³) until a new humidity
/// value is sent.
///
/// Before calling this method, the air quality measurements must have been
/// initialized using the [`init()`](Self::init) method.
/// Otherwise an [`Error::NotInitialized`] will be returned.
pub async fn set_humidity(
&mut self,
humidity: Option<&Humidity>,
) -> Result<(), Error<I2C::Error>> {
if !self.initialized {
// Measurements weren't initialized
return Err(Error::NotInitialized);
}

// Send command and data to sensor
let buf = match humidity {
Some(humi) => humi.as_bytes(),
None => [0, 0],
};
self.send_command_and_data(Command::SetHumidity, &buf)
.await?;

// Max duration according to datasheet (Table 10)
self.delay.delay_ms(10).await;

Ok(())
}

/// Get the feature set.
///
/// The SGP30 features a versioning system for the available set of
/// measurement commands and on-chip algorithms. This so called feature set
/// version number can be read out with this method.
pub async fn get_feature_set(&mut self) -> Result<FeatureSet, Error<I2C::Error>> {
// Send command to sensor
self.send_command(Command::GetFeatureSet).await?;

// Max duration according to datasheet (Table 10)
self.delay.delay_ms(2).await;

// Read result
let mut buf = [0; 3];
i2c_async::read_words_with_crc(&mut self.i2c, self.address, &mut buf).await?;

Ok(FeatureSet::parse(buf[0], buf[1]))
}
}

#[cfg(test)]
mod tests {
// TODO: `embedded-hal-mock`'s support for `embedded-hal-async` does not
// currently have a mock I2C implementation. When that's available, we
// should add tests for the async I2C functions here that are analogous to
// the ones in the `i2c` module.
}
Loading

0 comments on commit 6bdf2e7

Please sign in to comment.