Skip to main content

mbus_core/function_codes/
public.rs

1//! # Modbus Public Function Codes and Sub-functions
2//!
3//! This module defines the standard function codes and sub-function codes used in the
4//! Modbus Application Protocol. It provides enums for:
5//!
6//! - **[`FunctionCode`]**: The primary operation identifier (e.g., Read Coils, Write Register).
7//! - **[`DiagnosticSubFunction`]**: Sub-codes for serial-line diagnostics (FC 0x08).
8//! - **[`EncapsulatedInterfaceType`]**: MEI types for tunneling other protocols (FC 0x2B).
9//!
10//! All types implement `TryFrom` for safe conversion from raw bytes and include
11//! documentation referencing the Modbus Application Protocol Specification V1.1b3.
12//!
13//! This module is `no_std` compatible and uses `repr` attributes to ensure
14//! memory layout matches the protocol's byte-level requirements.
15
16use crate::errors::MbusError;
17
18/// Modbus Public Function Codes.
19///
20/// These are the standardized function codes defined in
21/// the Modbus Application Protocol Specification V1.1b3.
22///
23/// See:
24/// - Section 5.1 Public Function Code Definition
25/// - Section 6.x for individual function behaviors
26///
27/// Reference: :contentReference[oaicite:1]{index=1}
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29#[repr(u8)]
30pub enum FunctionCode {
31    // ============================================================
32    // Bit Access (Single-bit data)
33    // ============================================================
34    /// 0x00 — Undefined
35    /// This value is not defined in the specification and can be used as a placeholder
36    /// for uninitialized or unknown function codes.
37    /// It is not a valid function code for actual Modbus transactions.
38    #[default]
39    Default = 0x00, // Placeholder for uninitialized or unknown function code
40
41    #[cfg(feature = "coils")]
42    /// 0x01 — Read Coils
43    ///
44    /// Reads the ON/OFF status of discrete output coils.
45    /// Section 6.1
46    ReadCoils = 0x01,
47
48    #[cfg(feature = "discrete-inputs")]
49    /// 0x02 — Read Discrete Inputs
50    ///
51    /// Reads the ON/OFF status of discrete input contacts.
52    /// Section 6.2
53    ReadDiscreteInputs = 0x02,
54
55    #[cfg(feature = "coils")]
56    /// 0x05 — Write Single Coil
57    ///
58    /// Forces a single coil to ON (0xFF00) or OFF (0x0000).
59    /// Section 6.5
60    WriteSingleCoil = 0x05,
61
62    #[cfg(feature = "coils")]
63    /// 0x0F — Write Multiple Coils
64    ///
65    /// Forces multiple coils to ON/OFF.
66    /// Section 6.11
67    WriteMultipleCoils = 0x0F,
68
69    // ============================================================
70    // 16-bit Register Access
71    // ============================================================
72    #[cfg(feature = "registers")]
73    /// 0x03 — Read Holding Registers
74    ///
75    /// Reads one or more 16-bit holding registers.
76    /// Section 6.3
77    ReadHoldingRegisters = 0x03,
78
79    #[cfg(feature = "registers")]
80    /// 0x04 — Read Input Registers
81    ///
82    /// Reads one or more 16-bit input registers.
83    /// Section 6.4
84    ReadInputRegisters = 0x04,
85
86    /// 0x06 — Write Single Register
87    #[cfg(feature = "registers")]
88    ///
89    /// Writes a single 16-bit holding register.
90    /// Section 6.6
91    WriteSingleRegister = 0x06,
92
93    #[cfg(feature = "registers")]
94    /// 0x10 — Write Multiple Registers
95    ///
96    /// Writes multiple 16-bit holding registers.
97    /// Section 6.12
98    WriteMultipleRegisters = 0x10,
99
100    #[cfg(feature = "registers")]
101    /// 0x16 — Mask Write Register
102    ///
103    /// Performs a bitwise mask write on a single register.
104    /// Section 6.16
105    MaskWriteRegister = 0x16,
106
107    #[cfg(feature = "registers")]
108    /// 0x17 — Read/Write Multiple Registers
109    ///
110    /// Reads and writes multiple registers in a single transaction.
111    /// Section 6.17
112    ReadWriteMultipleRegisters = 0x17,
113
114    #[cfg(feature = "fifo")]
115    /// 0x18 — Read FIFO Queue
116    ///
117    /// Reads the contents of a FIFO queue.
118    /// Section 6.18
119    ReadFifoQueue = 0x18,
120
121    // ============================================================
122    // File Record Access
123    // ============================================================
124    #[cfg(feature = "file-record")]
125    /// 0x14 — Read File Record
126    ///
127    /// Reads structured file records.
128    /// Section 6.14
129    ReadFileRecord = 0x14,
130
131    /// 0x15 — Write File Record
132    #[cfg(feature = "file-record")]
133    ///
134    /// Writes structured file records.
135    /// Section 6.15
136    WriteFileRecord = 0x15,
137
138    // ============================================================
139    // Diagnostics & Device Information
140    // ============================================================
141    #[cfg(feature = "diagnostics")]
142    /// 0x07 — Read Exception Status (Serial Line Only)
143    ///
144    /// Returns 8-bit exception status.
145    /// Section 6.7
146    ReadExceptionStatus = 0x07,
147
148    #[cfg(feature = "diagnostics")]
149    /// 0x08 — Diagnostics (Serial Line Only)
150    ///
151    /// Provides diagnostic and loopback tests.
152    /// Requires sub-function codes.
153    /// Section 6.8
154    Diagnostics = 0x08,
155
156    #[cfg(feature = "diagnostics")]
157    /// 0x0B — Get Communication Event Counter (Serial Line Only)
158    ///
159    /// Returns communication event counter.
160    /// Section 6.9
161    GetCommEventCounter = 0x0B,
162
163    #[cfg(feature = "diagnostics")]
164    /// 0x0C — Get Communication Event Log (Serial Line Only)
165    ///
166    /// Returns communication event log.
167    /// Section 6.10
168    GetCommEventLog = 0x0C,
169
170    #[cfg(feature = "diagnostics")]
171    /// 0x11 — Report Server ID (Serial Line Only)
172    ///
173    /// Returns server identification.
174    /// Section 6.13
175    ReportServerId = 0x11,
176
177    #[cfg(feature = "diagnostics")]
178    /// 0x2B — Encapsulated Interface Transport
179    ///
180    /// Used for:
181    /// - CANopen General Reference (Sub-function 0x0D)
182    /// - Read Device Identification (Sub-function 0x0E)
183    ///
184    /// Section 6.19, 6.20, 6.21
185    EncapsulatedInterfaceTransport = 0x2B,
186}
187
188impl TryFrom<u8> for FunctionCode {
189    type Error = MbusError;
190
191    fn try_from(value: u8) -> Result<Self, Self::Error> {
192        use FunctionCode::*;
193
194        match value {
195            #[cfg(feature = "coils")]
196            0x01 => Ok(ReadCoils),
197            #[cfg(feature = "discrete-inputs")]
198            0x02 => Ok(ReadDiscreteInputs),
199            #[cfg(feature = "registers")]
200            0x03 => Ok(ReadHoldingRegisters),
201            #[cfg(feature = "registers")]
202            0x04 => Ok(ReadInputRegisters),
203            #[cfg(feature = "coils")]
204            0x05 => Ok(WriteSingleCoil),
205            #[cfg(feature = "registers")]
206            0x06 => Ok(WriteSingleRegister),
207            #[cfg(feature = "diagnostics")]
208            0x07 => Ok(ReadExceptionStatus),
209            #[cfg(feature = "diagnostics")]
210            0x08 => Ok(Diagnostics),
211            #[cfg(feature = "diagnostics")]
212            0x0B => Ok(GetCommEventCounter),
213            #[cfg(feature = "diagnostics")]
214            0x0C => Ok(GetCommEventLog),
215            #[cfg(feature = "coils")]
216            0x0F => Ok(WriteMultipleCoils),
217            #[cfg(feature = "registers")]
218            0x10 => Ok(WriteMultipleRegisters),
219            #[cfg(feature = "diagnostics")]
220            0x11 => Ok(ReportServerId),
221            #[cfg(feature = "file-record")]
222            0x14 => Ok(ReadFileRecord),
223            #[cfg(feature = "file-record")]
224            0x15 => Ok(WriteFileRecord),
225            #[cfg(feature = "registers")]
226            0x16 => Ok(MaskWriteRegister),
227            #[cfg(feature = "registers")]
228            0x17 => Ok(ReadWriteMultipleRegisters),
229            #[cfg(feature = "fifo")]
230            0x18 => Ok(ReadFifoQueue),
231            #[cfg(feature = "diagnostics")]
232            0x2B => Ok(EncapsulatedInterfaceTransport),
233            _ => Err(MbusError::UnsupportedFunction(value)),
234        }
235    }
236}
237
238/// Sub-function codes for Function Code 0x08 (Diagnostics).
239///
240/// Serial line only.
241/// See Modbus Application Protocol Specification V1.1b3, Section 6.8.
242///
243/// These values are 16-bit and encoded big-endian inside the PDU data field.
244#[derive(Debug, Clone, Copy, PartialEq, Eq)]
245#[repr(u16)]
246pub enum DiagnosticSubFunction {
247    /// 0x0000 — Return Query Data (Loopback test)
248    ReturnQueryData = 0x0000,
249
250    /// 0x0001 — Restart Communications Option
251    RestartCommunicationsOption = 0x0001,
252
253    /// 0x0002 — Return Diagnostic Register
254    ReturnDiagnosticRegister = 0x0002,
255
256    /// 0x0003 — Change ASCII Input Delimiter
257    ChangeAsciiInputDelimiter = 0x0003,
258
259    /// 0x0004 — Force Listen Only Mode
260    ForceListenOnlyMode = 0x0004,
261
262    /// 0x000A — Clear Counters and Diagnostic Register
263    ClearCountersAndDiagnosticRegister = 0x000A,
264
265    /// 0x000B — Return Bus Message Count
266    ReturnBusMessageCount = 0x000B,
267
268    /// 0x000C — Return Bus Communication Error Count
269    ReturnBusCommunicationErrorCount = 0x000C,
270
271    /// 0x000D — Return Bus Exception Error Count
272    ReturnBusExceptionErrorCount = 0x000D,
273
274    /// 0x000E — Return Server Message Count
275    ReturnServerMessageCount = 0x000E,
276
277    /// 0x000F — Return Server No Response Count
278    ReturnServerNoResponseCount = 0x000F,
279
280    /// 0x0010 — Return Server NAK Count
281    ReturnServerNakCount = 0x0010,
282
283    /// 0x0011 — Return Server Busy Count
284    ReturnServerBusyCount = 0x0011,
285
286    /// 0x0012 — Return Bus Character Overrun Count
287    ReturnBusCharacterOverrunCount = 0x0012,
288
289    /// 0x0014 — Clear Overrun Counter and Flag
290    ClearOverrunCounterAndFlag = 0x0014,
291}
292
293impl DiagnosticSubFunction {
294    /// Converts the `DiagnosticSubFunction` enum variant into its 2-byte big-endian representation.
295    pub fn to_be_bytes(self) -> [u8; 2] {
296        (self as u16).to_be_bytes()
297    }
298}
299
300impl From<DiagnosticSubFunction> for u16 {
301    fn from(sub_func: DiagnosticSubFunction) -> Self {
302        sub_func as u16
303    }
304}
305
306impl TryFrom<u16> for DiagnosticSubFunction {
307    type Error = MbusError;
308
309    fn try_from(value: u16) -> Result<Self, Self::Error> {
310        use DiagnosticSubFunction::*;
311
312        match value {
313            0x0000 => Ok(ReturnQueryData),
314            0x0001 => Ok(RestartCommunicationsOption),
315            0x0002 => Ok(ReturnDiagnosticRegister),
316            0x0003 => Ok(ChangeAsciiInputDelimiter),
317            0x0004 => Ok(ForceListenOnlyMode),
318
319            // 0x0005–0x0009 Reserved
320            0x000A => Ok(ClearCountersAndDiagnosticRegister),
321            0x000B => Ok(ReturnBusMessageCount),
322            0x000C => Ok(ReturnBusCommunicationErrorCount),
323            0x000D => Ok(ReturnBusExceptionErrorCount),
324            0x000E => Ok(ReturnServerMessageCount),
325            0x000F => Ok(ReturnServerNoResponseCount),
326            0x0010 => Ok(ReturnServerNakCount),
327            0x0011 => Ok(ReturnServerBusyCount),
328            0x0012 => Ok(ReturnBusCharacterOverrunCount),
329
330            // 0x0013 Reserved
331            0x0014 => Ok(ClearOverrunCounterAndFlag),
332
333            // Everything else reserved per spec
334            _ => Err(MbusError::ReservedSubFunction(value)),
335        }
336    }
337}
338
339/// MEI (Modbus Encapsulated Interface) types
340/// for Function Code 0x2B.
341///
342/// See Section 6.19–6.21 of the specification.
343///
344/// Encoded as 1 byte following the function code.
345#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
346#[repr(u8)]
347pub enum EncapsulatedInterfaceType {
348    /// Placeholder default value used before a concrete MEI type is parsed.
349    /// This value should not appear in a valid decoded protocol frame.
350    #[default]
351    Err,
352    /// 0x0D — CANopen General Reference
353    CanopenGeneralReference = 0x0D,
354
355    /// 0x0E — Read Device Identification
356    ReadDeviceIdentification = 0x0E,
357}
358
359impl From<EncapsulatedInterfaceType> for u8 {
360    fn from(val: EncapsulatedInterfaceType) -> Self {
361        val as u8
362    }
363}
364
365impl TryFrom<u8> for EncapsulatedInterfaceType {
366    type Error = MbusError;
367
368    fn try_from(value: u8) -> Result<Self, Self::Error> {
369        match value {
370            0x0D => Ok(Self::CanopenGeneralReference),
371            0x0E => Ok(Self::ReadDeviceIdentification),
372            _ => Err(MbusError::ReservedSubFunction(value as u16)),
373        }
374    }
375}