Skip to main content

pamoja_modbus/
function.rs

1//! Modbus function codes and the exception codes a device returns when it refuses.
2
3/// A Modbus function code, naming the operation a request asks for.
4///
5/// This enum covers the function codes that read and write the four Modbus data tables
6/// (coils, discrete inputs, holding registers, input registers), which is what the great
7/// majority of field devices use. A function code outside this set still travels fine
8/// through [`Pdu::raw`](crate::Pdu::raw) and [`Adu`](crate::Adu); this enum is the typed
9/// view of the common ones, not a limit on what the framing carries.
10///
11/// # Examples
12///
13/// ```
14/// use pamoja_modbus::Function;
15///
16/// assert_eq!(Function::ReadHoldingRegisters.code(), 0x03);
17/// assert_eq!(Function::from_code(0x10), Some(Function::WriteMultipleRegisters));
18/// assert_eq!(Function::from_code(0x99), None);
19/// ```
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum Function {
22    /// Read one or more coils (read/write bits). Function code `0x01`.
23    ReadCoils,
24    /// Read one or more discrete inputs (read-only bits). Function code `0x02`.
25    ReadDiscreteInputs,
26    /// Read one or more holding registers (read/write 16-bit words). Function code `0x03`.
27    ReadHoldingRegisters,
28    /// Read one or more input registers (read-only 16-bit words). Function code `0x04`.
29    ReadInputRegisters,
30    /// Write a single coil. Function code `0x05`.
31    WriteSingleCoil,
32    /// Write a single holding register. Function code `0x06`.
33    WriteSingleRegister,
34    /// Write a contiguous block of coils. Function code `0x0F`.
35    WriteMultipleCoils,
36    /// Write a contiguous block of holding registers. Function code `0x10`.
37    WriteMultipleRegisters,
38}
39
40impl Function {
41    /// Returns the wire byte for this function.
42    ///
43    /// # Returns
44    ///
45    /// The function code as it appears as the first byte of a PDU.
46    pub fn code(self) -> u8 {
47        match self {
48            Function::ReadCoils => 0x01,
49            Function::ReadDiscreteInputs => 0x02,
50            Function::ReadHoldingRegisters => 0x03,
51            Function::ReadInputRegisters => 0x04,
52            Function::WriteSingleCoil => 0x05,
53            Function::WriteSingleRegister => 0x06,
54            Function::WriteMultipleCoils => 0x0F,
55            Function::WriteMultipleRegisters => 0x10,
56        }
57    }
58
59    /// Returns the function a wire byte names, if this crate models it.
60    ///
61    /// # Arguments
62    ///
63    /// * `code` - the function code byte from the start of a PDU.
64    ///
65    /// # Returns
66    ///
67    /// The matching [`Function`], or [`None`] for a code this enum does not name
68    /// (including the exception responses, whose high bit is set).
69    pub fn from_code(code: u8) -> Option<Function> {
70        match code {
71            0x01 => Some(Function::ReadCoils),
72            0x02 => Some(Function::ReadDiscreteInputs),
73            0x03 => Some(Function::ReadHoldingRegisters),
74            0x04 => Some(Function::ReadInputRegisters),
75            0x05 => Some(Function::WriteSingleCoil),
76            0x06 => Some(Function::WriteSingleRegister),
77            0x0F => Some(Function::WriteMultipleCoils),
78            0x10 => Some(Function::WriteMultipleRegisters),
79            _ => None,
80        }
81    }
82}
83
84/// A Modbus exception code: the reason a device gives for refusing a request.
85///
86/// A device that cannot serve a request replies with the request's function code with
87/// its high bit set, followed by one of these codes. [`Adu::exception`](crate::Adu::exception)
88/// and [`Response::exception`](crate::Response::exception) surface it.
89///
90/// # Examples
91///
92/// ```
93/// use pamoja_modbus::Exception;
94///
95/// assert_eq!(Exception::IllegalDataAddress.code(), 0x02);
96/// assert_eq!(Exception::from_code(0x01), Some(Exception::IllegalFunction));
97/// ```
98#[derive(Clone, Copy, Debug, PartialEq, Eq)]
99pub enum Exception {
100    /// The function code is not allowed for this device. Exception code `0x01`.
101    IllegalFunction,
102    /// The data address is not allowed for this device. Exception code `0x02`.
103    IllegalDataAddress,
104    /// A value in the request is not allowed for this device. Exception code `0x03`.
105    IllegalDataValue,
106    /// The device failed while serving the request. Exception code `0x04`.
107    ServerDeviceFailure,
108    /// The device accepted a long-running request and is still processing it. Exception code `0x05`.
109    Acknowledge,
110    /// The device is busy with a long-running request; retry later. Exception code `0x06`.
111    ServerDeviceBusy,
112    /// The device detected a parity error in its memory. Exception code `0x08`.
113    MemoryParityError,
114    /// A gateway could not route the request to the target path. Exception code `0x0A`.
115    GatewayPathUnavailable,
116    /// A gateway got no response from the target device, which usually means the device is
117    /// not on the network. Exception code `0x0B`.
118    GatewayTargetFailedToRespond,
119}
120
121impl Exception {
122    /// Returns the wire byte for this exception.
123    ///
124    /// # Returns
125    ///
126    /// The exception code as it appears after the function code in an exception response.
127    pub fn code(self) -> u8 {
128        match self {
129            Exception::IllegalFunction => 0x01,
130            Exception::IllegalDataAddress => 0x02,
131            Exception::IllegalDataValue => 0x03,
132            Exception::ServerDeviceFailure => 0x04,
133            Exception::Acknowledge => 0x05,
134            Exception::ServerDeviceBusy => 0x06,
135            Exception::MemoryParityError => 0x08,
136            Exception::GatewayPathUnavailable => 0x0A,
137            Exception::GatewayTargetFailedToRespond => 0x0B,
138        }
139    }
140
141    /// Returns the exception a wire byte names, if it is a defined code.
142    ///
143    /// # Arguments
144    ///
145    /// * `code` - the exception code byte following the function code.
146    ///
147    /// # Returns
148    ///
149    /// The matching [`Exception`], or [`None`] for a code this enum does not name.
150    pub fn from_code(code: u8) -> Option<Exception> {
151        match code {
152            0x01 => Some(Exception::IllegalFunction),
153            0x02 => Some(Exception::IllegalDataAddress),
154            0x03 => Some(Exception::IllegalDataValue),
155            0x04 => Some(Exception::ServerDeviceFailure),
156            0x05 => Some(Exception::Acknowledge),
157            0x06 => Some(Exception::ServerDeviceBusy),
158            0x08 => Some(Exception::MemoryParityError),
159            0x0A => Some(Exception::GatewayPathUnavailable),
160            0x0B => Some(Exception::GatewayTargetFailedToRespond),
161            _ => None,
162        }
163    }
164}
165
166/// Writes the exception's name as the application protocol specification gives it, in lower
167/// case: `illegal data address` for `0x02`.
168impl core::fmt::Display for Exception {
169    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
170        f.write_str(match self {
171            Exception::IllegalFunction => "illegal function",
172            Exception::IllegalDataAddress => "illegal data address",
173            Exception::IllegalDataValue => "illegal data value",
174            Exception::ServerDeviceFailure => "server device failure",
175            Exception::Acknowledge => "acknowledge",
176            Exception::ServerDeviceBusy => "server device busy",
177            Exception::MemoryParityError => "memory parity error",
178            Exception::GatewayPathUnavailable => "gateway path unavailable",
179            Exception::GatewayTargetFailedToRespond => "gateway target device failed to respond",
180        })
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn function_codes_round_trip() {
190        for function in [
191            Function::ReadCoils,
192            Function::ReadDiscreteInputs,
193            Function::ReadHoldingRegisters,
194            Function::ReadInputRegisters,
195            Function::WriteSingleCoil,
196            Function::WriteSingleRegister,
197            Function::WriteMultipleCoils,
198            Function::WriteMultipleRegisters,
199        ] {
200            assert_eq!(Function::from_code(function.code()), Some(function));
201        }
202    }
203
204    #[test]
205    fn an_exception_response_byte_is_not_a_function() {
206        // 0x83 is read-holding-registers (0x03) with the exception bit set.
207        assert_eq!(Function::from_code(0x83), None);
208    }
209
210    #[test]
211    fn exception_codes_round_trip() {
212        for exception in [
213            Exception::IllegalFunction,
214            Exception::IllegalDataAddress,
215            Exception::IllegalDataValue,
216            Exception::ServerDeviceFailure,
217            Exception::Acknowledge,
218            Exception::ServerDeviceBusy,
219            Exception::MemoryParityError,
220            Exception::GatewayPathUnavailable,
221            Exception::GatewayTargetFailedToRespond,
222        ] {
223            assert_eq!(Exception::from_code(exception.code()), Some(exception));
224        }
225    }
226
227    #[test]
228    fn an_undefined_exception_code_is_none() {
229        assert_eq!(Exception::from_code(0x07), None);
230    }
231}