use thiserror::Error;
pub type ModbusResult<T> = Result<T, ModbusError>;
#[derive(Error, Debug, Clone, PartialEq)]
pub enum ModbusError {
#[error("I/O error: {message}")]
Io { message: String },
#[error("Connection error: {message}")]
Connection { message: String },
#[error("Timeout after {timeout_ms}ms: {operation}")]
Timeout { operation: String, timeout_ms: u64 },
#[error("Protocol error: {message}")]
Protocol { message: String },
#[error("Invalid function code: {code}")]
InvalidFunction { code: u8 },
#[error("Invalid address: start={start}, count={count}")]
InvalidAddress { start: u16, count: u16 },
#[error("Invalid data: {message}")]
InvalidData { message: String },
#[error("CRC validation failed: expected={expected:04X}, actual={actual:04X}")]
CrcMismatch { expected: u16, actual: u16 },
#[error("Modbus exception: function={function:02X}, code={code:02X} ({message})")]
Exception {
function: u8,
code: u8,
message: &'static str,
},
#[error("Frame error: {message}")]
Frame { message: String },
#[error("Configuration error: {message}")]
Configuration { message: String },
#[error("Device {slave_id} not responding")]
DeviceNotResponding { slave_id: u8 },
#[error("Transaction ID mismatch: expected={expected:04X}, actual={actual:04X}")]
TransactionIdMismatch { expected: u16, actual: u16 },
#[error("Internal error: {message}")]
Internal { message: String },
#[error("Timeout")]
#[deprecated(note = "Use Timeout with operation and timeout_ms fields")]
TimeoutLegacy,
#[error("Invalid frame")]
#[deprecated(note = "Use Frame with message field")]
InvalidFrame,
#[error("Invalid data value")]
#[deprecated(note = "Use InvalidData with message field")]
InvalidDataValue,
#[error("Illegal function")]
#[deprecated(note = "Use InvalidFunction with code field")]
IllegalFunction,
#[error("Internal error")]
#[deprecated(note = "Use Internal with message field")]
InternalError,
}
impl ModbusError {
pub fn io<S: Into<String>>(message: S) -> Self {
Self::Io {
message: message.into(),
}
}
pub fn connection<S: Into<String>>(message: S) -> Self {
Self::Connection {
message: message.into(),
}
}
pub fn timeout<S: Into<String>>(operation: S, timeout_ms: u64) -> Self {
Self::Timeout {
operation: operation.into(),
timeout_ms,
}
}
pub fn protocol<S: Into<String>>(message: S) -> Self {
Self::Protocol {
message: message.into(),
}
}
pub fn invalid_function(code: u8) -> Self {
Self::InvalidFunction { code }
}
pub fn invalid_address(start: u16, count: u16) -> Self {
Self::InvalidAddress { start, count }
}
pub fn invalid_data<S: Into<String>>(message: S) -> Self {
Self::InvalidData {
message: message.into(),
}
}
pub fn crc_mismatch(expected: u16, actual: u16) -> Self {
Self::CrcMismatch { expected, actual }
}
pub fn exception(function: u8, code: u8) -> Self {
let message: &'static str = match code {
0x01 => "Illegal Function",
0x02 => "Illegal Data Address",
0x03 => "Illegal Data Value",
0x04 => "Slave Device Failure",
0x05 => "Acknowledge",
0x06 => "Slave Device Busy",
0x08 => "Memory Parity Error",
0x0A => "Gateway Path Unavailable",
0x0B => "Gateway Target Device Failed to Respond",
_ => "Unknown Exception",
};
Self::Exception {
function,
code,
message,
}
}
pub fn frame<S: Into<String>>(message: S) -> Self {
Self::Frame {
message: message.into(),
}
}
pub fn configuration<S: Into<String>>(message: S) -> Self {
Self::Configuration {
message: message.into(),
}
}
pub fn device_not_responding(slave_id: u8) -> Self {
Self::DeviceNotResponding { slave_id }
}
pub fn transaction_id_mismatch(expected: u16, actual: u16) -> Self {
Self::TransactionIdMismatch { expected, actual }
}
pub fn internal<S: Into<String>>(message: S) -> Self {
Self::Internal {
message: message.into(),
}
}
pub fn is_recoverable(&self) -> bool {
match self {
Self::Io { .. } => true,
Self::Connection { .. } => true,
Self::Timeout { .. } => true,
Self::DeviceNotResponding { .. } => true,
Self::TransactionIdMismatch { .. } => true, Self::Exception { code, .. } => {
matches!(code, 0x05 | 0x06) }
_ => false,
}
}
pub fn is_transport_error(&self) -> bool {
matches!(
self,
Self::Io { .. } | Self::Connection { .. } | Self::Timeout { .. }
)
}
pub fn is_protocol_error(&self) -> bool {
matches!(
self,
Self::Protocol { .. }
| Self::InvalidFunction { .. }
| Self::Exception { .. }
| Self::Frame { .. }
| Self::CrcMismatch { .. }
| Self::TransactionIdMismatch { .. }
)
}
}
impl From<std::io::Error> for ModbusError {
fn from(err: std::io::Error) -> Self {
Self::io(err.to_string())
}
}
impl From<tokio::time::error::Elapsed> for ModbusError {
fn from(_: tokio::time::error::Elapsed) -> Self {
Self::timeout("Operation timeout", 0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_creation() {
let err = ModbusError::timeout("read_registers", 5000);
assert!(err.is_recoverable());
assert!(err.is_transport_error());
let err = ModbusError::exception(0x03, 0x02);
assert!(!err.is_recoverable());
assert!(err.is_protocol_error());
}
#[test]
fn test_error_display() {
let err = ModbusError::crc_mismatch(0x1234, 0x5678);
let msg = format!("{}", err);
assert!(msg.contains("CRC validation failed"));
assert!(msg.contains("1234"));
assert!(msg.contains("5678"));
}
}