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
93impl EtherNetIpError {
94    /// Returns true if the error is likely retriable (e.g. timeout, connection lost).
95    /// Use this to decide whether to retry an operation or reconnect.
96    #[must_use]
97    pub fn is_retriable(&self) -> bool {
98        matches!(
99            self,
100            EtherNetIpError::Timeout(_)
101                | EtherNetIpError::Connection(_)
102                | EtherNetIpError::ConnectionLost(_)
103                | EtherNetIpError::Io(_)
104        )
105    }
106}
107
108impl<T> From<std::sync::PoisonError<T>> for EtherNetIpError {
109    fn from(_: std::sync::PoisonError<T>) -> Self {
110        EtherNetIpError::Other("lock poisoned".to_string())
111    }
112}
113
114impl From<rust_ethernet_ip_tag_path::TagPathError> for EtherNetIpError {
115    fn from(error: rust_ethernet_ip_tag_path::TagPathError) -> Self {
116        EtherNetIpError::Protocol(error.to_string())
117    }
118}
119
120impl From<rust_ethernet_ip_protocol::ProtocolError> for EtherNetIpError {
121    fn from(error: rust_ethernet_ip_protocol::ProtocolError) -> Self {
122        EtherNetIpError::Protocol(error.to_string())
123    }
124}
125
126impl From<rust_ethernet_ip_types::TypeError> for EtherNetIpError {
127    fn from(error: rust_ethernet_ip_types::TypeError) -> Self {
128        EtherNetIpError::Protocol(error.to_string())
129    }
130}
131
132impl From<rust_ethernet_ip_udt::UdtError> for EtherNetIpError {
133    fn from(error: rust_ethernet_ip_udt::UdtError) -> Self {
134        match error {
135            rust_ethernet_ip_udt::UdtError::Protocol(message) => EtherNetIpError::Protocol(message),
136            rust_ethernet_ip_udt::UdtError::TagNotFound(tag) => EtherNetIpError::TagNotFound(tag),
137            rust_ethernet_ip_udt::UdtError::DataTypeMismatch { expected, actual } => {
138                EtherNetIpError::DataTypeMismatch { expected, actual }
139            }
140        }
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use std::sync::Mutex;
148
149    fn convert_poison_error() -> Result<()> {
150        let lock = Mutex::new(());
151        std::thread::scope(|scope| {
152            let handle = scope.spawn(|| {
153                let _guard = lock.lock().expect("test lock should not be poisoned yet");
154                panic!("poison test mutex");
155            });
156            assert!(handle.join().is_err());
157        });
158
159        let _guard = lock.lock()?;
160        Ok(())
161    }
162
163    #[test]
164    fn poison_error_converts_to_other_variant() {
165        let err = convert_poison_error().expect_err("poisoned mutex should convert into an error");
166        assert!(matches!(err, EtherNetIpError::Other(message) if message == "lock poisoned"));
167    }
168}