Skip to main content

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_to(&self, buf: &mut impl BufMut) {
43        buf.put_u8(self.function_code);
44        self.args.encode_to(buf);
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(buf: &mut impl Buf) -> Result<Self, Error> {
60        match buf.try_get_u8()? {
61            function_code if function_code == F::CODE => Ok(Self::Ok(F::Output::decode_from(buf)?)),
62            function_code if function_code >= 0x80 => {
63                #[cfg(feature = "tracing")]
64                if function_code != (F::CODE | 0x80) {
65                    // Sometimes, device returns a non-matching error code:
66                    tracing::warn!("unexpected response function code ({function_code})");
67                }
68
69                Ok(Self::Exception(Exception::decode_from(buf)?))
70            }
71            function_code => Err(Error::UnexpectedFunctionCode(function_code)),
72        }
73    }
74}
75
76impl<F: Function> Response<F> {
77    pub fn into_result(self) -> Result<F::Output, Error> {
78        match self {
79            Self::Ok(output) => Ok(output),
80            Self::Exception(exception) => Err(Error::Exception(exception)),
81        }
82    }
83}
84
85/// High-level protocol error.
86///
87/// The server received the request without a communication error, but could not handle it.
88#[must_use]
89#[derive(Copy, Clone, Debug, thiserror::Error)]
90pub enum Exception {
91    /// The function code received in the query is not an allowable action for the server:
92    ///
93    /// - the function was not implemented in the unit selected;
94    /// - the server is in the wrong state to process a request of this type.
95    #[error("illegal function")]
96    IllegalFunction,
97
98    /// The data address received in the query is not an allowable address for the server.
99    ///
100    /// The combination of reference number and transfer length is invalid.
101    #[error("illegal data address")]
102    IllegalDataAddress,
103
104    /// A value contained in the query data field is not an allowable value for server.
105    #[error("illegal data value")]
106    IllegalDataValue,
107
108    /// An unrecoverable error occurred while the server was attempting to perform the requested action.
109    #[error("server device failure")]
110    ServerDeviceFailure,
111
112    /// The server has accepted the request and is processing it, but a long duration of time will be
113    /// required to do so.
114    ///
115    /// This response is returned to prevent a timeout error from occurring in the client.
116    /// The client can next issue a «Poll Program Complete» message to determine if processing is completed.
117    #[error("acknowledge")]
118    Acknowledge,
119
120    /// The server is engaged in processing a long–duration program command.
121    ///
122    /// The client should retransmit the message later when the server is free.
123    #[error("server device busy")]
124    ServerDeviceBusy,
125
126    /// The server attempted to read record file, but  detected a parity error in the memory.
127    ///
128    /// The client can retry the request, but service may be required on the server device.
129    #[error("memory parity error")]
130    MemoryParityError,
131
132    /// The gateway was unable to allocate an internal communication path from the input port
133    /// to the output port for processing the request.
134    #[error("gateway path unavailable")]
135    GatewayPathUnavailable,
136
137    /// No response was obtained from the target device.
138    ///
139    /// Usually means that the device is not present on the network.
140    #[error("gateway target device failed to respond")]
141    GatewayTargetDeviceFailedToRespond,
142
143    /// Non-standard error code.
144    #[error("custom error ({0})")]
145    Custom(u8),
146}
147
148impl Decode for Exception {
149    fn decode_from(buf: &mut impl Buf) -> Result<Self, Error> {
150        match buf.try_get_u8()? {
151            0x01 => Ok(Self::IllegalFunction),
152            0x02 => Ok(Self::IllegalDataAddress),
153            0x03 => Ok(Self::IllegalDataValue),
154            0x04 => Ok(Self::ServerDeviceFailure),
155            0x05 => Ok(Self::Acknowledge),
156            0x06 => Ok(Self::ServerDeviceBusy),
157            0x08 => Ok(Self::MemoryParityError),
158            0x0A => Ok(Self::GatewayPathUnavailable),
159            0x0B => Ok(Self::GatewayTargetDeviceFailedToRespond),
160            exception_code => Ok(Self::Custom(exception_code)),
161        }
162    }
163}
164
165/// Trait that ties function code, arguments and output together.
166///
167/// Users are free to implement their own functions – be that custom Modbus functions
168/// or alternate standard function implementations. In the latter case, consider
169/// [making a pull request](https://github.com/eigenein/fennec/pulls).
170pub trait Function: function::Code {
171    /// Function arguments type.
172    ///
173    /// It must be encodable to get sent in the request.
174    type Args: Encode;
175
176    /// Function output type.
177    ///
178    /// It must be decodable from the response.
179    type Output: Decode + IntoValue;
180}
181
182/// Marker trait to separate addresses from any other encodable types.
183pub trait Address: Encode {}
184
185impl Address for u16 {}