Skip to main content

rust_ethernet_ip/
error.rs

1// use std::error::Error;
2use std::io;
3use std::time::Duration;
4use thiserror::Error;
5
6/// Result type alias for EtherNet/IP operations
7pub type Result<T> = std::result::Result<T, EtherNetIpError>;
8
9/// Error types that can occur during EtherNet/IP communication
10#[derive(Debug, Error)]
11#[non_exhaustive]
12pub enum EtherNetIpError {
13    /// IO error (network issues, connection problems)
14    #[error("IO error: {0}")]
15    Io(#[from] io::Error),
16
17    /// Protocol error (invalid packet format, unsupported features)
18    #[error("Protocol error: {0}")]
19    Protocol(String),
20
21    /// Tag not found in PLC
22    #[error("Tag not found: {0}")]
23    TagNotFound(String),
24
25    /// Data type mismatch
26    #[error("Data type mismatch: expected {expected}, got {actual}")]
27    DataTypeMismatch {
28        /// Data type required by the target.
29        expected: String,
30        /// Data type supplied by the caller or response.
31        actual: String,
32    },
33
34    /// Write error with status code
35    #[error("Write error: {message} (status: {status})")]
36    WriteError {
37        /// CIP general status code.
38        status: u8,
39        /// Human-readable write failure detail.
40        message: String,
41    },
42
43    /// Read error with status code
44    #[error("Read error: {message} (status: {status})")]
45    ReadError {
46        /// CIP general status code.
47        status: u8,
48        /// Human-readable read failure detail.
49        message: String,
50    },
51
52    /// Invalid response from PLC
53    #[error("Invalid response: {reason}")]
54    InvalidResponse {
55        /// Explanation of the malformed or inconsistent response.
56        reason: String,
57    },
58
59    /// Timeout error
60    #[error("Operation timed out after {0:?}")]
61    Timeout(Duration),
62
63    /// UDT error
64    #[error("UDT error: {0}")]
65    Udt(String),
66
67    /// Connection error (PLC not responding, session issues)
68    #[error("Connection error: {0}")]
69    Connection(String),
70
71    /// Connection lost (network closed, PLC unreachable)
72    #[error("Connection lost: {0}")]
73    ConnectionLost(String),
74
75    /// CIP protocol error with status code (from PLC)
76    #[error("CIP error 0x{code:02X}: {message}")]
77    CipError {
78        /// CIP general status code.
79        code: u8,
80        /// Human-readable controller response.
81        message: String,
82    },
83
84    /// String is too long for the PLC's string type
85    #[error("String too long: max length is {max_length}, but got {actual_length}")]
86    StringTooLong {
87        /// Maximum encoded string byte length for the target type/path.
88        max_length: usize,
89        /// Supplied UTF-8 byte length.
90        actual_length: usize,
91    },
92
93    /// String contains invalid characters
94    #[error("Invalid string: {reason}")]
95    InvalidString {
96        /// Explanation of the invalid length, encoding, or payload.
97        reason: String,
98    },
99
100    /// Tag error
101    #[error("Tag error: {0}")]
102    Tag(String),
103
104    /// Permission denied
105    #[error("Permission denied: {0}")]
106    Permission(String),
107
108    /// UTF-8 error
109    #[error("UTF-8 error: {0}")]
110    Utf8(#[from] std::string::FromUtf8Error),
111
112    /// Other error
113    #[error("Other error: {0}")]
114    Other(String),
115
116    /// Subscription error
117    #[error("Subscription error: {0}")]
118    Subscription(String),
119
120    /// Unsupported API surface retained only for 1.x compatibility.
121    #[error("Unsupported API `{api}`: {reason}")]
122    Unsupported {
123        /// API name that is no longer supported.
124        api: &'static str,
125        /// Reason the API is unsupported and the replacement to use.
126        reason: &'static str,
127    },
128}
129
130impl EtherNetIpError {
131    /// Returns true if the error is likely retriable (e.g. timeout, connection lost).
132    /// Use this to decide whether to retry an operation or reconnect.
133    #[must_use]
134    pub fn is_retriable(&self) -> bool {
135        matches!(
136            self,
137            EtherNetIpError::Timeout(_)
138                | EtherNetIpError::Connection(_)
139                | EtherNetIpError::ConnectionLost(_)
140                | EtherNetIpError::Io(_)
141        )
142    }
143}
144
145impl<T> From<std::sync::PoisonError<T>> for EtherNetIpError {
146    fn from(_: std::sync::PoisonError<T>) -> Self {
147        EtherNetIpError::Other("lock poisoned".to_string())
148    }
149}
150
151impl From<rust_ethernet_ip_tag_path::TagPathError> for EtherNetIpError {
152    fn from(error: rust_ethernet_ip_tag_path::TagPathError) -> Self {
153        EtherNetIpError::Protocol(error.to_string())
154    }
155}
156
157impl From<rust_ethernet_ip_protocol::ProtocolError> for EtherNetIpError {
158    fn from(error: rust_ethernet_ip_protocol::ProtocolError) -> Self {
159        EtherNetIpError::Protocol(error.to_string())
160    }
161}
162
163impl From<rust_ethernet_ip_types::TypeError> for EtherNetIpError {
164    fn from(error: rust_ethernet_ip_types::TypeError) -> Self {
165        EtherNetIpError::Protocol(error.to_string())
166    }
167}
168
169impl From<rust_ethernet_ip_udt::UdtError> for EtherNetIpError {
170    fn from(error: rust_ethernet_ip_udt::UdtError) -> Self {
171        match error {
172            rust_ethernet_ip_udt::UdtError::Protocol(message) => EtherNetIpError::Protocol(message),
173            rust_ethernet_ip_udt::UdtError::TagNotFound(tag) => EtherNetIpError::TagNotFound(tag),
174            rust_ethernet_ip_udt::UdtError::DataTypeMismatch { expected, actual } => {
175                EtherNetIpError::DataTypeMismatch { expected, actual }
176            }
177        }
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use std::sync::Mutex;
185
186    fn convert_poison_error() -> Result<()> {
187        let lock = Mutex::new(());
188        std::thread::scope(|scope| {
189            let handle = scope.spawn(|| {
190                let _guard = lock.lock().expect("test lock should not be poisoned yet");
191                panic!("poison test mutex");
192            });
193            assert!(handle.join().is_err());
194        });
195
196        let _guard = lock.lock()?;
197        Ok(())
198    }
199
200    #[test]
201    fn poison_error_converts_to_other_variant() {
202        let err = convert_poison_error().expect_err("poisoned mutex should convert into an error");
203        assert!(matches!(
204            err,
205            EtherNetIpError::Other(message) if message == "lock poisoned"
206        ));
207    }
208
209    #[test]
210    fn unsupported_error_names_api_and_reason() {
211        let err = EtherNetIpError::Unsupported {
212            api: "old_api",
213            reason: "use new_api",
214        };
215
216        assert_eq!(err.to_string(), "Unsupported API `old_api`: use new_api");
217    }
218}