Skip to main content

pamoja_sensors/
hdc1080.rs

1//! Texas Instruments HDC1080 low-power humidity and temperature sensor.
2//!
3//! The HDC1080 returns each measurement as a plain 16-bit fraction of full scale: the
4//! temperature register spans -40 to +125 °C and the humidity register 0 to 100 %RH,
5//! both linearly over the 16-bit code. There is no per-chip calibration to apply and
6//! no checksum on the frame, so the decode is the datasheet's two equations, done
7//! here in integer arithmetic so a node without floating point reads exact
8//! millidegrees and milli-percent.
9//!
10//! A caller writes a [`Configuration`] to the configuration register once, triggers
11//! an acquisition by writing the temperature pointer, waits the resolution's
12//! conversion time, and reads the four-byte frame into a [`Measurement`].
13
14use crate::SensorError;
15
16/// The fixed 7-bit I2C address, `1000000`.
17pub const I2C_ADDRESS: u8 = 0x40;
18/// The value the manufacturer-id register (0xFE) returns: Texas Instruments.
19pub const MANUFACTURER_ID: u16 = 0x5449;
20/// The value the device-id register (0xFF) returns for an HDC1080.
21pub const DEVICE_ID: u16 = 0x1050;
22/// The power-on value of the configuration register: acquisition mode set to
23/// temperature then humidity, 14-bit resolution on both, heater off.
24pub const CONFIGURATION_RESET: u16 = 0x1000;
25
26#[cfg(feature = "embedded-hal")]
27mod driver;
28#[cfg(feature = "embedded-hal")]
29pub mod sim;
30
31#[cfg(feature = "embedded-hal")]
32pub use driver::Hdc1080;
33
34/// The HDC1080 register addresses. Every register is 16 bits, sent MSB first.
35pub mod register {
36    /// Temperature result; writing this pointer also triggers an acquisition.
37    pub const TEMPERATURE: u8 = 0x00;
38    /// Humidity result; writing this pointer triggers a humidity-only acquisition.
39    pub const HUMIDITY: u8 = 0x01;
40    /// Configuration and status register.
41    pub const CONFIGURATION: u8 = 0x02;
42    /// Serial-id bits 40:25.
43    pub const SERIAL_ID_HIGH: u8 = 0xFB;
44    /// Serial-id bits 24:9.
45    pub const SERIAL_ID_MID: u8 = 0xFC;
46    /// Serial-id bits 8:0, in bits 15:7 of the register.
47    pub const SERIAL_ID_LOW: u8 = 0xFD;
48    /// Manufacturer id; reads [`super::MANUFACTURER_ID`].
49    pub const MANUFACTURER_ID: u8 = 0xFE;
50    /// Device id; reads [`super::DEVICE_ID`] for an HDC1080.
51    pub const DEVICE_ID: u8 = 0xFF;
52}
53
54const TEMPERATURE_SPAN_MILLI: i64 = 165_000;
55const TEMPERATURE_OFFSET_MILLI: i32 = 40_000;
56const HUMIDITY_SPAN_MILLI: u64 = 100_000;
57
58/// Decodes the temperature register to millidegrees Celsius.
59///
60/// The datasheet's `T = raw / 2^16 * 165 - 40`, computed in integers and rounded to
61/// the nearest millidegree.
62///
63/// # Arguments
64///
65/// * `raw` - the 16-bit temperature register.
66///
67/// # Returns
68///
69/// The temperature in millidegrees Celsius, from -40 000 to 124 997.
70pub fn milli_celsius(raw: u16) -> i32 {
71    let scaled = (raw as i64 * TEMPERATURE_SPAN_MILLI + (1 << 15)) >> 16;
72    scaled as i32 - TEMPERATURE_OFFSET_MILLI
73}
74
75/// Decodes the temperature register to degrees Celsius.
76///
77/// The floating-point convenience beside [`milli_celsius`].
78///
79/// # Arguments
80///
81/// * `raw` - the 16-bit temperature register.
82///
83/// # Returns
84///
85/// The temperature in degrees Celsius.
86pub fn celsius(raw: u16) -> f32 {
87    raw as f32 / 65_536.0 * 165.0 - 40.0
88}
89
90/// Decodes the humidity register to thousandths of a percent relative humidity.
91///
92/// The datasheet's `RH = raw / 2^16 * 100`, computed in integers and rounded to the
93/// nearest milli-percent.
94///
95/// # Arguments
96///
97/// * `raw` - the 16-bit humidity register.
98///
99/// # Returns
100///
101/// The relative humidity in milli-percent, from 0 to 99 998.
102pub fn milli_percent(raw: u16) -> u32 {
103    ((raw as u64 * HUMIDITY_SPAN_MILLI + (1 << 15)) >> 16) as u32
104}
105
106/// Decodes the humidity register to percent relative humidity.
107///
108/// The floating-point convenience beside [`milli_percent`].
109///
110/// # Arguments
111///
112/// * `raw` - the 16-bit humidity register.
113///
114/// # Returns
115///
116/// The relative humidity in percent.
117pub fn relative_humidity(raw: u16) -> f32 {
118    raw as f32 / 65_536.0 * 100.0
119}
120
121/// Builds the temperature register the sensor reports for a temperature.
122///
123/// The inverse of [`milli_celsius`]. The result is a 14-bit code in bits 15:2 with
124/// the two reserved low bits clear, which is what the part sends, so a decode of the
125/// built register lands within one 14-bit step (about 0.01 °C) of the input. Inputs
126/// outside -40 to +125 °C clamp to the ends of the scale.
127///
128/// # Arguments
129///
130/// * `milli_celsius` - the temperature in millidegrees Celsius.
131///
132/// # Returns
133///
134/// The 16-bit temperature register.
135pub fn temperature_register(milli_celsius: i32) -> u16 {
136    let offset = (milli_celsius as i64 + TEMPERATURE_OFFSET_MILLI as i64).max(0);
137    let code = (offset * 16_384 + TEMPERATURE_SPAN_MILLI / 2) / TEMPERATURE_SPAN_MILLI;
138    (code.min(16_383) as u16) << 2
139}
140
141/// Builds the humidity register the sensor reports for a relative humidity.
142///
143/// The inverse of [`milli_percent`], with the same 14-bit alignment as
144/// [`temperature_register`]. Inputs above 100 %RH clamp to full scale.
145///
146/// # Arguments
147///
148/// * `milli_percent` - the relative humidity in thousandths of a percent.
149///
150/// # Returns
151///
152/// The 16-bit humidity register.
153pub fn humidity_register(milli_percent: u32) -> u16 {
154    let code = (milli_percent as u64 * 16_384 + HUMIDITY_SPAN_MILLI / 2) / HUMIDITY_SPAN_MILLI;
155    (code.min(16_383) as u16) << 2
156}
157
158/// Assembles the serial number from its three registers.
159///
160/// The datasheet numbers the serial bits 40 down to 0, spread as bits 40:25 in
161/// register 0xFB, 24:9 in 0xFC, and 8:0 in bits 15:7 of 0xFD.
162///
163/// # Arguments
164///
165/// * `high` - the 0xFB register.
166/// * `mid` - the 0xFC register.
167/// * `low` - the 0xFD register.
168///
169/// # Returns
170///
171/// The serial number, in the low 41 bits.
172pub fn serial_id(high: u16, mid: u16, low: u16) -> u64 {
173    ((high as u64) << 25) | ((mid as u64) << 9) | ((low >> 7) as u64)
174}
175
176/// Splits a serial number into the three registers a sensor reports it in.
177///
178/// The inverse of [`serial_id`].
179///
180/// # Arguments
181///
182/// * `serial` - the serial number; bits above 40 are ignored.
183///
184/// # Returns
185///
186/// The 0xFB, 0xFC, and 0xFD registers in that order.
187pub fn serial_id_registers(serial: u64) -> [u16; 3] {
188    [
189        (serial >> 25) as u16,
190        (serial >> 9) as u16,
191        ((serial & 0x1FF) as u16) << 7,
192    ]
193}
194
195/// The MODE field: what one trigger acquires.
196#[derive(Clone, Copy, Debug, PartialEq, Eq)]
197pub enum AcquisitionMode {
198    /// Temperature or humidity alone, chosen by the pointer written to trigger it.
199    Single,
200    /// Temperature and humidity in sequence, temperature first, read as one frame.
201    TemperatureThenHumidity,
202}
203
204/// The TRES field: temperature measurement resolution.
205#[derive(Clone, Copy, Debug, PartialEq, Eq)]
206pub enum TemperatureResolution {
207    /// 14 bits, 6.35 ms conversion.
208    Bits14,
209    /// 11 bits, 3.65 ms conversion.
210    Bits11,
211}
212
213impl TemperatureResolution {
214    /// Returns the conversion time for this resolution.
215    ///
216    /// # Returns
217    ///
218    /// The datasheet's typical temperature conversion time in microseconds.
219    pub fn conversion_time_micros(self) -> u32 {
220        match self {
221            TemperatureResolution::Bits14 => 6_350,
222            TemperatureResolution::Bits11 => 3_650,
223        }
224    }
225}
226
227/// The HRES field: humidity measurement resolution.
228#[derive(Clone, Copy, Debug, PartialEq, Eq)]
229pub enum HumidityResolution {
230    /// 14 bits, 6.50 ms conversion.
231    Bits14,
232    /// 11 bits, 3.85 ms conversion.
233    Bits11,
234    /// 8 bits, 2.50 ms conversion.
235    Bits8,
236}
237
238impl HumidityResolution {
239    /// Returns the conversion time for this resolution.
240    ///
241    /// # Returns
242    ///
243    /// The datasheet's typical humidity conversion time in microseconds.
244    pub fn conversion_time_micros(self) -> u32 {
245        match self {
246            HumidityResolution::Bits14 => 6_500,
247            HumidityResolution::Bits11 => 3_850,
248            HumidityResolution::Bits8 => 2_500,
249        }
250    }
251}
252
253/// The configuration register (0x02), field by field.
254///
255/// [`Configuration::default`] is the power-on state, [`CONFIGURATION_RESET`].
256#[derive(Clone, Copy, Debug, PartialEq, Eq)]
257pub struct Configuration {
258    /// RST, bit 15: write `true` to reset the part; the bit clears itself.
259    pub software_reset: bool,
260    /// HEAT, bit 13: run the on-die heater during measurements.
261    pub heater: bool,
262    /// MODE, bit 12: what one trigger acquires.
263    pub mode: AcquisitionMode,
264    /// BTST, bit 11, read only: `true` when the supply is below 2.8 V, refreshed
265    /// after power-on and after every measurement request.
266    pub battery_low: bool,
267    /// TRES, bit 10.
268    pub temperature_resolution: TemperatureResolution,
269    /// HRES, bits 9:8.
270    pub humidity_resolution: HumidityResolution,
271}
272
273impl Default for Configuration {
274    fn default() -> Configuration {
275        Configuration {
276            software_reset: false,
277            heater: false,
278            mode: AcquisitionMode::TemperatureThenHumidity,
279            battery_low: false,
280            temperature_resolution: TemperatureResolution::Bits14,
281            humidity_resolution: HumidityResolution::Bits14,
282        }
283    }
284}
285
286impl Configuration {
287    /// Decodes the configuration register.
288    ///
289    /// Reserved bits (14 and 7:0) are ignored; the part reads them as zero.
290    ///
291    /// # Arguments
292    ///
293    /// * `raw` - the 16-bit configuration register.
294    ///
295    /// # Returns
296    ///
297    /// The decoded configuration.
298    ///
299    /// # Errors
300    ///
301    /// [`SensorError::Invalid`] if HRES holds `11`, the one code the datasheet
302    /// leaves undefined.
303    pub fn from_register(raw: u16) -> Result<Configuration, SensorError> {
304        let humidity_resolution = match (raw >> 8) & 0b11 {
305            0b00 => HumidityResolution::Bits14,
306            0b01 => HumidityResolution::Bits11,
307            0b10 => HumidityResolution::Bits8,
308            _ => return Err(SensorError::Invalid),
309        };
310        Ok(Configuration {
311            software_reset: raw & (1 << 15) != 0,
312            heater: raw & (1 << 13) != 0,
313            mode: if raw & (1 << 12) != 0 {
314                AcquisitionMode::TemperatureThenHumidity
315            } else {
316                AcquisitionMode::Single
317            },
318            battery_low: raw & (1 << 11) != 0,
319            temperature_resolution: if raw & (1 << 10) != 0 {
320                TemperatureResolution::Bits11
321            } else {
322                TemperatureResolution::Bits14
323            },
324            humidity_resolution,
325        })
326    }
327
328    /// Encodes the configuration register.
329    ///
330    /// # Returns
331    ///
332    /// The 16-bit register value, reserved bits clear.
333    pub fn to_register(&self) -> u16 {
334        let mut raw = 0;
335        if self.software_reset {
336            raw |= 1 << 15;
337        }
338        if self.heater {
339            raw |= 1 << 13;
340        }
341        if self.mode == AcquisitionMode::TemperatureThenHumidity {
342            raw |= 1 << 12;
343        }
344        if self.battery_low {
345            raw |= 1 << 11;
346        }
347        if self.temperature_resolution == TemperatureResolution::Bits11 {
348            raw |= 1 << 10;
349        }
350        raw |= match self.humidity_resolution {
351            HumidityResolution::Bits14 => 0b00,
352            HumidityResolution::Bits11 => 0b01,
353            HumidityResolution::Bits8 => 0b10,
354        } << 8;
355        raw
356    }
357
358    /// Returns how long to wait after a trigger before the result is readable.
359    ///
360    /// # Returns
361    ///
362    /// In [`AcquisitionMode::TemperatureThenHumidity`], the temperature and humidity
363    /// conversion times added together. In [`AcquisitionMode::Single`], the longer of
364    /// the two, which covers whichever pointer the caller triggered.
365    pub fn conversion_time_micros(&self) -> u32 {
366        let temperature = self.temperature_resolution.conversion_time_micros();
367        let humidity = self.humidity_resolution.conversion_time_micros();
368        match self.mode {
369            AcquisitionMode::TemperatureThenHumidity => temperature + humidity,
370            AcquisitionMode::Single => temperature.max(humidity),
371        }
372    }
373}
374
375/// One combined temperature and humidity result, as read in a single transaction
376/// after a trigger in [`AcquisitionMode::TemperatureThenHumidity`].
377#[derive(Clone, Copy, Debug, PartialEq, Eq)]
378pub struct Measurement {
379    /// The temperature register.
380    pub temperature: u16,
381    /// The humidity register.
382    pub humidity: u16,
383}
384
385impl Measurement {
386    /// Parses the four-byte frame: temperature MSB, LSB, humidity MSB, LSB.
387    ///
388    /// # Arguments
389    ///
390    /// * `bytes` - the four bytes read from the temperature pointer.
391    ///
392    /// # Returns
393    ///
394    /// The two raw registers.
395    pub fn parse(bytes: &[u8; 4]) -> Measurement {
396        Measurement {
397            temperature: u16::from_be_bytes([bytes[0], bytes[1]]),
398            humidity: u16::from_be_bytes([bytes[2], bytes[3]]),
399        }
400    }
401
402    /// Builds the four-byte frame the sensor sends for this measurement.
403    ///
404    /// The inverse of [`Measurement::parse`].
405    ///
406    /// # Returns
407    ///
408    /// Temperature MSB, LSB, humidity MSB, LSB.
409    pub fn to_bytes(&self) -> [u8; 4] {
410        let [t_hi, t_lo] = self.temperature.to_be_bytes();
411        let [h_hi, h_lo] = self.humidity.to_be_bytes();
412        [t_hi, t_lo, h_hi, h_lo]
413    }
414
415    /// Builds the measurement a sensor reports for a temperature and humidity.
416    ///
417    /// # Arguments
418    ///
419    /// * `milli_celsius` - the temperature in millidegrees Celsius.
420    /// * `milli_percent` - the relative humidity in thousandths of a percent.
421    ///
422    /// # Returns
423    ///
424    /// The measurement, built with [`temperature_register`] and
425    /// [`humidity_register`].
426    pub fn from_physical(milli_celsius: i32, milli_percent: u32) -> Measurement {
427        Measurement {
428            temperature: temperature_register(milli_celsius),
429            humidity: humidity_register(milli_percent),
430        }
431    }
432
433    /// Returns the temperature in millidegrees Celsius.
434    ///
435    /// # Returns
436    ///
437    /// [`milli_celsius`] of the temperature register.
438    pub fn milli_celsius(&self) -> i32 {
439        milli_celsius(self.temperature)
440    }
441
442    /// Returns the temperature in degrees Celsius.
443    ///
444    /// # Returns
445    ///
446    /// [`celsius`] of the temperature register.
447    pub fn celsius(&self) -> f32 {
448        celsius(self.temperature)
449    }
450
451    /// Returns the relative humidity in thousandths of a percent.
452    ///
453    /// # Returns
454    ///
455    /// [`milli_percent`] of the humidity register.
456    pub fn milli_percent(&self) -> u32 {
457        milli_percent(self.humidity)
458    }
459
460    /// Returns the relative humidity in percent.
461    ///
462    /// # Returns
463    ///
464    /// [`relative_humidity`] of the humidity register.
465    pub fn relative_humidity(&self) -> f32 {
466        relative_humidity(self.humidity)
467    }
468}
469
470#[cfg(test)]
471mod tests {
472    use super::*;
473
474    // Datasheet section 8.6.1, Equation 1: T(°C) = TEMPERATURE[15:0] / 2^16 * 165 - 40.
475    fn reference_celsius(raw: u16) -> f64 {
476        raw as f64 / 65_536.0 * 165.0 - 40.0
477    }
478
479    // Datasheet section 8.6.2, Equation 2: RH(%) = HUMIDITY[15:0] / 2^16 * 100.
480    fn reference_humidity(raw: u16) -> f64 {
481        raw as f64 / 65_536.0 * 100.0
482    }
483
484    #[test]
485    fn temperature_formula_holds_at_the_ends_and_mid_scale() {
486        // Equation 1: code 0 is the -40 °C floor, mid-scale is 165 / 2 - 40, and the
487        // top code sits one 16-bit step below +125 °C.
488        assert_eq!(milli_celsius(0x0000), -40_000);
489        assert_eq!(milli_celsius(0x8000), 42_500);
490        assert_eq!(milli_celsius(0xFFFF), 124_997);
491        assert_eq!(milli_celsius(0x6000), 21_875);
492        assert!((celsius(0x0000) + 40.0).abs() < 1e-5);
493        assert!((celsius(0x8000) - 42.5).abs() < 1e-5);
494        assert!((celsius(0xFFFF) - 124.9975).abs() < 1e-3);
495    }
496
497    #[test]
498    fn humidity_formula_holds_at_the_ends_and_mid_scale() {
499        // Equation 2: 0 %RH at code 0, 50 %RH at mid-scale, and the top code one
500        // 16-bit step below 100 %RH.
501        assert_eq!(milli_percent(0x0000), 0);
502        assert_eq!(milli_percent(0x8000), 50_000);
503        assert_eq!(milli_percent(0xFFFF), 99_998);
504        assert_eq!(milli_percent(0x4000), 25_000);
505        assert!(relative_humidity(0x0000).abs() < 1e-5);
506        assert!((relative_humidity(0x8000) - 50.0).abs() < 1e-5);
507        assert!((relative_humidity(0xFFFF) - 99.9985).abs() < 1e-3);
508    }
509
510    #[test]
511    fn integer_decode_tracks_the_floating_point_reference() {
512        // Every code in a stride across the register, plus the ends, must agree with
513        // the datasheet formula to within the half-unit that rounding allows.
514        let codes = (0..=0xFFFF_u32).step_by(97).chain([0xFFFF]);
515        for raw in codes.map(|code| code as u16) {
516            let t_ref = reference_celsius(raw) * 1_000.0;
517            let t_int = milli_celsius(raw) as f64;
518            assert!(
519                (t_int - t_ref).abs() <= 0.5,
520                "temperature {raw:#06x}: {t_int} vs {t_ref}"
521            );
522
523            let h_ref = reference_humidity(raw) * 1_000.0;
524            let h_int = milli_percent(raw) as f64;
525            assert!(
526                (h_int - h_ref).abs() <= 0.5,
527                "humidity {raw:#06x}: {h_int} vs {h_ref}"
528            );
529        }
530    }
531
532    #[test]
533    fn register_builders_invert_the_decoders_to_within_a_14_bit_step() {
534        // Tables 2 and 3: bits 1:0 of both result registers are always zero, so a
535        // built register is 14-bit aligned and a decode lands within one 14-bit LSB
536        // (165 000 / 16 384 millidegrees, 100 000 / 16 384 milli-percent).
537        assert_eq!(temperature_register(42_500), 0x8000);
538        assert_eq!(temperature_register(-40_000), 0x0000);
539        assert_eq!(temperature_register(21_875), 0x6000);
540        assert_eq!(humidity_register(50_000), 0x8000);
541        assert_eq!(humidity_register(0), 0x0000);
542        assert_eq!(humidity_register(25_000), 0x4000);
543        for milli_celsius_in in (-40_000..=125_000).step_by(1_234) {
544            let raw = temperature_register(milli_celsius_in);
545            assert_eq!(raw & 0b11, 0);
546            let back = milli_celsius(raw);
547            assert!(
548                (back - milli_celsius_in.min(124_997)).abs() <= 6,
549                "{milli_celsius_in} -> {raw:#06x} -> {back}"
550            );
551        }
552        for milli_percent_in in (0..=100_000).step_by(789) {
553            let raw = humidity_register(milli_percent_in);
554            assert_eq!(raw & 0b11, 0);
555            let back = milli_percent(raw);
556            assert!(
557                (back as i64 - milli_percent_in.min(99_994) as i64).abs() <= 4,
558                "{milli_percent_in} -> {raw:#06x} -> {back}"
559            );
560        }
561        assert_eq!(temperature_register(-100_000), 0x0000);
562        assert_eq!(temperature_register(200_000), 0xFFFC);
563        assert_eq!(humidity_register(150_000), 0xFFFC);
564    }
565
566    #[test]
567    fn identification_registers_read_the_datasheet_values() {
568        // Table 1: 0xFE reads 0x5449 (Texas Instruments), 0xFF reads 0x1050.
569        assert_eq!(MANUFACTURER_ID, 0x5449);
570        assert_eq!(DEVICE_ID, 0x1050);
571        assert_eq!(I2C_ADDRESS, 0b100_0000);
572        assert_eq!(register::MANUFACTURER_ID, 0xFE);
573        assert_eq!(register::DEVICE_ID, 0xFF);
574    }
575
576    #[test]
577    fn serial_id_reassembles_from_its_three_registers() {
578        // Tables 5 to 7: 0xFB carries bits 40:25, 0xFC bits 24:9, 0xFD bits 8:0 in
579        // its top nine bits with 6:0 reserved.
580        assert_eq!(serial_id(0xFFFF, 0xFFFF, 0xFF80), (1 << 41) - 1);
581        assert_eq!(serial_id(0x0001, 0x0000, 0x0000), 1 << 25);
582        assert_eq!(serial_id(0x0000, 0x0001, 0x0000), 1 << 9);
583        assert_eq!(serial_id(0x0000, 0x0000, 0x0080), 1);
584        let serial = 0x0123_4567_89AB;
585        let regs = serial_id_registers(serial);
586        assert_eq!(regs[2] & 0x7F, 0);
587        assert_eq!(serial_id(regs[0], regs[1], regs[2]), serial);
588    }
589
590    #[test]
591    fn configuration_reset_value_matches_the_register_map() {
592        // Table 1: the configuration register resets to 0x1000, which Table 4 reads
593        // as MODE = 1 with every other field zero.
594        assert_eq!(CONFIGURATION_RESET, 0x1000);
595        assert_eq!(Configuration::default().to_register(), CONFIGURATION_RESET);
596        assert_eq!(
597            Configuration::from_register(CONFIGURATION_RESET),
598            Ok(Configuration::default())
599        );
600        let reset = Configuration::default();
601        assert_eq!(reset.mode, AcquisitionMode::TemperatureThenHumidity);
602        assert_eq!(reset.temperature_resolution, TemperatureResolution::Bits14);
603        assert_eq!(reset.humidity_resolution, HumidityResolution::Bits14);
604        assert!(!reset.heater && !reset.software_reset && !reset.battery_low);
605    }
606
607    #[test]
608    fn configuration_fields_sit_at_the_bits_table_4_gives_them() {
609        // Table 4: RST bit 15, HEAT bit 13, MODE bit 12, BTST bit 11, TRES bit 10,
610        // HRES bits 9:8 (00 = 14 bit, 01 = 11 bit, 10 = 8 bit).
611        let all = Configuration {
612            software_reset: true,
613            heater: true,
614            mode: AcquisitionMode::TemperatureThenHumidity,
615            battery_low: true,
616            temperature_resolution: TemperatureResolution::Bits11,
617            humidity_resolution: HumidityResolution::Bits8,
618        };
619        assert_eq!(all.to_register(), 0xBE00);
620        assert_eq!(Configuration::from_register(0xBE00), Ok(all));
621
622        let eleven = Configuration {
623            humidity_resolution: HumidityResolution::Bits11,
624            ..Configuration::default()
625        };
626        assert_eq!(eleven.to_register(), 0x1100);
627        assert_eq!(Configuration::from_register(0x1100), Ok(eleven));
628
629        let single = Configuration {
630            mode: AcquisitionMode::Single,
631            ..Configuration::default()
632        };
633        assert_eq!(single.to_register(), 0x0000);
634
635        // Reserved bits are ignored on the way in and never set on the way out.
636        assert_eq!(
637            Configuration::from_register(0x50FF).map(|c| c.to_register()),
638            Ok(0x1000)
639        );
640    }
641
642    #[test]
643    fn an_undefined_humidity_resolution_code_is_rejected() {
644        // Table 4 defines HRES codes 00, 01, and 10 only.
645        assert_eq!(
646            Configuration::from_register(0x1300),
647            Err(SensorError::Invalid)
648        );
649    }
650
651    #[test]
652    fn conversion_times_follow_the_electrical_characteristics() {
653        // Electrical Characteristics: RHCT 2.50 / 3.85 / 6.50 ms for 8 / 11 / 14 bit,
654        // TEMPCT 3.65 / 6.35 ms for 11 / 14 bit.
655        assert_eq!(HumidityResolution::Bits8.conversion_time_micros(), 2_500);
656        assert_eq!(HumidityResolution::Bits11.conversion_time_micros(), 3_850);
657        assert_eq!(HumidityResolution::Bits14.conversion_time_micros(), 6_500);
658        assert_eq!(
659            TemperatureResolution::Bits11.conversion_time_micros(),
660            3_650
661        );
662        assert_eq!(
663            TemperatureResolution::Bits14.conversion_time_micros(),
664            6_350
665        );
666        assert_eq!(Configuration::default().conversion_time_micros(), 12_850);
667        let single = Configuration {
668            mode: AcquisitionMode::Single,
669            humidity_resolution: HumidityResolution::Bits8,
670            ..Configuration::default()
671        };
672        assert_eq!(single.conversion_time_micros(), 6_350);
673    }
674
675    #[test]
676    fn the_combined_frame_is_temperature_then_humidity_msb_first() {
677        // Figure 14: a read from pointer 0x00 returns the temperature MSB and LSB
678        // followed by the humidity MSB and LSB.
679        let frame = [0x60, 0x00, 0x40, 0x00];
680        let m = Measurement::parse(&frame);
681        assert_eq!(m.temperature, 0x6000);
682        assert_eq!(m.humidity, 0x4000);
683        assert_eq!(m.milli_celsius(), 21_875);
684        assert_eq!(m.milli_percent(), 25_000);
685        assert!((m.celsius() - 21.875).abs() < 1e-5);
686        assert!((m.relative_humidity() - 25.0).abs() < 1e-5);
687        assert_eq!(m.to_bytes(), frame);
688
689        let top = Measurement::parse(&[0xFF, 0xFF, 0xFF, 0xFF]);
690        assert_eq!(
691            (top.milli_celsius(), top.milli_percent()),
692            (124_997, 99_998)
693        );
694
695        let built = Measurement::from_physical(42_500, 50_000);
696        assert_eq!(built.to_bytes(), [0x80, 0x00, 0x80, 0x00]);
697        assert_eq!(Measurement::parse(&built.to_bytes()), built);
698    }
699}