Skip to main content

rusty_modbus_codec/request/
diagnostic.rs

1//! Diagnostic requests: FC 07, 08, 0B, 0C, 11.
2//!
3//! FC 07 (Read Exception Status), FC 0B (Get Comm Event Counter),
4//! FC 0C (Get Comm Event Log), and FC 11 (Report Server ID) have empty
5//! request PDUs — no data follows the function code byte. They are
6//! represented as unit variants in [`RequestPdu`](crate::pdu::RequestPdu).
7//!
8//! FC 08 (Diagnostics) carries a sub-function code and variable data.
9
10use rusty_modbus_types::{DiagnosticSubFunction, FunctionCode};
11
12use crate::error::{DecodeError, EncodeError};
13use crate::request::Encode;
14
15fn check_diagnostic_data_len_decode(data: &[u8]) -> Result<(), DecodeError> {
16    if data.len().is_multiple_of(2) {
17        Ok(())
18    } else {
19        Err(DecodeError::InvalidDiagnosticDataLength { length: data.len() })
20    }
21}
22
23fn check_diagnostic_data_len_encode(data: &[u8]) -> Result<(), EncodeError> {
24    if data.len().is_multiple_of(2) {
25        Ok(())
26    } else {
27        Err(EncodeError::InvalidDiagnosticDataLength { length: data.len() })
28    }
29}
30
31/// FC 0x08 — Diagnostics request.
32///
33/// The sub-function code selects the diagnostic action. Diagnostic data is
34/// encoded as `N x 2` bytes per Spec V1.1b3 §6.8.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct DiagnosticsRequest<'buf> {
37    /// Diagnostic sub-function code.
38    pub sub_function: DiagnosticSubFunction,
39    /// Sub-function data (typically 2 bytes, but variable for Return Query Data).
40    pub data: &'buf [u8],
41}
42
43impl<'buf> DiagnosticsRequest<'buf> {
44    /// Decode from PDU data after the function code byte.
45    ///
46    /// # Errors
47    ///
48    /// Returns [`DecodeError::Truncated`] if `data` is shorter than 2 bytes.
49    /// Returns [`DecodeError::UnknownFunctionCode`] if the sub-function code is not recognized
50    /// (reused as a general "unknown sub-code" error via the raw value).
51    /// Returns [`DecodeError::InvalidDiagnosticDataLength`] if the payload is not
52    /// an even number of bytes.
53    pub fn decode(data: &'buf [u8]) -> Result<Self, DecodeError> {
54        if data.len() < 2 {
55            return Err(DecodeError::Truncated {
56                expected: 2,
57                actual: data.len(),
58            });
59        }
60        let raw_sub = u16::from_be_bytes([data[0], data[1]]);
61        let sub_function = DiagnosticSubFunction::from_raw(raw_sub)
62            .ok_or(DecodeError::UnknownDiagnosticSubFunction(raw_sub))?;
63        let payload = &data[2..];
64        check_diagnostic_data_len_decode(payload)?;
65        Ok(Self {
66            sub_function,
67            data: payload,
68        })
69    }
70}
71
72impl Encode for DiagnosticsRequest<'_> {
73    fn encode_into(&self, buf: &mut [u8]) -> Result<usize, EncodeError> {
74        let len = self.encoded_len();
75        if buf.len() < len {
76            return Err(EncodeError::BufferTooSmall {
77                required: len,
78                available: buf.len(),
79            });
80        }
81        check_diagnostic_data_len_encode(self.data)?;
82        EncodeError::check_pdu_len(len)?;
83        buf[0] = FunctionCode::Diagnostics.code();
84        buf[1..3].copy_from_slice(&self.sub_function.code().to_be_bytes());
85        buf[3..3 + self.data.len()].copy_from_slice(self.data);
86        Ok(len)
87    }
88
89    fn encoded_len(&self) -> usize {
90        // FC(1) + sub_function(2) + data
91        3 + self.data.len()
92    }
93}