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