modbus_core/frame/
mod.rs

1use core::fmt;
2
3mod coils;
4mod data;
5pub(crate) mod rtu;
6pub(crate) mod tcp;
7
8pub use self::{coils::*, data::*};
9use byteorder::{BigEndian, ByteOrder};
10
11/// A Modbus function code.
12///
13/// It is represented by an unsigned 8 bit integer.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum FunctionCode {
16    /// Modbus Function Code: `01` (`0x01`).
17    ReadCoils,
18
19    /// Modbus Function Code: `02` (`0x02`).
20    ReadDiscreteInputs,
21
22    /// Modbus Function Code: `05` (`0x05`).
23    WriteSingleCoil,
24
25    /// Modbus Function Code: `06` (`0x06`).
26    WriteSingleRegister,
27
28    /// Modbus Function Code: `03` (`0x03`).
29    ReadHoldingRegisters,
30
31    /// Modbus Function Code: `04` (`0x04`).
32    ReadInputRegisters,
33
34    /// Modbus Function Code: `15` (`0x0F`).
35    WriteMultipleCoils,
36
37    /// Modbus Function Code: `16` (`0x10`).
38    WriteMultipleRegisters,
39
40    /// Modbus Function Code: `22` (`0x16`).
41    MaskWriteRegister,
42
43    /// Modbus Function Code: `23` (`0x17`).
44    ReadWriteMultipleRegisters,
45
46    #[cfg(feature = "rtu")]
47    ReadExceptionStatus,
48
49    #[cfg(feature = "rtu")]
50    Diagnostics,
51
52    #[cfg(feature = "rtu")]
53    GetCommEventCounter,
54
55    #[cfg(feature = "rtu")]
56    GetCommEventLog,
57
58    #[cfg(feature = "rtu")]
59    ReportServerId,
60
61    // TODO:
62    // - ReadFileRecord
63    // - WriteFileRecord
64    // TODO:
65    // - Read FifoQueue
66    // - EncapsulatedInterfaceTransport
67    // - CanOpenGeneralReferenceRequestAndResponsePdu
68    // - ReadDeviceIdentification
69    /// Custom Modbus Function Code.
70    Custom(u8),
71}
72
73impl FunctionCode {
74    /// Create a new [`FunctionCode`] with `value`.
75    #[must_use]
76    pub const fn new(value: u8) -> Self {
77        match value {
78            0x01 => Self::ReadCoils,
79            0x02 => Self::ReadDiscreteInputs,
80            0x05 => Self::WriteSingleCoil,
81            0x06 => Self::WriteSingleRegister,
82            0x03 => Self::ReadHoldingRegisters,
83            0x04 => Self::ReadInputRegisters,
84            0x0F => Self::WriteMultipleCoils,
85            0x10 => Self::WriteMultipleRegisters,
86            0x16 => Self::MaskWriteRegister,
87            0x17 => Self::ReadWriteMultipleRegisters,
88            #[cfg(feature = "rtu")]
89            0x07 => Self::ReadExceptionStatus,
90            #[cfg(feature = "rtu")]
91            0x08 => Self::Diagnostics,
92            #[cfg(feature = "rtu")]
93            0x0B => Self::GetCommEventCounter,
94            #[cfg(feature = "rtu")]
95            0x0C => Self::GetCommEventLog,
96            #[cfg(feature = "rtu")]
97            0x11 => Self::ReportServerId,
98            code => FunctionCode::Custom(code),
99        }
100    }
101
102    /// Get the [`u8`] value of the current [`FunctionCode`].
103    #[must_use]
104    pub const fn value(self) -> u8 {
105        match self {
106            Self::ReadCoils => 0x01,
107            Self::ReadDiscreteInputs => 0x02,
108            Self::WriteSingleCoil => 0x05,
109            Self::WriteSingleRegister => 0x06,
110            Self::ReadHoldingRegisters => 0x03,
111            Self::ReadInputRegisters => 0x04,
112            Self::WriteMultipleCoils => 0x0F,
113            Self::WriteMultipleRegisters => 0x10,
114            Self::MaskWriteRegister => 0x16,
115            Self::ReadWriteMultipleRegisters => 0x17,
116            #[cfg(feature = "rtu")]
117            Self::ReadExceptionStatus => 0x07,
118            #[cfg(feature = "rtu")]
119            Self::Diagnostics => 0x08,
120            #[cfg(feature = "rtu")]
121            Self::GetCommEventCounter => 0x0B,
122            #[cfg(feature = "rtu")]
123            Self::GetCommEventLog => 0x0C,
124            #[cfg(feature = "rtu")]
125            Self::ReportServerId => 0x11,
126            Self::Custom(code) => code,
127        }
128    }
129}
130
131impl fmt::Display for FunctionCode {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        self.value().fmt(f)
134    }
135}
136
137/// A Modbus sub-function code is represented by an unsigned 16 bit integer.
138#[cfg(feature = "rtu")]
139pub(crate) type SubFunctionCode = u16;
140
141/// A Modbus address is represented by 16 bit (from `0` to `65535`).
142pub(crate) type Address = u16;
143
144/// A Coil represents a single bit.
145///
146/// - `true` is equivalent to `ON`, `1` and `0xFF00`.
147/// - `false` is equivalent to `OFF`, `0` and `0x0000`.
148pub(crate) type Coil = bool;
149
150/// Modbus uses 16 bit for its data items (big-endian representation).
151pub(crate) type Word = u16;
152
153/// Number of items to process (`0` - `65535`).
154pub(crate) type Quantity = u16;
155
156/// Raw PDU data
157type RawData<'r> = &'r [u8];
158
159/// A request represents a message from the client (master) to the server (slave).
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum Request<'r> {
162    ReadCoils(Address, Quantity),
163    ReadDiscreteInputs(Address, Quantity),
164    WriteSingleCoil(Address, Coil),
165    WriteMultipleCoils(Address, Coils<'r>),
166    ReadInputRegisters(Address, Quantity),
167    ReadHoldingRegisters(Address, Quantity),
168    WriteSingleRegister(Address, Word),
169    WriteMultipleRegisters(Address, Data<'r>),
170    ReadWriteMultipleRegisters(Address, Quantity, Address, Data<'r>),
171    #[cfg(feature = "rtu")]
172    ReadExceptionStatus,
173    #[cfg(feature = "rtu")]
174    Diagnostics(SubFunctionCode, Data<'r>),
175    #[cfg(feature = "rtu")]
176    GetCommEventCounter,
177    #[cfg(feature = "rtu")]
178    GetCommEventLog,
179    #[cfg(feature = "rtu")]
180    ReportServerId,
181    //TODO:
182    //- ReadFileRecord
183    //- WriteFileRecord
184    //- MaskWriteRegiger
185    //TODO:
186    //- Read FifoQueue
187    //- EncapsulatedInterfaceTransport
188    //- CanOpenGeneralReferenceRequestAndResponsePdu
189    //- ReadDeviceIdentification
190    Custom(FunctionCode, &'r [u8]),
191}
192
193/// A server (slave) exception response.
194#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195pub struct ExceptionResponse {
196    pub function: FunctionCode,
197    pub exception: Exception,
198}
199
200/// Represents a message from the client (slave) to the server (master).
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub struct RequestPdu<'r>(pub Request<'r>);
203
204/// Represents a message from the server (slave) to the client (master).
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206pub struct ResponsePdu<'r>(pub Result<Response<'r>, ExceptionResponse>);
207
208#[cfg(feature = "rtu")]
209type Status = u16;
210#[cfg(feature = "rtu")]
211type EventCount = u16;
212#[cfg(feature = "rtu")]
213type MessageCount = u16;
214
215/// The response data of a successfull request.
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217pub enum Response<'r> {
218    ReadCoils(Coils<'r>),
219    ReadDiscreteInputs(Coils<'r>),
220    WriteSingleCoil(Address),
221    WriteMultipleCoils(Address, Quantity),
222    ReadInputRegisters(Data<'r>),
223    ReadHoldingRegisters(Data<'r>),
224    WriteSingleRegister(Address, Word),
225    WriteMultipleRegisters(Address, Quantity),
226    ReadWriteMultipleRegisters(Data<'r>),
227    #[cfg(feature = "rtu")]
228    ReadExceptionStatus(u8),
229    #[cfg(feature = "rtu")]
230    Diagnostics(Data<'r>),
231    #[cfg(feature = "rtu")]
232    GetCommEventCounter(Status, EventCount),
233    #[cfg(feature = "rtu")]
234    GetCommEventLog(Status, EventCount, MessageCount, &'r [u8]),
235    #[cfg(feature = "rtu")]
236    ReportServerId(&'r [u8], bool),
237    //TODO:
238    //- ReadFileRecord
239    //- WriteFileRecord
240    //- MaskWriteRegiger
241    //TODO:
242    //- Read FifoQueue
243    //- EncapsulatedInterfaceTransport
244    //- CanOpenGeneralReferenceRequestAndResponsePdu
245    //- ReadDeviceIdentification
246    Custom(FunctionCode, &'r [u8]),
247}
248
249impl<'r> From<Request<'r>> for FunctionCode {
250    fn from(r: Request<'r>) -> Self {
251        use Request as R;
252
253        match r {
254            R::ReadCoils(_, _) => Self::ReadCoils,
255            R::ReadDiscreteInputs(_, _) => Self::ReadDiscreteInputs,
256            R::WriteSingleCoil(_, _) => Self::WriteSingleCoil,
257            R::WriteMultipleCoils(_, _) => Self::WriteMultipleCoils,
258            R::ReadInputRegisters(_, _) => Self::ReadInputRegisters,
259            R::ReadHoldingRegisters(_, _) => Self::ReadHoldingRegisters,
260            R::WriteSingleRegister(_, _) => Self::WriteSingleRegister,
261            R::WriteMultipleRegisters(_, _) => Self::WriteMultipleRegisters,
262            R::ReadWriteMultipleRegisters(_, _, _, _) => Self::ReadWriteMultipleRegisters,
263            #[cfg(feature = "rtu")]
264            R::ReadExceptionStatus => Self::ReadExceptionStatus,
265            #[cfg(feature = "rtu")]
266            R::Diagnostics(_, _) => Self::Diagnostics,
267            #[cfg(feature = "rtu")]
268            R::GetCommEventCounter => Self::GetCommEventCounter,
269            #[cfg(feature = "rtu")]
270            R::GetCommEventLog => Self::GetCommEventLog,
271            #[cfg(feature = "rtu")]
272            R::ReportServerId => Self::ReportServerId,
273            R::Custom(code, _) => code,
274        }
275    }
276}
277
278impl<'r> From<Response<'r>> for FunctionCode {
279    fn from(r: Response<'r>) -> Self {
280        use Response as R;
281
282        match r {
283            R::ReadCoils(_) => Self::ReadCoils,
284            R::ReadDiscreteInputs(_) => Self::ReadDiscreteInputs,
285            R::WriteSingleCoil(_) => Self::WriteSingleCoil,
286            R::WriteMultipleCoils(_, _) => Self::WriteMultipleCoils,
287            R::ReadInputRegisters(_) => Self::ReadInputRegisters,
288            R::ReadHoldingRegisters(_) => Self::ReadHoldingRegisters,
289            R::WriteSingleRegister(_, _) => Self::WriteSingleRegister,
290            R::WriteMultipleRegisters(_, _) => Self::WriteMultipleRegisters,
291            R::ReadWriteMultipleRegisters(_) => Self::ReadWriteMultipleRegisters,
292            #[cfg(feature = "rtu")]
293            R::ReadExceptionStatus(_) => Self::ReadExceptionStatus,
294            #[cfg(feature = "rtu")]
295            R::Diagnostics(_) => Self::Diagnostics,
296            #[cfg(feature = "rtu")]
297            R::GetCommEventCounter(_, _) => Self::GetCommEventCounter,
298            #[cfg(feature = "rtu")]
299            R::GetCommEventLog(_, _, _, _) => Self::GetCommEventLog,
300            #[cfg(feature = "rtu")]
301            R::ReportServerId(_, _) => Self::ReportServerId,
302            R::Custom(code, _) => code,
303        }
304    }
305}
306
307/// A server (slave) exception.
308#[derive(Debug, Clone, Copy, PartialEq, Eq)]
309pub enum Exception {
310    IllegalFunction = 0x01,
311    IllegalDataAddress = 0x02,
312    IllegalDataValue = 0x03,
313    ServerDeviceFailure = 0x04,
314    Acknowledge = 0x05,
315    ServerDeviceBusy = 0x06,
316    MemoryParityError = 0x08,
317    GatewayPathUnavailable = 0x0A,
318    GatewayTargetDevice = 0x0B,
319}
320
321impl fmt::Display for Exception {
322    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
323        let desc = match *self {
324            Self::IllegalFunction => "Illegal function",
325            Self::IllegalDataAddress => "Illegal data address",
326            Self::IllegalDataValue => "Illegal data value",
327            Self::ServerDeviceFailure => "Server device failure",
328            Self::Acknowledge => "Acknowledge",
329            Self::ServerDeviceBusy => "Server device busy",
330            Self::MemoryParityError => "Memory parity error",
331            Self::GatewayPathUnavailable => "Gateway path unavailable",
332            Self::GatewayTargetDevice => "Gateway target device failed to respond",
333        };
334        write!(f, "{desc}")
335    }
336}
337
338impl<'r> Request<'r> {
339    /// Number of bytes required for a serialized PDU frame.
340    #[must_use]
341    pub fn pdu_len(&self) -> usize {
342        match *self {
343            Self::ReadCoils(_, _)
344            | Self::ReadDiscreteInputs(_, _)
345            | Self::ReadInputRegisters(_, _)
346            | Self::ReadHoldingRegisters(_, _)
347            | Self::WriteSingleRegister(_, _)
348            | Self::WriteSingleCoil(_, _) => 5,
349            Self::WriteMultipleCoils(_, coils) => 6 + coils.packed_len(),
350            Self::WriteMultipleRegisters(_, words) => 6 + words.data.len(),
351            Self::ReadWriteMultipleRegisters(_, _, _, words) => 10 + words.data.len(),
352            Self::Custom(_, data) => 1 + data.len(),
353            #[cfg(feature = "rtu")]
354            _ => todo!(), // TODO
355        }
356    }
357}
358
359impl<'r> Response<'r> {
360    /// Number of bytes required for a serialized PDU frame.
361    #[must_use]
362    pub fn pdu_len(&self) -> usize {
363        match *self {
364            Self::ReadCoils(coils) | Self::ReadDiscreteInputs(coils) => 2 + coils.packed_len(),
365            Self::WriteSingleCoil(_) => 3,
366            Self::WriteMultipleCoils(_, _)
367            | Self::WriteMultipleRegisters(_, _)
368            | Self::WriteSingleRegister(_, _) => 5,
369            Self::ReadInputRegisters(words)
370            | Self::ReadHoldingRegisters(words)
371            | Self::ReadWriteMultipleRegisters(words) => 2 + words.len() * 2,
372            Self::Custom(_, data) => 1 + data.len(),
373            Self::ReadExceptionStatus(_) => 2,
374            #[cfg(feature = "rtu")]
375            _ => unimplemented!(), // TODO
376        }
377    }
378}
379
380#[cfg(test)]
381mod tests {
382
383    use super::*;
384
385    #[test]
386    fn function_code_into_u8() {
387        let x: u8 = FunctionCode::WriteMultipleCoils.value();
388        assert_eq!(x, 15);
389        let x: u8 = FunctionCode::Custom(0xBB).value();
390        assert_eq!(x, 0xBB);
391    }
392
393    #[test]
394    fn function_code_from_u8() {
395        assert_eq!(FunctionCode::new(15), FunctionCode::WriteMultipleCoils);
396        assert_eq!(FunctionCode::new(0xBB), FunctionCode::Custom(0xBB));
397    }
398
399    #[test]
400    fn function_code_from_request() {
401        use Request::*;
402        let requests = &[
403            (ReadCoils(0, 0), 1),
404            (ReadDiscreteInputs(0, 0), 2),
405            (WriteSingleCoil(0, true), 5),
406            (
407                WriteMultipleCoils(
408                    0,
409                    Coils {
410                        quantity: 0,
411                        data: &[],
412                    },
413                ),
414                0x0F,
415            ),
416            (ReadInputRegisters(0, 0), 0x04),
417            (ReadHoldingRegisters(0, 0), 0x03),
418            (WriteSingleRegister(0, 0), 0x06),
419            (
420                WriteMultipleRegisters(
421                    0,
422                    Data {
423                        quantity: 0,
424                        data: &[],
425                    },
426                ),
427                0x10,
428            ),
429            (
430                ReadWriteMultipleRegisters(
431                    0,
432                    0,
433                    0,
434                    Data {
435                        quantity: 0,
436                        data: &[],
437                    },
438                ),
439                0x17,
440            ),
441            (Custom(FunctionCode::Custom(88), &[]), 88),
442        ];
443        for (req, expected) in requests {
444            let code: u8 = FunctionCode::from(*req).value();
445            assert_eq!(*expected, code);
446        }
447    }
448
449    #[test]
450    fn function_code_from_response() {
451        use Response::*;
452        let responses = &[
453            (
454                ReadCoils(Coils {
455                    quantity: 0,
456                    data: &[],
457                }),
458                1,
459            ),
460            (
461                ReadDiscreteInputs(Coils {
462                    quantity: 0,
463                    data: &[],
464                }),
465                2,
466            ),
467            (WriteSingleCoil(0x0), 5),
468            (WriteMultipleCoils(0x0, 0x0), 0x0F),
469            (
470                ReadInputRegisters(Data {
471                    quantity: 0,
472                    data: &[],
473                }),
474                0x04,
475            ),
476            (
477                ReadHoldingRegisters(Data {
478                    quantity: 0,
479                    data: &[],
480                }),
481                0x03,
482            ),
483            (WriteSingleRegister(0, 0), 0x06),
484            (WriteMultipleRegisters(0, 0), 0x10),
485            (
486                ReadWriteMultipleRegisters(Data {
487                    quantity: 0,
488                    data: &[],
489                }),
490                0x17,
491            ),
492            (Custom(FunctionCode::Custom(99), &[]), 99),
493        ];
494        for (req, expected) in responses {
495            let code: u8 = FunctionCode::from(*req).value();
496            assert_eq!(*expected, code);
497        }
498    }
499
500    #[test]
501    fn test_request_pdu_len() {
502        assert_eq!(Request::ReadCoils(0x12, 5).pdu_len(), 5);
503        assert_eq!(Request::WriteSingleRegister(0x12, 0x33).pdu_len(), 5);
504        let buf = &mut [0, 0];
505        assert_eq!(
506            Request::WriteMultipleCoils(0, Coils::from_bools(&[true, false], buf).unwrap())
507                .pdu_len(),
508            7
509        );
510        // TODO: extend test
511    }
512
513    #[test]
514    fn test_response_pdu_len() {
515        let buf = &mut [0, 0];
516        assert_eq!(
517            Response::ReadCoils(Coils::from_bools(&[true], buf).unwrap()).pdu_len(),
518            3
519        );
520        // TODO: extend test
521    }
522}