#![allow(deprecated)]
#[cfg(not(feature = "std"))]
use alloc::string::String;
#[cfg(feature = "std")]
use thiserror::Error;
#[cfg(not(feature = "std"))]
use core::fmt;
pub type ModbusResult<T> = Result<T, ModbusError>;
#[cfg_attr(feature = "std", derive(Error))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[derive(Debug, Clone, PartialEq)]
pub enum ModbusError {
#[cfg_attr(feature = "std", error("I/O error: {message}"))]
Io { message: String },
#[cfg_attr(feature = "std", error("Connection error: {message}"))]
Connection { message: String },
#[cfg_attr(feature = "std", error("Timeout after {timeout_ms}ms: {operation}"))]
Timeout { operation: String, timeout_ms: u64 },
#[cfg_attr(feature = "std", error("Protocol error: {message}"))]
Protocol { message: String },
#[cfg_attr(feature = "std", error("Invalid function code: {code}"))]
InvalidFunction { code: u8 },
#[cfg_attr(
feature = "std",
error("Invalid address: start={start}, count={count}")
)]
InvalidAddress { start: u16, count: u16 },
#[cfg_attr(feature = "std", error("Invalid data: {message}"))]
InvalidData { message: String },
#[cfg_attr(
feature = "std",
error("CRC validation failed: expected={expected:04X}, actual={actual:04X}")
)]
CrcMismatch { expected: u16, actual: u16 },
#[cfg_attr(
feature = "std",
error("Modbus exception: function={function:02X}, code={code:02X} ({message})")
)]
Exception {
function: u8,
code: u8,
message: &'static str,
},
#[cfg_attr(feature = "std", error("Frame error: {message}"))]
Frame { message: String },
#[cfg_attr(feature = "std", error("Configuration error: {message}"))]
Configuration { message: String },
#[cfg_attr(feature = "std", error("Device {slave_id} not responding"))]
DeviceNotResponding { slave_id: u8 },
#[cfg_attr(
feature = "std",
error("Transaction ID mismatch: expected={expected:04X}, actual={actual:04X}")
)]
TransactionIdMismatch { expected: u16, actual: u16 },
#[cfg_attr(feature = "std", error("Internal error: {message}"))]
Internal { message: String },
#[cfg_attr(feature = "std", error("Timeout"))]
#[deprecated(note = "Use Timeout with operation and timeout_ms fields")]
TimeoutLegacy,
#[cfg_attr(feature = "std", error("Invalid frame"))]
#[deprecated(note = "Use Frame with message field")]
InvalidFrame,
#[cfg_attr(feature = "std", error("Invalid data value"))]
#[deprecated(note = "Use InvalidData with message field")]
InvalidDataValue,
#[cfg_attr(feature = "std", error("Illegal function"))]
#[deprecated(note = "Use InvalidFunction with code field")]
IllegalFunction,
#[cfg_attr(feature = "std", error("Internal error"))]
#[deprecated(note = "Use Internal with message field")]
InternalError,
}
#[cfg(not(feature = "std"))]
impl fmt::Display for ModbusError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io { message } => write!(f, "I/O error: {}", message),
Self::Connection { message } => write!(f, "Connection error: {}", message),
Self::Timeout {
operation,
timeout_ms,
} => write!(f, "Timeout after {}ms: {}", timeout_ms, operation),
Self::Protocol { message } => write!(f, "Protocol error: {}", message),
Self::InvalidFunction { code } => write!(f, "Invalid function code: {}", code),
Self::InvalidAddress { start, count } => {
write!(f, "Invalid address: start={}, count={}", start, count)
}
Self::InvalidData { message } => write!(f, "Invalid data: {}", message),
Self::CrcMismatch { expected, actual } => write!(
f,
"CRC validation failed: expected={:04X}, actual={:04X}",
expected, actual
),
Self::Exception {
function,
code,
message,
} => write!(
f,
"Modbus exception: function={:02X}, code={:02X} ({})",
function, code, message
),
Self::Frame { message } => write!(f, "Frame error: {}", message),
Self::Configuration { message } => write!(f, "Configuration error: {}", message),
Self::DeviceNotResponding { slave_id } => {
write!(f, "Device {} not responding", slave_id)
}
Self::TransactionIdMismatch { expected, actual } => write!(
f,
"Transaction ID mismatch: expected={:04X}, actual={:04X}",
expected, actual
),
Self::Internal { message } => write!(f, "Internal error: {}", message),
#[allow(deprecated)]
Self::TimeoutLegacy => write!(f, "Timeout"),
#[allow(deprecated)]
Self::InvalidFrame => write!(f, "Invalid frame"),
#[allow(deprecated)]
Self::InvalidDataValue => write!(f, "Invalid data value"),
#[allow(deprecated)]
Self::IllegalFunction => write!(f, "Illegal function"),
#[allow(deprecated)]
Self::InternalError => write!(f, "Internal error"),
}
}
}
#[cfg(not(feature = "std"))]
impl core::error::Error for ModbusError {}
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 { .. }
)
}
}
#[cfg(feature = "std")]
impl From<std::io::Error> for ModbusError {
fn from(err: std::io::Error) -> Self {
Self::io(err.to_string())
}
}
#[cfg(feature = "std")]
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"));
}
}