Skip to main content

rustmod_core/pdu/
exception.rs

1use crate::encoding::{Reader, Writer};
2use crate::{DecodeError, EncodeError};
3
4/// Modbus exception codes returned by a device to indicate an error.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6#[cfg_attr(feature = "defmt", derive(defmt::Format))]
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
8#[non_exhaustive]
9pub enum ExceptionCode {
10    /// 0x01 — The function code is not supported.
11    IllegalFunction,
12    /// 0x02 — The data address is not valid.
13    IllegalDataAddress,
14    /// 0x03 — The data value is not valid.
15    IllegalDataValue,
16    /// 0x04 — An unrecoverable error occurred on the server.
17    ServerDeviceFailure,
18    /// 0x05 — The request has been accepted but will take time to process.
19    Acknowledge,
20    /// 0x06 — The server is busy processing another request.
21    ServerDeviceBusy,
22    /// 0x08 — Memory parity error detected.
23    MemoryParityError,
24    /// 0x0A — The gateway path is not available.
25    GatewayPathUnavailable,
26    /// 0x0B — The gateway target device failed to respond.
27    GatewayTargetFailedToRespond,
28    /// An exception code not defined in the standard.
29    Unknown(u8),
30}
31
32impl ExceptionCode {
33    /// Parse an exception code from its wire byte value.
34    pub const fn from_u8(value: u8) -> Self {
35        match value {
36            0x01 => Self::IllegalFunction,
37            0x02 => Self::IllegalDataAddress,
38            0x03 => Self::IllegalDataValue,
39            0x04 => Self::ServerDeviceFailure,
40            0x05 => Self::Acknowledge,
41            0x06 => Self::ServerDeviceBusy,
42            0x08 => Self::MemoryParityError,
43            0x0A => Self::GatewayPathUnavailable,
44            0x0B => Self::GatewayTargetFailedToRespond,
45            other => Self::Unknown(other),
46        }
47    }
48
49}
50
51impl core::fmt::Display for ExceptionCode {
52    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
53        match self {
54            Self::IllegalFunction => write!(f, "illegal function (0x01)"),
55            Self::IllegalDataAddress => write!(f, "illegal data address (0x02)"),
56            Self::IllegalDataValue => write!(f, "illegal data value (0x03)"),
57            Self::ServerDeviceFailure => write!(f, "server device failure (0x04)"),
58            Self::Acknowledge => write!(f, "acknowledge (0x05)"),
59            Self::ServerDeviceBusy => write!(f, "server device busy (0x06)"),
60            Self::MemoryParityError => write!(f, "memory parity error (0x08)"),
61            Self::GatewayPathUnavailable => write!(f, "gateway path unavailable (0x0A)"),
62            Self::GatewayTargetFailedToRespond => {
63                write!(f, "gateway target failed to respond (0x0B)")
64            }
65            Self::Unknown(code) => write!(f, "unknown exception (0x{code:02X})"),
66        }
67    }
68}
69
70impl ExceptionCode {
71    /// Return the wire byte value for this exception code.
72    pub const fn as_u8(self) -> u8 {
73        match self {
74            Self::IllegalFunction => 0x01,
75            Self::IllegalDataAddress => 0x02,
76            Self::IllegalDataValue => 0x03,
77            Self::ServerDeviceFailure => 0x04,
78            Self::Acknowledge => 0x05,
79            Self::ServerDeviceBusy => 0x06,
80            Self::MemoryParityError => 0x08,
81            Self::GatewayPathUnavailable => 0x0A,
82            Self::GatewayTargetFailedToRespond => 0x0B,
83            Self::Unknown(raw) => raw,
84        }
85    }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89#[cfg_attr(feature = "defmt", derive(defmt::Format))]
90#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
91/// A Modbus exception response (function code with bit 7 set + exception code).
92pub struct ExceptionResponse {
93    /// The original function code (without the exception bit).
94    pub function_code: u8,
95    /// The exception code describing the error.
96    pub exception_code: ExceptionCode,
97}
98
99impl core::fmt::Display for ExceptionResponse {
100    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
101        write!(
102            f,
103            "exception on FC 0x{:02X}: {}",
104            self.function_code, self.exception_code
105        )
106    }
107}
108
109impl ExceptionResponse {
110    pub fn encode(&self, w: &mut Writer<'_>) -> Result<(), EncodeError> {
111        w.write_u8(self.function_code | 0x80)?;
112        w.write_u8(self.exception_code.as_u8())?;
113        Ok(())
114    }
115
116    pub fn decode(function_byte: u8, r: &mut Reader<'_>) -> Result<Self, DecodeError> {
117        if (function_byte & 0x80) == 0 {
118            return Err(DecodeError::InvalidFunctionCode);
119        }
120        let exception = r.read_u8()?;
121        Ok(Self {
122            function_code: function_byte & 0x7F,
123            exception_code: ExceptionCode::from_u8(exception),
124        })
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::{ExceptionCode, ExceptionResponse};
131    use crate::encoding::{Reader, Writer};
132
133    #[test]
134    fn roundtrip_exception_response() {
135        let mut buf = [0u8; 2];
136        let mut w = Writer::new(&mut buf);
137        let resp = ExceptionResponse {
138            function_code: 0x03,
139            exception_code: ExceptionCode::ServerDeviceBusy,
140        };
141        resp.encode(&mut w).unwrap();
142        assert_eq!(w.as_written(), &[0x83, 0x06]);
143
144        let mut r = Reader::new(w.as_written());
145        let fc = r.read_u8().unwrap();
146        let decoded = ExceptionResponse::decode(fc, &mut r).unwrap();
147        assert_eq!(decoded, resp);
148    }
149
150    #[test]
151    fn preserves_unknown_exception_codes() {
152        let mut r = Reader::new(&[0x11]);
153        let decoded = ExceptionResponse::decode(0x83, &mut r).unwrap();
154        assert_eq!(decoded.exception_code, ExceptionCode::Unknown(0x11));
155    }
156}