Skip to main content

oms_modbus/
error.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//!
3//! Modbus error types and message constants.
4
5use std::fmt;
6
7// ── ModbusError label strings (used by label(), Display, detail()) ────
8
9pub const ERR_OK: &str = "OK";
10pub const ERR_TIMEOUT: &str = "TIMEOUT";
11pub const ERR_CONNECTION: &str = "CONNECTION ERROR";
12pub const ERR_PROTOCOL: &str = "PROTOCOL ERROR";
13pub const ERR_EXCEPTION: &str = "MODBUS EXCEPTION";
14pub const ERR_SERIAL: &str = "PORT ERROR";
15pub const ERR_OTHER: &str = "ERROR";
16
17// ── Diagnostic prefixes (used by detail()) ──────────────────────────────
18
19pub const TIMEOUT_PREFIX: &str = "TIMEOUT:";
20pub const CONNECTION_PREFIX: &str = "CONNECTION ERROR:";
21pub const PROTOCOL_PREFIX: &str = "PROTOCOL ERROR:";
22pub const PORT_PREFIX: &str = "PORT ERROR:";
23
24// ── Exception names ─────────────────────────────────────────────────────
25
26pub const EXN_ILLEGAL_FUNCTION: &str = "Illegal Function";
27pub const EXN_ILLEGAL_DATA_ADDRESS: &str = "Illegal Data Address";
28pub const EXN_ILLEGAL_DATA_VALUE: &str = "Illegal Data Value";
29pub const EXN_SERVER_DEVICE_FAILURE: &str = "Server Device Failure";
30pub const EXN_ACKNOWLEDGE: &str = "Acknowledge";
31pub const EXN_SERVER_DEVICE_BUSY: &str = "Server Device Busy";
32pub const EXN_NEGATIVE_ACKNOWLEDGE: &str = "Negative Acknowledge";
33pub const EXN_MEMORY_PARITY_ERROR: &str = "Memory Parity Error";
34pub const EXN_GATEWAY_PATH_UNAVAILABLE: &str = "Gateway Path Unavailable";
35pub const EXN_GATEWAY_TARGET_FAILED: &str = "Gateway Target Device Failed to Respond";
36pub const EXN_UNKNOWN: &str = "Unknown Exception";
37
38// ── Transport messages (used by TransportOps) ───────────────────────────
39
40/// Generic send timeout — used by shared `send_frame` across all transports.
41pub const SEND_TIMEOUT: &str = "send timed out";
42/// Generic receive timeout — used by shared `read_at_least` across all transports.
43pub const RECV_TIMEOUT: &str = "recv timed out";
44
45pub const TCP_SEND_TIMEOUT: &str = "TCP send timed out";
46pub const TCP_RECV_TIMEOUT: &str = "TCP recv timed out";
47pub const TCP_SEND_ERROR: &str = "TCP send:";
48pub const TCP_RECV_ERROR: &str = "TCP recv:";
49pub const CONN_CLOSED: &str = "connection closed";
50pub const TCP_EMPTY_RESP: &str = "empty TCP response PDU";
51
52pub const RTU_SEND_TIMEOUT: &str = "RTU send timed out";
53pub const RTU_RECV_TIMEOUT: &str = "RTU recv timed out";
54pub const RTU_SEND_ERROR: &str = "RTU send:";
55pub const RTU_RECV_ERROR: &str = "RTU recv:";
56pub const RTU_EMPTY_RESP: &str = "empty RTU response";
57
58pub const ASCII_SEND_TIMEOUT: &str = "ASCII send timed out";
59pub const ASCII_RECV_TIMEOUT: &str = "ASCII recv timed out";
60pub const ASCII_SEND_ERROR: &str = "ASCII send:";
61pub const ASCII_RECV_ERROR: &str = "ASCII recv:";
62pub const ASCII_EMPTY_RESP: &str = "empty ASCII response";
63
64pub const PDU_ENCODE_ERROR: &str = "PDU encode:";
65pub const PDU_DECODE_ERROR: &str = "PDU decode:";
66pub const SLAVE_ID_MISMATCH: &str = "slave ID mismatch: expected";
67
68/// Structured Modbus / transport error — no string parsing needed.
69///
70/// Use `Display` for short UI messages and
71/// [`detail`](ModbusError::detail) for full diagnostic logs.
72///
73/// # Short label for UI
74///
75/// Use [`label`](ModbusError::label) for a short human-readable tag
76/// suitable for display in a status bar or error column.
77///
78/// # Example
79///
80/// ```
81/// use oms_modbus::ModbusError;
82///
83/// // Match on specific error kinds
84/// let result: Result<Vec<u16>, ModbusError> = Err(ModbusError::timeout("RTU recv timed out"));
85///
86/// match &result {
87///     Err(ModbusError::Timeout(_)) => println!("Retry or re-connect"),
88///     Err(ModbusError::Exception { function: _, code: _ }) => println!("Modbus exception"),
89///     Err(e) => eprintln!("{} — {}", e.label(), e.detail()),
90///     Ok(_) => {}
91/// }
92/// ```
93#[derive(Clone, Debug, PartialEq)]
94#[non_exhaustive]
95pub enum ModbusError {
96    /// No error — normal/expected response.
97    NoError,
98    Timeout(String),
99    Connection(String),
100    Protocol(String),
101    Exception {
102        function: u8,
103        code: u8,
104    },
105    Serial(String),
106    Other(String),
107}
108
109/// Human-readable description for a Modbus exception code.
110fn exception_name(code: u8) -> &'static str {
111    match code {
112        1 => EXN_ILLEGAL_FUNCTION,
113        2 => EXN_ILLEGAL_DATA_ADDRESS,
114        3 => EXN_ILLEGAL_DATA_VALUE,
115        4 => EXN_SERVER_DEVICE_FAILURE,
116        5 => EXN_ACKNOWLEDGE,
117        6 => EXN_SERVER_DEVICE_BUSY,
118        7 => EXN_NEGATIVE_ACKNOWLEDGE,
119        8 => EXN_MEMORY_PARITY_ERROR,
120        10 => EXN_GATEWAY_PATH_UNAVAILABLE,
121        11 => EXN_GATEWAY_TARGET_FAILED,
122        _ => EXN_UNKNOWN,
123    }
124}
125
126impl fmt::Display for ModbusError {
127    /// Short summary for UI display. Delegates to [`label`](ModbusError::label)
128    /// for simple variants; adds context for `Exception` and `Other`.
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        match self {
131            ModbusError::Exception { function: _, code } => {
132                write!(f, "{} (code={})", exception_name(*code), code)
133            }
134            ModbusError::Other(m) => write!(f, "{m}"),
135            _ => write!(f, "{}", self.label()),
136        }
137    }
138}
139
140impl ModbusError {
141    /// Full diagnostic message using centralized message prefixes.
142    /// Use this for log output; `Display` gives a short summary for UI.
143    pub fn detail(&self) -> String {
144        match self {
145            ModbusError::NoError => "OK".to_string(),
146            ModbusError::Timeout(m) => format!("{TIMEOUT_PREFIX} {m}"),
147            ModbusError::Connection(m) => format!("{CONNECTION_PREFIX} {m}"),
148            ModbusError::Protocol(m) => format!("{PROTOCOL_PREFIX} {m}"),
149            ModbusError::Exception { function, code } => {
150                let ex_name = exception_name(*code);
151                format!("{ex_name} (FC={function}, code={code})")
152            }
153            ModbusError::Serial(m) => format!("{PORT_PREFIX} {m}"),
154            ModbusError::Other(m) => m.clone(),
155        }
156    }
157
158    /// Short label for UI display (status bar, error column).
159    pub fn label(&self) -> &'static str {
160        match self {
161            ModbusError::NoError => ERR_OK,
162            ModbusError::Timeout(_) => ERR_TIMEOUT,
163            ModbusError::Connection(_) => ERR_CONNECTION,
164            ModbusError::Protocol(_) => ERR_PROTOCOL,
165            ModbusError::Exception { .. } => ERR_EXCEPTION,
166            ModbusError::Serial(_) => ERR_SERIAL,
167            ModbusError::Other(_) => ERR_OTHER,
168        }
169    }
170
171    /// Create a successful (no-error) result. `label()` returns `"OK"`.
172    pub const fn no_error() -> Self {
173        ModbusError::NoError
174    }
175    /// Create a timeout error. Covers send and recv timeouts.
176    pub fn timeout(msg: impl Into<String>) -> Self {
177        ModbusError::Timeout(msg.into())
178    }
179    /// Create a connection error. Covers TCP connect/read/write failures.
180    pub fn connection(msg: impl Into<String>) -> Self {
181        ModbusError::Connection(msg.into())
182    }
183    /// Create a protocol error. Covers CRC mismatch, slave ID mismatch, invalid PDU.
184    pub fn protocol(msg: impl Into<String>) -> Self {
185        ModbusError::Protocol(msg.into())
186    }
187    /// Create a Modbus exception error from a server response.
188    pub fn exception(function: u8, code: u8) -> Self {
189        ModbusError::Exception { function, code }
190    }
191    /// Create a serial port error. Covers port open/read/write failures.
192    pub fn serial(msg: impl Into<String>) -> Self {
193        ModbusError::Serial(msg.into())
194    }
195    /// Create a miscellaneous error. Used for uncategorized failures.
196    pub fn other(msg: impl Into<String>) -> Self {
197        ModbusError::Other(msg.into())
198    }
199}
200
201impl From<String> for ModbusError {
202    fn from(s: String) -> Self {
203        ModbusError::Other(s)
204    }
205}
206
207impl From<std::io::Error> for ModbusError {
208    fn from(e: std::io::Error) -> Self {
209        use std::io::ErrorKind;
210        match e.kind() {
211            ErrorKind::TimedOut | ErrorKind::WouldBlock => ModbusError::timeout(e.to_string()),
212            ErrorKind::ConnectionRefused
213            | ErrorKind::ConnectionReset
214            | ErrorKind::ConnectionAborted
215            | ErrorKind::BrokenPipe
216            | ErrorKind::NotConnected => ModbusError::connection(e.to_string()),
217            _ => ModbusError::Other(e.to_string()),
218        }
219    }
220}
221
222impl std::error::Error for ModbusError {}