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}