1use crate::SensorError;
15
16pub const I2C_ADDRESS: u8 = 0x40;
18pub const MANUFACTURER_ID: u16 = 0x5449;
20pub const DEVICE_ID: u16 = 0x1050;
22pub const CONFIGURATION_RESET: u16 = 0x1000;
25
26pub mod register {
28 pub const TEMPERATURE: u8 = 0x00;
30 pub const HUMIDITY: u8 = 0x01;
32 pub const CONFIGURATION: u8 = 0x02;
34 pub const SERIAL_ID_HIGH: u8 = 0xFB;
36 pub const SERIAL_ID_MID: u8 = 0xFC;
38 pub const SERIAL_ID_LOW: u8 = 0xFD;
40 pub const MANUFACTURER_ID: u8 = 0xFE;
42 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
50pub 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
67pub fn celsius(raw: u16) -> f32 {
79 raw as f32 / 65_536.0 * 165.0 - 40.0
80}
81
82pub fn milli_percent(raw: u16) -> u32 {
95 ((raw as u64 * HUMIDITY_SPAN_MILLI + (1 << 15)) >> 16) as u32
96}
97
98pub fn relative_humidity(raw: u16) -> f32 {
110 raw as f32 / 65_536.0 * 100.0
111}
112
113pub 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
133pub 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
150pub 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
168pub 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
189pub enum AcquisitionMode {
190 Single,
192 TemperatureThenHumidity,
194}
195
196#[derive(Clone, Copy, Debug, PartialEq, Eq)]
198pub enum TemperatureResolution {
199 Bits14,
201 Bits11,
203}
204
205impl TemperatureResolution {
206 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
221pub enum HumidityResolution {
222 Bits14,
224 Bits11,
226 Bits8,
228}
229
230impl HumidityResolution {
231 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
249pub struct Configuration {
250 pub software_reset: bool,
252 pub heater: bool,
254 pub mode: AcquisitionMode,
256 pub battery_low: bool,
259 pub temperature_resolution: TemperatureResolution,
261 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 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 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 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
370pub struct Measurement {
371 pub temperature: u16,
373 pub humidity: u16,
375}
376
377impl Measurement {
378 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 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 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 pub fn milli_celsius(&self) -> i32 {
431 milli_celsius(self.temperature)
432 }
433
434 pub fn celsius(&self) -> f32 {
440 celsius(self.temperature)
441 }
442
443 pub fn milli_percent(&self) -> u32 {
449 milli_percent(self.humidity)
450 }
451
452 pub fn relative_humidity(&self) -> f32 {
458 relative_humidity(self.humidity)
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465
466 fn reference_celsius(raw: u16) -> f64 {
468 raw as f64 / 65_536.0 * 165.0 - 40.0
469 }
470
471 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 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 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 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 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 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 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 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 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 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 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 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 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}