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 reached the target device but got no response. Exception code `0x0B`.
117 GatewayTargetFailedToRespond,
118}
119
120impl Exception {
121 /// Returns the wire byte for this exception.
122 ///
123 /// # Returns
124 ///
125 /// The exception code as it appears after the function code in an exception response.
126 pub fn code(self) -> u8 {
127 match self {
128 Exception::IllegalFunction => 0x01,
129 Exception::IllegalDataAddress => 0x02,
130 Exception::IllegalDataValue => 0x03,
131 Exception::ServerDeviceFailure => 0x04,
132 Exception::Acknowledge => 0x05,
133 Exception::ServerDeviceBusy => 0x06,
134 Exception::MemoryParityError => 0x08,
135 Exception::GatewayPathUnavailable => 0x0A,
136 Exception::GatewayTargetFailedToRespond => 0x0B,
137 }
138 }
139
140 /// Returns the exception a wire byte names, if it is a defined code.
141 ///
142 /// # Arguments
143 ///
144 /// * `code` - the exception code byte following the function code.
145 ///
146 /// # Returns
147 ///
148 /// The matching [`Exception`], or [`None`] for a code this enum does not name.
149 pub fn from_code(code: u8) -> Option<Exception> {
150 match code {
151 0x01 => Some(Exception::IllegalFunction),
152 0x02 => Some(Exception::IllegalDataAddress),
153 0x03 => Some(Exception::IllegalDataValue),
154 0x04 => Some(Exception::ServerDeviceFailure),
155 0x05 => Some(Exception::Acknowledge),
156 0x06 => Some(Exception::ServerDeviceBusy),
157 0x08 => Some(Exception::MemoryParityError),
158 0x0A => Some(Exception::GatewayPathUnavailable),
159 0x0B => Some(Exception::GatewayTargetFailedToRespond),
160 _ => None,
161 }
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn function_codes_round_trip() {
171 for function in [
172 Function::ReadCoils,
173 Function::ReadDiscreteInputs,
174 Function::ReadHoldingRegisters,
175 Function::ReadInputRegisters,
176 Function::WriteSingleCoil,
177 Function::WriteSingleRegister,
178 Function::WriteMultipleCoils,
179 Function::WriteMultipleRegisters,
180 ] {
181 assert_eq!(Function::from_code(function.code()), Some(function));
182 }
183 }
184
185 #[test]
186 fn an_exception_response_byte_is_not_a_function() {
187 // 0x83 is read-holding-registers (0x03) with the exception bit set.
188 assert_eq!(Function::from_code(0x83), None);
189 }
190
191 #[test]
192 fn exception_codes_round_trip() {
193 for exception in [
194 Exception::IllegalFunction,
195 Exception::IllegalDataAddress,
196 Exception::IllegalDataValue,
197 Exception::ServerDeviceFailure,
198 Exception::Acknowledge,
199 Exception::ServerDeviceBusy,
200 Exception::MemoryParityError,
201 Exception::GatewayPathUnavailable,
202 Exception::GatewayTargetFailedToRespond,
203 ] {
204 assert_eq!(Exception::from_code(exception.code()), Some(exception));
205 }
206 }
207
208 #[test]
209 fn an_undefined_exception_code_is_none() {
210 assert_eq!(Exception::from_code(0x07), None);
211 }
212}