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 { expected: String, actual: String },
28
29    /// Write error with status code
30    #[error("Write error: {message} (status: {status})")]
31    WriteError { status: u8, message: String },
32
33    /// Read error with status code
34    #[error("Read error: {message} (status: {status})")]
35    ReadError { status: u8, message: String },
36
37    /// Invalid response from PLC
38    #[error("Invalid response: {reason}")]
39    InvalidResponse { reason: String },
40
41    /// Timeout error
42    #[error("Operation timed out after {0:?}")]
43    Timeout(Duration),
44
45    /// UDT error
46    #[error("UDT error: {0}")]
47    Udt(String),
48
49    /// Connection error (PLC not responding, session issues)
50    #[error("Connection error: {0}")]
51    Connection(String),
52
53    /// Connection lost (network closed, PLC unreachable)
54    #[error("Connection lost: {0}")]
55    ConnectionLost(String),
56
57    /// CIP protocol error with status code (from PLC)
58    #[error("CIP error 0x{code:02X}: {message}")]
59    CipError { code: u8, message: String },
60
61    /// String is too long for the PLC's string type
62    #[error("String too long: max length is {max_length}, but got {actual_length}")]
63    StringTooLong {
64        max_length: usize,
65        actual_length: usize,
66    },
67
68    /// String contains invalid characters
69    #[error("Invalid string: {reason}")]
70    InvalidString { reason: String },
71
72    /// Tag error
73    #[error("Tag error: {0}")]
74    Tag(String),
75
76    /// Permission denied
77    #[error("Permission denied: {0}")]
78    Permission(String),
79
80    /// UTF-8 error
81    #[error("UTF-8 error: {0}")]
82    Utf8(#[from] std::string::FromUtf8Error),
83
84    /// Other error
85    #[error("Other error: {0}")]
86    Other(String),
87
88    /// Subscription error
89    #[error("Subscription error: {0}")]
90    Subscription(String),
91
92    /// Unsupported API surface retained only for 1.x compatibility.
93    #[error("Unsupported API `{api}`: {reason}")]
94    Unsupported {
95        /// API name that is no longer supported.
96        api: &'static str,
97        /// Reason the API is unsupported and the replacement to use.
98        reason: &'static str,
99    },
100}
101
102impl EtherNetIpError {
103    /// Returns true if the error is likely retriable (e.g. timeout, connection lost).
104    /// Use this to decide whether to retry an operation or reconnect.
105    #[must_use]
106    pub fn is_retriable(&self) -> bool {
107        matches!(
108            self,
109            EtherNetIpError::Timeout(_)
110                | EtherNetIpError::Connection(_)
111                | EtherNetIpError::ConnectionLost(_)
112                | EtherNetIpError::Io(_)
113        )
114    }
115}
116
117impl<T> From<std::sync::PoisonError<T>> for EtherNetIpError {
118    fn from(_: std::sync::PoisonError<T>) -> Self {
119        EtherNetIpError::Other("lock poisoned".to_string())
120    }
121}
122
123impl From<rust_ethernet_ip_tag_path::TagPathError> for EtherNetIpError {
124    fn from(error: rust_ethernet_ip_tag_path::TagPathError) -> Self {
125        EtherNetIpError::Protocol(error.to_string())
126    }
127}
128
129impl From<rust_ethernet_ip_protocol::ProtocolError> for EtherNetIpError {
130    fn from(error: rust_ethernet_ip_protocol::ProtocolError) -> Self {
131        EtherNetIpError::Protocol(error.to_string())
132    }
133}
134
135impl From<rust_ethernet_ip_types::TypeError> for EtherNetIpError {
136    fn from(error: rust_ethernet_ip_types::TypeError) -> Self {
137        EtherNetIpError::Protocol(error.to_string())
138    }
139}
140
141impl From<rust_ethernet_ip_udt::UdtError> for EtherNetIpError {
142    fn from(error: rust_ethernet_ip_udt::UdtError) -> Self {
143        match error {
144            rust_ethernet_ip_udt::UdtError::Protocol(message) => EtherNetIpError::Protocol(message),
145            rust_ethernet_ip_udt::UdtError::TagNotFound(tag) => EtherNetIpError::TagNotFound(tag),
146            rust_ethernet_ip_udt::UdtError::DataTypeMismatch { expected, actual } => {
147                EtherNetIpError::DataTypeMismatch { expected, actual }
148            }
149        }
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use std::assert_matches;
157    use std::sync::Mutex;
158
159    fn convert_poison_error() -> Result<()> {
160        let lock = Mutex::new(());
161        std::thread::scope(|scope| {
162            let handle = scope.spawn(|| {
163                let _guard = lock.lock().expect("test lock should not be poisoned yet");
164                panic!("poison test mutex");
165            });
166            assert!(handle.join().is_err());
167        });
168
169        let _guard = lock.lock()?;
170        Ok(())
171    }
172
173    #[test]
174    fn poison_error_converts_to_other_variant() {
175        let err = convert_poison_error().expect_err("poisoned mutex should convert into an error");
176        assert_matches!(err, EtherNetIpError::Other(message) if message == "lock poisoned");
177    }
178
179    #[test]
180    fn unsupported_error_names_api_and_reason() {
181        let err = EtherNetIpError::Unsupported {
182            api: "old_api",
183            reason: "use new_api",
184        };
185
186        assert_eq!(err.to_string(), "Unsupported API `old_api`: use new_api");
187    }
188}