modbus_core/frame/
mod.rs

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