Skip to main content

ph_veml7700_als/
config.rs

1//! Configuration-register value types and codec.
2
3/// Driver gain-codec reaction to `S-14`.
4#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5#[cfg_attr(feature = "defmt", derive(defmt::Format))]
6pub enum Gain {
7    /// Gain ×1.
8    X1,
9    /// Gain ×2.
10    X2,
11    /// Gain ×1/8.
12    Div8,
13    /// Gain ×1/4.
14    Div4,
15}
16
17impl Gain {
18    pub(crate) const fn bits(self) -> u16 {
19        match self {
20            Self::X1 => 0b00 << 11,
21            Self::X2 => 0b01 << 11,
22            Self::Div8 => 0b10 << 11,
23            Self::Div4 => 0b11 << 11,
24        }
25    }
26
27    pub(crate) const fn from_bits(bits: u16) -> Self {
28        match (bits >> 11) & 0b11 {
29            0b00 => Self::X1,
30            0b01 => Self::X2,
31            0b10 => Self::Div8,
32            _ => Self::Div4,
33        }
34    }
35}
36
37/// Driver integration-codec reaction to `S-15`.
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39#[cfg_attr(feature = "defmt", derive(defmt::Format))]
40pub enum IntegrationTime {
41    /// 25 ms.
42    Ms25,
43    /// 50 ms.
44    Ms50,
45    /// 100 ms.
46    Ms100,
47    /// 200 ms.
48    Ms200,
49    /// 400 ms.
50    Ms400,
51    /// 800 ms.
52    Ms800,
53}
54
55impl IntegrationTime {
56    /// Return the nominal integration time in milliseconds.
57    pub const fn milliseconds(self) -> u32 {
58        match self {
59            Self::Ms25 => 25,
60            Self::Ms50 => 50,
61            Self::Ms100 => 100,
62            Self::Ms200 => 200,
63            Self::Ms400 => 400,
64            Self::Ms800 => 800,
65        }
66    }
67
68    pub(crate) const fn bits(self) -> u16 {
69        match self {
70            Self::Ms25 => 0b1100 << 6,
71            Self::Ms50 => 0b1000 << 6,
72            Self::Ms100 => 0b0000 << 6,
73            Self::Ms200 => 0b0001 << 6,
74            Self::Ms400 => 0b0010 << 6,
75            Self::Ms800 => 0b0011 << 6,
76        }
77    }
78
79    pub(crate) const fn from_bits(bits: u16) -> Result<Self, ConfigDecodeError> {
80        match (bits >> 6) & 0b1111 {
81            0b1100 => Ok(Self::Ms25),
82            0b1000 => Ok(Self::Ms50),
83            0b0000 => Ok(Self::Ms100),
84            0b0001 => Ok(Self::Ms200),
85            0b0010 => Ok(Self::Ms400),
86            0b0011 => Ok(Self::Ms800),
87            observed => Err(ConfigDecodeError::ReservedIntegrationTime { observed }),
88        }
89    }
90}
91
92/// Threshold persistence protect number (`ALS_PERS`).
93///
94/// # What this selects, and what it does not promise
95///
96/// The driver programs the four persistence encodings recorded by `S-16`.
97/// `S-39`, `S-49`, and `S-50` leave the assertion rule incomplete.
98///
99/// This driver therefore promises nothing about *when*
100/// [`read_threshold_status`](crate::Veml7700::read_threshold_status) will report
101/// a flag for any persistence value.
102///
103/// Poll the status. Do not compute an expected assertion time from the count and
104/// refresh cadence. The driver stays silent rather than supplying the two
105/// missing propositions.
106#[derive(Clone, Copy, Debug, PartialEq, Eq)]
107#[cfg_attr(feature = "defmt", derive(defmt::Format))]
108pub enum Persistence {
109    /// Protect number 1 (encoded count 1).
110    One,
111    /// Protect number 2.
112    Two,
113    /// Protect number 4.
114    Four,
115    /// Protect number 8.
116    Eight,
117}
118
119impl Persistence {
120    /// Return the programmed protect number.
121    ///
122    /// This is the encoded field value, not an input to any timing calculation
123    /// the driver performs — nothing in this driver reads it.
124    pub const fn count(self) -> u8 {
125        match self {
126            Self::One => 1,
127            Self::Two => 2,
128            Self::Four => 4,
129            Self::Eight => 8,
130        }
131    }
132
133    pub(crate) const fn bits(self) -> u16 {
134        match self {
135            Self::One => 0b00 << 4,
136            Self::Two => 0b01 << 4,
137            Self::Four => 0b10 << 4,
138            Self::Eight => 0b11 << 4,
139        }
140    }
141
142    pub(crate) const fn from_bits(bits: u16) -> Self {
143        match (bits >> 4) & 0b11 {
144            0b00 => Self::One,
145            0b01 => Self::Two,
146            0b10 => Self::Four,
147            _ => Self::Eight,
148        }
149    }
150}
151
152/// Driver power-state codec reaction to `S-17`.
153#[derive(Clone, Copy, Debug, PartialEq, Eq)]
154#[cfg_attr(feature = "defmt", derive(defmt::Format))]
155pub enum PowerState {
156    /// Conversions are enabled.
157    Active,
158    /// Conversion circuitry is shut down; the driver treats data retention as
159    /// the separate consequence of `S-25`.
160    Shutdown,
161}
162
163impl PowerState {
164    pub(crate) const fn bit(self) -> u16 {
165        match self {
166            Self::Active => 0,
167            Self::Shutdown => 1,
168        }
169    }
170
171    pub(crate) const fn from_word(word: u16) -> Self {
172        if word & 1 == 0 {
173            Self::Active
174        } else {
175            Self::Shutdown
176        }
177    }
178}
179
180/// Driver monitor-enable codec reaction to `S-17`.
181#[derive(Clone, Copy, Debug, PartialEq, Eq)]
182#[cfg_attr(feature = "defmt", derive(defmt::Format))]
183pub enum ThresholdMonitorState {
184    /// Threshold monitoring is disabled.
185    Disabled,
186    /// Threshold monitoring is enabled; this driver exposes status only by
187    /// polling (`S-41`).
188    Enabled,
189}
190
191impl ThresholdMonitorState {
192    pub(crate) const fn bit(self) -> u16 {
193        match self {
194            Self::Disabled => 0,
195            Self::Enabled => 1 << 1,
196        }
197    }
198
199    pub(crate) const fn from_word(word: u16) -> Self {
200        if word & (1 << 1) == 0 {
201            Self::Disabled
202        } else {
203            Self::Enabled
204        }
205    }
206}
207
208/// Gain and integration-time pair defining one measurement domain.
209#[derive(Clone, Copy, Debug, PartialEq, Eq)]
210#[cfg_attr(feature = "defmt", derive(defmt::Format))]
211pub struct MeasurementConfig {
212    gain: Gain,
213    integration_time: IntegrationTime,
214}
215
216impl MeasurementConfig {
217    /// Construct a measurement configuration.
218    pub const fn new(gain: Gain, integration_time: IntegrationTime) -> Self {
219        Self {
220            gain,
221            integration_time,
222        }
223    }
224
225    /// Driver decoding of the reset-domain measurement fields (`S-12`, `S-14`,
226    /// `S-15`), not a recommendation.
227    pub const fn silicon_reset_default() -> Self {
228        Self::new(Gain::X1, IntegrationTime::Ms100)
229    }
230
231    /// Driver starting policy for unknown brightness (`S-28`, `S-34`).
232    pub const fn maximum_range_start() -> Self {
233        Self::new(Gain::Div8, IntegrationTime::Ms25)
234    }
235
236    /// Return the selected gain.
237    pub const fn gain(self) -> Gain {
238        self.gain
239    }
240
241    /// Return the selected integration time.
242    pub const fn integration_time(self) -> IntegrationTime {
243        self.integration_time
244    }
245
246    pub(crate) const fn bits(self) -> u16 {
247        self.gain.bits() | self.integration_time.bits()
248    }
249}
250
251impl Default for MeasurementConfig {
252    /// This crate's software policy, **not** the device's reset state.
253    ///
254    /// Returns [`maximum_range_start`](Self::maximum_range_start), the driver's
255    /// `S-28`/`S-34` starting policy. A maximum raw code remains ambiguous
256    /// (`S-51`, `S-52`), so
257    /// [`AlsCounts::is_max_code`](crate::AlsCounts::is_max_code) must be
258    /// checked regardless of configuration. The device's own reset domain is
259    /// [`silicon_reset_default`](Self::silicon_reset_default) and is different —
260    /// a caller who wants what the hardware powers up in must ask for it by
261    /// name.
262    ///
263    /// The two are deliberately distinct. Conflating them is how a caller ends
264    /// up believing `Default` describes the device.
265    fn default() -> Self {
266        Self::maximum_range_start()
267    }
268}
269
270/// Decoded configuration-register snapshot.
271#[derive(Clone, Copy, Debug, PartialEq, Eq)]
272#[cfg_attr(feature = "defmt", derive(defmt::Format))]
273pub struct ConfigurationSnapshot {
274    /// Observed measurement domain.
275    pub measurement: MeasurementConfig,
276    /// Observed threshold persistence.
277    pub persistence: Persistence,
278    /// Observed threshold-monitor enable state.
279    pub threshold_monitor: ThresholdMonitorState,
280    /// Observed sensor power state.
281    pub power_state: PowerState,
282}
283
284impl ConfigurationSnapshot {
285    /// Return the documented reset value decoded as a snapshot.
286    pub const fn silicon_reset_default() -> Self {
287        Self {
288            measurement: MeasurementConfig::silicon_reset_default(),
289            persistence: Persistence::One,
290            threshold_monitor: ThresholdMonitorState::Disabled,
291            power_state: PowerState::Shutdown,
292        }
293    }
294
295    pub(crate) const fn encode(self) -> u16 {
296        self.measurement.bits()
297            | self.persistence.bits()
298            | self.threshold_monitor.bit()
299            | self.power_state.bit()
300    }
301
302    pub(crate) const fn with_measurement(mut self, measurement: MeasurementConfig) -> Self {
303        self.measurement = measurement;
304        self
305    }
306
307    pub(crate) const fn with_persistence(mut self, persistence: Persistence) -> Self {
308        self.persistence = persistence;
309        self
310    }
311
312    pub(crate) const fn with_monitor(mut self, state: ThresholdMonitorState) -> Self {
313        self.threshold_monitor = state;
314        self
315    }
316
317    pub(crate) const fn with_power_state(mut self, state: PowerState) -> Self {
318        self.power_state = state;
319        self
320    }
321}
322
323/// Failure decoding a configuration register.
324#[derive(Clone, Copy, Debug, PartialEq, Eq)]
325#[cfg_attr(feature = "defmt", derive(defmt::Format))]
326#[non_exhaustive]
327pub enum ConfigDecodeError {
328    /// A reserved bit was observed set.
329    ReservedBits {
330        /// Reserved bits that were observed set.
331        observed: u16,
332    },
333    /// An undocumented integration-time encoding was observed.
334    ReservedIntegrationTime {
335        /// Undocumented integration-time field value.
336        observed: u16,
337    },
338}
339
340pub(crate) struct ConfigWord(u16);
341
342impl ConfigWord {
343    pub(crate) const fn from_raw(raw: u16) -> Self {
344        Self(raw)
345    }
346
347    pub(crate) const fn from_snapshot(snapshot: ConfigurationSnapshot) -> Self {
348        Self(snapshot.encode())
349    }
350
351    pub(crate) const fn raw(self) -> u16 {
352        self.0
353    }
354
355    pub(crate) fn decode(self) -> Result<ConfigurationSnapshot, ConfigDecodeError> {
356        // Driver reserved-field reaction to `S-13` and `S-18`.
357        let reserved = self.0 & 0b1110_0100_0000_1100;
358        if reserved != 0 {
359            return Err(ConfigDecodeError::ReservedBits { observed: reserved });
360        }
361        Ok(ConfigurationSnapshot {
362            measurement: MeasurementConfig::new(
363                Gain::from_bits(self.0),
364                IntegrationTime::from_bits(self.0)?,
365            ),
366            persistence: Persistence::from_bits(self.0),
367            threshold_monitor: ThresholdMonitorState::from_word(self.0),
368            power_state: PowerState::from_word(self.0),
369        })
370    }
371}
372
373impl core::fmt::Display for ConfigDecodeError {
374    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
375        match self {
376            Self::ReservedBits { observed } => {
377                write!(f, "reserved configuration bits were set: {observed:#06x}")
378            }
379            Self::ReservedIntegrationTime { observed } => {
380                write!(f, "undocumented integration-time encoding {observed:#06b}")
381            }
382        }
383    }
384}
385
386impl core::error::Error for ConfigDecodeError {}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391
392    #[test]
393    fn reset_word_decodes() {
394        assert_eq!(
395            ConfigWord(0x0001).decode(),
396            Ok(ConfigurationSnapshot::silicon_reset_default())
397        );
398    }
399
400    /// Literal words from `docs/HARDWARE_CONTRACT.md` `S-12` / `S-14` / `S-15`, not round trips.
401    ///
402    /// The exhaustive round-trip test below proves the encoder and decoder agree
403    /// with each other. It cannot detect them agreeing on the *wrong* bit
404    /// position: shift both fields by one and every round trip still passes.
405    /// These vectors are the only tests here that would fail.
406    ///
407    /// Each field is placed alone so a failure names the field rather than the
408    /// word. The encodings deliberately include the two cases where bit order
409    /// and magnitude order disagree — gain `10` is ×1/8 while `11` is ×1/4, and
410    /// integration `1100` is the *shortest* time — because a plausible-looking
411    /// table sorted by magnitude would encode both backwards.
412    #[test]
413    fn configuration_fields_occupy_the_contract_bit_positions() {
414        let base = ConfigurationSnapshot {
415            measurement: MeasurementConfig::new(Gain::X1, IntegrationTime::Ms100),
416            persistence: Persistence::One,
417            threshold_monitor: ThresholdMonitorState::Disabled,
418            power_state: PowerState::Active,
419        };
420        // Every field at its zero encoding is the all-zero word.
421        assert_eq!(base.encode(), 0x0000);
422
423        // Gain, bits 12:11.
424        for (gain, bits) in [
425            (Gain::X1, 0b00_u16),
426            (Gain::X2, 0b01),
427            (Gain::Div8, 0b10),
428            (Gain::Div4, 0b11),
429        ] {
430            let word = ConfigurationSnapshot {
431                measurement: MeasurementConfig::new(gain, IntegrationTime::Ms100),
432                ..base
433            }
434            .encode();
435            assert_eq!(word, bits << 11, "gain {gain:?} must occupy bits 12:11");
436        }
437
438        // Integration time, bits 9:6.
439        for (integration_time, bits) in [
440            (IntegrationTime::Ms25, 0b1100_u16),
441            (IntegrationTime::Ms50, 0b1000),
442            (IntegrationTime::Ms100, 0b0000),
443            (IntegrationTime::Ms200, 0b0001),
444            (IntegrationTime::Ms400, 0b0010),
445            (IntegrationTime::Ms800, 0b0011),
446        ] {
447            let word = ConfigurationSnapshot {
448                measurement: MeasurementConfig::new(Gain::X1, integration_time),
449                ..base
450            }
451            .encode();
452            assert_eq!(
453                word,
454                bits << 6,
455                "integration time {integration_time:?} must occupy bits 9:6"
456            );
457        }
458
459        // Persistence, bits 5:4.
460        for (persistence, bits) in [
461            (Persistence::One, 0b00_u16),
462            (Persistence::Two, 0b01),
463            (Persistence::Four, 0b10),
464            (Persistence::Eight, 0b11),
465        ] {
466            let word = ConfigurationSnapshot {
467                persistence,
468                ..base
469            }
470            .encode();
471            assert_eq!(
472                word,
473                bits << 4,
474                "persistence {persistence:?} must occupy bits 5:4"
475            );
476        }
477
478        // Monitor enable is bit 1; shutdown is bit 0.
479        assert_eq!(
480            ConfigurationSnapshot {
481                threshold_monitor: ThresholdMonitorState::Enabled,
482                ..base
483            }
484            .encode(),
485            1 << 1
486        );
487        assert_eq!(
488            ConfigurationSnapshot {
489                power_state: PowerState::Shutdown,
490                ..base
491            }
492            .encode(),
493            1 << 0
494        );
495
496        // One word carrying every field at once, decoded back. ×1/4 gain,
497        // 800 ms, persistence 8, monitor enabled, shut down:
498        // 0b0001_1000_1111_0011.
499        let combined = (0b11 << 11) | (0b0011 << 6) | (0b11 << 4) | (1 << 1) | 1;
500        assert_eq!(combined, 0x18F3);
501        assert_eq!(
502            ConfigWord(combined).decode(),
503            Ok(ConfigurationSnapshot {
504                measurement: MeasurementConfig::new(Gain::Div4, IntegrationTime::Ms800),
505                persistence: Persistence::Eight,
506                threshold_monitor: ThresholdMonitorState::Enabled,
507                power_state: PowerState::Shutdown,
508            })
509        );
510    }
511
512    #[test]
513    fn every_documented_configuration_field_combination_round_trips() {
514        let gains = [Gain::X1, Gain::X2, Gain::Div8, Gain::Div4];
515        let times = [
516            IntegrationTime::Ms25,
517            IntegrationTime::Ms50,
518            IntegrationTime::Ms100,
519            IntegrationTime::Ms200,
520            IntegrationTime::Ms400,
521            IntegrationTime::Ms800,
522        ];
523        let persistence_values = [
524            Persistence::One,
525            Persistence::Two,
526            Persistence::Four,
527            Persistence::Eight,
528        ];
529        let monitor_states = [
530            ThresholdMonitorState::Disabled,
531            ThresholdMonitorState::Enabled,
532        ];
533        let power_states = [PowerState::Active, PowerState::Shutdown];
534
535        for gain in gains {
536            for integration_time in times {
537                for persistence in persistence_values {
538                    for threshold_monitor in monitor_states {
539                        for power_state in power_states {
540                            let expected = ConfigurationSnapshot {
541                                measurement: MeasurementConfig::new(gain, integration_time),
542                                persistence,
543                                threshold_monitor,
544                                power_state,
545                            };
546                            assert_eq!(ConfigWord(expected.encode()).decode(), Ok(expected));
547                        }
548                    }
549                }
550            }
551        }
552    }
553
554    #[test]
555    fn every_reserved_configuration_bit_is_rejected() {
556        for bit in [2_u32, 3, 10, 13, 14, 15] {
557            let raw = 1_u16 << bit;
558            assert_eq!(
559                ConfigWord(raw).decode(),
560                Err(ConfigDecodeError::ReservedBits { observed: raw })
561            );
562        }
563    }
564
565    #[test]
566    fn every_reserved_integration_encoding_is_rejected() {
567        for observed in [4_u16, 5, 6, 7, 9, 10, 11, 13, 14, 15] {
568            assert_eq!(
569                ConfigWord(observed << 6).decode(),
570                Err(ConfigDecodeError::ReservedIntegrationTime { observed })
571            );
572        }
573    }
574
575    #[test]
576    fn public_configuration_accessors_match_the_selected_domain() {
577        let config = MeasurementConfig::new(Gain::X2, IntegrationTime::Ms800);
578        assert_eq!(config.gain(), Gain::X2);
579        assert_eq!(config.integration_time(), IntegrationTime::Ms800);
580        assert_eq!(IntegrationTime::Ms25.milliseconds(), 25);
581        assert_eq!(IntegrationTime::Ms800.milliseconds(), 800);
582        assert_eq!(Persistence::One.count(), 1);
583        assert_eq!(Persistence::Eight.count(), 8);
584    }
585}