fennec_modbus/protocol.rs
1//! The lowest protocol level.
2//!
3//! It operates with PDU's and independent of any transport.
4//! If you're implementing transport like PDU, you're going to need this module:
5//!
6//! - **Data units** are the PDU's that you're going to wrap into your transport.
7//! - **Functions** are the actual Modbus functions expressed in terms of function code,
8//! request arguments and output.
9
10pub mod address;
11pub mod codec;
12pub mod function;
13
14use bytes::{Buf, BufMut};
15
16use crate::{
17 Error,
18 protocol::{
19 codec::{Decode, Encode},
20 function::IntoValue,
21 },
22};
23
24/// Request Protocol Data Unit.
25#[derive(Copy, Clone)]
26pub struct Request<A> {
27 /// Modbus function code.
28 pub function_code: u8,
29
30 /// Function-dependent arguments that follow the function code.
31 pub args: A,
32}
33
34impl<A> Request<A> {
35 /// Wrap the function arguments into PDU.
36 pub const fn wrap<F: Function<Args = A>>(args: A) -> Self {
37 Self { function_code: F::CODE, args }
38 }
39}
40
41impl<A: Encode> Encode for Request<A> {
42 fn encode(&self, to: &mut impl BufMut) {
43 to.put_u8(self.function_code);
44 self.args.encode(to);
45 }
46}
47
48/// Response Protocol Data Unit.
49#[derive(Copy, Clone)]
50pub enum Response<F: Function> {
51 /// Successful response.
52 Ok(F::Output),
53
54 /// The connection is healthy, but the response is a Modbus exception.
55 Exception(Exception),
56}
57
58impl<F: Function> Decode for Response<F> {
59 fn decode(from: &mut impl Buf) -> Result<Self, Error> {
60 match from.try_get_u8()? {
61 function_code if function_code == F::CODE => Ok(Self::Ok(F::Output::decode(from)?)),
62 function_code if function_code == (F::CODE | 0x80) => {
63 Ok(Self::Exception(Exception::decode(from)?))
64 }
65 function_code => Err(Error::UnexpectedFunctionCode(function_code)),
66 }
67 }
68}
69
70impl<F: Function> Response<F> {
71 pub fn into_result(self) -> Result<F::Output, Error> {
72 match self {
73 Self::Ok(output) => Ok(output),
74 Self::Exception(exception) => Err(Error::Exception(exception)),
75 }
76 }
77}
78
79/// High-level protocol error.
80///
81/// The server received the request without a communication error, but could not handle it.
82#[must_use]
83#[derive(Copy, Clone, Debug, thiserror::Error)]
84pub enum Exception {
85 /// The function code received in the query is not an allowable action for the server:
86 ///
87 /// - the function was not implemented in the unit selected;
88 /// - the server is in the wrong state to process a request of this type.
89 #[error("illegal function")]
90 IllegalFunction,
91
92 /// The data address received in the query is not an allowable address for the server.
93 ///
94 /// The combination of reference number and transfer length is invalid.
95 #[error("illegal data address")]
96 IllegalDataAddress,
97
98 /// A value contained in the query data field is not an allowable value for server.
99 #[error("illegal data value")]
100 IllegalDataValue,
101
102 /// An unrecoverable error occurred while the server was attempting to perform the requested action.
103 #[error("server device failure")]
104 ServerDeviceFailure,
105
106 /// The server has accepted the request and is processing it, but a long duration of time will be
107 /// required to do so.
108 ///
109 /// This response is returned to prevent a timeout error from occurring in the client.
110 /// The client can next issue a «Poll Program Complete» message to determine if processing is completed.
111 #[error("acknowledge")]
112 Acknowledge,
113
114 /// The server is engaged in processing a long–duration program command.
115 ///
116 /// The client should retransmit the message later when the server is free.
117 #[error("server device busy")]
118 ServerDeviceBusy,
119
120 /// The server attempted to read record file, but detected a parity error in the memory.
121 ///
122 /// The client can retry the request, but service may be required on the server device.
123 #[error("memory parity error")]
124 MemoryParityError,
125
126 /// The gateway was unable to allocate an internal communication path from the input port
127 /// to the output port for processing the request.
128 #[error("gateway path unavailable")]
129 GatewayPathUnavailable,
130
131 /// No response was obtained from the target device.
132 ///
133 /// Usually means that the device is not present on the network.
134 #[error("gateway target device failed to respond")]
135 GatewayTargetDeviceFailedToRespond,
136
137 /// Non-standard error code.
138 #[error("custom error ({0})")]
139 Custom(u8),
140}
141
142impl Decode for Exception {
143 fn decode(from: &mut impl Buf) -> Result<Self, Error> {
144 match from.try_get_u8()? {
145 0x01 => Ok(Self::IllegalFunction),
146 0x02 => Ok(Self::IllegalDataAddress),
147 0x03 => Ok(Self::IllegalDataValue),
148 0x04 => Ok(Self::ServerDeviceFailure),
149 0x05 => Ok(Self::Acknowledge),
150 0x06 => Ok(Self::ServerDeviceBusy),
151 0x08 => Ok(Self::MemoryParityError),
152 0x0A => Ok(Self::GatewayPathUnavailable),
153 0x0B => Ok(Self::GatewayTargetDeviceFailedToRespond),
154 exception_code => Ok(Self::Custom(exception_code)),
155 }
156 }
157}
158
159/// Trait that ties function code, arguments and output together.
160///
161/// Users are free to implement their own functions – be that custom Modbus functions
162/// or alternate standard function implementations. In the latter case, consider
163/// [making a pull request](https://github.com/eigenein/fennec/pulls).
164pub trait Function: function::Code {
165 /// Function arguments type.
166 ///
167 /// It must be encodable to get sent in the request.
168 type Args: Encode;
169
170 /// Function output type.
171 ///
172 /// It must be decodable from the response.
173 type Output: Decode + IntoValue;
174}
175
176/// Marker trait to separate addresses from any other encodable types.
177pub trait Address: Encode {}
178
179impl Address for u16 {}