Skip to main content

pamoja_sensors/
opt3001.rs

1//! Texas Instruments OPT3001 ambient light sensor.
2//!
3//! The OPT3001 is a single-chip lux meter with an optical filter matched to the
4//! photopic response of the human eye. It reports illuminance as a 16-bit word holding
5//! a 4-bit exponent and a 12-bit mantissa, across twelve binary-weighted full-scale
6//! ranges the part can select on its own; the low- and high-limit registers that drive
7//! its interrupt pin share that encoding. This module decodes the word, encodes a lux
8//! threshold back into it, and builds and parses the configuration register field by
9//! field, following the datasheet's register tables and its table of worked decoding
10//! examples.
11//!
12//! Illuminance is returned in integer milli-lux, which holds every register value
13//! exactly since the smallest LSB is 0.01 lux; an `f32` lux convenience sits beside it.
14
15/// The I2C address with the ADDR pin tied to GND.
16pub const I2C_ADDRESS_GND: u8 = 0x44;
17/// The I2C address with the ADDR pin tied to VDD.
18pub const I2C_ADDRESS_VDD: u8 = 0x45;
19/// The I2C address with the ADDR pin tied to SDA.
20pub const I2C_ADDRESS_SDA: u8 = 0x46;
21/// The I2C address with the ADDR pin tied to SCL.
22pub const I2C_ADDRESS_SCL: u8 = 0x47;
23
24/// The value the manufacturer ID register returns: `0x5449`, the ASCII bytes "TI".
25pub const MANUFACTURER_ID: u16 = 0x5449;
26/// The value the device ID register returns for an OPT3001.
27pub const DEVICE_ID: u16 = 0x3001;
28
29#[cfg(feature = "embedded-hal")]
30mod driver;
31#[cfg(feature = "embedded-hal")]
32pub mod sim;
33
34#[cfg(feature = "embedded-hal")]
35pub use driver::{Opt3001, POLL_INTERVAL_MILLIS, STATUS_POLLS};
36
37/// The OPT3001 register addresses. Every register is 16 bits, sent most significant
38/// byte first.
39pub mod register {
40    /// Result register: the exponent and mantissa of the latest conversion.
41    pub const RESULT: u8 = 0x00;
42    /// Configuration register: range, conversion time, mode, status flags, and the
43    /// interrupt reporting settings.
44    pub const CONFIGURATION: u8 = 0x01;
45    /// Low-limit register, in the result register's encoding.
46    pub const LOW_LIMIT: u8 = 0x02;
47    /// High-limit register, in the result register's encoding.
48    pub const HIGH_LIMIT: u8 = 0x03;
49    /// Manufacturer ID register; reads [`super::MANUFACTURER_ID`].
50    pub const MANUFACTURER_ID: u8 = 0x7E;
51    /// Device ID register; reads [`super::DEVICE_ID`] for an OPT3001.
52    pub const DEVICE_ID: u8 = 0x7F;
53}
54
55/// The power-on value of the configuration register (0xC810): automatic full-scale
56/// range, 800 ms conversion time, shutdown mode, latched window-style comparison, an
57/// active-low INT pin, the exponent not masked, and a fault count of one.
58pub const CONFIGURATION_RESET: u16 = 0xC810;
59/// The power-on value of the low-limit register: exponent 0, mantissa 0, or 0 lux.
60pub const LOW_LIMIT_RESET: u16 = 0x0000;
61/// The power-on value of the high-limit register: exponent 11, mantissa 0xFFF, the
62/// largest encodable threshold of 83865.60 lux.
63pub const HIGH_LIMIT_RESET: u16 = 0xBFFF;
64/// The low-limit register value that selects end-of-conversion mode, where the INT
65/// pin and the flags report every completed conversion: the exponent's two most
66/// significant bits set to `11b`.
67pub const LOW_LIMIT_END_OF_CONVERSION: u16 = 0xC000;
68
69/// The range number that selects automatic full-scale setting, `1100b`. Range numbers
70/// `0` to `11` select one of the fixed full-scale ranges; `13` to `15` are reserved.
71pub const RANGE_AUTOMATIC: u8 = 0b1100;
72/// The largest range number that selects a fixed full-scale range.
73pub const RANGE_MAX: u8 = 11;
74
75/// Returns the LSB size, in milli-lux, of a result or limit register at an exponent.
76///
77/// The datasheet's `LSB_Size = 0.01 lux * 2^E`, so 10 milli-lux doubled per step.
78///
79/// # Arguments
80///
81/// * `exponent` - the 4-bit exponent, a range number from `0` to `11`.
82///
83/// # Returns
84///
85/// The LSB size in milli-lux, or `None` if `exponent` is above [`RANGE_MAX`].
86pub fn lsb_milli_lux(exponent: u8) -> Option<u32> {
87    (exponent <= RANGE_MAX).then(|| 10u32 << exponent)
88}
89
90/// Returns the full-scale illuminance, in milli-lux, of a range number.
91///
92/// Full scale is the 12-bit mantissa's maximum, 4095, at that range's LSB size, so
93/// 40.95 lux at range `0` up to 83865.60 lux at range `11`.
94///
95/// # Arguments
96///
97/// * `range_number` - the range number, `0` to `11`.
98///
99/// # Returns
100///
101/// The full-scale illuminance in milli-lux, or `None` for the automatic and reserved
102/// range numbers, which have no single full scale.
103pub fn full_scale_milli_lux(range_number: u8) -> Option<u32> {
104    lsb_milli_lux(range_number).map(|lsb| lsb * 0xFFF)
105}
106
107/// Decodes a result or limit register word to milli-lux.
108///
109/// The datasheet's `lux = 0.01 * 2^E[3:0] * R[11:0]`, exact in integer milli-lux for
110/// every possible word.
111///
112/// # Arguments
113///
114/// * `raw` - the 16-bit register word, exponent in bits 15:12 and mantissa in 11:0.
115///
116/// # Returns
117///
118/// The illuminance in milli-lux.
119pub fn milli_lux(raw: u16) -> u32 {
120    let exponent = raw >> 12;
121    let mantissa = u32::from(raw & 0x0FFF);
122    (10u32 << exponent) * mantissa
123}
124
125/// Decodes a result or limit register word to lux.
126///
127/// # Arguments
128///
129/// * `raw` - the 16-bit register word.
130///
131/// # Returns
132///
133/// The illuminance in lux.
134pub fn lux(raw: u16) -> f32 {
135    milli_lux(raw) as f32 / 1000.0
136}
137
138/// Encodes an illuminance into the result and limit registers' exponent-and-mantissa
139/// word, at the smallest exponent that holds the value.
140///
141/// The inverse of [`milli_lux`]. The smallest exponent gives the finest LSB, so the
142/// encoded threshold is the closest one the part can compare against; the mantissa is
143/// truncated to that LSB. An illuminance above the largest full scale, 83865.60 lux,
144/// saturates to [`HIGH_LIMIT_RESET`], the largest encodable word.
145///
146/// # Arguments
147///
148/// * `milli_lux` - the illuminance in milli-lux.
149///
150/// # Returns
151///
152/// The 16-bit register word.
153pub fn raw_from_milli_lux(milli_lux: u32) -> u16 {
154    for exponent in 0..=RANGE_MAX {
155        let mantissa = milli_lux / (10u32 << exponent);
156        if mantissa <= 0x0FFF {
157            return (u16::from(exponent) << 12) | mantissa as u16;
158        }
159    }
160    HIGH_LIMIT_RESET
161}
162
163/// Assembles a register word from the two bytes the part sends, most significant
164/// byte first.
165///
166/// # Arguments
167///
168/// * `bytes` - the two data bytes of a register read.
169///
170/// # Returns
171///
172/// The 16-bit register word.
173pub fn word_from_bytes(bytes: [u8; 2]) -> u16 {
174    u16::from_be_bytes(bytes)
175}
176
177/// Splits a register word into the two bytes written to the part, most significant
178/// byte first.
179///
180/// # Arguments
181///
182/// * `word` - the 16-bit register word.
183///
184/// # Returns
185///
186/// The two data bytes of a register write.
187pub fn word_to_bytes(word: u16) -> [u8; 2] {
188    word.to_be_bytes()
189}
190
191/// The conversion time (configuration bit 11).
192#[derive(Clone, Copy, Debug, PartialEq, Eq)]
193pub enum ConversionTime {
194    /// 100 ms; on ranges `0` to `5` this drops one to three bits of resolution.
195    Ms100,
196    /// 800 ms, the full specified resolution on every range (the default).
197    Ms800,
198}
199
200impl ConversionTime {
201    /// Returns the conversion time in milliseconds.
202    pub fn millis(self) -> u16 {
203        match self {
204            ConversionTime::Ms100 => 100,
205            ConversionTime::Ms800 => 800,
206        }
207    }
208}
209
210/// The mode of conversion operation (configuration bits 10:9).
211#[derive(Clone, Copy, Debug, PartialEq, Eq)]
212pub enum Mode {
213    /// Shutdown; the flags and INT pin keep their last state (the default).
214    Shutdown,
215    /// One conversion, after which the field reads back as shutdown.
216    SingleShot,
217    /// Continuous conversions.
218    Continuous,
219}
220
221impl Mode {
222    /// Returns the 2-bit field code for this mode.
223    pub fn code(self) -> u8 {
224        match self {
225            Mode::Shutdown => 0b00,
226            Mode::SingleShot => 0b01,
227            Mode::Continuous => 0b10,
228        }
229    }
230
231    /// Builds a mode from a 2-bit field code.
232    ///
233    /// Codes `10` and `11` both select continuous conversion and map to
234    /// [`Mode::Continuous`].
235    pub fn from_code(code: u8) -> Mode {
236        match code & 0b11 {
237            0b00 => Mode::Shutdown,
238            0b01 => Mode::SingleShot,
239            _ => Mode::Continuous,
240        }
241    }
242}
243
244/// The interrupt reporting style, the latch field (configuration bit 4).
245#[derive(Clone, Copy, Debug, PartialEq, Eq)]
246pub enum Latch {
247    /// Transparent hysteresis-style comparison: the INT pin and flags follow the
248    /// comparison directly, with no clearing event.
249    TransparentHysteresis,
250    /// Latched window-style comparison: the INT pin and flags hold until the
251    /// configuration register is read (the default).
252    LatchedWindow,
253}
254
255/// The INT pin polarity (configuration bit 3).
256#[derive(Clone, Copy, Debug, PartialEq, Eq)]
257pub enum Polarity {
258    /// INT pulls low on an interrupt event (the default).
259    ActiveLow,
260    /// INT goes high impedance on an interrupt event, to be pulled high.
261    ActiveHigh,
262}
263
264/// The number of consecutive fault events that trigger a report (configuration
265/// bits 1:0).
266#[derive(Clone, Copy, Debug, PartialEq, Eq)]
267pub enum FaultCount {
268    /// One fault (the default).
269    One,
270    /// Two consecutive faults.
271    Two,
272    /// Four consecutive faults.
273    Four,
274    /// Eight consecutive faults.
275    Eight,
276}
277
278impl FaultCount {
279    /// Returns the 2-bit field code for this fault count.
280    pub fn code(self) -> u8 {
281        match self {
282            FaultCount::One => 0b00,
283            FaultCount::Two => 0b01,
284            FaultCount::Four => 0b10,
285            FaultCount::Eight => 0b11,
286        }
287    }
288
289    /// Builds a fault count from a 2-bit field code.
290    pub fn from_code(code: u8) -> FaultCount {
291        match code & 0b11 {
292            0b00 => FaultCount::One,
293            0b01 => FaultCount::Two,
294            0b10 => FaultCount::Four,
295            _ => FaultCount::Eight,
296        }
297    }
298
299    /// Returns the number of consecutive faults this setting requires.
300    pub fn count(self) -> u8 {
301        match self {
302            FaultCount::One => 1,
303            FaultCount::Two => 2,
304            FaultCount::Four => 4,
305            FaultCount::Eight => 8,
306        }
307    }
308}
309
310/// A decoded OPT3001 configuration register.
311///
312/// Build one, set the fields, and turn it into the 16-bit register value with
313/// [`bits`](Configuration::bits); or parse a register read with
314/// [`from_bits`](Configuration::from_bits). [`Configuration::default`] is the power-on
315/// state, [`CONFIGURATION_RESET`]. The four status fields are read-only on the part
316/// and are ignored by [`bits`](Configuration::bits).
317///
318/// # Examples
319///
320/// ```
321/// use pamoja_sensors::opt3001::{Configuration, Mode};
322///
323/// // Convert continuously with the range set automatically, everything else default.
324/// let config = Configuration {
325///     mode: Mode::Continuous,
326///     ..Configuration::default()
327/// };
328/// assert_eq!(config.bits(), 0xCC10);
329/// assert_eq!(Configuration::from_bits(0xCC10), config);
330/// ```
331#[derive(Clone, Copy, Debug, PartialEq, Eq)]
332pub struct Configuration {
333    /// The range number (bits 15:12): `0` to `11` for a fixed full-scale range, or
334    /// [`RANGE_AUTOMATIC`] to let the part choose and report its choice in the
335    /// result's exponent.
336    pub range_number: u8,
337    /// The conversion time.
338    pub conversion_time: ConversionTime,
339    /// The mode of conversion operation.
340    pub mode: Mode,
341    /// Read-only: the last conversion overflowed its full-scale range.
342    pub overflow: bool,
343    /// Read-only: a conversion has completed since the register was last read or
344    /// written with a non-shutdown mode.
345    pub conversion_ready: bool,
346    /// Read-only: the result exceeded the high limit for the fault count.
347    pub flag_high: bool,
348    /// Read-only: the result fell below the low limit for the fault count.
349    pub flag_low: bool,
350    /// The interrupt reporting style.
351    pub latch: Latch,
352    /// The INT pin polarity.
353    pub polarity: Polarity,
354    /// Force the result's exponent to zero on a fixed range, so the mantissa alone
355    /// is the reading at that range's LSB.
356    pub mask_exponent: bool,
357    /// The consecutive faults required to trigger a report.
358    pub fault_count: FaultCount,
359}
360
361impl Default for Configuration {
362    fn default() -> Self {
363        Configuration {
364            range_number: RANGE_AUTOMATIC,
365            conversion_time: ConversionTime::Ms800,
366            mode: Mode::Shutdown,
367            overflow: false,
368            conversion_ready: false,
369            flag_high: false,
370            flag_low: false,
371            latch: Latch::LatchedWindow,
372            polarity: Polarity::ActiveLow,
373            mask_exponent: false,
374            fault_count: FaultCount::One,
375        }
376    }
377}
378
379impl Configuration {
380    /// Assembles the 16-bit configuration register value.
381    ///
382    /// The read-only status bits are written as zero, and the range number is
383    /// truncated to its four bits.
384    ///
385    /// # Returns
386    ///
387    /// The register value to write, most significant byte first.
388    pub fn bits(self) -> u16 {
389        let mut bits = 0u16;
390        bits |= u16::from(self.range_number & 0x0F) << 12;
391        bits |= u16::from(matches!(self.conversion_time, ConversionTime::Ms800)) << 11;
392        bits |= u16::from(self.mode.code()) << 9;
393        bits |= u16::from(matches!(self.latch, Latch::LatchedWindow)) << 4;
394        bits |= u16::from(matches!(self.polarity, Polarity::ActiveHigh)) << 3;
395        bits |= u16::from(self.mask_exponent) << 2;
396        bits |= u16::from(self.fault_count.code());
397        bits
398    }
399
400    /// Parses a 16-bit configuration register value.
401    ///
402    /// # Arguments
403    ///
404    /// * `bits` - the register value, as read from the device.
405    ///
406    /// # Returns
407    ///
408    /// The decoded configuration, status flags included.
409    pub fn from_bits(bits: u16) -> Configuration {
410        Configuration {
411            range_number: (bits >> 12) as u8,
412            conversion_time: if bits & (1 << 11) != 0 {
413                ConversionTime::Ms800
414            } else {
415                ConversionTime::Ms100
416            },
417            mode: Mode::from_code((bits >> 9) as u8),
418            overflow: bits & (1 << 8) != 0,
419            conversion_ready: bits & (1 << 7) != 0,
420            flag_high: bits & (1 << 6) != 0,
421            flag_low: bits & (1 << 5) != 0,
422            latch: if bits & (1 << 4) != 0 {
423                Latch::LatchedWindow
424            } else {
425                Latch::TransparentHysteresis
426            },
427            polarity: if bits & (1 << 3) != 0 {
428                Polarity::ActiveHigh
429            } else {
430                Polarity::ActiveLow
431            },
432            mask_exponent: bits & (1 << 2) != 0,
433            fault_count: FaultCount::from_code(bits as u8),
434        }
435    }
436
437    /// Returns whether the range number selects automatic full-scale setting.
438    pub fn is_automatic_range(&self) -> bool {
439        self.range_number == RANGE_AUTOMATIC
440    }
441}
442
443/// One result register word, with the conversion to illuminance.
444///
445/// # Examples
446///
447/// ```
448/// use pamoja_sensors::opt3001::Reading;
449///
450/// // Exponent 0 over mantissa 3200: 0.01 lux per count, 32 lux.
451/// let reading = Reading::new(0x0C80);
452/// assert_eq!(reading.milli_lux(), 32_000);
453/// assert_eq!(u16::from_be_bytes(reading.to_bytes()), 0x0C80, "high byte first");
454/// ```
455#[derive(Clone, Copy, Debug, PartialEq, Eq)]
456pub struct Reading {
457    raw: u16,
458}
459
460impl Reading {
461    /// Wraps a result register word.
462    ///
463    /// # Arguments
464    ///
465    /// * `raw` - the word: a four-bit exponent over a twelve-bit mantissa.
466    ///
467    /// # Returns
468    ///
469    /// The reading.
470    pub fn new(raw: u16) -> Reading {
471        Reading { raw }
472    }
473
474    /// Returns the register word.
475    pub fn raw(&self) -> u16 {
476        self.raw
477    }
478
479    /// Returns the illuminance in millilux, exact in integer arithmetic.
480    pub fn milli_lux(&self) -> u32 {
481        milli_lux(self.raw)
482    }
483
484    /// Returns the illuminance in lux.
485    pub fn lux(&self) -> f32 {
486        lux(self.raw)
487    }
488
489    /// Returns the register bytes, most significant first, as the part sends them.
490    pub fn to_bytes(&self) -> [u8; 2] {
491        word_to_bytes(self.raw)
492    }
493}
494
495#[cfg(test)]
496mod driver_support_tests {
497    use super::*;
498
499    #[test]
500    fn a_reading_converts_its_word_like_the_free_functions() {
501        let raw = raw_from_milli_lux(1_000_000);
502        let reading = Reading::new(raw);
503        assert_eq!(reading.raw(), raw);
504        assert_eq!(reading.milli_lux(), milli_lux(raw));
505        assert!((reading.lux() - 1000.0).abs() < 1.0);
506        assert_eq!(reading.to_bytes(), word_to_bytes(raw));
507    }
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513
514    #[test]
515    fn addresses_follow_the_addr_pin_table() {
516        // Table 1: 1000100b with ADDR to GND, then VDD, SDA, SCL.
517        assert_eq!(I2C_ADDRESS_GND, 0b100_0100);
518        assert_eq!(I2C_ADDRESS_VDD, 0b100_0101);
519        assert_eq!(I2C_ADDRESS_SDA, 0b100_0110);
520        assert_eq!(I2C_ADDRESS_SCL, 0b100_0111);
521    }
522
523    #[test]
524    fn register_map_and_ids_match_the_datasheet() {
525        // Table 6 register map; Tables 14 and 15 for the ID values.
526        assert_eq!(register::RESULT, 0x00);
527        assert_eq!(register::CONFIGURATION, 0x01);
528        assert_eq!(register::LOW_LIMIT, 0x02);
529        assert_eq!(register::HIGH_LIMIT, 0x03);
530        assert_eq!(register::MANUFACTURER_ID, 0x7E);
531        assert_eq!(register::DEVICE_ID, 0x7F);
532        assert_eq!(MANUFACTURER_ID, 0x5449);
533        assert_eq!(MANUFACTURER_ID.to_be_bytes(), *b"TI");
534        assert_eq!(DEVICE_ID, 0x3001);
535    }
536
537    #[test]
538    fn result_register_decodes_per_the_datasheet_examples() {
539        // Table 9, every row: register word, LSB weight, and resulting lux.
540        let rows: [(u16, u32, u32); 10] = [
541            (0x0001, 10, 10),
542            (0x0FFF, 10, 40_950),
543            (0x3456, 80, 88_800),
544            (0x789A, 1_280, 2_818_560),
545            (0x8800, 2_560, 5_242_880),
546            (0x9400, 5_120, 5_242_880),
547            (0xA200, 10_240, 5_242_880),
548            (0xB100, 20_480, 5_242_880),
549            (0xB001, 20_480, 20_480),
550            (0xBFFF, 20_480, 83_865_600),
551        ];
552        for (raw, lsb, expected) in rows {
553            assert_eq!(
554                lsb_milli_lux((raw >> 12) as u8),
555                Some(lsb),
556                "lsb {raw:#06x}"
557            );
558            assert_eq!(milli_lux(raw), expected, "milli-lux {raw:#06x}");
559            assert!(
560                (lux(raw) - expected as f32 / 1000.0).abs() < 0.001,
561                "lux {raw:#06x}"
562            );
563        }
564    }
565
566    #[test]
567    fn full_scale_table_matches_each_range_number() {
568        // Table 8: full-scale range and LSB size for exponents 0000b to 1011b.
569        let rows: [(u8, u32, u32); 12] = [
570            (0, 40_950, 10),
571            (1, 81_900, 20),
572            (2, 163_800, 40),
573            (3, 327_600, 80),
574            (4, 655_200, 160),
575            (5, 1_310_400, 320),
576            (6, 2_620_800, 640),
577            (7, 5_241_600, 1_280),
578            (8, 10_483_200, 2_560),
579            (9, 20_966_400, 5_120),
580            (10, 41_932_800, 10_240),
581            (11, 83_865_600, 20_480),
582        ];
583        for (range, full_scale, lsb) in rows {
584            assert_eq!(
585                full_scale_milli_lux(range),
586                Some(full_scale),
587                "range {range}"
588            );
589            assert_eq!(lsb_milli_lux(range), Some(lsb), "range {range}");
590            assert_eq!(milli_lux((u16::from(range) << 12) | 0x0FFF), full_scale);
591        }
592        // The automatic and reserved range numbers have no full scale of their own.
593        for range in RANGE_AUTOMATIC..=0x0F {
594            assert_eq!(full_scale_milli_lux(range), None);
595            assert_eq!(lsb_milli_lux(range), None);
596        }
597        // Electrical Characteristics: 0.01 lux resolution, 83865.6 lux full scale.
598        assert_eq!(lsb_milli_lux(0), Some(10));
599        assert_eq!(full_scale_milli_lux(RANGE_MAX), Some(83_865_600));
600    }
601
602    #[test]
603    fn encoder_picks_the_smallest_exponent_that_holds_the_value() {
604        // Table 9 lists four words for 5242.88 lux; 08h/800h is the smallest exponent.
605        assert_eq!(raw_from_milli_lux(5_242_880), 0x8800);
606        // 88.80 lux fits a 0.04 lux LSB, one step finer than Table 9's 03h/456h.
607        assert_eq!(raw_from_milli_lux(88_800), 0x28AC);
608        assert_eq!(milli_lux(0x28AC), 88_800);
609        assert_eq!(raw_from_milli_lux(10), 0x0001);
610        assert_eq!(raw_from_milli_lux(40_950), 0x0FFF);
611        assert_eq!(raw_from_milli_lux(40_960), 0x1800);
612        assert_eq!(raw_from_milli_lux(0), 0x0000);
613        // The largest encodable threshold, and saturation above it.
614        assert_eq!(raw_from_milli_lux(83_865_600), HIGH_LIMIT_RESET);
615        assert_eq!(raw_from_milli_lux(83_865_601), HIGH_LIMIT_RESET);
616        assert_eq!(raw_from_milli_lux(u32::MAX), HIGH_LIMIT_RESET);
617        // A value between two LSB steps truncates to the step below.
618        assert_eq!(raw_from_milli_lux(15), 0x0001);
619    }
620
621    #[test]
622    fn every_result_word_survives_a_round_trip_through_the_encoder() {
623        for raw in 0..=HIGH_LIMIT_RESET {
624            let value = milli_lux(raw);
625            let encoded = raw_from_milli_lux(value);
626            assert_eq!(milli_lux(encoded), value, "word {raw:#06x}");
627            assert!(
628                encoded >> 12 <= raw >> 12,
629                "word {raw:#06x} re-encoded {encoded:#06x}"
630            );
631        }
632    }
633
634    #[test]
635    fn integer_decode_tracks_the_floating_point_reference() {
636        // Equation 3, transcribed: lux = 0.01 * 2^E[3:0] * R[11:0].
637        for raw in 0..=HIGH_LIMIT_RESET {
638            let exponent = f64::from(raw >> 12);
639            let mantissa = f64::from(raw & 0x0FFF);
640            let reference = 0.01 * exponent.exp2() * mantissa;
641            let integer = f64::from(milli_lux(raw)) / 1000.0;
642            assert!(
643                (integer - reference).abs() < 1e-6,
644                "word {raw:#06x}: {integer} vs {reference}"
645            );
646        }
647    }
648
649    #[test]
650    fn register_words_travel_most_significant_byte_first() {
651        // Figures 20 and 21: data MSByte then data LSByte.
652        assert_eq!(word_from_bytes([0x34, 0x56]), 0x3456);
653        assert_eq!(word_to_bytes(0x3456), [0x34, 0x56]);
654        assert_eq!(milli_lux(word_from_bytes([0x78, 0x9A])), 2_818_560);
655        assert_eq!(word_from_bytes(word_to_bytes(0xC810)), 0xC810);
656    }
657
658    #[test]
659    fn default_configuration_is_the_datasheet_reset_value() {
660        // Section 7.6.1.1.2: configuration register reset C810h, Table 10 per field.
661        assert_eq!(Configuration::default().bits(), CONFIGURATION_RESET);
662        assert_eq!(
663            Configuration::from_bits(CONFIGURATION_RESET),
664            Configuration::default()
665        );
666        let reset = Configuration::default();
667        assert_eq!(reset.range_number, 0b1100);
668        assert!(reset.is_automatic_range());
669        assert_eq!(reset.conversion_time, ConversionTime::Ms800);
670        assert_eq!(reset.mode, Mode::Shutdown);
671        assert_eq!(reset.latch, Latch::LatchedWindow);
672        assert_eq!(reset.polarity, Polarity::ActiveLow);
673        assert!(!reset.mask_exponent);
674        assert_eq!(reset.fault_count, FaultCount::One);
675    }
676
677    #[test]
678    fn limit_registers_reset_per_the_datasheet() {
679        // Table 11: LE = 0h, TL = 000h. Table 13: HE = Bh, TH = FFFh.
680        assert_eq!(LOW_LIMIT_RESET, 0x0000);
681        assert_eq!(milli_lux(LOW_LIMIT_RESET), 0);
682        assert_eq!(HIGH_LIMIT_RESET, 0xBFFF);
683        assert_eq!(milli_lux(HIGH_LIMIT_RESET), 83_865_600);
684        // Section 7.4.2.3: end-of-conversion mode is LE[3:2] = 11b.
685        assert_eq!(LOW_LIMIT_END_OF_CONVERSION >> 14, 0b11);
686    }
687
688    #[test]
689    fn configuration_field_codes_match_the_datasheet() {
690        // Table 10: CT 1 = 800 ms; M 01 = single-shot, 10 and 11 = continuous;
691        // FC 00, 01, 10, 11 = one, two, four, eight faults.
692        assert_eq!(ConversionTime::Ms100.millis(), 100);
693        assert_eq!(ConversionTime::Ms800.millis(), 800);
694        assert_eq!(Mode::SingleShot.code(), 0b01);
695        assert_eq!(Mode::from_code(0b10), Mode::Continuous);
696        assert_eq!(Mode::from_code(0b11), Mode::Continuous);
697        assert_eq!(FaultCount::Eight.code(), 0b11);
698        assert_eq!(FaultCount::from_code(0b10).count(), 4);
699        // Continuous conversion on the automatic range is the reset word with M = 10b.
700        let continuous = Configuration {
701            mode: Mode::Continuous,
702            ..Configuration::default()
703        };
704        assert_eq!(continuous.bits(), 0xCC10);
705        // A read with OVF, CRF, and FH set: the flags decode and are not written back.
706        let status = Configuration::from_bits(0xCDD0);
707        assert!(status.overflow);
708        assert!(status.conversion_ready);
709        assert!(status.flag_high);
710        assert!(!status.flag_low);
711        assert_eq!(status.bits(), 0xCC10);
712    }
713
714    #[test]
715    fn configuration_round_trips_through_bits() {
716        let config = Configuration {
717            range_number: 6,
718            conversion_time: ConversionTime::Ms100,
719            mode: Mode::SingleShot,
720            latch: Latch::TransparentHysteresis,
721            polarity: Polarity::ActiveHigh,
722            mask_exponent: true,
723            fault_count: FaultCount::Four,
724            ..Configuration::default()
725        };
726        assert_eq!(config.bits(), 0x620E);
727        assert_eq!(Configuration::from_bits(config.bits()), config);
728        assert!(!config.is_automatic_range());
729        assert_eq!(full_scale_milli_lux(config.range_number), Some(2_620_800));
730    }
731}