diff --git a/Cargo.lock b/Cargo.lock --- a/Cargo.lock +++ b/Cargo.lock @@ -3,10 +3,22 @@ version = 4 [[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "byteorder" @@ -15,12 +27,59 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "defmt" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" +dependencies = [ + "defmt 1.0.1", +] + +[[package]] name = "defmt" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78" dependencies = [ - "bitflags", + "bitflags 1.3.2", "defmt-macros", ] @@ -47,6 +106,61 @@ ] [[package]] +name = "embedded-hal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" +dependencies = [ + "defmt 0.3.100", +] + +[[package]] +name = "embedded-hal-async" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884" +dependencies = [ + "embedded-hal", +] + +[[package]] +name = "embedded-hal-mock" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a0f04f8886106faf281c47b6a0e4054a369baedaf63591fdb8da9761f3f379" +dependencies = [ + "embedded-hal", + "embedded-hal-nb", +] + +[[package]] +name = "embedded-hal-nb" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba4268c14288c828995299e59b12babdbe170f6c6d73731af1b4648142e8605" +dependencies = [ + "embedded-hal", + "nb", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "gpio-cdev" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09831ec59b80be69e75d29cf36e16afbbe5fd1af9c1bf4689ad91c77db5aa6a6" +dependencies = [ + "bitflags 2.10.0", + "libc", + "nix 0.27.1", +] + +[[package]] name = "hash32" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -61,10 +175,132 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed" dependencies = [ - "defmt", + "defmt 1.0.1", "hash32", "stable_deref_trait", ] + +[[package]] +name = "i2cdev" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b940f7497c4f95b863b21cd34c3737b53a67d80d94cf29055d7f7eeca6ffdb4" +dependencies = [ + "bitflags 2.10.0", + "byteorder", + "libc", + "nix 0.26.4", +] + +[[package]] +name = "io-kit-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b" +dependencies = [ + "core-foundation-sys", + "mach2", +] + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "linux-embedded-hal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8a605c95f708c78554738a12153b213f107d3bd5323f7ce32d6deb3faafb40" +dependencies = [ + "cast", + "embedded-hal", + "embedded-hal-nb", + "gpio-cdev", + "i2cdev", + "nb", + "nix 0.27.1", + "serialport", + "spidev", + "sysfs_gpio", +] + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "nb" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" + +[[package]] +name = "nix" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" +dependencies = [ + "bitflags 1.3.2", + "cc", + "cfg-if", + "libc", + "memoffset 0.6.5", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.7.1", + "pin-utils", +] + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "libc", +] + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "proc-macro-error-attr2" @@ -99,9 +335,9 @@ [[package]] name = "quote" -version = "1.0.42" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] @@ -114,7 +350,7 @@ name = "sachy-bthome" version = "0.1.0" dependencies = [ - "defmt", + "defmt 1.0.1", "heapless", "sachy-fmt", ] @@ -123,7 +359,60 @@ name = "sachy-fmt" version = "0.1.0" dependencies = [ - "defmt", + "defmt 1.0.1", +] + +[[package]] +name = "sachy-shtc3" +version = "0.1.0" +dependencies = [ + "defmt 1.0.1", + "embedded-hal", + "embedded-hal-async", + "embedded-hal-mock", + "linux-embedded-hal", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serialport" +version = "4.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21f60a586160667241d7702c420fc223939fb3c0bb8d3fac84f78768e8970dee" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "core-foundation", + "core-foundation-sys", + "io-kit-sys", + "mach2", + "nix 0.26.4", + "quote", + "scopeguard", + "unescaper", + "windows-sys", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "spidev" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32dadd0a877f0652fa52dbc4d2ed9f4877bea5cd30725507b36e1970a5ef0519" +dependencies = [ + "bitflags 2.10.0", + "libc", + "nix 0.26.4", ] [[package]] @@ -141,6 +430,15 @@ "proc-macro2", "quote", "unicode-ident", +] + +[[package]] +name = "sysfs_gpio" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8808c55bc926565c62ef7838bcaa8add51585236803e2bdfa1472e3a3ab5e17" +dependencies = [ + "nix 0.23.2", ] [[package]] @@ -164,7 +462,89 @@ ] [[package]] +name = "unescaper" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c01d12e3a56a4432a8b436f293c25f4808bdf9e9f9f98f9260bba1f1bc5a1f26" +dependencies = [ + "thiserror", +] + +[[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["sachy-battery","sachy-bthome", "sachy-fmt"] +members = ["sachy-battery","sachy-bthome", "sachy-fmt", "sachy-shtc3"] [workspace.package] authors = ["Sachymetsu "] diff --git a/sachy-shtc3/Cargo.toml b/sachy-shtc3/Cargo.toml new file mode 100644 --- /dev/null +++ b/sachy-shtc3/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "sachy-shtc3" +description = "A SHTC3 driver crate" +version = { workspace = true } +edition = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +license = { workspace = true } +rust-version = { workspace = true } + +[dependencies] +defmt = { version = "1.0.1", optional = true } +embedded-hal = { version = "1.0.0" } +embedded-hal-async = { version = "1.0.0" } + +[dev-dependencies] +embedded-hal-mock = { version = "0.11.1", features = ["eh1"], default-features = false } +linux-embedded-hal = "0.4.0" + +[features] +defmt = ["dep:defmt", "embedded-hal/defmt-03"] +default = [] diff --git a/sachy-shtc3/src/crc.rs b/sachy-shtc3/src/crc.rs new file mode 100644 --- /dev/null +++ b/sachy-shtc3/src/crc.rs @@ -0,0 +1,39 @@ +/// Calculate the CRC8 checksum. +/// +/// Implementation based on the reference implementation by Sensirion. +#[inline] +pub(crate) const fn crc8(data: &[u8]) -> u8 { + const CRC8_POLYNOMIAL: u8 = 0x31; + let mut crc: u8 = u8::MAX; + let mut i = 0; + + while i < data.len() { + crc ^= data[i]; + i += 1; + + let mut c = 0; + while c < 8 { + c += 1; + if (crc & 0x80) > 0 { + crc = (crc << 1) ^ CRC8_POLYNOMIAL; + } else { + crc <<= 1; + } + } + } + + crc +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Test the crc8 function against the test value provided in the + /// SHTC3 datasheet (section 5.10). + #[test] + fn crc8_test_value() { + assert_eq!(crc8(&[0x00]), 0xac); + assert_eq!(crc8(&[0xbe, 0xef]), 0x92); + } +} diff --git a/sachy-shtc3/src/lib.rs b/sachy-shtc3/src/lib.rs new file mode 100644 --- /dev/null +++ b/sachy-shtc3/src/lib.rs @@ -0,0 +1,966 @@ +//! # Introduction +//! +//! This is a platform agnostic Rust driver for the Sensirion SHTC3 temperature / +//! humidity sensor, based on the +//! [`embedded-hal`](https://github.com/rust-embedded/embedded-hal) traits. +//! +//! ## Supported Devices +//! +//! Tested with the following sensors: +//! - [SHTC3](https://www.sensirion.com/shtc3/) +//! +//! ## Blocking / Non-Blocking Modes +//! +//! This driver provides blocking and non-blocking calls. The blocking calls delay the execution +//! until the measurement is done and return the results. The non-blocking ones just start the +//! measurement and allow the application code to do other stuff and get the results afterwards. +//! +//! ## Clock Stretching +//! +//! While the sensor would provide measurement commands with clock stretching to indicate when the +//! measurement is done, this is not implemented and probably won't be. +//! +//! ## Usage +//! +//! ### Setup +//! +//! Instantiate a new driver instance using a [blocking I²C HAL +//! implementation](https://docs.rs/embedded-hal/0.2.*/embedded_hal/blocking/i2c/index.html) +//! and a [blocking `Delay` +//! instance](https://docs.rs/embedded-hal/0.2.*/embedded_hal/blocking/delay/index.html). +//! For example, using `linux-embedded-hal` and an SHTC3 sensor: +//! +//! ```no_run +//! use linux_embedded_hal::{Delay, I2cdev}; +//! use sachy_shtc3::ShtC3; +//! +//! let dev = I2cdev::new("/dev/i2c-1").unwrap(); +//! let mut sht = ShtC3::new(dev); +//! ``` +//! +//! ### Device Info +//! +//! Then, you can query information about the sensor: +//! +//! ```no_run +//! use linux_embedded_hal::{Delay, I2cdev}; +//! use sachy_shtc3::ShtC3; +//! let mut sht = ShtC3::new(I2cdev::new("/dev/i2c-1").unwrap()); +//! let device_id = sht.device_identifier().unwrap(); +//! let raw_id = sht.raw_id_register().unwrap(); +//! ``` +//! +//! ### Measurements (Blocking) +//! +//! For measuring your environment, you can either measure just temperature, +//! just humidity, or both: +//! +//! ```no_run +//! use linux_embedded_hal::{Delay, I2cdev}; +//! use sachy_shtc3::{ShtC3, PowerMode}; +//! +//! let mut sht = ShtC3::new(I2cdev::new("/dev/i2c-1").unwrap()); +//! let mut delay = Delay; +//! +//! let temperature = sht.measure_temperature(PowerMode::NormalMode, &mut delay).unwrap(); +//! let humidity = sht.measure_humidity(PowerMode::NormalMode, &mut delay).unwrap(); +//! let combined = sht.measure(PowerMode::NormalMode, &mut delay).unwrap(); +//! +//! println!("Temperature: {} °C", temperature.as_degrees_celsius()); +//! println!("Humidity: {} %RH", humidity.as_percent()); +//! println!("Combined: {} °C / {} %RH", +//! combined.temperature.as_degrees_celsius(), +//! combined.humidity.as_percent()); +//! ``` +//! +//! You can also use the low power mode for less power consumption, at the cost +//! of reduced repeatability and accuracy of the sensor signals. For more +//! information, see the ["Low Power Measurement Mode" application note][low-power]. +//! +//! [low-power]: https://www.sensirion.com/fileadmin/user_upload/customers/sensirion/Dokumente/2_Humidity_Sensors/Sensirion_Humidity_Sensors_SHTC3_Low_Power_Measurement_Mode.pdf +//! +//! ```no_run +//! use linux_embedded_hal::{Delay, I2cdev}; +//! use sachy_shtc3::{ShtC3, PowerMode}; +//! let mut sht = ShtC3::new(I2cdev::new("/dev/i2c-1").unwrap()); +//! let mut delay = Delay; +//! let measurement = sht.measure(PowerMode::LowPower, &mut delay).unwrap(); +//! ``` +//! +//! ### Measurements (Non-Blocking) +//! +//! If you want to avoid blocking measurements, you can use the non-blocking +//! commands instead. You are, however, responsible for ensuring the correct +//! timing of the calls. +//! +//! ```no_run +//! use linux_embedded_hal::I2cdev; +//! use sachy_shtc3::{ShtC3, PowerMode}; +//! +//! let mut sht = ShtC3::new(I2cdev::new("/dev/i2c-1").unwrap()); +//! +//! sht.start_measurement(PowerMode::NormalMode).unwrap(); +//! // Wait for at least `max_measurement_duration(&sht, PowerMode::NormalMode)` µs +//! let result = sht.get_measurement_result().unwrap(); +//! ``` +//! +//! In non-blocking mode, if desired, you can also read the raw 16-bit +//! measurement results from the sensor by using the following two methods +//! instead: +//! +//! - [`get_raw_measurement_result`](crate::ShtC3::get_raw_measurement_result()) +//! - [`get_raw_partial_measurement_result`](crate::ShtC3::get_raw_partial_measurement_result()) +//! +//! The raw values are of type u16. They require a conversion formula for +//! conversion to a temperature / humidity value (see datasheet). +//! +//! Invoking any command other than +//! [`wakeup`](crate::ShtC3::wakeup()) while the sensor is in +//! sleep mode will result in an error. +//! +//! ### Soft Reset +//! +//! The SHTC3 provides a soft reset mechanism that forces the system into a +//! well-defined state without removing the power supply. If the system is in +//! its idle state (i.e. if no measurement is in progress) the soft reset +//! command can be sent. This triggers the sensor to reset all internal state +//! machines and reload calibration data from the memory. +//! +//! ```no_run +//! use linux_embedded_hal::{Delay, I2cdev}; +//! use sachy_shtc3::{ShtC3, PowerMode}; +//! let mut sht = ShtC3::new(I2cdev::new("/dev/i2c-1").unwrap()); +//! let mut delay = Delay; +//! sht.reset(&mut delay).unwrap(); +//! ``` +#![deny(unsafe_code, missing_docs)] +#![no_std] + +mod crc; +mod types; + +use embedded_hal::{ + delay::DelayNs as BlockingDelayNs, + i2c::{self, I2c, SevenBitAddress}, +}; + +use crc::crc8; +use embedded_hal_async::delay::DelayNs; +pub use types::*; + +/// Whether temperature or humidity is returned first when doing a measurement. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +enum MeasurementOrder { + TemperatureFirst, + HumidityFirst, +} + +/// Measurement power mode: Normal mode or low power mode. +/// +/// The sensors provides a low power measurement mode. Using the low power mode +/// significantly shortens the measurement duration and thus minimizes the +/// energy consumption per measurement. The benefit of ultra-low power +/// consumption comes at the cost of reduced repeatability of the sensor +/// signals: while the impact on the relative humidity signal is negligible and +/// does not affect accuracy, it has an effect on temperature accuracy. +/// +/// More details can be found in the ["Low Power Measurement Mode" application +/// note][an-low-power] by Sensirion. +/// +/// [an-low-power]: https://www.sensirion.com/fileadmin/user_upload/customers/sensirion/Dokumente/2_Humidity_Sensors/Sensirion_Humidity_Sensors_SHTC3_Low_Power_Measurement_Mode.pdf +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum PowerMode { + /// Normal measurement. + NormalMode, + /// Low power measurement: Less energy consumption, but repeatability and + /// accuracy of measurements are negatively impacted. + LowPower, +} + +/// All possible errors in this crate +#[derive(Debug, PartialEq, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Error { + /// I²C bus error + I2c(E), + /// CRC checksum validation failed + Crc, +} + +impl From for Error { + fn from(_value: CrcError) -> Self { + Self::Crc + } +} + +#[derive(Debug, PartialEq, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +struct CrcError; + +impl From for Error +where + E: i2c::Error, +{ + fn from(e: E) -> Self { + Error::I2c(e) + } +} + +/// I²C commands sent to the sensor. +#[derive(Debug, Copy, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +enum Command { + /// Go into sleep mode. + Sleep, + /// Wake up from sleep mode. + WakeUp, + /// Measurement commands. + Measure { + power_mode: PowerMode, + order: MeasurementOrder, + }, + /// Software reset. + SoftwareReset, + /// Read ID register. + ReadIdRegister, +} + +impl Command { + fn as_bytes(self) -> [u8; 2] { + match self { + Command::Sleep => [0xB0, 0x98], + Command::WakeUp => [0x35, 0x17], + Command::Measure { + power_mode: PowerMode::NormalMode, + order: MeasurementOrder::TemperatureFirst, + } => [0x78, 0x66], + Command::Measure { + power_mode: PowerMode::NormalMode, + order: MeasurementOrder::HumidityFirst, + } => [0x58, 0xE0], + Command::Measure { + power_mode: PowerMode::LowPower, + order: MeasurementOrder::TemperatureFirst, + } => [0x60, 0x9C], + Command::Measure { + power_mode: PowerMode::LowPower, + order: MeasurementOrder::HumidityFirst, + } => [0x40, 0x1A], + Command::ReadIdRegister => [0xEF, 0xC8], + Command::SoftwareReset => [0x80, 0x5D], + } + } +} + +/// Driver for the SHTC3 sensor. +#[derive(Debug, Default)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ShtC3 { + /// The concrete I²C device implementation. + i2c: I2C, + /// The I²C device address. + address: u8, +} + +impl ShtC3 { + /// Create a new instance of the driver for the SHTC3. + #[inline] + pub const fn new(i2c: I2C) -> Self { + Self { i2c, address: 0x70 } + } + + /// Get the device's wakeup delay duration in microseconds + #[inline(always)] + pub const fn wakeup_duration(&self) -> u32 { + 240 + } + + /// Destroy driver instance, return I²C bus instance. + pub fn destroy(self) -> I2C { + self.i2c + } + + /// Return the maximum measurement duration (depending on the mode) in + /// microseconds. + /// + /// Maximum measurement duration (SHTC3 datasheet 3.1): + /// - Normal mode: 12.1 ms + /// - Low power mode: 0.8 ms + #[inline(always)] + pub const fn max_measurement_duration(&self, mode: PowerMode) -> u32 { + match mode { + PowerMode::NormalMode => 12100, + PowerMode::LowPower => 800, + } + } + + /// Returns the reset duration for the SHTC3 in microseconds + #[inline(always)] + pub const fn reset_duration(&self) -> u32 { + 240_000 + } + + /// Iterate over the provided buffer and validate the CRC8 checksum. + /// + /// If the checksum is wrong, return `CrcError`. + /// + /// Note: This method will consider every third byte a checksum byte. If + /// the buffer size is not a multiple of 3, then not all data will be + /// validated. + fn validate_crc(&self, buf: &[u8]) -> Result<(), CrcError> { + let mut chunks = buf.chunks_exact(3); + + for chunk in chunks.by_ref() { + if crc8(&chunk[..2]) != chunk[2] { + return Err(CrcError); + } + } + + #[cfg(feature = "defmt")] + if !chunks.remainder().is_empty() { + defmt::warn!("Remaining data in buffer was not CRC8 validated"); + } + + Ok(()) + } +} + +impl ShtC3 +where + I2C: embedded_hal_async::i2c::I2c, +{ + /// Write an I²C command to the sensor. + async fn send_command_async(&mut self, command: Command) -> Result<(), Error> { + self.i2c + .write(self.address, &command.as_bytes()) + .await + .map_err(Error::I2c) + } + + /// Read data into the provided buffer and validate the CRC8 checksum. + /// + /// If the checksum is wrong, return `Error::Crc`. + /// + /// Note: This method will consider every third byte a checksum byte. If + /// the buffer size is not a multiple of 3, then not all data will be + /// validated. + async fn read_with_crc_async(&mut self, buf: &mut [u8]) -> Result<(), Error> { + self.i2c.read(self.address, buf).await?; + self.validate_crc(buf)?; + Ok(()) + } + + /// Return the raw ID register. + pub async fn raw_id_register_async(&mut self) -> Result> { + // Request serial number + self.send_command_async(Command::ReadIdRegister).await?; + + // Read id register + let mut buf = [0; 3]; + self.read_with_crc_async(&mut buf).await?; + + Ok(u16::from_be_bytes([buf[0], buf[1]])) + } + + /// Return the 7-bit device identifier. + /// + /// Should be 0x47 (71) for the SHTC3. + pub async fn device_identifier_async(&mut self) -> Result> { + let ident = self.raw_id_register_async().await?; + let lsb = (ident & 0b0011_1111) as u8; + let msb = ((ident & 0b0000_1000_0000_0000) >> 5) as u8; + Ok(lsb | msb) + } + + /// Set sensor to sleep mode. + /// + /// When in sleep mode, the sensor consumes around 0.3-0.6 µA. It requires + /// a dedicated [`wakeup`](#method.wakeup) command to enable further I2C + /// communication. + pub async fn sleep_async(&mut self) -> Result<(), Error> { + self.send_command_async(Command::Sleep).await + } + + /// Trigger a soft reset. (async) + /// + /// The SHTC3 provides a soft reset mechanism that forces the system into a + /// well-defined state without removing the power supply. If the system is + /// in its idle state (i.e. if no measurement is in progress) the soft + /// reset command can be sent. This triggers the sensor to reset all + /// internal state machines and reload calibration data from the memory. + pub async fn reset_async(&mut self, delay: &mut impl DelayNs) -> Result<(), Error> { + self.send_command_async(Command::SoftwareReset).await?; + // Table 5: 180-240 µs + delay.delay_us(self.reset_duration()).await; + Ok(()) + } + + /// Wake up sensor from [sleep mode](#method.sleep) and wait until it is ready. (async) + pub async fn wakeup_async( + &mut self, + delay: &mut impl DelayNs, + ) -> Result<(), Error> { + self.send_command_async(Command::WakeUp).await?; + delay.delay_us(self.wakeup_duration()).await; + Ok(()) + } + + /// Run a temperature/humidity measurement and return the combined result. + /// + /// This is an async function call. + pub async fn measure_async( + &mut self, + mode: PowerMode, + delay: &mut impl DelayNs, + ) -> Result> { + self.send_command_async(Command::Measure { + power_mode: mode, + order: MeasurementOrder::TemperatureFirst, + }) + .await?; + + delay.delay_us(self.max_measurement_duration(mode)).await; + + let mut buf = [0; 6]; + self.read_with_crc_async(&mut buf).await?; + + Ok(RawMeasurement { + temperature: u16::from_be_bytes([buf[0], buf[1]]), + humidity: u16::from_be_bytes([buf[3], buf[4]]), + } + .into()) + } +} + +/// General blocking functions. +impl ShtC3 +where + I2C: I2c, +{ + /// Write an I²C command to the sensor. + fn send_command(&mut self, command: Command) -> Result<(), Error> { + self.i2c + .write(self.address, &command.as_bytes()) + .map_err(Error::I2c) + } + + /// Read data into the provided buffer and validate the CRC8 checksum. + /// + /// If the checksum is wrong, return `Error::Crc`. + /// + /// Note: This method will consider every third byte a checksum byte. If + /// the buffer size is not a multiple of 3, then not all data will be + /// validated. + fn read_with_crc(&mut self, buf: &mut [u8]) -> Result<(), Error> { + self.i2c.read(self.address, buf)?; + self.validate_crc(buf)?; + Ok(()) + } + + /// Return the raw ID register. + pub fn raw_id_register(&mut self) -> Result> { + // Request serial number + self.send_command(Command::ReadIdRegister)?; + + // Read id register + let mut buf = [0; 3]; + self.read_with_crc(&mut buf)?; + + Ok(u16::from_be_bytes([buf[0], buf[1]])) + } + + /// Return the 7-bit device identifier. + /// + /// Should be 0x47 (71) for the SHTC3. + pub fn device_identifier(&mut self) -> Result> { + let ident = self.raw_id_register()?; + let lsb = (ident & 0b0011_1111) as u8; + let msb = ((ident & 0b0000_1000_0000_0000) >> 5) as u8; + Ok(lsb | msb) + } + + /// Trigger a soft reset. (blocking) + /// + /// The SHTC3 provides a soft reset mechanism that forces the system into a + /// well-defined state without removing the power supply. If the system is + /// in its idle state (i.e. if no measurement is in progress) the soft + /// reset command can be sent. This triggers the sensor to reset all + /// internal state machines and reload calibration data from the memory. + pub fn reset(&mut self, delay: &mut impl BlockingDelayNs) -> Result<(), Error> { + self.send_command(Command::SoftwareReset)?; + // Table 5: 180-240 µs + delay.delay_us(self.reset_duration()); + Ok(()) + } + + /// Trigger a soft reset. + /// + /// The SHTC3 provides a soft reset mechanism that forces the system into a + /// well-defined state without removing the power supply. If the system is + /// in its idle state (i.e. if no measurement is in progress) the soft + /// reset command can be sent. This triggers the sensor to reset all + /// internal state machines and reload calibration data from the memory. + pub fn start_reset(&mut self) -> Result<(), Error> { + self.send_command(Command::SoftwareReset) + } + + /// Set sensor to sleep mode. + /// + /// When in sleep mode, the sensor consumes around 0.3-0.6 µA. It requires + /// a dedicated [`wakeup`](#method.wakeup) command to enable further I2C + /// communication. + pub fn sleep(&mut self) -> Result<(), Error> { + self.send_command(Command::Sleep) + } + + /// Wake up sensor from [sleep mode](#method.sleep) and wait until it is ready. + pub fn wakeup(&mut self, delay: &mut impl BlockingDelayNs) -> Result<(), Error> { + self.start_wakeup()?; + delay.delay_us(self.wakeup_duration()); + Ok(()) + } +} + +/// Non-blocking functions for starting / reading measurements. +impl ShtC3 +where + I2C: I2c, +{ + /// Start a measurement with the specified measurement order and write the + /// result into the provided buffer. + /// + /// If you just need one of the two measurements, provide a 3-byte buffer + /// instead of a 6-byte buffer. + fn start_measure_partial( + &mut self, + power_mode: PowerMode, + order: MeasurementOrder, + ) -> Result<(), Error> { + // Request measurement + self.send_command(Command::Measure { power_mode, order }) + } + + /// Start a combined temperature / humidity measurement. + pub fn start_measurement(&mut self, mode: PowerMode) -> Result<(), Error> { + self.start_measure_partial(mode, MeasurementOrder::TemperatureFirst) + } + + /// Start a temperature measurement. + pub fn start_temperature_measurement( + &mut self, + mode: PowerMode, + ) -> Result<(), Error> { + self.start_measure_partial(mode, MeasurementOrder::TemperatureFirst) + } + + /// Start a humidity measurement. + pub fn start_humidity_measurement(&mut self, mode: PowerMode) -> Result<(), Error> { + self.start_measure_partial(mode, MeasurementOrder::HumidityFirst) + } + + /// Read the result of a temperature / humidity measurement. + pub fn get_measurement_result(&mut self) -> Result> { + let raw = self.get_raw_measurement_result()?; + Ok(raw.into()) + } + + /// Read the result of a temperature measurement. + pub fn get_temperature_measurement_result(&mut self) -> Result> { + let raw = self.get_raw_partial_measurement_result()?; + Ok(Temperature::from_raw(raw)) + } + + /// Read the result of a humidity measurement. + pub fn get_humidity_measurement_result(&mut self) -> Result> { + let raw = self.get_raw_partial_measurement_result()?; + Ok(Humidity::from_raw(raw)) + } + + /// Read the raw result of a combined temperature / humidity measurement. + pub fn get_raw_measurement_result(&mut self) -> Result> { + let mut buf = [0; 6]; + self.read_with_crc(&mut buf)?; + Ok(RawMeasurement { + temperature: u16::from_be_bytes([buf[0], buf[1]]), + humidity: u16::from_be_bytes([buf[3], buf[4]]), + }) + } + + /// Read the raw result of a partial temperature or humidity measurement. + /// + /// Return the raw 3-byte buffer (after validating CRC). + pub fn get_raw_partial_measurement_result(&mut self) -> Result> { + let mut buf = [0; 3]; + self.read_with_crc(&mut buf)?; + Ok(u16::from_be_bytes([buf[0], buf[1]])) + } + + /// Wake up sensor from [sleep mode](#method.sleep). + pub fn start_wakeup(&mut self) -> Result<(), Error> { + self.send_command(Command::WakeUp) + } +} + +/// Blocking functions for doing measurements. +impl ShtC3 +where + I2C: I2c, +{ + /// Wait the maximum time needed for the given measurement mode + pub fn wait_for_measurement(&mut self, mode: PowerMode, delay: &mut impl BlockingDelayNs) { + delay.delay_us(self.max_measurement_duration(mode)); + } + + /// Run a temperature/humidity measurement and return the combined result. + /// + /// This is a blocking function call. + pub fn measure( + &mut self, + mode: PowerMode, + delay: &mut impl BlockingDelayNs, + ) -> Result> { + self.start_measurement(mode)?; + self.wait_for_measurement(mode, delay); + self.get_measurement_result() + } + + /// Run a temperature measurement and return the result. + /// + /// This is a blocking function call. + /// + /// Internally, it will request a measurement in "temperature first" mode + /// and only read the first half of the measurement response. + pub fn measure_temperature( + &mut self, + mode: PowerMode, + delay: &mut impl BlockingDelayNs, + ) -> Result> { + self.start_temperature_measurement(mode)?; + self.wait_for_measurement(mode, delay); + self.get_temperature_measurement_result() + } + + /// Run a humidity measurement and return the result. + /// + /// This is a blocking function call. + /// + /// Internally, it will request a measurement in "humidity first" mode + /// and only read the first half of the measurement response. + pub fn measure_humidity( + &mut self, + mode: PowerMode, + delay: &mut impl BlockingDelayNs, + ) -> Result> { + self.start_humidity_measurement(mode)?; + self.wait_for_measurement(mode, delay); + self.get_humidity_measurement_result() + } +} + +#[cfg(test)] +mod tests { + extern crate alloc; + + use super::*; + + use embedded_hal::i2c::ErrorKind; + use embedded_hal_mock::eh1::{ + delay::NoopDelay, + i2c::{Mock as I2cMock, Transaction}, + }; + + const SHT_ADDR: u8 = 0x70; + + mod core { + use super::*; + + /// Test whether the `send_command` function propagates I²C errors. + #[test] + fn send_command_error() { + let expectations = + [Transaction::write(SHT_ADDR, alloc::vec![0xef, 0xc8]) + .with_error(ErrorKind::Other)]; + let mock = I2cMock::new(&expectations); + let mut sht = ShtC3::new(mock); + let err = sht.send_command(Command::ReadIdRegister).unwrap_err(); + assert_eq!(err, Error::I2c(ErrorKind::Other)); + sht.destroy().done(); + } + + /// Test the `validate_crc` function. + #[test] + fn validate_crc() { + let mock = I2cMock::new(&[]); + let sht = ShtC3::new(mock); + + // Not enough data + sht.validate_crc(&[]).unwrap(); + sht.validate_crc(&[0xbe]).unwrap(); + sht.validate_crc(&[0xbe, 0xef]).unwrap(); + + // Valid CRC + sht.validate_crc(&[0xbe, 0xef, 0x92]).unwrap(); + + // Invalid CRC + match sht.validate_crc(&[0xbe, 0xef, 0x91]) { + Err(CrcError) => {} + Ok(_) => panic!("CRC check did not fail"), + } + + // Valid CRC (8 bytes) + sht.validate_crc(&[0xbe, 0xef, 0x92, 0xbe, 0xef, 0x92, 0x00, 0x00]) + .unwrap(); + + // Invalid CRC (8 bytes) + match sht.validate_crc(&[0xbe, 0xef, 0x92, 0xbe, 0xef, 0xff, 0x00, 0x00]) { + Err(CrcError) => {} + Ok(_) => panic!("CRC check did not fail"), + } + + sht.destroy().done(); + } + + /// Test the `read_with_crc` function. + #[test] + fn read_with_crc() { + let mut buf = [0; 3]; + + // Valid CRC + let expectations = [Transaction::read(SHT_ADDR, alloc::vec![0xbe, 0xef, 0x92])]; + let mock = I2cMock::new(&expectations); + let mut sht = ShtC3::new(mock); + sht.read_with_crc(&mut buf).unwrap(); + assert_eq!(buf, [0xbe, 0xef, 0x92]); + sht.destroy().done(); + + // Invalid CRC + let expectations = [Transaction::read(SHT_ADDR, alloc::vec![0xbe, 0xef, 0x00])]; + let mock = I2cMock::new(&expectations); + let mut sht = ShtC3::new(mock); + match sht.read_with_crc(&mut buf) { + Err(Error::Crc) => {} + Err(_) => panic!("Invalid error: Must be Crc"), + Ok(_) => panic!("CRC check did not fail"), + } + assert_eq!(buf, [0xbe, 0xef, 0x00]); // Buf was changed + sht.destroy().done(); + } + } + + mod factory_functions { + use super::*; + + #[test] + fn new_shtc3() { + let mock = I2cMock::new(&[]); + let sht = ShtC3::new(mock); + assert_eq!(sht.address, 0x70); + sht.destroy().done(); + } + } + + mod device_info { + use super::*; + + /// Test the `raw_id_register` function. + #[test] + fn raw_id_register() { + let msb = 0b00001000; + let lsb = 0b00000111; + let crc = crc8(&[msb, lsb]); + let expectations = [ + Transaction::write(SHT_ADDR, alloc::vec![0xef, 0xc8]), + Transaction::read(SHT_ADDR, alloc::vec![msb, lsb, crc]), + ]; + let mock = I2cMock::new(&expectations); + let mut sht = ShtC3::new(mock); + let val = sht.raw_id_register().unwrap(); + assert_eq!(val, (msb as u16) << 8 | (lsb as u16)); + sht.destroy().done(); + } + + /// Test the `device_identifier` function. + #[test] + fn device_identifier() { + let msb = 0b00001000; + let lsb = 0b00000111; + let crc = crc8(&[msb, lsb]); + let expectations = [ + Transaction::write(SHT_ADDR, alloc::vec![0xef, 0xc8]), + Transaction::read(SHT_ADDR, alloc::vec![msb, lsb, crc]), + ]; + let mock = I2cMock::new(&expectations); + let mut sht = ShtC3::new(mock); + let ident = sht.device_identifier().unwrap(); + assert_eq!(ident, 0b01000111); + sht.destroy().done(); + } + } + + mod measurements { + use super::*; + + #[test] + fn measure_normal() { + let expectations = [ + // Expect a write command: Normal mode measurement, temperature + // first, no clock stretching. + Transaction::write(SHT_ADDR, alloc::vec![0x78, 0x66]), + // Return the measurement result (using example values from the + // datasheet, section 5.4 "Measuring and Reading the Signals") + Transaction::read( + SHT_ADDR, + alloc::vec![ + 0b0110_0100, + 0b1000_1011, + 0b1100_0111, + 0b1010_0001, + 0b0011_0011, + 0b0001_1100, + ], + ), + ]; + let mock = I2cMock::new(&expectations); + let mut sht = ShtC3::new(mock); + let mut delay = NoopDelay; + let measurement = sht.measure(PowerMode::NormalMode, &mut delay).unwrap(); + assert_eq!(measurement.temperature.as_millidegrees_celsius(), 23_730); // 23.7°C + assert_eq!(measurement.humidity.as_millipercent(), 62_968); // 62.9 %RH + sht.destroy().done(); + } + + #[test] + fn measure_low_power() { + let expectations = [ + // Expect a write command: Low power mode measurement, temperature + // first, no clock stretching. + Transaction::write(SHT_ADDR, alloc::vec![0x60, 0x9C]), + // Return the measurement result (using example values from the + // datasheet, section 5.4 "Measuring and Reading the Signals") + Transaction::read( + SHT_ADDR, + alloc::vec![ + 0b0110_0100, + 0b1000_1011, + 0b1100_0111, + 0b1010_0001, + 0b0011_0011, + 0b0001_1100, + ], + ), + ]; + let mock = I2cMock::new(&expectations); + let mut sht = ShtC3::new(mock); + let mut delay = NoopDelay; + let measurement = sht.measure(PowerMode::LowPower, &mut delay).unwrap(); + assert_eq!(measurement.temperature.as_millidegrees_celsius(), 23_730); // 23.7°C + assert_eq!(measurement.humidity.as_millipercent(), 62_968); // 62.9 %RH + sht.destroy().done(); + } + + #[test] + fn measure_temperature_only() { + let expectations = [ + // Expect a write command: Normal mode measurement, temperature + // first, no clock stretching. + Transaction::write(SHT_ADDR, alloc::vec![0x78, 0x66]), + // Return the measurement result (using example values from the + // datasheet, section 5.4 "Measuring and Reading the Signals") + Transaction::read(SHT_ADDR, alloc::vec![0b0110_0100, 0b1000_1011, 0b1100_0111]), + ]; + let mock = I2cMock::new(&expectations); + let mut sht = ShtC3::new(mock); + let mut delay = NoopDelay; + let temperature = sht + .measure_temperature(PowerMode::NormalMode, &mut delay) + .unwrap(); + assert_eq!(temperature.as_millidegrees_celsius(), 23_730); // 23.7°C + sht.destroy().done(); + } + + #[test] + fn measure_humidity_only() { + let expectations = [ + // Expect a write command: Normal mode measurement, humidity + // first, no clock stretching. + Transaction::write(SHT_ADDR, alloc::vec![0x58, 0xE0]), + // Return the measurement result (using example values from the + // datasheet, section 5.4 "Measuring and Reading the Signals") + Transaction::read(SHT_ADDR, alloc::vec![0b1010_0001, 0b0011_0011, 0b0001_1100]), + ]; + let mock = I2cMock::new(&expectations); + let mut sht = ShtC3::new(mock); + let mut delay = NoopDelay; + let humidity = sht + .measure_humidity(PowerMode::NormalMode, &mut delay) + .unwrap(); + assert_eq!(humidity.as_millipercent(), 62_968); // 62.9 %RH + sht.destroy().done(); + } + + /// Ensure that I²C write errors are handled when measuring. + #[test] + fn measure_write_error() { + let expectations = + [Transaction::write(SHT_ADDR, alloc::vec![0x60, 0x9C]) + .with_error(ErrorKind::Other)]; + let mock = I2cMock::new(&expectations); + let mut sht = ShtC3::new(mock); + let err = sht + .measure(PowerMode::LowPower, &mut NoopDelay) + .unwrap_err(); + assert_eq!(err, Error::I2c(ErrorKind::Other)); + sht.destroy().done(); + } + } + + mod power_management { + use super::*; + + /// Test the `sleep` function. + #[test] + fn sleep() { + let expectations = [Transaction::write(SHT_ADDR, alloc::vec![0xB0, 0x98])]; + let mock = I2cMock::new(&expectations); + let mut sht = ShtC3::new(mock); + sht.sleep().unwrap(); + sht.destroy().done(); + } + + /// Test the `wakeup` function. + #[test] + fn wakeup() { + let expectations = [Transaction::write(SHT_ADDR, alloc::vec![0x35, 0x17])]; + let mock = I2cMock::new(&expectations); + let mut sht = ShtC3::new(mock); + sht.wakeup(&mut NoopDelay).unwrap(); + sht.destroy().done(); + } + + /// Test the `reset` function. + #[test] + fn reset() { + let expectations = [Transaction::write(SHT_ADDR, alloc::vec![0x80, 0x5D])]; + let mock = I2cMock::new(&expectations); + let mut sht = ShtC3::new(mock); + sht.reset(&mut NoopDelay).unwrap(); + sht.destroy().done(); + } + } + + mod max_measurement_duration { + use super::*; + + #[test] + fn shortcut_function() { + let c3 = ShtC3::new(I2cMock::new(&[])); + + assert_eq!(c3.max_measurement_duration(PowerMode::NormalMode), 12100); + assert_eq!(c3.max_measurement_duration(PowerMode::LowPower), 800); + + c3.destroy().done(); + } + } +} diff --git a/sachy-shtc3/src/types.rs b/sachy-shtc3/src/types.rs new file mode 100644 --- /dev/null +++ b/sachy-shtc3/src/types.rs @@ -0,0 +1,199 @@ +/// A temperature measurement. +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Temperature(i32); + +/// A humidity measurement. +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Humidity(i32); + +/// A combined temperature / humidity measurement. +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Measurement { + /// The measured temperature. + pub temperature: Temperature, + /// The measured humidity. + pub humidity: Humidity, +} + +impl core::ops::AddAssign for Measurement { + fn add_assign(&mut self, rhs: Self) { + self.temperature.0 += rhs.temperature.0; + self.humidity.0 += rhs.humidity.0; + } +} + +impl core::ops::DivAssign for Measurement { + fn div_assign(&mut self, rhs: i32) { + self.temperature.0 /= rhs; + self.humidity.0 /= rhs; + } +} + +/// A combined raw temperature / humidity measurement. +/// +/// The raw values are of type u16. They require a conversion formula for +/// conversion to a temperature / humidity value (see datasheet). +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct RawMeasurement { + /// The measured temperature (raw value). + pub temperature: u16, + /// The measured humidity (raw value). + pub humidity: u16, +} + +impl From for Measurement { + fn from(other: RawMeasurement) -> Self { + Self { + temperature: Temperature::from_raw(other.temperature), + humidity: Humidity::from_raw(other.humidity), + } + } +} + +impl Temperature { + /// Create a new `Temperature` from a raw measurement result. + pub const fn from_raw(raw: u16) -> Self { + Self(convert_temperature(raw)) + } + + /// Return temperature in milli-degrees celsius. + pub const fn as_millidegrees_celsius(&self) -> i32 { + self.0 + } + + /// Return temperature in degrees celcius with 0.01 precision + pub const fn as_10mk_celsius(&self) -> i16 { + (self.0 / 10) as i16 + } + + /// Return temperature in degrees celsius. + pub const fn as_degrees_celsius(&self) -> f32 { + self.0 as f32 / 1000.0 + } +} + +impl Humidity { + /// Create a new `Humidity` from a raw measurement result. + pub const fn from_raw(raw: u16) -> Self { + Self(convert_humidity(raw)) + } + + /// Return relative humidity in 1/100 %RH + pub const fn as_10mk_percent(&self) -> u16 { + (self.0 / 10).unsigned_abs() as u16 + } + + /// Return relative humidity in 1/1000 %RH. + pub const fn as_millipercent(&self) -> i32 { + self.0 + } + + /// Return relative humidity in 1 %RH + pub const fn as_1k_percent(&self) -> u8 { + (self.0 / 1000).unsigned_abs() as u8 + } + + /// Return relative humidity in %RH. + pub const fn as_percent(&self) -> f32 { + self.0 as f32 / 1000.0 + } +} + +/// Convert raw temperature measurement to milli-degrees celsius. +/// +/// Formula (datasheet 5.11): -45 + 175 * (val / 2^16), +/// optimized for fixed point math. +#[inline] +const fn convert_temperature(temp_raw: u16) -> i32 { + (((temp_raw as u32) * 21875) >> 13) as i32 - 45000 +} + +/// Convert raw humidity measurement to relative humidity. +/// +/// Formula (datasheet 5.11): 100 * (val / 2^16), +/// optimized for fixed point math. +#[inline] +const fn convert_humidity(humi_raw: u16) -> i32 { + (((humi_raw as u32) * 12500) >> 13) as i32 +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Test conversion of raw measurement results into °C. + #[test] + fn test_convert_temperature() { + let test_data = [ + (0x0000, -45000), + // Datasheet setion 5.11 "Conversion of Sensor Output" + ((0b0110_0100_u16 << 8) | 0b1000_1011, 23730), + ]; + for td in &test_data { + assert_eq!(convert_temperature(td.0), td.1); + } + } + + /// Test conversion of raw measurement results into %RH. + #[test] + fn test_convert_humidity() { + let test_data = [ + (0x0000, 0), + // Datasheet setion 5.11 "Conversion of Sensor Output" + ((0b1010_0001_u16 << 8) | 0b0011_0011, 62968), + ]; + for td in &test_data { + assert_eq!(convert_humidity(td.0), td.1); + } + } + + /// Test conversion of raw measurement results into °C and %RH. + #[test] + fn measurement_conversion() { + // Datasheet setion 5.11 "Conversion of Sensor Output" + let temperature = convert_temperature((0b0110_0100_u16 << 8) | 0b1000_1011); + let humidity = convert_humidity((0b1010_0001_u16 << 8) | 0b0011_0011); + assert_eq!(temperature, 23730); + assert_eq!(humidity, 62968); + } + + #[test] + fn temperature() { + let temp = Temperature(24123); + assert_eq!(temp.as_millidegrees_celsius(), 24123); + assert_eq!(temp.as_degrees_celsius(), 24.123); + } + + #[test] + fn humidity() { + let humi = Humidity(65432); + assert_eq!(humi.as_millipercent(), 65432); + assert_eq!(humi.as_percent(), 65.432); + } + + #[test] + fn measurement_from_into() { + // Datasheet setion 5.11 "Conversion of Sensor Output" + let raw = RawMeasurement { + temperature: (0b0110_0100_u16 << 8) | 0b1000_1011, + humidity: (0b1010_0001_u16 << 8) | 0b0011_0011, + }; + + // std::convert::From + let measurement1 = Measurement::from(raw); + assert_eq!(measurement1.temperature.0, 23730); + assert_eq!(measurement1.humidity.0, 62968); + + // std::convert::Into + let measurement2: Measurement = raw.into(); + assert_eq!(measurement2.temperature.0, 23730); + assert_eq!(measurement2.humidity.0, 62968); + + // std::cmp::PartialEq + assert_eq!(measurement1, measurement2); + } +}