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}