pamoja_sensors/sht3x.rs
1//! Sensirion SHT3x-DIS humidity and temperature sensor (SHT30, SHT31, SHT35).
2//!
3//! The SHT3x returns fully calibrated, linearized 16-bit temperature and humidity
4//! words over I2C, each followed by a CRC-8, and converts to physical units with two
5//! fixed linear formulas. This module holds the command words, verifies the CRC,
6//! decodes a six-byte measurement frame, applies the datasheet's conversion formulas
7//! in integer arithmetic, and decodes the status register, so a node reads degrees and
8//! percent relative humidity without carrying a floating-point library.
9//!
10//! A caller writes a word from [`command`] most-significant byte first, waits the
11//! measurement duration for the chosen repeatability, reads six bytes, and hands them
12//! to [`Measurement::parse`]. In periodic mode the same six bytes follow
13//! [`command::FETCH_DATA`]. The [`Sht3x`] driver (the `embedded-hal` feature) runs the
14//! single-shot sequence over an I2C bus.
15//!
16//! Temperatures are returned in millidegrees and humidity in thousandths of a percent,
17//! rounded to nearest, which is finer than the part's 0.01 °C and 0.01 %RH resolution.
18
19#[cfg(feature = "embedded-hal")]
20mod driver;
21#[cfg(feature = "alloc")]
22pub mod sim;
23
24#[cfg(feature = "embedded-hal")]
25pub use driver::Sht3x;
26
27use crate::SensorError;
28
29/// I2C address A, selected with the ADDR pin at logic low; the default.
30pub const I2C_ADDRESS_A: u8 = 0x44;
31/// I2C address B, selected with the ADDR pin at logic high.
32pub const I2C_ADDRESS_B: u8 = 0x45;
33
34/// The shortest gap between two commands, in microseconds: the part needs 1 ms after
35/// a command before it accepts another.
36pub const MIN_COMMAND_GAP_MICROS: u32 = 1_000;
37
38/// How long the part takes to come back after [`command::SOFT_RESET`], in
39/// microseconds: the datasheet's maximum of 1.5 ms between the acknowledge of the
40/// command and the part entering idle state, which also covers the gap before the
41/// next command.
42pub const SOFT_RESET_MICROS: u32 = 1_500;
43
44/// The 16-bit command words, sent most-significant byte first.
45///
46/// Each word already carries the part's 3-bit command checksum, so it is written as
47/// two bytes with nothing appended.
48pub mod command {
49 /// Single shot, high repeatability, clock stretching enabled.
50 pub const SINGLE_SHOT_HIGH_STRETCH: u16 = 0x2C06;
51 /// Single shot, medium repeatability, clock stretching enabled.
52 pub const SINGLE_SHOT_MEDIUM_STRETCH: u16 = 0x2C0D;
53 /// Single shot, low repeatability, clock stretching enabled.
54 pub const SINGLE_SHOT_LOW_STRETCH: u16 = 0x2C10;
55 /// Single shot, high repeatability, clock stretching disabled.
56 pub const SINGLE_SHOT_HIGH: u16 = 0x2400;
57 /// Single shot, medium repeatability, clock stretching disabled.
58 pub const SINGLE_SHOT_MEDIUM: u16 = 0x240B;
59 /// Single shot, low repeatability, clock stretching disabled.
60 pub const SINGLE_SHOT_LOW: u16 = 0x2416;
61
62 /// Periodic, 0.5 measurements per second, high repeatability.
63 pub const PERIODIC_0_5_MPS_HIGH: u16 = 0x2032;
64 /// Periodic, 0.5 measurements per second, medium repeatability.
65 pub const PERIODIC_0_5_MPS_MEDIUM: u16 = 0x2024;
66 /// Periodic, 0.5 measurements per second, low repeatability.
67 pub const PERIODIC_0_5_MPS_LOW: u16 = 0x202F;
68 /// Periodic, 1 measurement per second, high repeatability.
69 pub const PERIODIC_1_MPS_HIGH: u16 = 0x2130;
70 /// Periodic, 1 measurement per second, medium repeatability.
71 pub const PERIODIC_1_MPS_MEDIUM: u16 = 0x2126;
72 /// Periodic, 1 measurement per second, low repeatability.
73 pub const PERIODIC_1_MPS_LOW: u16 = 0x212D;
74 /// Periodic, 2 measurements per second, high repeatability.
75 pub const PERIODIC_2_MPS_HIGH: u16 = 0x2236;
76 /// Periodic, 2 measurements per second, medium repeatability.
77 pub const PERIODIC_2_MPS_MEDIUM: u16 = 0x2220;
78 /// Periodic, 2 measurements per second, low repeatability.
79 pub const PERIODIC_2_MPS_LOW: u16 = 0x222B;
80 /// Periodic, 4 measurements per second, high repeatability.
81 pub const PERIODIC_4_MPS_HIGH: u16 = 0x2334;
82 /// Periodic, 4 measurements per second, medium repeatability.
83 pub const PERIODIC_4_MPS_MEDIUM: u16 = 0x2322;
84 /// Periodic, 4 measurements per second, low repeatability.
85 pub const PERIODIC_4_MPS_LOW: u16 = 0x2329;
86 /// Periodic, 10 measurements per second, high repeatability. Self-heating can
87 /// occur at this rate.
88 pub const PERIODIC_10_MPS_HIGH: u16 = 0x2737;
89 /// Periodic, 10 measurements per second, medium repeatability.
90 pub const PERIODIC_10_MPS_MEDIUM: u16 = 0x2721;
91 /// Periodic, 10 measurements per second, low repeatability.
92 pub const PERIODIC_10_MPS_LOW: u16 = 0x272A;
93 /// Periodic acquisition with the accelerated response time (ART) feature, which
94 /// samples at 4 Hz.
95 pub const PERIODIC_ART: u16 = 0x2B32;
96
97 /// Reads the latest periodic-mode data pair; the read is NACKed if none is ready,
98 /// and the data memory is cleared once it is fetched.
99 pub const FETCH_DATA: u16 = 0xE000;
100 /// Stops periodic acquisition, aborting any measurement in progress, and returns
101 /// the part to single-shot mode within 1 ms.
102 pub const BREAK: u16 = 0x3093;
103 /// Resets the system controller and reloads the calibration data without removing
104 /// power; the part is idle again within 1.5 ms.
105 pub const SOFT_RESET: u16 = 0x30A2;
106 /// The I2C general-call reset: address byte 0x00 followed by 0x06. It resets every
107 /// device on the bus that honors the general call, not just this part.
108 pub const GENERAL_CALL_RESET: u16 = 0x0006;
109 /// Switches the plausibility-check heater on.
110 pub const HEATER_ENABLE: u16 = 0x306D;
111 /// Switches the heater off, which is its state after any reset.
112 pub const HEATER_DISABLE: u16 = 0x3066;
113 /// Reads the status register as one CRC-protected word.
114 pub const READ_STATUS: u16 = 0xF32D;
115 /// Clears the alert-pending, tracking-alert, and reset-detected flags.
116 pub const CLEAR_STATUS: u16 = 0x3041;
117}
118
119/// The measurement repeatability, which sets noise, duration, and energy per sample.
120#[derive(Clone, Copy, Debug, PartialEq, Eq)]
121pub enum Repeatability {
122 /// 0.21 %RH and 0.15 °C typical noise, 4 ms maximum measurement.
123 Low,
124 /// 0.15 %RH and 0.08 °C typical noise, 6 ms maximum measurement.
125 Medium,
126 /// 0.08 %RH and 0.04 °C typical noise, 15 ms maximum measurement.
127 High,
128}
129
130impl Repeatability {
131 /// Returns the longest a measurement takes at this repeatability, in microseconds.
132 ///
133 /// These are the datasheet's maxima for a supply of 2.4 V to 5.5 V; below 2.4 V
134 /// each is 500 µs longer.
135 ///
136 /// # Returns
137 ///
138 /// `4000`, `6000`, or `15000`.
139 pub fn max_measurement_micros(self) -> u32 {
140 match self {
141 Repeatability::Low => 4_000,
142 Repeatability::Medium => 6_000,
143 Repeatability::High => 15_000,
144 }
145 }
146
147 /// Returns the typical measurement duration at this repeatability, in microseconds.
148 ///
149 /// # Returns
150 ///
151 /// `2500`, `4500`, or `12500`.
152 pub fn typical_measurement_micros(self) -> u32 {
153 match self {
154 Repeatability::Low => 2_500,
155 Repeatability::Medium => 4_500,
156 Repeatability::High => 12_500,
157 }
158 }
159}
160
161/// The periodic-mode acquisition rate, in measurements per second (mps).
162#[derive(Clone, Copy, Debug, PartialEq, Eq)]
163pub enum Rate {
164 /// One measurement every two seconds.
165 HalfMps,
166 /// One measurement per second.
167 OneMps,
168 /// Two measurements per second.
169 TwoMps,
170 /// Four measurements per second.
171 FourMps,
172 /// Ten measurements per second; self-heating can occur at this rate.
173 TenMps,
174}
175
176impl Rate {
177 /// Returns the interval between measurements at this rate, in microseconds.
178 ///
179 /// # Returns
180 ///
181 /// `2000000` for half a measurement per second down to `100000` for ten.
182 pub fn interval_micros(self) -> u32 {
183 match self {
184 Rate::HalfMps => 2_000_000,
185 Rate::OneMps => 1_000_000,
186 Rate::TwoMps => 500_000,
187 Rate::FourMps => 250_000,
188 Rate::TenMps => 100_000,
189 }
190 }
191}
192
193/// Returns the single-shot measurement command for a repeatability and clock mode.
194///
195/// With clock stretching the part holds SCL low until the result is ready, so the
196/// read that follows the command blocks instead of being NACKed.
197///
198/// # Arguments
199///
200/// * `repeatability` - the repeatability to measure at.
201/// * `clock_stretching` - whether the part may stretch the clock during the read.
202///
203/// # Returns
204///
205/// The 16-bit command word, one of the `SINGLE_SHOT_*` constants in [`command`].
206pub fn single_shot(repeatability: Repeatability, clock_stretching: bool) -> u16 {
207 match (repeatability, clock_stretching) {
208 (Repeatability::High, true) => command::SINGLE_SHOT_HIGH_STRETCH,
209 (Repeatability::Medium, true) => command::SINGLE_SHOT_MEDIUM_STRETCH,
210 (Repeatability::Low, true) => command::SINGLE_SHOT_LOW_STRETCH,
211 (Repeatability::High, false) => command::SINGLE_SHOT_HIGH,
212 (Repeatability::Medium, false) => command::SINGLE_SHOT_MEDIUM,
213 (Repeatability::Low, false) => command::SINGLE_SHOT_LOW,
214 }
215}
216
217/// Returns the periodic-mode command for a repeatability and rate.
218///
219/// # Arguments
220///
221/// * `repeatability` - the repeatability to measure at.
222/// * `rate` - how many measurements per second the part should take.
223///
224/// # Returns
225///
226/// The 16-bit command word, one of the `PERIODIC_*_MPS_*` constants in [`command`].
227pub fn periodic(repeatability: Repeatability, rate: Rate) -> u16 {
228 use Repeatability::{High, Low, Medium};
229 match (rate, repeatability) {
230 (Rate::HalfMps, High) => command::PERIODIC_0_5_MPS_HIGH,
231 (Rate::HalfMps, Medium) => command::PERIODIC_0_5_MPS_MEDIUM,
232 (Rate::HalfMps, Low) => command::PERIODIC_0_5_MPS_LOW,
233 (Rate::OneMps, High) => command::PERIODIC_1_MPS_HIGH,
234 (Rate::OneMps, Medium) => command::PERIODIC_1_MPS_MEDIUM,
235 (Rate::OneMps, Low) => command::PERIODIC_1_MPS_LOW,
236 (Rate::TwoMps, High) => command::PERIODIC_2_MPS_HIGH,
237 (Rate::TwoMps, Medium) => command::PERIODIC_2_MPS_MEDIUM,
238 (Rate::TwoMps, Low) => command::PERIODIC_2_MPS_LOW,
239 (Rate::FourMps, High) => command::PERIODIC_4_MPS_HIGH,
240 (Rate::FourMps, Medium) => command::PERIODIC_4_MPS_MEDIUM,
241 (Rate::FourMps, Low) => command::PERIODIC_4_MPS_LOW,
242 (Rate::TenMps, High) => command::PERIODIC_10_MPS_HIGH,
243 (Rate::TenMps, Medium) => command::PERIODIC_10_MPS_MEDIUM,
244 (Rate::TenMps, Low) => command::PERIODIC_10_MPS_LOW,
245 }
246}
247
248/// Computes the CRC-8 the part appends to every data word.
249///
250/// Polynomial 0x31 (x^8 + x^5 + x^4 + 1), initial value 0xFF, no input or output
251/// reflection, and no final XOR; the datasheet's check value is `CRC(0xBEEF) = 0x92`.
252/// The part covers exactly the two data bytes that precede each CRC byte.
253///
254/// # Arguments
255///
256/// * `bytes` - the bytes the CRC covers, in transmission order.
257///
258/// # Returns
259///
260/// The 8-bit CRC.
261pub fn crc(bytes: &[u8]) -> u8 {
262 let mut register = 0xFFu8;
263 for &byte in bytes {
264 register ^= byte;
265 for _ in 0..8 {
266 register = if register & 0x80 != 0 {
267 (register << 1) ^ 0x31
268 } else {
269 register << 1
270 };
271 }
272 }
273 register
274}
275
276/// Decodes one CRC-protected 16-bit word as the part sends it.
277///
278/// # Arguments
279///
280/// * `bytes` - the most-significant data byte, the least-significant data byte, and
281/// the CRC over the two.
282///
283/// # Returns
284///
285/// The 16-bit word.
286///
287/// # Errors
288///
289/// Returns [`SensorError::Crc`] if the CRC byte does not match the two data bytes, so
290/// the read was corrupted and must be repeated.
291pub fn word(bytes: &[u8; 3]) -> Result<u16, SensorError> {
292 if crc(&bytes[..2]) != bytes[2] {
293 return Err(SensorError::Crc);
294 }
295 Ok(u16::from_be_bytes([bytes[0], bytes[1]]))
296}
297
298/// Builds the three bytes that carry a 16-bit word with its CRC.
299///
300/// The inverse of [`word`]. The part only accepts written data that is followed by a
301/// correct CRC, so a write of any data word goes through this.
302///
303/// # Arguments
304///
305/// * `value` - the 16-bit word.
306///
307/// # Returns
308///
309/// The word most-significant byte first, then its CRC.
310pub fn word_bytes(value: u16) -> [u8; 3] {
311 let [msb, lsb] = value.to_be_bytes();
312 [msb, lsb, crc(&[msb, lsb])]
313}
314
315/// Converts a raw temperature word to millidegrees Celsius.
316///
317/// This is the datasheet's `T = -45 + 175 * S_T / (2^16 - 1)` in integer arithmetic,
318/// rounded to the nearest millidegree.
319///
320/// # Arguments
321///
322/// * `raw` - the 16-bit temperature word.
323///
324/// # Returns
325///
326/// The temperature in millidegrees Celsius, from `-45000` to `130000`.
327pub fn milli_celsius(raw: u16) -> i32 {
328 ((175_000 * raw as u64 + 32_767) / 65_535) as i32 - 45_000
329}
330
331/// Converts a raw temperature word to degrees Celsius.
332///
333/// # Arguments
334///
335/// * `raw` - the 16-bit temperature word.
336///
337/// # Returns
338///
339/// The temperature in degrees Celsius.
340pub fn celsius(raw: u16) -> f32 {
341 -45.0 + 175.0 * raw as f32 / 65_535.0
342}
343
344/// Converts a raw temperature word to millidegrees Fahrenheit.
345///
346/// This is the datasheet's `T = -49 + 315 * S_T / (2^16 - 1)` in integer arithmetic,
347/// rounded to the nearest millidegree.
348///
349/// # Arguments
350///
351/// * `raw` - the 16-bit temperature word.
352///
353/// # Returns
354///
355/// The temperature in millidegrees Fahrenheit, from `-49000` to `266000`.
356pub fn milli_fahrenheit(raw: u16) -> i32 {
357 ((315_000 * raw as u64 + 32_767) / 65_535) as i32 - 49_000
358}
359
360/// Converts a raw temperature word to degrees Fahrenheit.
361///
362/// # Arguments
363///
364/// * `raw` - the 16-bit temperature word.
365///
366/// # Returns
367///
368/// The temperature in degrees Fahrenheit.
369pub fn fahrenheit(raw: u16) -> f32 {
370 -49.0 + 315.0 * raw as f32 / 65_535.0
371}
372
373/// Converts a raw humidity word to thousandths of a percent relative humidity.
374///
375/// This is the datasheet's `RH = 100 * S_RH / (2^16 - 1)` in integer arithmetic,
376/// rounded to the nearest thousandth of a percent.
377///
378/// # Arguments
379///
380/// * `raw` - the 16-bit humidity word.
381///
382/// # Returns
383///
384/// The relative humidity in thousandths of a percent, from `0` to `100000`.
385pub fn milli_percent(raw: u16) -> u32 {
386 ((100_000 * raw as u64 + 32_767) / 65_535) as u32
387}
388
389/// Converts a raw humidity word to percent relative humidity.
390///
391/// # Arguments
392///
393/// * `raw` - the 16-bit humidity word.
394///
395/// # Returns
396///
397/// The relative humidity in percent.
398pub fn relative_humidity(raw: u16) -> f32 {
399 100.0 * raw as f32 / 65_535.0
400}
401
402/// Builds the raw temperature word a part reports for a temperature.
403///
404/// The inverse of [`milli_celsius`], rounded to the nearest count and clamped to the
405/// part's -45 °C to 130 °C output range, so a node can be tested against what a
406/// sensor would send without one attached.
407///
408/// # Arguments
409///
410/// * `milli_celsius` - the temperature in millidegrees Celsius.
411///
412/// # Returns
413///
414/// The 16-bit temperature word.
415pub fn temperature_raw_from_milli_celsius(milli_celsius: i32) -> u16 {
416 let offset = (milli_celsius.clamp(-45_000, 130_000) + 45_000) as u64;
417 ((offset * 65_535 + 87_500) / 175_000) as u16
418}
419
420/// Builds the raw temperature word a part reports for a temperature in Celsius.
421///
422/// # Arguments
423///
424/// * `celsius` - the temperature in degrees Celsius.
425///
426/// # Returns
427///
428/// The 16-bit temperature word.
429pub fn temperature_raw_from_celsius(celsius: f32) -> u16 {
430 temperature_raw_from_milli_celsius(round_to_milli(celsius))
431}
432
433/// Builds the raw temperature word a part reports for a temperature in Fahrenheit.
434///
435/// The inverse of [`milli_fahrenheit`], rounded to the nearest count and clamped to
436/// the part's -49 °F to 266 °F output range.
437///
438/// # Arguments
439///
440/// * `milli_fahrenheit` - the temperature in millidegrees Fahrenheit.
441///
442/// # Returns
443///
444/// The 16-bit temperature word.
445pub fn temperature_raw_from_milli_fahrenheit(milli_fahrenheit: i32) -> u16 {
446 let offset = (milli_fahrenheit.clamp(-49_000, 266_000) + 49_000) as u64;
447 ((offset * 65_535 + 157_500) / 315_000) as u16
448}
449
450/// Builds the raw humidity word a part reports for a relative humidity.
451///
452/// The inverse of [`milli_percent`], rounded to the nearest count and clamped to 0 to
453/// 100 %RH.
454///
455/// # Arguments
456///
457/// * `milli_percent` - the relative humidity in thousandths of a percent.
458///
459/// # Returns
460///
461/// The 16-bit humidity word.
462pub fn humidity_raw_from_milli_percent(milli_percent: u32) -> u16 {
463 let clamped = milli_percent.min(100_000) as u64;
464 ((clamped * 65_535 + 50_000) / 100_000) as u16
465}
466
467/// Builds the raw humidity word a part reports for a relative humidity in percent.
468///
469/// # Arguments
470///
471/// * `percent` - the relative humidity in percent.
472///
473/// # Returns
474///
475/// The 16-bit humidity word.
476pub fn humidity_raw_from_relative_humidity(percent: f32) -> u16 {
477 humidity_raw_from_milli_percent(round_to_milli(percent).max(0) as u32)
478}
479
480// Rounds a value in whole units to the nearest thousandth without `f32::round`, which
481// is not available without `std`.
482fn round_to_milli(value: f32) -> i32 {
483 let scaled = value * 1_000.0;
484 (if scaled >= 0.0 {
485 scaled + 0.5
486 } else {
487 scaled - 0.5
488 }) as i32
489}
490
491/// One temperature and humidity data pair, as the part returns it.
492///
493/// The part sends the temperature word, its CRC, the humidity word, and its CRC, in
494/// that order, after a single-shot command or [`command::FETCH_DATA`]. [`parse`]
495/// checks both CRCs before exposing either word.
496///
497/// [`parse`]: Measurement::parse
498///
499/// # Examples
500///
501/// ```
502/// use pamoja_sensors::sht3x::{
503/// humidity_raw_from_milli_percent, temperature_raw_from_milli_celsius, Measurement,
504/// };
505///
506/// // What a part at 25.0 °C and 60.0 %RH puts on the bus after a single-shot command:
507/// // two words, each followed by its CRC.
508/// let bytes = Measurement {
509/// temperature_raw: temperature_raw_from_milli_celsius(25_000),
510/// humidity_raw: humidity_raw_from_milli_percent(60_000),
511/// }
512/// .to_bytes();
513/// assert_eq!(bytes.len(), 6);
514///
515/// let measurement = Measurement::parse(&bytes)?;
516/// assert_eq!(measurement.temperature_milli_celsius(), 25_000);
517/// assert_eq!(measurement.humidity_milli_percent(), 60_000);
518/// # Ok::<(), pamoja_sensors::SensorError>(())
519/// ```
520#[derive(Clone, Copy, Debug, PartialEq, Eq)]
521pub struct Measurement {
522 /// The 16-bit temperature word, `S_T` in the datasheet.
523 pub temperature_raw: u16,
524 /// The 16-bit humidity word, `S_RH` in the datasheet.
525 pub humidity_raw: u16,
526}
527
528impl Measurement {
529 /// Parses and CRC-checks a six-byte measurement frame.
530 ///
531 /// # Arguments
532 ///
533 /// * `bytes` - the six bytes in the order the part sends them: temperature MSB,
534 /// LSB, CRC, then humidity MSB, LSB, CRC.
535 ///
536 /// # Returns
537 ///
538 /// The two raw words.
539 ///
540 /// # Errors
541 ///
542 /// Returns [`SensorError::Crc`] if either CRC byte does not match its two data
543 /// bytes, so the read was corrupted and must be repeated.
544 pub fn parse(bytes: &[u8; 6]) -> Result<Measurement, SensorError> {
545 Ok(Measurement {
546 temperature_raw: word(&[bytes[0], bytes[1], bytes[2]])?,
547 humidity_raw: word(&[bytes[3], bytes[4], bytes[5]])?,
548 })
549 }
550
551 /// Returns the six bytes a part holding this pair puts on the bus.
552 ///
553 /// The inverse of [`parse`](Self::parse), with both CRCs filled in so the result
554 /// parses.
555 ///
556 /// # Returns
557 ///
558 /// Temperature MSB, LSB, CRC, then humidity MSB, LSB, CRC.
559 pub fn to_bytes(&self) -> [u8; 6] {
560 let [t0, t1, t2] = word_bytes(self.temperature_raw);
561 let [h0, h1, h2] = word_bytes(self.humidity_raw);
562 [t0, t1, t2, h0, h1, h2]
563 }
564
565 /// Returns the temperature in millidegrees Celsius.
566 ///
567 /// # Returns
568 ///
569 /// The temperature, rounded to the nearest millidegree.
570 pub fn temperature_milli_celsius(&self) -> i32 {
571 milli_celsius(self.temperature_raw)
572 }
573
574 /// Returns the temperature in degrees Celsius.
575 ///
576 /// # Returns
577 ///
578 /// The temperature in degrees Celsius.
579 pub fn temperature_celsius(&self) -> f32 {
580 celsius(self.temperature_raw)
581 }
582
583 /// Returns the temperature in millidegrees Fahrenheit.
584 ///
585 /// # Returns
586 ///
587 /// The temperature, rounded to the nearest millidegree.
588 pub fn temperature_milli_fahrenheit(&self) -> i32 {
589 milli_fahrenheit(self.temperature_raw)
590 }
591
592 /// Returns the temperature in degrees Fahrenheit.
593 ///
594 /// # Returns
595 ///
596 /// The temperature in degrees Fahrenheit.
597 pub fn temperature_fahrenheit(&self) -> f32 {
598 fahrenheit(self.temperature_raw)
599 }
600
601 /// Returns the relative humidity in thousandths of a percent.
602 ///
603 /// # Returns
604 ///
605 /// The relative humidity, rounded to the nearest thousandth of a percent.
606 pub fn humidity_milli_percent(&self) -> u32 {
607 milli_percent(self.humidity_raw)
608 }
609
610 /// Returns the relative humidity in percent.
611 ///
612 /// # Returns
613 ///
614 /// The relative humidity in percent.
615 pub fn relative_humidity(&self) -> f32 {
616 relative_humidity(self.humidity_raw)
617 }
618}
619
620/// The status register, read with [`command::READ_STATUS`] as one CRC-protected word.
621///
622/// It reports the heater, the alert state, whether a reset has happened, and how the
623/// last command and write were received. The flag bits (alert pending, the two
624/// tracking alerts, and reset detected) are cleared by [`command::CLEAR_STATUS`].
625#[derive(Clone, Copy, Debug, PartialEq, Eq)]
626pub struct Status {
627 bits: u16,
628}
629
630impl Status {
631 /// Bit 15: at least one alert is pending.
632 pub const ALERT_PENDING: u16 = 1 << 15;
633 /// Bit 13: the heater is on.
634 pub const HEATER_ON: u16 = 1 << 13;
635 /// Bit 11: a relative-humidity tracking alert.
636 pub const HUMIDITY_TRACKING_ALERT: u16 = 1 << 11;
637 /// Bit 10: a temperature tracking alert.
638 pub const TEMPERATURE_TRACKING_ALERT: u16 = 1 << 10;
639 /// Bit 4: a hard reset, soft reset, or supply failure happened since the register
640 /// was last cleared.
641 pub const RESET_DETECTED: u16 = 1 << 4;
642 /// Bit 1: the last command was not processed, being invalid or failing the
643 /// command checksum.
644 pub const COMMAND_FAILED: u16 = 1 << 1;
645 /// Bit 0: the checksum of the last write transfer failed.
646 pub const WRITE_CHECKSUM_FAILED: u16 = 1 << 0;
647
648 /// The register's value after power-up (0x8010): alert pending and reset detected
649 /// set, everything else clear. The datasheet leaves bits 9:5 unspecified; they are
650 /// zero here.
651 pub const DEFAULT: u16 = Self::ALERT_PENDING | Self::RESET_DETECTED;
652
653 /// Wraps a raw status word.
654 ///
655 /// # Arguments
656 ///
657 /// * `bits` - the 16-bit register value, for example the [`Self::DEFAULT`] word
658 /// or a combination of the bit constants.
659 ///
660 /// # Returns
661 ///
662 /// The status holding those bits.
663 pub fn from_bits(bits: u16) -> Status {
664 Status { bits }
665 }
666
667 /// Parses and CRC-checks the three bytes returned by [`command::READ_STATUS`].
668 ///
669 /// # Arguments
670 ///
671 /// * `bytes` - the register most-significant byte first, then its CRC.
672 ///
673 /// # Returns
674 ///
675 /// The decoded status.
676 ///
677 /// # Errors
678 ///
679 /// Returns [`SensorError::Crc`] if the CRC byte does not match the register bytes.
680 pub fn parse(bytes: &[u8; 3]) -> Result<Status, SensorError> {
681 word(bytes).map(Status::from_bits)
682 }
683
684 /// Returns the three bytes a part in this state answers a status read with.
685 ///
686 /// The inverse of [`parse`](Self::parse).
687 ///
688 /// # Returns
689 ///
690 /// The register most-significant byte first, then its CRC.
691 pub fn to_bytes(&self) -> [u8; 3] {
692 word_bytes(self.bits)
693 }
694
695 /// Returns the raw register value.
696 pub fn bits(&self) -> u16 {
697 self.bits
698 }
699
700 /// Returns whether at least one alert is pending (bit 15).
701 pub fn alert_pending(&self) -> bool {
702 self.bits & Self::ALERT_PENDING != 0
703 }
704
705 /// Returns whether the heater is on (bit 13).
706 pub fn heater_on(&self) -> bool {
707 self.bits & Self::HEATER_ON != 0
708 }
709
710 /// Returns whether a relative-humidity tracking alert is raised (bit 11).
711 pub fn humidity_tracking_alert(&self) -> bool {
712 self.bits & Self::HUMIDITY_TRACKING_ALERT != 0
713 }
714
715 /// Returns whether a temperature tracking alert is raised (bit 10).
716 pub fn temperature_tracking_alert(&self) -> bool {
717 self.bits & Self::TEMPERATURE_TRACKING_ALERT != 0
718 }
719
720 /// Returns whether a reset has happened since the register was last cleared
721 /// (bit 4).
722 pub fn reset_detected(&self) -> bool {
723 self.bits & Self::RESET_DETECTED != 0
724 }
725
726 /// Returns whether the last command was rejected (bit 1).
727 pub fn command_failed(&self) -> bool {
728 self.bits & Self::COMMAND_FAILED != 0
729 }
730
731 /// Returns whether the last write transfer failed its checksum (bit 0).
732 pub fn write_checksum_failed(&self) -> bool {
733 self.bits & Self::WRITE_CHECKSUM_FAILED != 0
734 }
735}
736
737#[cfg(test)]
738mod tests {
739 use super::*;
740
741 #[test]
742 fn crc_matches_the_datasheet_check_value() {
743 // Table 20: CRC(0xBEEF) = 0x92, polynomial 0x31, initialization 0xFF.
744 assert_eq!(crc(&[0xBE, 0xEF]), 0x92);
745 // With no input the register is left at its initial value.
746 assert_eq!(crc(&[]), 0xFF);
747 }
748
749 #[test]
750 fn a_word_is_returned_only_when_its_crc_matches() {
751 assert_eq!(word(&[0xBE, 0xEF, 0x92]), Ok(0xBEEF));
752 assert_eq!(word(&[0xBE, 0xEF, 0x93]), Err(SensorError::Crc));
753 assert_eq!(word(&[0xBE, 0xEE, 0x92]), Err(SensorError::Crc));
754 }
755
756 #[test]
757 fn word_bytes_carry_the_datasheet_crc_and_parse_back() {
758 assert_eq!(word_bytes(0xBEEF), [0xBE, 0xEF, 0x92]);
759 for value in [0x0000, 0x0001, 0x6666, 0x8010, 0x9999, 0xFFFF] {
760 assert_eq!(word(&word_bytes(value)), Ok(value));
761 }
762 }
763
764 #[test]
765 fn the_conversion_endpoints_match_the_datasheet_formulas() {
766 // Section 4.13: T = -45 + 175 * S_T / (2^16 - 1), so 0 is -45 °C and 65535
767 // is 130 °C; T = -49 + 315 * S_T / (2^16 - 1) in Fahrenheit; RH = 100 * S_RH
768 // / (2^16 - 1), so 0 is 0 %RH and 65535 is 100 %RH.
769 assert_eq!(milli_celsius(0), -45_000);
770 assert_eq!(milli_celsius(65_535), 130_000);
771 assert_eq!(milli_fahrenheit(0), -49_000);
772 assert_eq!(milli_fahrenheit(65_535), 266_000);
773 assert_eq!(milli_percent(0), 0);
774 assert_eq!(milli_percent(65_535), 100_000);
775 assert_eq!(celsius(0), -45.0);
776 assert!((celsius(65_535) - 130.0).abs() < 1e-4);
777 assert_eq!(fahrenheit(0), -49.0);
778 assert!((fahrenheit(65_535) - 266.0).abs() < 1e-4);
779 assert_eq!(relative_humidity(0), 0.0);
780 assert!((relative_humidity(65_535) - 100.0).abs() < 1e-4);
781 }
782
783 #[test]
784 fn exact_fractions_of_full_scale_decode_to_whole_degrees_and_percent() {
785 // 0x6666 is exactly 0.4 of 65535, so T = -45 + 70 = 25 °C and 77 °F; 0x9999
786 // is exactly 0.6, so RH = 60 %.
787 assert_eq!(milli_celsius(0x6666), 25_000);
788 assert_eq!(milli_fahrenheit(0x6666), 77_000);
789 assert_eq!(milli_percent(0x9999), 60_000);
790 assert!((celsius(0x6666) - 25.0).abs() < 1e-4);
791 assert!((fahrenheit(0x6666) - 77.0).abs() < 1e-4);
792 assert!((relative_humidity(0x9999) - 60.0).abs() < 1e-4);
793 }
794
795 #[test]
796 fn integer_conversion_tracks_the_floating_point_formula() {
797 // Every raw word, against a direct transcription of the section 4.13
798 // formulas; rounding to nearest keeps the integer result within half a
799 // millidegree or half a thousandth of a percent.
800 for raw in 0..=u16::MAX {
801 let t = -45.0 + 175.0 * raw as f64 / 65_535.0;
802 let f = -49.0 + 315.0 * raw as f64 / 65_535.0;
803 let rh = 100.0 * raw as f64 / 65_535.0;
804 assert!(
805 (milli_celsius(raw) as f64 - t * 1_000.0).abs() <= 0.5,
806 "raw {raw:#06x}: {} vs {t}",
807 milli_celsius(raw)
808 );
809 assert!(
810 (milli_fahrenheit(raw) as f64 - f * 1_000.0).abs() <= 0.5,
811 "raw {raw:#06x}: {} vs {f}",
812 milli_fahrenheit(raw)
813 );
814 assert!(
815 (milli_percent(raw) as f64 - rh * 1_000.0).abs() <= 0.5,
816 "raw {raw:#06x}: {} vs {rh}",
817 milli_percent(raw)
818 );
819 }
820 }
821
822 #[test]
823 fn every_raw_word_survives_a_round_trip_through_its_builder() {
824 // One count is 2.67 millidegrees Celsius, 4.81 millidegrees Fahrenheit, and
825 // 1.53 thousandths of a percent, all wider than the rounding in the decode,
826 // so the builders recover every word exactly.
827 for raw in 0..=u16::MAX {
828 assert_eq!(temperature_raw_from_milli_celsius(milli_celsius(raw)), raw);
829 assert_eq!(
830 temperature_raw_from_milli_fahrenheit(milli_fahrenheit(raw)),
831 raw
832 );
833 assert_eq!(humidity_raw_from_milli_percent(milli_percent(raw)), raw);
834 }
835 }
836
837 #[test]
838 fn the_builders_clamp_to_the_output_range() {
839 assert_eq!(temperature_raw_from_milli_celsius(-45_000), 0);
840 assert_eq!(temperature_raw_from_milli_celsius(-100_000), 0);
841 assert_eq!(temperature_raw_from_milli_celsius(130_000), 65_535);
842 assert_eq!(temperature_raw_from_milli_celsius(200_000), 65_535);
843 assert_eq!(temperature_raw_from_milli_fahrenheit(-49_000), 0);
844 assert_eq!(temperature_raw_from_milli_fahrenheit(300_000), 65_535);
845 assert_eq!(humidity_raw_from_milli_percent(0), 0);
846 assert_eq!(humidity_raw_from_milli_percent(100_000), 65_535);
847 assert_eq!(humidity_raw_from_milli_percent(150_000), 65_535);
848 }
849
850 #[test]
851 fn the_floating_point_builders_land_on_the_same_words() {
852 assert_eq!(temperature_raw_from_celsius(25.0), 0x6666);
853 assert_eq!(temperature_raw_from_celsius(-45.0), 0);
854 assert_eq!(temperature_raw_from_celsius(130.0), 65_535);
855 assert_eq!(humidity_raw_from_relative_humidity(60.0), 0x9999);
856 assert_eq!(humidity_raw_from_relative_humidity(-1.0), 0);
857 assert_eq!(humidity_raw_from_relative_humidity(100.0), 65_535);
858 for celsius_in in [-40.0f32, -10.5, 0.0, 23.73, 85.0, 125.0] {
859 let back = celsius(temperature_raw_from_celsius(celsius_in));
860 assert!((back - celsius_in).abs() < 0.002, "{celsius_in} vs {back}");
861 }
862 for percent_in in [0.0f32, 12.5, 50.0, 63.37, 99.99] {
863 let back = relative_humidity(humidity_raw_from_relative_humidity(percent_in));
864 assert!((back - percent_in).abs() < 0.001, "{percent_in} vs {back}");
865 }
866 }
867
868 #[test]
869 fn a_measurement_frame_carries_temperature_then_humidity() {
870 // Section 4.4: the temperature word and its CRC come first, then humidity.
871 let bytes = [0x66, 0x66, 0x93, 0x99, 0x99, 0xBE];
872 let measurement = Measurement::parse(&bytes).expect("valid crcs");
873 assert_eq!(measurement.temperature_raw, 0x6666);
874 assert_eq!(measurement.humidity_raw, 0x9999);
875 assert_eq!(measurement.temperature_milli_celsius(), 25_000);
876 assert_eq!(measurement.temperature_milli_fahrenheit(), 77_000);
877 assert_eq!(measurement.humidity_milli_percent(), 60_000);
878 assert!((measurement.temperature_celsius() - 25.0).abs() < 1e-4);
879 assert!((measurement.temperature_fahrenheit() - 77.0).abs() < 1e-4);
880 assert!((measurement.relative_humidity() - 60.0).abs() < 1e-4);
881 assert_eq!(measurement.to_bytes(), bytes);
882 }
883
884 #[test]
885 fn a_built_measurement_parses_back_to_what_it_was_built_from() {
886 let built = Measurement {
887 temperature_raw: temperature_raw_from_milli_celsius(23_732),
888 humidity_raw: humidity_raw_from_milli_percent(63_368),
889 };
890 let parsed = Measurement::parse(&built.to_bytes()).expect("a built frame is valid");
891 assert_eq!(parsed, built);
892 assert_eq!(parsed.temperature_raw, 0x648B);
893 assert_eq!(parsed.humidity_raw, 0xA238);
894 assert_eq!(built.to_bytes(), [0x64, 0x8B, 0xC7, 0xA2, 0x38, 0xDB]);
895 assert_eq!(parsed.temperature_milli_celsius(), 23_732);
896 assert_eq!(parsed.humidity_milli_percent(), 63_368);
897 }
898
899 #[test]
900 fn a_corrupted_measurement_frame_fails_the_crc() {
901 let good = [0x66, 0x66, 0x93, 0x99, 0x99, 0xBE];
902 let mut temperature_hit = good;
903 temperature_hit[1] ^= 0x01;
904 assert_eq!(Measurement::parse(&temperature_hit), Err(SensorError::Crc));
905 let mut humidity_hit = good;
906 humidity_hit[4] ^= 0x80;
907 assert_eq!(Measurement::parse(&humidity_hit), Err(SensorError::Crc));
908 let mut crc_hit = good;
909 crc_hit[5] = 0x00;
910 assert_eq!(Measurement::parse(&crc_hit), Err(SensorError::Crc));
911 }
912
913 #[test]
914 fn single_shot_commands_match_table_9() {
915 // Table 9: MSB 0x2C with clock stretching, 0x24 without; the datasheet's own
916 // example is 0x2C06 for high repeatability with clock stretching.
917 assert_eq!(single_shot(Repeatability::High, true), 0x2C06);
918 assert_eq!(single_shot(Repeatability::Medium, true), 0x2C0D);
919 assert_eq!(single_shot(Repeatability::Low, true), 0x2C10);
920 assert_eq!(single_shot(Repeatability::High, false), 0x2400);
921 assert_eq!(single_shot(Repeatability::Medium, false), 0x240B);
922 assert_eq!(single_shot(Repeatability::Low, false), 0x2416);
923 }
924
925 #[test]
926 fn periodic_commands_match_table_10() {
927 // Table 10, one MSB per rate; the datasheet's own example is 0x2130 for one
928 // high-repeatability measurement per second.
929 use Repeatability::{High, Low, Medium};
930 assert_eq!(periodic(High, Rate::OneMps), 0x2130);
931 let table: &[(Rate, u16, u16, u16)] = &[
932 (Rate::HalfMps, 0x2032, 0x2024, 0x202F),
933 (Rate::OneMps, 0x2130, 0x2126, 0x212D),
934 (Rate::TwoMps, 0x2236, 0x2220, 0x222B),
935 (Rate::FourMps, 0x2334, 0x2322, 0x2329),
936 (Rate::TenMps, 0x2737, 0x2721, 0x272A),
937 ];
938 for &(rate, high, medium, low) in table {
939 assert_eq!(periodic(High, rate), high, "{rate:?} high");
940 assert_eq!(periodic(Medium, rate), medium, "{rate:?} medium");
941 assert_eq!(periodic(Low, rate), low, "{rate:?} low");
942 }
943 assert_eq!(command::PERIODIC_ART, 0x2B32);
944 }
945
946 #[test]
947 fn control_commands_match_tables_11_to_19() {
948 assert_eq!(command::FETCH_DATA, 0xE000);
949 assert_eq!(command::BREAK, 0x3093);
950 assert_eq!(command::SOFT_RESET, 0x30A2);
951 assert_eq!(command::GENERAL_CALL_RESET.to_be_bytes(), [0x00, 0x06]);
952 assert_eq!(command::HEATER_ENABLE, 0x306D);
953 assert_eq!(command::HEATER_DISABLE, 0x3066);
954 assert_eq!(command::READ_STATUS, 0xF32D);
955 assert_eq!(command::CLEAR_STATUS, 0x3041);
956 }
957
958 #[test]
959 fn the_addresses_and_timings_match_the_datasheet() {
960 // Table 8, Table 4 (2.4 V to 5.5 V), and the 1 ms command spacing of
961 // section 4.
962 assert_eq!(I2C_ADDRESS_A, 0x44);
963 assert_eq!(I2C_ADDRESS_B, 0x45);
964 assert_eq!(Repeatability::Low.max_measurement_micros(), 4_000);
965 assert_eq!(Repeatability::Medium.max_measurement_micros(), 6_000);
966 assert_eq!(Repeatability::High.max_measurement_micros(), 15_000);
967 assert_eq!(Repeatability::Low.typical_measurement_micros(), 2_500);
968 assert_eq!(Repeatability::Medium.typical_measurement_micros(), 4_500);
969 assert_eq!(Repeatability::High.typical_measurement_micros(), 12_500);
970 assert_eq!(Rate::HalfMps.interval_micros(), 2_000_000);
971 assert_eq!(Rate::TenMps.interval_micros(), 100_000);
972 assert_eq!(MIN_COMMAND_GAP_MICROS, 1_000);
973 assert_eq!(SOFT_RESET_MICROS, 1_500);
974 }
975
976 #[test]
977 fn the_status_register_default_matches_table_18() {
978 // Table 18: bit 15 (alert pending) and bit 4 (reset detected) default to 1,
979 // every other defined bit to 0.
980 assert_eq!(Status::DEFAULT, 0x8010);
981 let status = Status::from_bits(Status::DEFAULT);
982 assert!(status.alert_pending());
983 assert!(status.reset_detected());
984 assert!(!status.heater_on());
985 assert!(!status.humidity_tracking_alert());
986 assert!(!status.temperature_tracking_alert());
987 assert!(!status.command_failed());
988 assert!(!status.write_checksum_failed());
989 assert_eq!(status.to_bytes(), [0x80, 0x10, 0xE1]);
990 assert_eq!(Status::parse(&[0x80, 0x10, 0xE1]), Ok(status));
991 }
992
993 #[test]
994 fn each_status_bit_decodes_to_its_flag() {
995 let all = Status::from_bits(
996 Status::ALERT_PENDING
997 | Status::HEATER_ON
998 | Status::HUMIDITY_TRACKING_ALERT
999 | Status::TEMPERATURE_TRACKING_ALERT
1000 | Status::RESET_DETECTED
1001 | Status::COMMAND_FAILED
1002 | Status::WRITE_CHECKSUM_FAILED,
1003 );
1004 assert_eq!(all.bits(), 0xAC13);
1005 assert!(all.alert_pending());
1006 assert!(all.heater_on());
1007 assert!(all.humidity_tracking_alert());
1008 assert!(all.temperature_tracking_alert());
1009 assert!(all.reset_detected());
1010 assert!(all.command_failed());
1011 assert!(all.write_checksum_failed());
1012 let heater_only = Status::from_bits(Status::HEATER_ON);
1013 assert_eq!(heater_only.bits(), 0x2000);
1014 assert!(heater_only.heater_on());
1015 assert!(!heater_only.alert_pending());
1016 assert_eq!(Status::parse(&heater_only.to_bytes()), Ok(heater_only));
1017 }
1018
1019 #[test]
1020 fn a_corrupted_status_word_fails_the_crc() {
1021 assert_eq!(Status::parse(&[0x80, 0x11, 0xE1]), Err(SensorError::Crc));
1022 assert_eq!(Status::parse(&[0x80, 0x10, 0x00]), Err(SensorError::Crc));
1023 }
1024}