Skip to main content

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}