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 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}