Skip to main content

laser_dac/
error.rs

1//! Error types for the laser-dac crate.
2
3use std::error::Error as StdError;
4use std::fmt;
5
6// =============================================================================
7// Streaming Error
8// =============================================================================
9
10/// Streaming-specific error type.
11///
12/// This error type is designed for the streaming API and includes variants
13/// that enforce the uniform backpressure contract across all backends.
14#[derive(Debug)]
15pub enum Error {
16    /// The device/library cannot accept more data right now.
17    WouldBlock,
18
19    /// The stream was explicitly stopped via `StreamControl::stop()`.
20    Stopped,
21
22    /// The device disconnected or became unreachable.
23    Disconnected(String),
24
25    /// The OS denied access to the device (e.g. a USB permission failure).
26    ///
27    /// Distinct from [`Error::Backend`] so consumers can detect a fixable
28    /// setup problem — on Linux a USB laser DAC needs a udev rule granting the
29    /// user access to the device node — and guide the user rather than surface
30    /// a generic error. Retriable: once access is granted (udev rule installed
31    /// and the device replugged/re-triggered) a later connect attempt succeeds.
32    PermissionDenied(String),
33
34    /// Invalid configuration or API misuse.
35    InvalidConfig(String),
36
37    /// Backend/protocol error (wrapped).
38    Backend(Box<dyn StdError + Send + Sync>),
39}
40
41impl fmt::Display for Error {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        match self {
44            Error::WouldBlock => write!(f, "would block: device cannot accept more data"),
45            Error::Stopped => write!(f, "stopped: stream was explicitly stopped"),
46            Error::Disconnected(msg) => write!(f, "disconnected: {}", msg),
47            Error::PermissionDenied(msg) => write!(f, "permission denied: {}", msg),
48            Error::InvalidConfig(msg) => write!(f, "invalid configuration: {}", msg),
49            Error::Backend(e) => write!(f, "backend error: {}", e),
50        }
51    }
52}
53
54impl StdError for Error {
55    fn source(&self) -> Option<&(dyn StdError + 'static)> {
56        match self {
57            Error::Backend(e) => Some(e.as_ref()),
58            _ => None,
59        }
60    }
61}
62
63impl Error {
64    /// Create a disconnected error with a message.
65    pub fn disconnected(msg: impl Into<String>) -> Self {
66        Error::Disconnected(msg.into())
67    }
68
69    /// Create a permission-denied error with a message.
70    pub fn permission_denied(msg: impl Into<String>) -> Self {
71        Error::PermissionDenied(msg.into())
72    }
73
74    /// Create a permission-denied error for a USB access failure, appending a
75    /// platform-appropriate hint. On Linux, USB laser DACs are inaccessible to
76    /// non-root users until a udev rule grants access to the device node, so the
77    /// message points at that fix; other platforms get the bare context.
78    pub fn usb_permission_denied(context: impl std::fmt::Display) -> Self {
79        #[cfg(target_os = "linux")]
80        let msg = format!(
81            "{context}: USB access denied — this laser DAC needs a udev rule \
82             granting the user access to the device node (then replug the device)"
83        );
84        #[cfg(not(target_os = "linux"))]
85        let msg = format!("{context}: USB access denied");
86        Error::PermissionDenied(msg)
87    }
88
89    /// Create an invalid config error with a message.
90    pub fn invalid_config(msg: impl Into<String>) -> Self {
91        Error::InvalidConfig(msg.into())
92    }
93
94    /// Create a backend error from any error type.
95    pub fn backend(err: impl StdError + Send + Sync + 'static) -> Self {
96        Error::Backend(Box::new(err))
97    }
98
99    /// Returns true if this is a WouldBlock error.
100    pub fn is_would_block(&self) -> bool {
101        matches!(self, Error::WouldBlock)
102    }
103
104    /// Returns true if this is a Disconnected error.
105    pub fn is_disconnected(&self) -> bool {
106        matches!(self, Error::Disconnected(_))
107    }
108
109    /// Returns true if this is a PermissionDenied error.
110    ///
111    /// Consumers can branch on this to guide the user through granting device
112    /// access (e.g. installing a udev rule on Linux) instead of showing a
113    /// generic failure.
114    pub fn is_permission_denied(&self) -> bool {
115        matches!(self, Error::PermissionDenied(_))
116    }
117
118    /// Returns true if this is a Stopped error.
119    pub fn is_stopped(&self) -> bool {
120        matches!(self, Error::Stopped)
121    }
122}
123
124impl From<std::io::Error> for Error {
125    fn from(err: std::io::Error) -> Self {
126        if err.kind() == std::io::ErrorKind::WouldBlock {
127            Error::WouldBlock
128        } else {
129            Error::Backend(Box::new(err))
130        }
131    }
132}
133
134/// Result type for streaming operations.
135pub type Result<T> = std::result::Result<T, Error>;
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn permission_denied_is_detected_and_distinct_from_backend() {
143        let err = Error::permission_denied("no access");
144        assert!(err.is_permission_denied());
145        assert!(!err.is_disconnected());
146        assert!(!matches!(err, Error::Backend(_)));
147    }
148
149    #[test]
150    fn usb_permission_denied_carries_context() {
151        let err = Error::usb_permission_denied("helios open");
152        assert!(err.is_permission_denied());
153        let msg = err.to_string();
154        assert!(
155            msg.contains("helios open"),
156            "message keeps the context: {msg}"
157        );
158        // On Linux the message must point at the udev-rule fix so a consumer can
159        // relay actionable guidance; elsewhere it is just the bare access note.
160        #[cfg(target_os = "linux")]
161        assert!(
162            msg.contains("udev"),
163            "linux message names the udev fix: {msg}"
164        );
165    }
166}