modbius_core/
functions.rs

1//! Mapping of modbus function codes and routines to read and check them
2//! 
3//! The main way to interact with modbus function codes is to construct [ModbusFunction] instances
4//! The [PublicModbusFunction] enum maps publicly documented function codes.
5//! For reference see <https://www.modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf>
6
7use core::{convert::From, mem};
8
9/// Gets the function code of the given modbus data.
10///
11/// None is returned if data contains less than 1 byte
12pub fn get_function(data: &[u8]) -> (Option<ModbusFunction>, Option<&[u8]>) {
13    (
14        data.get(0).map(|byte| ModbusFunction::new(*byte)),
15        data.get(1..),
16    )
17}
18
19/// Gets the function code of the given modbus data.
20///
21/// # Safety
22/// Providing data with less than one byte is undefined behavior
23pub unsafe fn get_function_unchecked(data: &[u8]) -> (ModbusFunction, &[u8]) {
24    (
25        ModbusFunction::new(*data.get_unchecked(0)),
26        data.get_unchecked(1..),
27    )
28}
29
30/// A modbus function. This struct may store public as well as custom functions. The only invalid function is 0.
31///
32/// This structure is a new type wrapper over u8 which adds function code identification methods.
33#[derive(Debug, Clone, Copy, Hash, Default, PartialEq, Eq, PartialOrd, Ord)]
34pub struct ModbusFunction(pub u8);
35
36impl ModbusFunction {
37    pub const fn new(function_code: u8) -> Self {
38        Self(function_code)
39    }
40
41    /// Create a modbus function from a public Modbus function
42    pub const fn new_public(public_function: PublicModbusFunction) -> Self {
43        Self(public_function as u8)
44    }
45
46    /// Checks if this function matches the given public function
47    pub const fn is(self, public_function: PublicModbusFunction) -> bool {
48        self.0 == Self::new_public(public_function).0
49    }
50
51    /// Check if this function code is valid. Only 0 is invalid.
52    pub const fn is_valid(self) -> bool {
53        self.0 != 0
54    }
55
56    /// Checks if this function is a publicly documented modbus function.
57    ///
58    /// This function returns true if this is a public reserved AND publicly documented function specified in the
59    /// [modbus spec](https://www.modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf)
60    pub const fn is_public(self) -> bool {
61        PublicModbusFunction::is_public_function(self.0)
62    }
63
64    /// Checks if this function is a public reserved function. Any valid and non custom function is reserved.
65    pub const fn is_public_reserved(self) -> bool {
66        self.is_valid() && !self.is_custom()
67    }
68
69    /// Checks if this function is a custom function code
70    pub const fn is_custom(self) -> bool {
71        self.0 >= 65 && self.0 <= 72 || self.0 >= 100 && self.0 <= 110
72    }
73
74    /// Checks if this function code is an expection code. Which means that self >= 128
75    pub const fn is_exception(self) -> bool {
76        self.0 >= 128
77    }
78
79    /// Gets the function code of the given modbus data.
80    ///
81    /// None is returned if data contains less than 1 byte
82    pub fn from_data(data: &[u8]) -> (Option<Self>, Option<&[u8]>) {
83        get_function(data)
84    }
85
86    /// Gets the function code of the given modbus data.
87    ///
88    /// # Safety
89    /// Providing data with less than one byte is undefined behavior
90    pub unsafe fn from_data_unchecked(data: &[u8]) -> (Self, &[u8]) {
91        get_function_unchecked(data)
92    }
93}
94
95impl PartialEq<PublicModbusFunction> for ModbusFunction {
96    fn eq(&self, other: &PublicModbusFunction) -> bool {
97        self.is(*other)
98    }
99}
100
101impl From<u8> for ModbusFunction {
102    fn from(b: u8) -> Self {
103        Self::new(b)
104    }
105}
106
107impl Into<u8> for ModbusFunction {
108    fn into(self) -> u8 {
109        self.0
110    }
111}
112
113impl From<PublicModbusFunction> for ModbusFunction {
114    fn from(public_function: PublicModbusFunction) -> Self {
115        Self::new_public(public_function)
116    }
117}
118
119macro_rules! public_modbus_function {
120    ($($name:ident = $fcode:expr,)*) => {
121        /// An enum mapping all publicly documented modbus function codes
122        ///
123        /// [Invalid](PublicModbusFunction::Invalid) is defined as 0, any undocumented or any custom function code.
124        /// All publicly documented function codes are specified here <https://www.modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf>
125        #[repr(u8)]
126        #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
127        pub enum PublicModbusFunction {
128            $($name = $fcode,)*
129        }
130
131        impl PublicModbusFunction {
132            /// Check if the given code is a publicly documented and valid function code
133            pub const fn is_public_function(code: u8) -> bool {
134                if code == 0 {
135                    return false;
136                }
137                match code {
138                    $($fcode => true,)*
139                    _ => false,
140                }
141            }
142        }
143    };
144}
145
146public_modbus_function! {
147    Invalid = 0,
148    ReadCoils = 1,
149    ReadDiscreteInputs = 2,
150    ReadHoldingRegisters = 3,
151    ReadInputRegisters = 4,
152    WriteSingleCoil = 5,
153    WriteSingleRegister = 6,
154    ReadExceptionStatus = 7,
155    Diagnostics = 8,
156    GetCommEventCounter = 11,
157    GetCommEventLog = 12,
158    WriteMultipleCoils = 15,
159    WriteMultipleregisters = 16,
160    ReportServerID = 17,
161    ReadFileRecord = 20,
162    WriteFileRecord = 21,
163    MaskWriteRegister = 22,
164    ReadWriteMultipleRegisters = 23,
165    ReadFIFOQueue = 24,
166    EncapsulatedInterfaceTransport = 43,
167}
168
169impl PublicModbusFunction {
170    /// Create a [PublicModbusFunction] from a single byte. Every no public function code returns [PublicModbusFunction::Invalid]
171    pub fn new(code: u8) -> Self {
172        if Self::is_public_function(code) {
173            unsafe { Self::new_unchecked(code) }
174        } else {
175            Self::Invalid
176        }
177    }
178
179    /// Transmutes a ModbusFunction from a byte
180    ///
181    /// # Safety
182    /// Providing a code where [is_public_function](PublicModbusFunction::is_public_function) code invokes undefined behavior
183    pub unsafe fn new_unchecked(code: u8) -> Self {
184        mem::transmute(code)
185    }
186}
187
188impl PartialEq<ModbusFunction> for PublicModbusFunction {
189    fn eq(&self, other: &ModbusFunction) -> bool {
190        other == self
191    }
192}
193
194impl From<ModbusFunction> for PublicModbusFunction {
195    fn from(mf: ModbusFunction) -> Self {
196        Self::new(mf.0)
197    }
198}
199
200impl From<u8> for PublicModbusFunction {
201    fn from(code: u8) -> Self {
202        Self::new(code)
203    }
204}
205
206impl Into<u8> for PublicModbusFunction {
207    fn into(self) -> u8 {
208        self as u8
209    }
210}
211
212#[cfg(test)]
213mod test {
214    use super::{get_function, ModbusFunction, PublicModbusFunction};
215
216    #[test]
217    fn invalid() {
218        assert!(!ModbusFunction::from(0).is_valid())
219    }
220
221    #[test]
222    fn public() {
223        assert!(ModbusFunction::from(1).is_public());
224    }
225
226    #[test]
227    fn custom() {
228        let mbf = ModbusFunction::from(69);
229        assert!(mbf.is_custom());
230    }
231
232    #[test]
233    fn custom_all() {
234        for i in 65..=72 {
235            let mbf = ModbusFunction::from(i);
236            assert!(mbf.is_custom() && mbf.is_valid());
237        }
238
239        for i in 100..=110 {
240            let mbf = ModbusFunction::from(i);
241            assert!(mbf.is_custom() && mbf.is_valid());
242        }
243    }
244
245    #[test]
246    fn invalid_from_data_no_tail() {
247        let data = [0];
248        let (function, tail) = get_function(&data);
249        assert_eq!(
250            function,
251            Some(ModbusFunction::new_public(PublicModbusFunction::Invalid))
252        );
253        assert_eq!(tail, Some(&[] as &[u8]));
254    }
255
256    #[test]
257    fn no_data() {
258        let data = [];
259        let (function, tail) = get_function(&data);
260        assert_eq!(function, None);
261        assert_eq!(tail, None);
262    }
263
264    #[test]
265    fn invalid_from_data() {
266        let data = [0, 1];
267        let (function, tail) = get_function(&data);
268        assert_eq!(
269            function,
270            Some(ModbusFunction::new_public(PublicModbusFunction::Invalid))
271        );
272        assert_eq!(tail, Some(&[1u8] as &[u8]));
273    }
274
275    #[test]
276    fn read_coils_from_data() {
277        let data = [1, 1];
278        let (function, tail) = get_function(&data);
279        assert_eq!(
280            function,
281            Some(ModbusFunction::new_public(PublicModbusFunction::ReadCoils))
282        );
283        assert_eq!(tail, Some(&[1u8] as &[u8]));
284    }
285}