Skip to main content

ph_veml7700_als/
error.rs

1//! Public error taxonomy.
2
3use crate::config::{ConfigDecodeError, IntegrationTime};
4use crate::measurement::MeasurementCapture;
5use crate::power::PowerSavingDecodeError;
6use crate::threshold::ThresholdStatusDecodeError;
7
8/// High-level operation associated with a bus failure.
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10#[cfg_attr(feature = "defmt", derive(defmt::Format))]
11#[non_exhaustive]
12pub enum Operation {
13    /// Read-only inspection.
14    Inspect,
15    /// Snapshot measurement.
16    Snapshot,
17    /// Controlled one-shot measurement sequence.
18    MeasureOnce,
19    /// Ordinary configuration change.
20    Configure,
21    /// Threshold-monitor configuration.
22    ThresholdMonitor,
23}
24
25/// Exact register-level bus context.
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27#[cfg_attr(feature = "defmt", derive(defmt::Format))]
28#[non_exhaustive]
29pub enum BusContext {
30    /// Read configuration register.
31    ReadConfiguration,
32    /// Write configuration register.
33    WriteConfiguration,
34    /// Read power-saving register.
35    ReadPowerSaving,
36    /// Write power-saving register.
37    WritePowerSaving,
38    /// Read ambient-light data.
39    ReadAls,
40    /// Read white-channel data.
41    ReadWhite,
42    /// Read device ID.
43    ReadDeviceId,
44    /// Read threshold status.
45    ReadThresholdStatus,
46    /// Read low threshold.
47    ReadLowThreshold,
48    /// Read high threshold.
49    ReadHighThreshold,
50    /// Write low threshold.
51    WriteLowThreshold,
52    /// Write high threshold.
53    WriteHighThreshold,
54}
55
56/// Configuration failure independent of the transport.
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58#[cfg_attr(feature = "defmt", derive(defmt::Format))]
59#[non_exhaustive]
60pub enum ConfigurationError {
61    /// Configuration register contained an undocumented encoding.
62    ConfigurationDecode(ConfigDecodeError),
63    /// Power-saving register contained reserved bits.
64    PowerSavingDecode(PowerSavingDecodeError),
65    /// Threshold-status register contained reserved bits.
66    ThresholdStatusDecode(ThresholdStatusDecodeError),
67    /// Observed threshold registers were reversed.
68    ReversedThresholds,
69    /// An enabled threshold monitor would be silently retargeted.
70    ThresholdMonitorOwnsDomain,
71    /// Explicit timing was derived for a different integration-time setting.
72    TimingIntegrationMismatch {
73        /// Integration time requested for the measurement.
74        measurement: IntegrationTime,
75        /// Integration time used to derive the supplied timing.
76        timing: IntegrationTime,
77    },
78}
79
80/// Ordinary driver failure preserving the concrete I²C error.
81///
82/// # Reporting without allocation
83///
84/// Every error type here implements [`core::fmt::Display`], and implements
85/// [`core::error::Error`] when the bus error does too. A chain can therefore be
86/// walked into a fixed buffer with no allocator, no `String`, and no `std`:
87///
88/// ```rust
89/// use core::fmt::Write;
90///
91/// /// Write an error and its causes into a caller-owned buffer.
92/// fn report<W: Write>(sink: &mut W, error: &dyn core::error::Error) -> core::fmt::Result {
93///     write!(sink, "{error}")?;
94///     let mut cause = error.source();
95///     while let Some(next) = cause {
96///         write!(sink, ": {next}")?;
97///         cause = next.source();
98///     }
99///     Ok(())
100/// }
101/// ```
102///
103/// Applied to a failed one-shot measurement, that produces something like:
104///
105/// ```text
106/// one-shot measurement failed at activating: one-shot measurement failed during a
107/// configuration write: arbitration lost
108/// ```
109///
110/// The last link is the HAL's own error, preserved rather than flattened into a
111/// string at the point of failure.
112///
113/// # If the bus error is not a `core::error::Error`
114///
115/// [`embedded_hal_async::i2c::Error`] requires only [`core::fmt::Debug`], so many
116/// HAL error types are not [`core::error::Error`] and some are not
117/// [`core::fmt::Display`]. That is why `Display` here is **not** bounded on the
118/// bus error: the semantic context this crate owns — operation, register, stage —
119/// is always printable. Only the chain needs more, and only the chain is lost.
120///
121/// [`embedded_hal_async::i2c::Error`]: https://docs.rs/embedded-hal-async
122#[derive(Debug, PartialEq, Eq)]
123#[cfg_attr(feature = "defmt", derive(defmt::Format))]
124#[non_exhaustive]
125pub enum Error<E> {
126    /// I²C transaction failed.
127    Bus {
128        /// Semantic operation.
129        operation: Operation,
130        /// Register-level context.
131        context: BusContext,
132        /// Underlying HAL error.
133        source: E,
134    },
135    /// Device state or requested configuration was invalid.
136    Configuration(ConfigurationError),
137}
138
139/// Probe-specific failure.
140#[derive(Debug, PartialEq, Eq)]
141#[cfg_attr(feature = "defmt", derive(defmt::Format))]
142#[non_exhaustive]
143pub enum ProbeError<E> {
144    /// Fixed address did not acknowledge.
145    NotPresent,
146    /// A non-address-NACK bus failure occurred.
147    Bus(E),
148    /// Full ID register did not match the supported VEML7700 identity word.
149    WrongDevice {
150        /// Raw unexpected ID register value.
151        observed: u16,
152    },
153}
154
155/// Stage of a complete one-shot measurement.
156#[derive(Clone, Copy, Debug, PartialEq, Eq)]
157#[cfg_attr(feature = "defmt", derive(defmt::Format))]
158#[non_exhaustive]
159pub enum MeasureStage {
160    /// Validate that explicit timing belongs to the requested integration time.
161    ValidateTiming,
162    /// Observe original configuration.
163    ObserveConfiguration,
164    /// Observe original power-saving state.
165    ObservePowerSaving,
166    /// Enter shutdown in the original domain before reconfiguring.
167    ///
168    /// Only reached when the operation started from an active device. Under the
169    /// driver reaction to `S-56`, this write changes only the shutdown bit.
170    EnterShutdown,
171    /// Disable autonomous power-saving cadence.
172    DisablePowerSaving,
173    /// Install the requested gain/integration fields while shut down.
174    PrepareMeasurement,
175    /// Leave shutdown to create a known wake edge and start conversion.
176    ActivateMeasurement,
177    /// Freeze the completed result by entering shutdown.
178    FreezeResult,
179    /// Read ALS data.
180    ReadAls,
181    /// Read white data.
182    ReadWhite,
183    /// Restore original configuration.
184    RestoreConfiguration,
185    /// Restore original power-saving register.
186    RestorePowerSaving,
187}
188
189/// Complete one-shot-measurement failure.
190#[derive(Debug, PartialEq, Eq)]
191#[cfg_attr(feature = "defmt", derive(defmt::Format))]
192#[non_exhaustive]
193pub enum MeasureOnceError<E> {
194    /// Failure before a pair was captured.
195    Operation {
196        /// Failing stage.
197        stage: MeasureStage,
198        /// Underlying driver failure.
199        source: Error<E>,
200    },
201    /// The primary operation failed and restoration also failed; hardware state is uncertain.
202    RecoveryFailed {
203        /// Original failing stage.
204        failed_stage: MeasureStage,
205        /// Original failure.
206        source: Error<E>,
207        /// Restoration stage that also failed.
208        recovery_stage: MeasureStage,
209        /// Restoration failure.
210        recovery_source: Error<E>,
211    },
212    /// A pair was captured, but restoration failed and hardware state is uncertain.
213    RestoreFailed {
214        /// Captured sample remains useful with explicit qualification.
215        sample: MeasurementCapture,
216        /// Failing restoration stage.
217        stage: MeasureStage,
218        /// Underlying driver failure.
219        source: Error<E>,
220    },
221}
222
223/// Stage of threshold-monitor programming.
224#[derive(Clone, Copy, Debug, PartialEq, Eq)]
225#[cfg_attr(feature = "defmt", derive(defmt::Format))]
226#[non_exhaustive]
227pub enum ThresholdMonitorStage {
228    /// Observe current configuration.
229    ObserveConfiguration,
230    /// Enter shutdown with the monitored domain intact.
231    ///
232    /// Only reached when re-arming an enabled monitor on an active device. The
233    /// shutdown and monitor bits cannot move in one write there, so shutdown
234    /// goes first while the monitor bit remains enabled.
235    EnterShutdown,
236    /// Disable the threshold monitor before changing its domain.
237    DisableMonitor,
238    /// Write low threshold.
239    WriteLowThreshold,
240    /// Write high threshold.
241    WriteHighThreshold,
242    /// Install power-saving cadence.
243    ApplyPowerSaving,
244    /// Enable the final monitored domain.
245    EnableMonitor,
246}
247
248/// Threshold-monitor programming failure.
249///
250/// Programming begins with one configuration read and then follows a
251/// start-state-dependent write sequence. The fields distinguish confirmed
252/// writes from the step that failed.
253///
254/// # What each field establishes
255///
256/// - [`stage`](Self::stage) identifies the read or write that failed.
257///   [`ThresholdMonitorStage::ObserveConfiguration`] means no device-state write
258///   was attempted.
259/// - [`confirmed`](Self::confirmed) is the most recent write in the actual
260///   branch that returned success. `None` means no write was confirmed.
261/// - When `stage` is a write, its commit status is unknown: the device may remain
262///   at `confirmed`, or may also contain that write's effect. Later stages were
263///   not attempted. No rollback is claimed.
264///
265/// # Recovering
266///
267/// Read the registers back rather than inferring. [`read_configuration`],
268/// [`read_thresholds`] and [`read_power_saving`] together establish the actual
269/// state, and re-arming from there installs a known domain.
270///
271/// A confirmed [`ThresholdMonitorStage::DisableMonitor`] establishes disabled
272/// and shut down until a later confirmed write. Before that point, the original
273/// active/enabled state may remain. A failed final enable may leave the device
274/// disabled and shut down or fully active in the requested domain.
275///
276/// [`read_configuration`]: crate::Veml7700::read_configuration
277/// [`read_thresholds`]: crate::Veml7700::read_thresholds
278/// [`read_power_saving`]: crate::Veml7700::read_power_saving
279#[derive(Debug, PartialEq, Eq)]
280#[cfg_attr(feature = "defmt", derive(defmt::Format))]
281#[non_exhaustive]
282pub struct ThresholdMonitorError<E> {
283    /// Stage that failed. A write stage has unknown commit status; an observation
284    /// stage made no device-state write.
285    pub stage: ThresholdMonitorStage,
286    /// Most recent write that completed successfully, or `None` if none did.
287    pub confirmed: Option<ThresholdMonitorStage>,
288    /// Underlying driver failure.
289    pub source: Error<E>,
290}
291
292// Standard error integration.
293//
294// Two deliberate bound choices shape everything below.
295//
296// `Display` is implemented **without bounding `E`**. `embedded_hal::i2c::Error`
297// requires only `Debug`, so a great many real HAL error types are not `Display`;
298// bounding on it would make these impls unavailable to exactly the callers this
299// driver exists for. The message therefore carries the semantic context this
300// crate owns -- operation, register, stage -- and leaves the concrete bus error
301// to `source()`.
302//
303// `core::error::Error` *is* bounded, on `E: core::error::Error + 'static`. That
304// is what `source()` requires, and it is additive: a bus error that does not
305// implement `Error` simply does not get these impls on the wrapper. It does not
306// stop the driver being used, which is why the bound sits on the impl rather
307// than on the type.
308
309// The context enums print as short lowercase phrases so a chained report reads
310// as a sentence rather than as a list of type names.
311
312impl core::fmt::Display for Operation {
313    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
314        f.write_str(match self {
315            Self::Inspect => "inspection",
316            Self::Snapshot => "snapshot",
317            Self::MeasureOnce => "one-shot measurement",
318            Self::Configure => "configuration change",
319            Self::ThresholdMonitor => "threshold-monitor programming",
320        })
321    }
322}
323
324impl core::fmt::Display for BusContext {
325    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
326        f.write_str(match self {
327            Self::ReadConfiguration => "a configuration read",
328            Self::WriteConfiguration => "a configuration write",
329            Self::ReadPowerSaving => "a power-saving read",
330            Self::WritePowerSaving => "a power-saving write",
331            Self::ReadAls => "an ALS read",
332            Self::ReadWhite => "a white-channel read",
333            Self::ReadDeviceId => "a device-ID read",
334            Self::ReadThresholdStatus => "a threshold-status read",
335            Self::ReadLowThreshold => "a low-threshold read",
336            Self::ReadHighThreshold => "a high-threshold read",
337            Self::WriteLowThreshold => "a low-threshold write",
338            Self::WriteHighThreshold => "a high-threshold write",
339        })
340    }
341}
342
343impl core::fmt::Display for MeasureStage {
344    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
345        f.write_str(match self {
346            Self::ValidateTiming => "timing validation",
347            Self::ObserveConfiguration => "observing configuration",
348            Self::ObservePowerSaving => "observing power saving",
349            Self::EnterShutdown => "entering shutdown",
350            Self::DisablePowerSaving => "disabling power saving",
351            Self::PrepareMeasurement => "installing the measurement domain",
352            Self::ActivateMeasurement => "activating",
353            Self::FreezeResult => "freezing the result",
354            Self::ReadAls => "reading ALS",
355            Self::ReadWhite => "reading white",
356            Self::RestoreConfiguration => "restoring configuration",
357            Self::RestorePowerSaving => "restoring power saving",
358        })
359    }
360}
361
362impl core::fmt::Display for ThresholdMonitorStage {
363    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
364        f.write_str(match self {
365            Self::ObserveConfiguration => "observing configuration",
366            Self::EnterShutdown => "entering shutdown",
367            Self::DisableMonitor => "disabling the monitor",
368            Self::WriteLowThreshold => "writing the low threshold",
369            Self::WriteHighThreshold => "writing the high threshold",
370            Self::ApplyPowerSaving => "applying power saving",
371            Self::EnableMonitor => "enabling the monitor",
372        })
373    }
374}
375
376impl core::fmt::Display for ConfigurationError {
377    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
378        match self {
379            Self::ConfigurationDecode(_) => f.write_str("configuration register did not decode"),
380            Self::PowerSavingDecode(_) => f.write_str("power-saving register did not decode"),
381            Self::ThresholdStatusDecode(_) => {
382                f.write_str("threshold-status register did not decode")
383            }
384            Self::ReversedThresholds => f.write_str("thresholds were reversed"),
385            Self::ThresholdMonitorOwnsDomain => {
386                f.write_str("an enabled threshold monitor owns this domain")
387            }
388            Self::TimingIntegrationMismatch {
389                measurement,
390                timing,
391            } => write!(
392                f,
393                "timing was derived for {} ms but the measurement selects {} ms",
394                timing.milliseconds(),
395                measurement.milliseconds()
396            ),
397        }
398    }
399}
400
401impl core::error::Error for ConfigurationError {
402    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
403        match self {
404            Self::ConfigurationDecode(source) => Some(source),
405            Self::PowerSavingDecode(source) => Some(source),
406            Self::ThresholdStatusDecode(source) => Some(source),
407            _ => None,
408        }
409    }
410}
411
412impl<E> core::fmt::Display for Error<E> {
413    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
414        match self {
415            Self::Bus {
416                operation, context, ..
417            } => write!(f, "{operation} failed during {context}"),
418            // Deliberately not the inner message. This level's information is
419            // that the failure was a configuration rejection rather than a bus
420            // fault; the inner error is the next link, and repeating it here
421            // would print it twice in a chained report.
422            Self::Configuration(_) => f.write_str("configuration was rejected"),
423        }
424    }
425}
426
427impl<E: core::error::Error + 'static> core::error::Error for Error<E> {
428    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
429        match self {
430            Self::Bus { source, .. } => Some(source),
431            Self::Configuration(source) => Some(source),
432        }
433    }
434}
435
436impl<E> core::fmt::Display for ProbeError<E> {
437    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
438        match self {
439            Self::NotPresent => f.write_str("no device acknowledged the fixed address"),
440            Self::Bus(_) => f.write_str("probe transaction failed"),
441            Self::WrongDevice { observed } => {
442                write!(f, "identity {observed:#06x} is not a supported VEML7700")
443            }
444        }
445    }
446}
447
448impl<E: core::error::Error + 'static> core::error::Error for ProbeError<E> {
449    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
450        match self {
451            Self::Bus(source) => Some(source),
452            // Neither absence nor a wrong identity has an underlying cause: both
453            // are conclusions this driver reached, not failures it forwarded.
454            _ => None,
455        }
456    }
457}
458
459impl<E> core::fmt::Display for MeasureOnceError<E> {
460    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
461        match self {
462            Self::Operation { stage, .. } => write!(f, "one-shot measurement failed at {stage}"),
463            Self::RecoveryFailed {
464                failed_stage,
465                recovery_stage,
466                ..
467            } => write!(
468                f,
469                "one-shot measurement failed at {failed_stage} and restoration failed at \
470                 {recovery_stage}; device state is uncertain"
471            ),
472            Self::RestoreFailed { stage, .. } => write!(
473                f,
474                "a sample was captured but restoration failed at {stage}; device state is uncertain"
475            ),
476        }
477    }
478}
479
480impl<E: core::error::Error + 'static> core::error::Error for MeasureOnceError<E> {
481    /// The **primary** failure, in every variant.
482    ///
483    /// `RecoveryFailed` carries two errors and a chain can only express one.
484    /// Reporting the recovery failure as the cause would invert what happened:
485    /// the primary failure is why the operation stopped, and the recovery
486    /// failure is why the device was left uncertain. The recovery error stays
487    /// available as an ordinary field, which is the honest place for a second
488    /// independent failure.
489    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
490        match self {
491            Self::Operation { source, .. }
492            | Self::RecoveryFailed { source, .. }
493            | Self::RestoreFailed { source, .. } => Some(source),
494        }
495    }
496}
497
498impl<E> core::fmt::Display for ThresholdMonitorError<E> {
499    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
500        match self.confirmed {
501            Some(confirmed) => write!(
502                f,
503                "threshold programming failed at {}; {} was the last confirmed write",
504                self.stage, confirmed
505            ),
506            None => write!(
507                f,
508                "threshold programming failed at {}; no write was confirmed",
509                self.stage
510            ),
511        }
512    }
513}
514
515impl<E: core::error::Error + 'static> core::error::Error for ThresholdMonitorError<E> {
516    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
517        Some(&self.source)
518    }
519}
520
521#[cfg(test)]
522mod standard_error_tests {
523    use super::*;
524    use crate::config::{Gain, IntegrationTime, MeasurementConfig};
525    use crate::measurement::{AlsCounts, MeasurementPairCoherence, WhiteCounts};
526    use core::error::Error as _;
527    use core::fmt::Write as _;
528
529    /// A HAL error that participates in the standard chain.
530    #[derive(Debug, PartialEq, Eq)]
531    struct ReportableBusFault;
532
533    impl core::fmt::Display for ReportableBusFault {
534        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
535            f.write_str("arbitration lost")
536        }
537    }
538
539    impl core::error::Error for ReportableBusFault {}
540
541    /// A HAL error that implements only `Debug`, which is all
542    /// `embedded_hal::i2c::Error` requires. Many real ones look like this.
543    #[derive(Debug, PartialEq, Eq)]
544    struct BareBusFault;
545
546    /// Fixed-capacity sink: no allocator, no `String`, no `std`.
547    struct Sink {
548        buffer: [u8; 256],
549        used: usize,
550    }
551
552    impl Sink {
553        const fn new() -> Self {
554            Self {
555                buffer: [0; 256],
556                used: 0,
557            }
558        }
559
560        fn as_str(&self) -> &str {
561            core::str::from_utf8(&self.buffer[..self.used]).expect("valid UTF-8")
562        }
563    }
564
565    impl core::fmt::Write for Sink {
566        fn write_str(&mut self, text: &str) -> core::fmt::Result {
567            let bytes = text.as_bytes();
568            let end = self.used + bytes.len();
569            if end > self.buffer.len() {
570                return Err(core::fmt::Error);
571            }
572            self.buffer[self.used..end].copy_from_slice(bytes);
573            self.used = end;
574            Ok(())
575        }
576    }
577
578    /// Walk a standard error chain into a fixed buffer.
579    fn report(error: &dyn core::error::Error) -> Sink {
580        let mut sink = Sink::new();
581        write!(sink, "{error}").expect("fits");
582        let mut cause = error.source();
583        while let Some(next) = cause {
584            write!(sink, ": {next}").expect("fits");
585            cause = next.source();
586        }
587        sink
588    }
589
590    fn bus_failure<E>(source: E) -> Error<E> {
591        Error::Bus {
592            operation: Operation::MeasureOnce,
593            context: BusContext::WriteConfiguration,
594            source,
595        }
596    }
597
598    #[test]
599    fn a_reportable_bus_error_reaches_the_end_of_the_chain() {
600        let error = bus_failure(ReportableBusFault);
601        assert_eq!(
602            report(&error).as_str(),
603            "one-shot measurement failed during a configuration write: arbitration lost"
604        );
605    }
606
607    #[test]
608    fn a_bus_error_that_is_not_a_standard_error_still_works() {
609        // The wrapper is `Display` regardless: the semantic context this crate
610        // owns is never hidden behind a bound the HAL does not satisfy. Only the
611        // `Error` impl -- and therefore the chain -- requires more of `E`.
612        let error = bus_failure(BareBusFault);
613        let mut sink = Sink::new();
614        write!(sink, "{error}").expect("fits");
615        assert_eq!(
616            sink.as_str(),
617            "one-shot measurement failed during a configuration write"
618        );
619    }
620
621    #[test]
622    fn a_configuration_failure_chains_to_its_decode_cause() {
623        let error: Error<ReportableBusFault> =
624            Error::Configuration(ConfigurationError::ConfigurationDecode(
625                ConfigDecodeError::ReservedBits { observed: 0x2000 },
626            ));
627        assert_eq!(
628            report(&error).as_str(),
629            concat!(
630                "configuration was rejected: configuration register did not decode: ",
631                "reserved configuration bits were set: 0x2000"
632            )
633        );
634    }
635
636    #[test]
637    fn a_conclusion_this_driver_reached_has_no_cause() {
638        // Absence and wrong identity are findings, not forwarded failures, so
639        // reporting a cause for them would invent one.
640        let absent: ProbeError<ReportableBusFault> = ProbeError::NotPresent;
641        assert!(absent.source().is_none());
642        assert_eq!(
643            report(&absent).as_str(),
644            "no device acknowledged the fixed address"
645        );
646
647        let mismatch: ProbeError<ReportableBusFault> = ProbeError::WrongDevice { observed: 0x1234 };
648        assert!(mismatch.source().is_none());
649    }
650
651    #[test]
652    fn a_nested_recovery_failure_reports_the_primary_cause() {
653        let error = MeasureOnceError::RecoveryFailed {
654            failed_stage: MeasureStage::ActivateMeasurement,
655            source: bus_failure(ReportableBusFault),
656            recovery_stage: MeasureStage::RestoreConfiguration,
657            recovery_source: bus_failure(ReportableBusFault),
658        };
659        // Both failures are named in the message, because both happened. The
660        // chain follows the primary one: it is why the operation stopped.
661        assert_eq!(
662            report(&error).as_str(),
663            "one-shot measurement failed at activating and restoration failed at restoring \
664             configuration; device state is uncertain: one-shot measurement failed during a \
665             configuration write: arbitration lost"
666        );
667        let MeasureOnceError::RecoveryFailed {
668            recovery_source, ..
669        } = &error
670        else {
671            unreachable!()
672        };
673        assert!(matches!(recovery_source, Error::Bus { .. }));
674    }
675
676    #[test]
677    fn a_captured_sample_survives_a_reported_restoration_failure() {
678        let configuration = MeasurementConfig::new(Gain::Div8, IntegrationTime::Ms100);
679        let error = MeasureOnceError::RestoreFailed {
680            sample: MeasurementCapture {
681                als: AlsCounts::from_counts(0x1234),
682                white: WhiteCounts::from_counts(0x5678),
683                configuration,
684                nominal_illuminance: AlsCounts::from_counts(0x1234)
685                    .nominal_micro_lux(configuration),
686                requested_wait_us: 133_500,
687                coherence: MeasurementPairCoherence::FrozenAfterRequestedWait,
688            },
689            stage: MeasureStage::RestorePowerSaving,
690            source: bus_failure(ReportableBusFault),
691        };
692        assert!(
693            report(&error).as_str().starts_with(
694                "a sample was captured but restoration failed at restoring power saving"
695            )
696        );
697        let MeasureOnceError::RestoreFailed { sample, .. } = &error else {
698            unreachable!()
699        };
700        assert_eq!(sample.als, AlsCounts::from_counts(0x1234));
701    }
702
703    #[test]
704    fn threshold_failures_report_confirmed_progress() {
705        let unconfirmed = ThresholdMonitorError {
706            stage: ThresholdMonitorStage::DisableMonitor,
707            confirmed: None,
708            source: bus_failure(ReportableBusFault),
709        };
710        assert_eq!(
711            report(&unconfirmed).as_str(),
712            "threshold programming failed at disabling the monitor; no write was confirmed: \
713             one-shot measurement failed during a configuration write: arbitration lost"
714        );
715
716        let partial = ThresholdMonitorError {
717            stage: ThresholdMonitorStage::ApplyPowerSaving,
718            confirmed: Some(ThresholdMonitorStage::WriteHighThreshold),
719            source: bus_failure(ReportableBusFault),
720        };
721        assert!(report(&partial).as_str().starts_with(
722            "threshold programming failed at applying power saving; writing the high threshold \
723             was the last confirmed write"
724        ));
725    }
726}