Skip to main content

rusty_modbus_codec/
error.rs

1//! Codec error types.
2
3/// Errors that can occur when decoding a Modbus PDU.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum DecodeError {
6    /// PDU is shorter than the minimum for the function code.
7    Truncated {
8        /// Minimum expected length.
9        expected: usize,
10        /// Actual length received.
11        actual: usize,
12    },
13    /// PDU has extra bytes for a fixed-length function code.
14    LengthMismatch {
15        /// Exact expected length.
16        expected: usize,
17        /// Actual length received.
18        actual: usize,
19    },
20    /// PDU exceeds the Modbus maximum of 253 bytes.
21    PduTooLarge {
22        /// Actual PDU length.
23        length: usize,
24        /// Maximum allowed PDU length.
25        maximum: usize,
26    },
27    /// Function code byte is not recognized.
28    UnknownFunctionCode(u8),
29    /// Byte count field does not match actual remaining data length.
30    ByteCountMismatch {
31        /// Byte count declared in the PDU.
32        declared: usize,
33        /// Actual remaining data length.
34        actual: usize,
35    },
36    /// Byte count field is outside the allowed range for this function code.
37    ByteCountOutOfRange {
38        /// The invalid byte count.
39        count: usize,
40        /// The minimum allowed byte count.
41        minimum: usize,
42        /// The maximum allowed byte count.
43        maximum: usize,
44    },
45    /// Quantity value is outside the allowed range for this function code.
46    QuantityOutOfRange {
47        /// The invalid quantity value.
48        quantity: u16,
49    },
50    /// Coil value is neither 0xFF00 nor 0x0000.
51    InvalidCoilValue(u16),
52    /// File sub-request reference type is not 6.
53    InvalidReferenceType(u8),
54    /// Packed file-record data cannot be split into valid sub-record groups.
55    InvalidFileRecordLength {
56        /// Invalid packed file-record byte length.
57        length: usize,
58    },
59    /// File-record address fields are outside the Modbus file-record model.
60    FileRecordOutOfRange {
61        /// File number.
62        file_number: u16,
63        /// Starting record number.
64        record_number: u16,
65        /// Number of records.
66        record_length: u16,
67    },
68    /// MEI type byte is not recognized.
69    UnknownMeiType(u8),
70    /// Exception code byte is not recognized.
71    UnknownExceptionCode(u8),
72    /// Diagnostic sub-function code is not recognized.
73    UnknownDiagnosticSubFunction(u16),
74    /// Diagnostics data is not an even number of bytes.
75    InvalidDiagnosticDataLength {
76        /// Invalid diagnostics data length.
77        length: usize,
78    },
79    /// Device ID code byte is not recognized (FC 0x2B / MEI 0x0E).
80    InvalidDeviceIdCode(u8),
81    /// Device ID conformity level byte is not recognized (FC 0x2B / MEI 0x0E).
82    InvalidDeviceIdConformityLevel(u8),
83    /// Device ID More Follows byte is neither 0x00 nor 0xFF.
84    InvalidDeviceIdMoreFollows(u8),
85    /// Device ID Next Object ID must be zero when More Follows is 0x00.
86    InvalidDeviceIdNextObjectId(u8),
87    /// Individual Device ID access must return exactly one object.
88    InvalidDeviceIdObjectCount(u8),
89}
90
91impl DecodeError {
92    /// Check that a fixed-length PDU data slice has exactly `expected` bytes.
93    pub(crate) fn check_exact_len(data: &[u8], expected: usize) -> Result<(), Self> {
94        match data.len().cmp(&expected) {
95            core::cmp::Ordering::Less => Err(Self::Truncated {
96                expected,
97                actual: data.len(),
98            }),
99            core::cmp::Ordering::Equal => Ok(()),
100            core::cmp::Ordering::Greater => Err(Self::LengthMismatch {
101                expected,
102                actual: data.len(),
103            }),
104        }
105    }
106}
107
108impl core::fmt::Display for DecodeError {
109    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
110        match self {
111            Self::Truncated { expected, actual } => {
112                write!(f, "PDU truncated: expected {expected} bytes, got {actual}")
113            }
114            Self::LengthMismatch { expected, actual } => {
115                write!(
116                    f,
117                    "PDU length mismatch: expected {expected} bytes, got {actual}"
118                )
119            }
120            Self::PduTooLarge { length, maximum } => {
121                write!(f, "PDU too large: {length} bytes (maximum {maximum})")
122            }
123            Self::UnknownFunctionCode(fc) => write!(f, "unknown function code: {fc:#04X}"),
124            Self::ByteCountMismatch { declared, actual } => {
125                write!(
126                    f,
127                    "byte count mismatch: declared {declared}, actual {actual}"
128                )
129            }
130            Self::ByteCountOutOfRange {
131                count,
132                minimum,
133                maximum,
134            } => write!(
135                f,
136                "byte count out of range: {count} (expected {minimum}..={maximum})"
137            ),
138            Self::QuantityOutOfRange { quantity } => {
139                write!(f, "quantity out of range: {quantity}")
140            }
141            Self::InvalidCoilValue(v) => write!(f, "invalid coil value: {v:#06X}"),
142            Self::InvalidReferenceType(rt) => write!(f, "invalid reference type: {rt}"),
143            Self::InvalidFileRecordLength { length } => {
144                write!(f, "invalid file-record data length: {length}")
145            }
146            Self::FileRecordOutOfRange {
147                file_number,
148                record_number,
149                record_length,
150            } => write!(
151                f,
152                "file record out of range: file {file_number}, record {record_number}, length {record_length}"
153            ),
154            Self::UnknownMeiType(mt) => write!(f, "unknown MEI type: {mt:#04X}"),
155            Self::UnknownExceptionCode(ec) => write!(f, "unknown exception code: {ec:#04X}"),
156            Self::UnknownDiagnosticSubFunction(sf) => {
157                write!(f, "unknown diagnostic sub-function: {sf:#06X}")
158            }
159            Self::InvalidDiagnosticDataLength { length } => {
160                write!(
161                    f,
162                    "invalid diagnostics data length: {length} (expected a multiple of 2)"
163                )
164            }
165            Self::InvalidDeviceIdCode(code) => write!(f, "invalid device ID code: {code:#04X}"),
166            Self::InvalidDeviceIdConformityLevel(level) => {
167                write!(f, "invalid device ID conformity level: {level:#04X}")
168            }
169            Self::InvalidDeviceIdMoreFollows(value) => {
170                write!(f, "invalid device ID More Follows value: {value:#04X}")
171            }
172            Self::InvalidDeviceIdNextObjectId(object_id) => write!(
173                f,
174                "invalid device ID Next Object ID with More Follows = 0x00: {object_id:#04X}"
175            ),
176            Self::InvalidDeviceIdObjectCount(count) => {
177                write!(f, "invalid individual device ID object count: {count}")
178            }
179        }
180    }
181}
182
183#[cfg(feature = "std")]
184impl std::error::Error for DecodeError {}
185
186/// Errors that can occur when encoding a Modbus PDU.
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub enum EncodeError {
189    /// Provided buffer is too small.
190    BufferTooSmall {
191        /// Required buffer size.
192        required: usize,
193        /// Available buffer size.
194        available: usize,
195    },
196    /// Encoded PDU would exceed the Modbus maximum of 253 bytes.
197    PduTooLarge {
198        /// Encoded PDU length.
199        length: usize,
200        /// Maximum allowed PDU length.
201        maximum: usize,
202    },
203    /// Quantity exceeds protocol limits.
204    QuantityOutOfRange {
205        /// The invalid quantity value.
206        quantity: u16,
207    },
208    /// Declared byte count does not match the payload length.
209    ByteCountMismatch {
210        /// Declared byte count.
211        declared: usize,
212        /// Actual payload length.
213        actual: usize,
214    },
215    /// Byte count is outside the allowed range for this function code.
216    ByteCountOutOfRange {
217        /// The invalid byte count.
218        count: usize,
219        /// The minimum allowed byte count.
220        minimum: usize,
221        /// The maximum allowed byte count.
222        maximum: usize,
223    },
224    /// File sub-request reference type is not 6.
225    InvalidReferenceType(u8),
226    /// Packed file-record data cannot be split into valid sub-record groups.
227    InvalidFileRecordLength {
228        /// Invalid packed file-record byte length.
229        length: usize,
230    },
231    /// File-record address fields are outside the Modbus file-record model.
232    FileRecordOutOfRange {
233        /// File number.
234        file_number: u16,
235        /// Starting record number.
236        record_number: u16,
237        /// Number of records.
238        record_length: u16,
239    },
240    /// Diagnostics data is not an even number of bytes.
241    InvalidDiagnosticDataLength {
242        /// Invalid diagnostics data length.
243        length: usize,
244    },
245}
246
247impl core::fmt::Display for EncodeError {
248    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
249        match self {
250            Self::BufferTooSmall {
251                required,
252                available,
253            } => {
254                write!(
255                    f,
256                    "buffer too small: need {required} bytes, have {available}"
257                )
258            }
259            Self::PduTooLarge { length, maximum } => {
260                write!(f, "PDU too large: {length} bytes (maximum {maximum})")
261            }
262            Self::QuantityOutOfRange { quantity } => {
263                write!(f, "quantity out of range: {quantity}")
264            }
265            Self::ByteCountMismatch { declared, actual } => {
266                write!(
267                    f,
268                    "byte count mismatch: declared {declared}, actual {actual}"
269                )
270            }
271            Self::ByteCountOutOfRange {
272                count,
273                minimum,
274                maximum,
275            } => write!(
276                f,
277                "byte count out of range: {count} (expected {minimum}..={maximum})"
278            ),
279            Self::InvalidReferenceType(rt) => write!(f, "invalid reference type: {rt}"),
280            Self::InvalidFileRecordLength { length } => {
281                write!(f, "invalid file-record data length: {length}")
282            }
283            Self::FileRecordOutOfRange {
284                file_number,
285                record_number,
286                record_length,
287            } => write!(
288                f,
289                "file record out of range: file {file_number}, record {record_number}, length {record_length}"
290            ),
291            Self::InvalidDiagnosticDataLength { length } => {
292                write!(
293                    f,
294                    "invalid diagnostics data length: {length} (expected a multiple of 2)"
295                )
296            }
297        }
298    }
299}
300
301impl EncodeError {
302    pub(crate) fn check_pdu_len(length: usize) -> Result<(), Self> {
303        let maximum = rusty_modbus_types::MAX_PDU_SIZE;
304        if length > maximum {
305            Err(Self::PduTooLarge { length, maximum })
306        } else {
307            Ok(())
308        }
309    }
310
311    pub(crate) fn check_quantity(quantity: u16, max: u16) -> Result<(), Self> {
312        if quantity == 0 || quantity > max {
313            Err(Self::QuantityOutOfRange { quantity })
314        } else {
315            Ok(())
316        }
317    }
318
319    pub(crate) fn check_byte_count(declared: usize, actual: usize) -> Result<(), Self> {
320        if declared == actual {
321            Ok(())
322        } else {
323            Err(Self::ByteCountMismatch { declared, actual })
324        }
325    }
326
327    pub(crate) fn check_byte_count_range(
328        count: usize,
329        minimum: usize,
330        maximum: usize,
331    ) -> Result<(), Self> {
332        if (minimum..=maximum).contains(&count) {
333            Ok(())
334        } else {
335            Err(Self::ByteCountOutOfRange {
336                count,
337                minimum,
338                maximum,
339            })
340        }
341    }
342}
343
344#[cfg(feature = "std")]
345impl std::error::Error for EncodeError {}