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