Skip to main content

mbus_core/data_unit/
common.rs

1//! Modbus Common Data Unit Module
2//!
3//! This module defines the core building blocks of the Modbus protocol, including:
4//! - **PDU (Protocol Data Unit)**: The function code and data payload independent of the communication layer.
5//! - **ADU (Application Data Unit)**: The complete frame including addressing and error checking.
6//! - **MBAP Header**: The specific header used for Modbus TCP transactions.
7//! - **Transport Agnostic Logic**: Functions to compile and decompile frames for TCP, RTU, and ASCII modes.
8//!
9//! The module is designed for `no_std` environments, utilizing `heapless` vectors for fixed-capacity
10//! memory management to ensure deterministic behavior in embedded systems.
11
12use crate::errors::MbusError;
13use crate::function_codes::public::FunctionCode;
14use crate::transport::{SerialMode, TransportType, UnitIdOrSlaveAddr, checksum};
15use heapless::Vec;
16
17/// Maximum data length for a PDU (excluding function code)
18pub const MAX_PDU_DATA_LEN: usize = 252;
19
20/// Modbus Protocol Identifier (PID)
21pub const MODBUS_PROTOCOL_ID: u16 = 0x0000;
22
23/// Maximum length of a Modbus TCP/RTU ADU in bytes.
24pub const MAX_ADU_FRAME_LEN_TCP_RTU: usize = 260;
25
26/// Maximum length of a Modbus ASCII ADU in bytes.
27pub const MAX_ADU_FRAME_LEN_ASCII: usize = 513;
28
29/// Maximum ADU frame length used by internal buffers.
30///
31/// - When `serial-ascii` feature is enabled, this is `513` (ASCII upper bound).
32/// - Otherwise this is `260` (TCP/RTU upper bound), reducing stack usage.
33#[cfg(feature = "serial-ascii")]
34pub const MAX_ADU_FRAME_LEN: usize = MAX_ADU_FRAME_LEN_ASCII;
35
36/// Maximum ADU frame length used by internal buffers.
37#[cfg(not(feature = "serial-ascii"))]
38pub const MAX_ADU_FRAME_LEN: usize = MAX_ADU_FRAME_LEN_TCP_RTU;
39
40#[cfg(test)]
41mod frame_len_tests {
42    use super::*;
43
44    #[test]
45    #[cfg(feature = "serial-ascii")]
46    fn test_max_adu_frame_len_ascii_enabled() {
47        assert_eq!(MAX_ADU_FRAME_LEN, 513);
48    }
49
50    #[test]
51    #[cfg(not(feature = "serial-ascii"))]
52    fn test_max_adu_frame_len_ascii_disabled() {
53        assert_eq!(MAX_ADU_FRAME_LEN, 260);
54    }
55}
56
57/// Size of the Modbus Application Protocol (MBAP) Header in bytes.
58pub const MBAP_HEADER_SIZE: usize = 7;
59
60/// Minimum size of a Modbus RTU ADU in bytes (Address + Function Code + CRC).
61pub const MIN_RTU_ADU_LEN: usize = 4;
62
63/// Minimum size of a Modbus ASCII ADU in bytes (Start + Address + Function Code + LRC + End).
64pub const MIN_ASCII_ADU_LEN: usize = 9;
65
66/// Size of the Modbus RTU CRC field in bytes.
67pub const RTU_CRC_SIZE: usize = 2;
68
69/// Offset of the Transaction ID in the MBAP header.
70pub const MBAP_TXN_ID_OFFSET_1B: usize = 0;
71/// Offset of the Transaction ID in the MBAP header.
72pub const MBAP_TXN_ID_OFFSET_2B: usize = MBAP_TXN_ID_OFFSET_1B + 1;
73/// Offset of the Protocol ID in the MBAP header.
74pub const MBAP_PROTO_ID_OFFSET_1B: usize = 2;
75/// Offset of the Protocol ID in the MBAP header.
76pub const MBAP_PROTO_ID_OFFSET_2B: usize = MBAP_PROTO_ID_OFFSET_1B + 1;
77/// Offset of  Length field in the MBAP header.
78pub const MBAP_LENGTH_OFFSET_1B: usize = 4;
79/// Offset of the 2nd byte of the Length field in the MBAP header.
80pub const MBAP_LENGTH_OFFSET_2B: usize = MBAP_LENGTH_OFFSET_1B + 1;
81/// Offset of the Unit ID in the MBAP header.
82pub const MBAP_UNIT_ID_OFFSET: usize = 6;
83
84/// Number of bytes in a Modbus ASCII Start character.
85pub const ASCII_START_SIZE: usize = 1;
86
87/// Number of bytes in a Modbus ASCII End sequence (CR LF).
88pub const ASCII_END_SIZE: usize = 2;
89
90/// Bit mask used to indicate a Modbus exception in the function code byte.
91pub const ERROR_BIT_MASK: u8 = 0x80;
92
93/// Bit mask used to extract the base function code from the function code byte.
94pub const FUNCTION_CODE_MASK: u8 = 0x7F;
95
96/// Checks if the given function code byte indicates an exception (error bit is set).
97///
98/// # Arguments
99/// * `function_code_byte` - The raw function code byte from the PDU.
100///
101/// # Returns
102/// `true` if the highest bit is set, indicating a Modbus exception response.
103#[inline]
104pub fn is_exception_code(function_code_byte: u8) -> bool {
105    function_code_byte & ERROR_BIT_MASK != 0
106}
107
108/// Clears the exception bit from the function code byte to retrieve the base function code.
109///
110/// # Arguments
111/// * `function_code_byte` - The raw function code byte from the PDU.
112///
113/// # Returns
114/// The base function code with the error bit cleared.
115#[inline]
116pub fn clear_exception_bit(function_code_byte: u8) -> u8 {
117    function_code_byte & FUNCTION_CODE_MASK
118}
119
120/// Modbus Protocol Data Unit (PDU).
121///
122/// The PDU is the core of the Modbus message, consisting of a function code
123/// and the associated data payload.
124#[derive(Debug, Clone)]
125pub struct Pdu {
126    /// The Modbus function code identifying the operation.
127    function_code: FunctionCode,
128    /// Optional error code for exception responses (only valid if function_code indicates an error).
129    error_code: Option<u8>,
130    /// The data payload associated with the function code.
131    data: heapless::Vec<u8, MAX_PDU_DATA_LEN>,
132    /// The actual length of the data payload (excluding the function code).
133    data_len: u8,
134}
135
136/// Modbus TCP Application Data Unit (ADU) Header (MBAP).
137#[derive(Debug, Clone, Copy)]
138pub struct MbapHeader {
139    /// Identification of a Modbus Request/Response transaction.
140    pub transaction_id: u16,
141    /// Protocol Identifier (0 = Modbus protocol).
142    pub protocol_id: u16,
143    /// Number of remaining bytes in the message (Unit ID + PDU).
144    pub length: u16,
145    /// Identification of a remote server on a non-TCP/IP network.
146    pub unit_id: u8,
147}
148
149impl MbapHeader {
150    /// Creates a new `MbapHeader` instance.
151    ///
152    /// # Arguments
153    /// * `transaction_id` - The transaction ID for the Modbus message.
154    /// * `protocol_id` - The protocol identifier (should be 0 for Modbus).
155    /// * `length` - The length of the remaining message (Unit ID + PDU).
156    /// * `unit_id` - The unit identifier for the target slave device.
157    ///
158    /// # Returns
159    /// A new `MbapHeader` instance.
160    pub fn new(transaction_id: u16, length: u16, unit_id: u8) -> Self {
161        Self {
162            transaction_id,
163            protocol_id: 0, /* Must be 0 for Modbus */
164            length,
165            unit_id,
166        }
167    }
168}
169
170/// Represents a Modbus slave address for RTU/ASCII messages.
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub struct SlaveAddress(u8);
173
174impl SlaveAddress {
175    /// Creates a new `SlaveAddress` instance.
176    pub fn new(address: u8) -> Result<Self, MbusError> {
177        if !(0..=247).contains(&address) {
178            return Err(MbusError::InvalidSlaveAddress);
179        }
180        Ok(Self(address))
181    }
182
183    /// Accessor for the slave address.
184    pub fn address(&self) -> u8 {
185        self.0
186    }
187}
188
189/// Additional address field for Modbus RTU/TCP messages.
190#[derive(Debug, Clone, Copy)]
191#[allow(dead_code)]
192pub enum AdditionalAddress {
193    /// The MBAP header for Modbus TCP messages. This includes Transaction ID, Protocol ID, Length, and Unit ID.
194    MbapHeader(MbapHeader),
195    /// The slave address for Modbus RTU/ASCII messages.
196    SlaveAddress(SlaveAddress),
197}
198
199/// Represents a complete Modbus message, including the additional address, PDU and Error check.
200#[derive(Debug, Clone)]
201pub struct ModbusMessage {
202    /// The MBAP header for Modbus TCP messages.
203    pub additional_address: AdditionalAddress,
204    /// The Protocol Data Unit (PDU) containing the function code and data.
205    pub pdu: Pdu,
206    // Error check (CRC for RTU, LRC for ASCII) would be handled separately based on the transport layer.
207}
208
209impl ModbusMessage {
210    /// Creates a new `ModbusMessage` instance.
211    ///
212    /// # Arguments
213    /// * `additional_address` - The additional address information (MBAP header or slave address).
214    /// * `pdu` - The Protocol Data Unit containing the function code and data.
215    ///
216    /// # Returns
217    /// A new `ModbusMessage` instance.
218    pub fn new(additional_address: AdditionalAddress, pdu: Pdu) -> Self {
219        Self {
220            additional_address,
221            pdu,
222        }
223    }
224
225    /// Accessor for the additional address.
226    pub fn additional_address(&self) -> &AdditionalAddress {
227        &self.additional_address
228    }
229
230    /// Accessor for the Protocol Data Unit (PDU).
231    ///
232    /// The PDU contains the function code and the data payload, which are
233    /// independent of the underlying transport layer (TCP, RTU, or ASCII).
234    ///
235    pub fn pdu(&self) -> &Pdu {
236        &self.pdu
237    }
238
239    /// Extracts the target device identifier from the message.
240    ///
241    /// This method abstracts the difference between TCP (Unit ID) and Serial (Slave Address)
242    /// addressing, returning a unified `UnitIdOrSlaveAddr` type.
243    ///
244    /// # Returns
245    /// A `UnitIdOrSlaveAddr` representing the destination or source device.
246    pub fn unit_id_or_slave_addr(&self) -> UnitIdOrSlaveAddr {
247        match self.additional_address {
248            AdditionalAddress::MbapHeader(header) => {
249                UnitIdOrSlaveAddr::try_from(header.unit_id).unwrap_or(UnitIdOrSlaveAddr::default())
250            }
251            AdditionalAddress::SlaveAddress(slave_address) => {
252                UnitIdOrSlaveAddr::try_from(slave_address.address())
253                    .unwrap_or(UnitIdOrSlaveAddr::default())
254            }
255        }
256    }
257
258    /// Retrieves the transaction identifier for the message.
259    ///
260    /// For TCP messages, this returns the ID from the MBAP header.
261    /// For Serial (RTU/ASCII) messages, this returns 0 as they are inherently synchronous.
262    pub fn transaction_id(&self) -> u16 {
263        match self.additional_address {
264            AdditionalAddress::MbapHeader(header) => header.transaction_id,
265            AdditionalAddress::SlaveAddress(_) => 0,
266        }
267    }
268
269    /// Accessor for the function code from the PDU.
270    pub fn function_code(&self) -> FunctionCode {
271        self.pdu.function_code()
272    }
273
274    /// Accessor for the data payload from the PDU.
275    pub fn data(&self) -> &heapless::Vec<u8, MAX_PDU_DATA_LEN> {
276        self.pdu.data()
277    }
278
279    /// Accessor for the actual length of the data payload.
280    pub fn data_len(&self) -> u8 {
281        self.pdu.data_len()
282    }
283
284    /// Converts the `ModbusMessage` into its byte representation.
285    ///
286    /// This method serializes the additional address (MBAP header or slave address)
287    /// followed by the PDU.
288    ///
289    /// # Returns
290    /// `Ok(Vec<u8, MAX_ADU_LEN>)` containing the ADU bytes, or an `MbusError` if
291    /// the message cannot be serialized.
292    pub fn to_bytes(&self) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, MbusError> {
293        let mut adu_bytes = Vec::new();
294
295        match &self.additional_address {
296            AdditionalAddress::MbapHeader(header) => {
297                // MBAP Header: TID (2), PID (2), Length (2), Unit ID (1)
298                adu_bytes
299                    .extend_from_slice(&header.transaction_id.to_be_bytes())
300                    .map_err(|_| MbusError::Unexpected)?;
301                adu_bytes
302                    .extend_from_slice(&header.protocol_id.to_be_bytes())
303                    .map_err(|_| MbusError::Unexpected)?;
304                adu_bytes
305                    .extend_from_slice(&header.length.to_be_bytes())
306                    .map_err(|_| MbusError::Unexpected)?;
307                adu_bytes
308                    .push(header.unit_id)
309                    .map_err(|_| MbusError::Unexpected)?;
310            }
311            AdditionalAddress::SlaveAddress(address) => {
312                adu_bytes
313                    .push(address.address())
314                    .map_err(|_| MbusError::Unexpected)?;
315            }
316        }
317
318        let pdu_bytes = self.pdu.to_bytes()?;
319        adu_bytes
320            .extend_from_slice(&pdu_bytes)
321            .map_err(|_| MbusError::Unexpected)?;
322
323        Ok(adu_bytes)
324    }
325
326    /// Creates a `ModbusMessage` from its byte representation (ADU).
327    ///
328    /// This method parses the MBAP header and the PDU from the given byte slice.
329    ///
330    /// # Arguments
331    /// * `bytes` - A byte slice containing the complete Modbus TCP ADU.
332    ///
333    /// # Returns
334    /// `Ok((ModbusMessage, usize))` containing the parsed message and the number of consumed bytes.
335    pub fn from_bytes(bytes: &[u8]) -> Result<Self, MbusError> {
336        // Minimum ADU length: MBAP header + 1 byte Function Code
337        if bytes.len() < MBAP_HEADER_SIZE + 1 {
338            return Err(MbusError::InvalidAduLength); // Reusing for general invalid length
339        }
340
341        // Parse MBAP Header
342        // Transaction ID: 2 bytes starting at offset 0
343        let transaction_id =
344            u16::from_be_bytes([bytes[MBAP_TXN_ID_OFFSET_1B], bytes[MBAP_TXN_ID_OFFSET_2B]]);
345        // Protocol ID: 2 bytes starting at offset 2
346        let protocol_id = u16::from_be_bytes([
347            bytes[MBAP_PROTO_ID_OFFSET_1B],
348            bytes[MBAP_PROTO_ID_OFFSET_2B],
349        ]);
350        // Length: 2 bytes starting at offset 4
351        let length =
352            u16::from_be_bytes([bytes[MBAP_LENGTH_OFFSET_1B], bytes[MBAP_LENGTH_OFFSET_2B]]);
353        // Unit ID: 1 byte at offset 6
354        let unit_id = bytes[MBAP_UNIT_ID_OFFSET];
355
356        // Validate Protocol Identifier
357        if protocol_id != MODBUS_PROTOCOL_ID {
358            return Err(MbusError::BasicParseError); // Invalid protocol ID
359        }
360
361        // Validate Length field
362        // The length field specifies the number of following bytes, including the Unit ID and PDU.
363        // So, actual_pdu_and_unit_id_len = bytes.len() - 6 (MBAP header without length field)
364        // And length field value should be actual_pdu_and_unit_id_len
365        const INITIAL_FRAME_LEN: usize = MBAP_HEADER_SIZE - 1; // MBAP
366        let expected_total_len_from_header = length as usize + INITIAL_FRAME_LEN; // 6 bytes for TID, PID, Length field itself
367
368        // Ensure we have enough bytes in the buffer to form the complete expected frame
369        if bytes.len() < expected_total_len_from_header {
370            return Err(MbusError::InvalidPduLength);
371        }
372
373        // Slice exactly the frame length indicated by the header to support pipelined streams
374        let frame_bytes = &bytes[..expected_total_len_from_header];
375        // The PDU starts after the MBAP header
376        let pdu_bytes_slice = &frame_bytes[MBAP_HEADER_SIZE..];
377
378        // Parse PDU using the existing Pdu::from_bytes method
379        let pdu = Pdu::from_bytes(pdu_bytes_slice)?;
380
381        let additional_addr = AdditionalAddress::MbapHeader(MbapHeader {
382            transaction_id,
383            protocol_id,
384            length,
385            unit_id,
386        });
387
388        Ok(ModbusMessage::new(additional_addr, pdu))
389    }
390
391    /// Converts the `ModbusMessage` into its ASCII ADU byte representation.
392    ///
393    /// This method serializes the message to binary, calculates the LRC,
394    /// and then encodes the result into Modbus ASCII format (Start ':', Hex, End CR LF).
395    ///
396    /// # Returns
397    /// `Ok(Vec<u8, MAX_ADU_FRAME_LEN>)` containing the ASCII ADU bytes.
398    ///
399    /// # Errors
400    /// Returns `MbusError::BufferTooSmall` if the resulting ASCII frame exceeds `MAX_ADU_FRAME_LEN`.
401    pub fn to_ascii_bytes(&self) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, MbusError> {
402        let mut binary_data = self.to_bytes()?;
403
404        // Calculate LRC on Address + PDU
405        let lrc = checksum::lrc(&binary_data);
406
407        // Append LRC to binary data temporarily to iterate over it
408        binary_data
409            .push(lrc)
410            .map_err(|_| MbusError::BufferTooSmall)?;
411
412        let mut ascii_data = Vec::new();
413
414        // Start character ':'
415        ascii_data
416            .push(b':')
417            .map_err(|_| MbusError::BufferTooSmall)?;
418
419        for byte in binary_data {
420            let high = (byte >> 4) & 0x0F;
421            let low = byte & 0x0F;
422
423            ascii_data
424                .push(nibble_to_hex(high))
425                .map_err(|_| MbusError::BufferTooSmall)?;
426            ascii_data
427                .push(nibble_to_hex(low))
428                .map_err(|_| MbusError::BufferTooSmall)?;
429        }
430
431        // End characters CR LF
432        ascii_data
433            .push(b'\r')
434            .map_err(|_| MbusError::BufferTooSmall)?;
435        ascii_data
436            .push(b'\n')
437            .map_err(|_| MbusError::BufferTooSmall)?;
438
439        Ok(ascii_data)
440    }
441
442    /// Creates a `ModbusMessage` from a raw Modbus RTU byte slice.
443    ///
444    /// This method validates the RTU frame by checking the minimum length and
445    /// verifying the 16-bit CRC (Cyclic Redundancy Check).
446    ///
447    /// # Arguments
448    /// * `frame` - A byte slice containing the complete Modbus RTU ADU.
449    ///
450    /// # Returns
451    /// * `Ok(ModbusMessage)` if the CRC is valid and the PDU is correctly parsed.
452    /// * `Err(MbusError)` if the frame is too short, the checksum fails, or the PDU is invalid.
453    pub fn from_rtu_bytes(frame: &[u8]) -> Result<Self, MbusError> {
454        // RTU Frame: [Slave Address (1)] [PDU (N)] [CRC (2)]
455        // Minimum length: MIN_RTU_ADU_LEN (4)
456        if frame.len() < MIN_RTU_ADU_LEN {
457            return Err(MbusError::InvalidAduLength);
458        }
459
460        // The CRC is the last 2 bytes of the frame
461        let data_len = frame.len() - RTU_CRC_SIZE;
462        let data_to_check = &frame[..data_len];
463
464        // Modbus RTU uses Little-Endian for CRC transmission
465        let received_crc = u16::from_le_bytes([frame[data_len], frame[data_len + 1]]);
466        let calculated_crc = checksum::crc16(data_to_check);
467
468        // Verify data integrity
469        if calculated_crc != received_crc {
470            return Err(MbusError::ChecksumError); // CRC Mismatch
471        }
472
473        // Extract Slave Address (1st byte)
474        let slave_address = SlaveAddress::new(frame[0])?;
475
476        // PDU is from byte 1 to end of data (excluding CRC)
477        let pdu_bytes = &data_to_check[1..];
478        let pdu = Pdu::from_bytes(pdu_bytes)?;
479
480        Ok(ModbusMessage::new(
481            AdditionalAddress::SlaveAddress(slave_address),
482            pdu,
483        ))
484    }
485
486    /// Creates a `ModbusMessage` from a raw Modbus ASCII byte slice.
487    ///
488    /// This method performs the following validation and transformation steps:
489    /// 1. Validates the frame structure (starts with ':', ends with "\r\n").
490    /// 2. Decodes the hexadecimal ASCII representation into binary data.
491    /// 3. Verifies the Longitudinal Redundancy Check (LRC) checksum.
492    /// 4. Parses the resulting binary into a `SlaveAddress` and `Pdu`.
493    ///
494    /// # Arguments
495    /// * `frame` - A byte slice containing the complete Modbus ASCII ADU.
496    ///
497    /// # Returns
498    /// * `Ok(ModbusMessage)` if the frame is valid and checksum matches.
499    /// * `Err(MbusError)` for invalid length, malformed hex, or checksum failure.
500    pub fn from_ascii_bytes(frame: &[u8]) -> Result<Self, MbusError> {
501        // ASCII Frame: [Start (:)] [Address (2)] [PDU (N)] [LRC (2)] [End (\r\n)]
502        // Minimum length: MIN_ASCII_ADU_LEN (9)
503        if frame.len() < MIN_ASCII_ADU_LEN {
504            return Err(MbusError::InvalidAduLength);
505        }
506
507        // Check Start and End characters
508        if frame[0] != b':' {
509            return Err(MbusError::BasicParseError); // Missing start char
510        }
511        if frame[frame.len() - 2] != b'\r' || frame[frame.len() - 1] != b'\n' {
512            return Err(MbusError::BasicParseError); // Missing end chars
513        }
514
515        // Extract Hex content (excluding ':' and '\r\n')
516        let hex_content = &frame[ASCII_START_SIZE..frame.len() - ASCII_END_SIZE];
517        if !hex_content.len().is_multiple_of(2) {
518            return Err(MbusError::BasicParseError); // Odd length hex string
519        }
520
521        // Decode Hex to Binary
522        // Max binary length = (513 - 3) / 2 = 255. Using 260 for safety.
523        let mut binary_data: Vec<u8, 260> = Vec::new();
524        for chunk in hex_content.chunks(2) {
525            let byte = hex_pair_to_byte(chunk[0], chunk[1])?;
526            binary_data
527                .push(byte)
528                .map_err(|_| MbusError::BufferTooSmall)?;
529        }
530
531        // Binary structure: [Slave Address (1)] [PDU (N)] [LRC (1)]
532        if binary_data.len() < 2 {
533            return Err(MbusError::InvalidAduLength);
534        }
535
536        let data_len = binary_data.len() - 1;
537        let data_to_check = &binary_data[..data_len];
538        let received_lrc = binary_data[data_len];
539
540        let calculated_lrc = checksum::lrc(data_to_check);
541
542        if calculated_lrc != received_lrc {
543            return Err(MbusError::ChecksumError); // LRC Mismatch
544        }
545
546        let slave_address = SlaveAddress::new(binary_data[0])?;
547        let pdu_bytes = &binary_data[1..data_len];
548        let pdu = Pdu::from_bytes(pdu_bytes)?;
549
550        Ok(ModbusMessage::new(
551            AdditionalAddress::SlaveAddress(slave_address),
552            pdu,
553        ))
554    }
555}
556
557impl Pdu {
558    ///
559    /// Creates a new `Pdu` instance.
560    ///
561    /// # Arguments
562    /// * `function_code` - The Modbus function code.
563    /// * `data` - The data payload (either raw bytes or structured sub-codes).
564    /// * `data_len` - The actual length of the data payload in bytes.
565    ///
566    /// # Returns
567    /// A new `Pdu` instance.
568    pub fn new(
569        function_code: FunctionCode,
570        data: heapless::Vec<u8, MAX_PDU_DATA_LEN>,
571        data_len: u8,
572    ) -> Self {
573        Self {
574            function_code,
575            error_code: None, // Default to None for normal responses; can be set for exceptions
576            data,             // Ensure the heapless::Vec is moved here
577            data_len,
578        }
579    }
580
581    /// Accessor for the function code.
582    pub fn function_code(&self) -> FunctionCode {
583        self.function_code
584    }
585
586    /// Accessor for the data payload.
587    pub fn data(&self) -> &Vec<u8, MAX_PDU_DATA_LEN> {
588        &self.data
589    }
590
591    /// Accessor for the actual length of the data payload.
592    pub fn data_len(&self) -> u8 {
593        self.data_len
594    }
595
596    /// Accessor for the error code from the PDU.
597    pub fn error_code(&self) -> Option<u8> {
598        self.error_code
599    }
600
601    /// Converts the PDU into its byte representation.
602    ///
603    /// This method serializes the function code and its associated data payload.
604    /// It uses an `unsafe` block to access the `Data` union, assuming that
605    /// `self.data.bytes` contains the full data payload and `self.data_len`
606    /// accurately reflects its length.
607    ///
608    /// # Returns
609    /// `Ok(Vec<u8, 253>)` containing the PDU bytes, or an `MbusError` if
610    /// the PDU cannot be serialized (e.g., due to buffer overflow).
611    pub fn to_bytes(&self) -> Result<Vec<u8, 253>, MbusError> {
612        let mut pdu_bytes = Vec::new(); // Capacity is 253 (1 byte FC + 252 bytes data)
613        pdu_bytes
614            .push(self.function_code as u8)
615            .map_err(|_| MbusError::Unexpected)?; // Function code (1 byte)
616
617        pdu_bytes
618            .extend_from_slice(&self.data.as_slice()[..self.data_len as usize])
619            .map_err(|_| MbusError::BufferLenMissmatch)?; // Data bytes (variable length)
620
621        Ok(pdu_bytes)
622    }
623
624    /// Creates a PDU from its byte representation.
625    ///
626    /// This method parses the function code and data payload from the given byte slice.
627    ///
628    /// # Arguments
629    /// * `bytes` - A byte slice containing the PDU (Function Code + Data).
630    ///
631    /// # Returns
632    /// `Ok(Pdu)` if the bytes represent a valid PDU, or an `MbusError` otherwise.
633    pub fn from_bytes(bytes: &[u8]) -> Result<Self, MbusError> {
634        if bytes.is_empty() || bytes.len() < 2 {
635            return Err(MbusError::InvalidPduLength);
636        }
637
638        let error_code = if is_exception_code(bytes[0]) {
639            Some(bytes[1]) // The second byte is the exception code for error responses
640        } else {
641            None
642        };
643        let function_code = clear_exception_bit(bytes[0]); // Mask out the error bit to get the actual function code
644
645        let function_code = FunctionCode::try_from(function_code)?;
646
647        let data_slice = &bytes[1..];
648        let data_len = data_slice.len();
649
650        if data_len > MAX_PDU_DATA_LEN {
651            return Err(MbusError::InvalidPduLength);
652        }
653
654        let mut data = heapless::Vec::new();
655        data.extend_from_slice(data_slice)
656            .map_err(|_| MbusError::BufferLenMissmatch)?;
657
658        Ok(Pdu {
659            function_code,
660            error_code,
661            data,
662            data_len: data_len as u8,
663        })
664    }
665}
666
667/// Helper to build the ADU from PDU based on transport type.
668pub fn compile_adu_frame(
669    txn_id: u16,
670    unit_id: u8,
671    pdu: Pdu,
672    transport_type: TransportType,
673) -> Result<Vec<u8, MAX_ADU_FRAME_LEN>, MbusError> {
674    match transport_type {
675        TransportType::StdTcp | TransportType::CustomTcp => {
676            let pdu_bytes_len = pdu.to_bytes()?.len() as u16;
677            let mbap_header = MbapHeader::new(txn_id, pdu_bytes_len + 1, unit_id);
678            ModbusMessage::new(AdditionalAddress::MbapHeader(mbap_header), pdu).to_bytes()
679        }
680        TransportType::StdSerial(serial_mode) | TransportType::CustomSerial(serial_mode) => {
681            let slave_address = SlaveAddress(unit_id);
682            let adu_bytes = match serial_mode {
683                SerialMode::Rtu => {
684                    let mut adu_bytes =
685                        ModbusMessage::new(AdditionalAddress::SlaveAddress(slave_address), pdu)
686                            .to_bytes()?;
687                    // Calculate the 16-bit CRC for the Slave Address + PDU.
688                    let crc16 = checksum::crc16(adu_bytes.as_slice());
689                    // Modbus RTU transmits CRC in Little-Endian (LSB first) according to the spec.
690                    let crc_bytes = crc16.to_le_bytes();
691                    adu_bytes
692                        .extend_from_slice(&crc_bytes)
693                        .map_err(|_| MbusError::Unexpected)?;
694
695                    adu_bytes
696                }
697                SerialMode::Ascii => {
698                    ModbusMessage::new(AdditionalAddress::SlaveAddress(slave_address), pdu)
699                        .to_ascii_bytes()?
700                }
701            };
702
703            Ok(adu_bytes)
704        }
705    }
706}
707
708/// Decodes a raw transport frame into a ModbusMessage based on the transport type.
709pub fn decompile_adu_frame(
710    frame: &[u8],
711    transport_type: TransportType,
712) -> Result<ModbusMessage, MbusError> {
713    match transport_type {
714        TransportType::StdTcp | TransportType::CustomTcp => {
715            // Parse MBAP header and PDU
716            ModbusMessage::from_bytes(frame)
717        }
718        TransportType::StdSerial(serial_mode) | TransportType::CustomSerial(serial_mode) => {
719            match serial_mode {
720                SerialMode::Rtu => ModbusMessage::from_rtu_bytes(frame),
721                SerialMode::Ascii => ModbusMessage::from_ascii_bytes(frame),
722            }
723        }
724    }
725}
726
727/// Derives the expected total length of a Modbus frame from its initial bytes.
728///
729/// This function is used by stream-based transports to determine if a complete
730/// Application Data Unit (ADU) has been received before attempting full decompression.
731///
732/// # Arguments
733/// * `frame` - The raw byte buffer containing the partial or full frame.
734/// * `transport_type` - The Modbus variant (TCP, RTU, or ASCII).
735///
736/// # Returns
737/// * `Some(usize)` - The calculated total length of the frame if enough metadata is present.
738/// * `None` - If the buffer is too short to determine the length.
739pub fn derive_length_from_bytes(frame: &[u8], transport_type: TransportType) -> Option<usize> {
740    match transport_type {
741        TransportType::StdTcp | TransportType::CustomTcp => {
742            // TCP (MBAP) requires at least 6 bytes to read the 'Length' field.
743            // MBAP structure: TID(2), PID(2), Length(2), UnitID(1)
744            if frame.len() < 6 {
745                return None;
746            }
747
748            // In Modbus TCP, the Protocol ID MUST be 0x0000.
749            // If it's not, this is garbage data. We return a huge length to trigger a parse error
750            // downstream and force the window to slide and resync.
751            let protocol_id = u16::from_be_bytes([frame[2], frame[3]]);
752            if protocol_id != MODBUS_PROTOCOL_ID {
753                return Some(usize::MAX);
754            }
755
756            // The Length field in MBAP (offset 4) counts all following bytes (UnitID + PDU).
757            // Total ADU = 6 bytes (Header up to Length) + value of Length field.
758            let length_field = u16::from_be_bytes([frame[4], frame[5]]) as usize;
759            Some(6 + length_field)
760        }
761        TransportType::StdSerial(SerialMode::Rtu)
762        | TransportType::CustomSerial(SerialMode::Rtu) => {
763            if frame.len() < 2 {
764                return None;
765            }
766
767            let fc = frame[1];
768
769            // Exception responses are universally 5 bytes: [ID][FC+0x80][ExcCode][CRC_L][CRC_H]
770            if is_exception_code(fc) {
771                return Some(5);
772            }
773
774            // Helper: Opportunistically verifies if a potential frame boundary has a valid CRC.
775            // This allows us to disambiguate Requests vs Responses dynamically as the stream arrives.
776            let check_crc = |len: usize| -> bool {
777                if frame.len() >= len && len >= MIN_RTU_ADU_LEN {
778                    let data_len = len - RTU_CRC_SIZE;
779                    let received_crc = u16::from_le_bytes([frame[data_len], frame[data_len + 1]]);
780                    checksum::crc16(&frame[..data_len]) == received_crc
781                } else {
782                    false
783                }
784            };
785
786            get_byte_count_from_frame(frame, fc, check_crc)
787        }
788        TransportType::StdSerial(SerialMode::Ascii)
789        | TransportType::CustomSerial(SerialMode::Ascii) => {
790            // ASCII frames are delimited by ':' and '\r\n'.
791            // We scan for the end-of-frame marker.
792            if frame.len() < MIN_ASCII_ADU_LEN {
793                return None;
794            }
795
796            // Fast linear scan for the LF character which terminates the frame
797            frame.iter().position(|&b| b == b'\n').map(|pos| pos + 1)
798        }
799    }
800}
801
802fn get_byte_count_from_frame(
803    frame: &[u8],
804    fc: u8,
805    check_crc: impl Fn(usize) -> bool,
806) -> Option<usize> {
807    let mut candidates = heapless::Vec::<usize, 4>::new();
808    let mut min_needed = usize::MAX;
809
810    // Helper function to safely calculate dynamic lengths based on a byte in the frame.
811    // We pass candidates as a mutable reference to avoid "multiple mutable borrow" errors
812    // that occur when a closure captures a mutable variable and is called multiple times.
813    let mut add_dyn = |cands: &mut heapless::Vec<usize, 4>, offset: usize, base: usize| {
814        if frame.len() > offset {
815            let _ = cands.push(base + frame[offset] as usize);
816        } else {
817            min_needed = core::cmp::min(min_needed, offset + 1);
818        }
819    };
820
821    // Map structural candidates (Requests and Responses combined) based on Modbus definitions
822    match fc {
823        1..=4 => {
824            let _ = candidates.push(8);
825            add_dyn(&mut candidates, 2, 5);
826        }
827        5 | 6 | 8 => {
828            let _ = candidates.push(8);
829        }
830        7 => {
831            let _ = candidates.push(4);
832            let _ = candidates.push(5);
833        }
834        11 => {
835            let _ = candidates.push(4);
836            let _ = candidates.push(8);
837        }
838        12 | 17 => {
839            let _ = candidates.push(4);
840            add_dyn(&mut candidates, 2, 5);
841        }
842        15 | 16 => {
843            let _ = candidates.push(8);
844            add_dyn(&mut candidates, 6, 9);
845        }
846        20 | 21 => add_dyn(&mut candidates, 2, 5),
847        22 => {
848            let _ = candidates.push(10);
849        }
850        23 => {
851            add_dyn(&mut candidates, 2, 5);
852            add_dyn(&mut candidates, 10, 13);
853        }
854        24 => {
855            let _ = candidates.push(6);
856            if frame.len() >= 4 {
857                let byte_count = u16::from_be_bytes([frame[2], frame[3]]) as usize;
858                let _ = candidates.push(6 + byte_count);
859            } else {
860                min_needed = core::cmp::min(min_needed, 4);
861            }
862        }
863        43 => {
864            if check_crc(7) {
865                return Some(7);
866            }
867            // Response is unpredictable. Scan opportunistically forwards to support pipelined frames.
868            for len in MIN_RTU_ADU_LEN..=frame.len() {
869                if check_crc(len) {
870                    return Some(len);
871                }
872            }
873            return None;
874        }
875        _ => {
876            for len in MIN_RTU_ADU_LEN..=frame.len() {
877                if check_crc(len) {
878                    return Some(len);
879                }
880            }
881            return None;
882        }
883    }
884
885    // 1. Opportunistic CRC checks to lock in an exact frame boundary
886    for &len in &candidates {
887        if check_crc(len) {
888            return Some(len);
889        }
890    }
891
892    // 2. If no CRC matched yet, determine the max length we might need to wait for
893    let max_candidate = candidates.iter().copied().max().unwrap_or(0);
894    let target = if min_needed != usize::MAX {
895        core::cmp::max(min_needed, max_candidate)
896    } else {
897        max_candidate
898    };
899
900    if target > 0 { Some(target) } else { None }
901}
902
903/// Helper function to convert a 4-bit nibble to its ASCII hex representation.
904fn nibble_to_hex(nibble: u8) -> u8 {
905    match nibble {
906        0..=9 => b'0' + nibble,
907        10..=15 => b'A' + (nibble - 10),
908        _ => b'?', // Should not happen for a valid nibble
909    }
910}
911
912/// Helper function to convert a hex character to its 4-bit nibble value.
913fn hex_char_to_nibble(c: u8) -> Result<u8, MbusError> {
914    match c {
915        b'0'..=b'9' => Ok(c - b'0'),
916        b'A'..=b'F' => Ok(c - b'A' + 10),
917        b'a'..=b'f' => Ok(c - b'a' + 10),
918        _ => Err(MbusError::BasicParseError),
919    }
920}
921
922/// Helper function to convert two hex characters to a byte.
923fn hex_pair_to_byte(high: u8, low: u8) -> Result<u8, MbusError> {
924    let h = hex_char_to_nibble(high)?;
925    let l = hex_char_to_nibble(low)?;
926    Ok((h << 4) | l)
927}
928
929#[cfg(test)]
930mod tests {
931    use super::*;
932    use crate::function_codes::public::FunctionCode;
933    use heapless::Vec;
934
935    // --- Tests for Pdu::from_bytes ---
936
937    /// Test case: `Pdu::from_bytes` with a PDU that has no data bytes (only FC).
938    ///
939    /// According to the implementation, a PDU must be at least 2 bytes.
940    #[test]
941    fn test_pdu_from_bytes_invalid_no_data() {
942        let bytes = [0x11];
943        let err = Pdu::from_bytes(&bytes).expect_err("Should return error for PDU with only FC");
944        assert_eq!(err, MbusError::InvalidPduLength);
945    }
946
947    /// Test case: `Pdu::from_bytes` with a valid `Read Coils` request PDU.
948    ///
949    /// This tests parsing a function code followed by address and quantity bytes.
950    ///
951    /// Modbus Specification Reference: V1.1b3, Section 6.1 (Read Coils).
952    #[test]
953    fn test_pdu_from_bytes_valid_read_coils_request() {
954        // Read Coils (0x01) request: FC (1 byte) + Starting Address (2 bytes) + Quantity of Coils (2 bytes)
955        // Example: Read 10 coils starting at address 0x0000
956        let bytes = [0x01, 0x00, 0x00, 0x00, 0x0A]; // FC, Addr_Hi, Addr_Lo, Qty_Hi, Qty_Lo
957        let pdu = Pdu::from_bytes(&bytes).expect("Should successfully parse Read Coils request");
958
959        assert_eq!(pdu.function_code, FunctionCode::ReadCoils);
960        assert_eq!(pdu.data_len, 4);
961        assert_eq!(pdu.data.as_slice(), &[0x00, 0x00, 0x00, 0x0A]);
962    }
963
964    /// Test case: `Pdu::from_bytes` with a valid `Read Holding Registers` response PDU.
965    ///
966    /// This tests parsing a function code, a byte count, and then the actual data bytes.
967    ///
968    /// Modbus Specification Reference: V1.1b3, Section 6.3 (Read Holding Registers).
969    #[test]
970    fn test_pdu_from_bytes_valid_read_holding_registers_response() {
971        // Read Holding Registers (0x03) response: FC (1 byte) + Byte Count (1 byte) + Data (N bytes)
972        // Example: Response with 2 registers (4 bytes of data)
973        let bytes = [0x03, 0x04, 0x12, 0x34, 0x56, 0x78]; // FC, Byte Count, Reg1_Hi, Reg1_Lo, Reg2_Hi, Reg2_Lo
974        let pdu = Pdu::from_bytes(&bytes)
975            .expect("Should successfully parse Read Holding Registers response");
976
977        assert_eq!(pdu.function_code, FunctionCode::ReadHoldingRegisters);
978        assert_eq!(pdu.data_len, 5); // Byte Count (0x04) + 4 data bytes
979        assert_eq!(pdu.data.as_slice(), &[0x04, 0x12, 0x34, 0x56, 0x78]);
980    }
981
982    /// Test case: `Pdu::from_bytes` with a PDU containing the maximum allowed data length.
983    ///
984    /// The maximum PDU data length is 252 bytes (total PDU size 253 bytes including FC).
985    ///
986    /// Modbus Specification Reference: V1.1b3, Section 4.1 (PDU Size).
987    #[test]
988    fn test_pdu_from_bytes_valid_max_data_length() {
989        // Max PDU data length is 252 bytes.
990        let mut bytes_vec: Vec<u8, 253> = Vec::new();
991        let _ = bytes_vec.push(0x03); // Dummy FC (Read Holding Registers)
992        for i in 0..252 {
993            let _ = bytes_vec.push(i as u8);
994        }
995        let bytes = bytes_vec.as_slice();
996        let pdu = Pdu::from_bytes(bytes).expect("Should parse valid PDU with max data");
997
998        assert_eq!(pdu.function_code, FunctionCode::ReadHoldingRegisters);
999        assert_eq!(pdu.data_len, 252);
1000        assert_eq!(pdu.data.as_slice(), &bytes[1..]);
1001    }
1002
1003    /// Test case: `Pdu::from_bytes` with an empty byte slice.
1004    ///
1005    /// An empty slice is an invalid PDU as it lacks even a function code.
1006    #[test]
1007    fn test_pdu_from_bytes_empty_slice_error() {
1008        let bytes = [];
1009        let err = Pdu::from_bytes(&bytes).expect_err("Should return error for empty slice");
1010        assert_eq!(err, MbusError::InvalidPduLength);
1011    }
1012
1013    /// Test case: `Pdu::from_bytes` with an invalid or unsupported function code.
1014    ///
1015    /// This checks for `MbusError::UnsupportedFunction` when the function code is not recognized.
1016    /// Modbus Specification Reference: V1.1b3, Section 5.1 (Public Function Code Definition).
1017    #[test]
1018    fn test_pdu_from_bytes_invalid_function_code_error() {
1019        // 0x00 is not a valid public function code
1020        let bytes = [0x00, 0x01, 0x02];
1021        let err = Pdu::from_bytes(&bytes).expect_err("Should return error for invalid FC 0x00");
1022        assert_eq!(err, MbusError::UnsupportedFunction(0x00));
1023
1024        // 0xFF is also not a valid public function code
1025        let bytes = [0xFF, 0x01, 0x02];
1026        let err = Pdu::from_bytes(&bytes).expect_err("Should return error for invalid FC 0xFF");
1027        assert_eq!(err, MbusError::UnsupportedFunction(0x7F));
1028    }
1029
1030    /// Test case: `Pdu::from_bytes` with a data payload exceeding the maximum allowed length.
1031    ///
1032    /// The maximum PDU data length is 252 bytes. This test provides 253 data bytes.
1033    ///
1034    /// Modbus Specification Reference: V1.1b3, Section 4.1 (PDU Size).
1035    #[test]
1036    fn test_pdu_from_bytes_data_too_long_error() {
1037        // PDU data length > 252 bytes (total PDU length > 253 bytes)
1038        let mut bytes_vec: Vec<u8, 254> = Vec::new();
1039        let _ = bytes_vec.push(0x03); // Dummy FC
1040        for i in 0..253 {
1041            // 253 data bytes, which is too many
1042            let _ = bytes_vec.push(i as u8);
1043        }
1044        let bytes = bytes_vec.as_slice();
1045        let err = Pdu::from_bytes(bytes).expect_err("Should return error for too much data");
1046        assert_eq!(err, MbusError::InvalidPduLength);
1047    }
1048
1049    // --- Tests for Pdu::to_bytes ---
1050
1051    /// Test case: `Pdu::to_bytes` with a PDU that has no data bytes.
1052    ///
1053    /// This covers function codes like `ReportServerId` (0x11) which consist only of the function code.
1054    ///
1055    /// Modbus Specification Reference: V1.1b3, Section 6.13 (Report Server ID).
1056    #[test]
1057    fn test_pdu_to_bytes_no_data() {
1058        let pdu = Pdu::new(FunctionCode::ReportServerId, Vec::new(), 0);
1059        let bytes = pdu.to_bytes().expect("Should convert PDU to bytes");
1060        assert_eq!(bytes.as_slice(), &[0x11]);
1061    }
1062
1063    /// Test case: `Pdu::to_bytes` with a PDU containing a typical data payload.
1064    ///
1065    /// Example: `Read Coils` request with address and quantity.
1066    ///
1067    /// Modbus Specification Reference: V1.1b3, Section 6.1 (Read Coils).
1068    #[test]
1069    fn test_pdu_to_bytes_with_data() {
1070        let mut data_vec = Vec::new();
1071        data_vec
1072            .extend_from_slice(&[0x00, 0x00, 0x00, 0x0A])
1073            .unwrap(); // Read 10 coils
1074
1075        let pdu = Pdu::new(FunctionCode::ReadCoils, data_vec, 4);
1076        let bytes = pdu.to_bytes().expect("Should convert PDU to bytes");
1077        assert_eq!(bytes.as_slice(), &[0x01, 0x00, 0x00, 0x00, 0x0A]);
1078    }
1079
1080    /// Test case: `Pdu::to_bytes` with a PDU containing the maximum allowed data length.
1081    ///
1082    /// The maximum PDU data length is 252 bytes.
1083    ///
1084    /// Modbus Specification Reference: V1.1b3, Section 4.1 (PDU Size).
1085    #[test]
1086    fn test_pdu_to_bytes_max_data() {
1087        let mut data_vec = Vec::new();
1088        for i in 0..252 {
1089            data_vec.push(i as u8).unwrap();
1090        }
1091
1092        let pdu = Pdu::new(FunctionCode::ReadHoldingRegisters, data_vec, 252);
1093        let bytes = pdu
1094            .to_bytes()
1095            .expect("Should convert PDU to bytes with max data");
1096        let mut expected_bytes_vec: Vec<u8, 253> = Vec::new();
1097        let _ = expected_bytes_vec.push(0x03);
1098        for i in 0..252 {
1099            let _ = expected_bytes_vec.push(i as u8);
1100        }
1101        assert_eq!(bytes.as_slice(), expected_bytes_vec.as_slice());
1102    }
1103
1104    /// Test case: `ModbusMessage::to_bytes` for a Modbus TCP message.
1105    ///
1106    /// Verifies that a `ModbusMessage` with an `MbapHeader` and `Pdu` is correctly
1107    /// serialized into its ADU byte representation.
1108    #[test]
1109    fn test_modbus_message_to_bytes_tcp() {
1110        let mbap_header = MbapHeader {
1111            transaction_id: 0x1234,
1112            protocol_id: 0x0000,
1113            length: 0x0005, // Length of PDU (FC + Data) + Unit ID
1114            unit_id: 0x01,
1115        };
1116
1117        let mut pdu_data_vec: Vec<u8, MAX_PDU_DATA_LEN> = Vec::new();
1118        pdu_data_vec.extend_from_slice(&[0x00, 0x00, 0x00]).unwrap();
1119
1120        let pdu = Pdu::new(
1121            FunctionCode::ReadHoldingRegisters,
1122            pdu_data_vec,
1123            3, // 3 data bytes
1124        );
1125
1126        let modbus_message = ModbusMessage::new(AdditionalAddress::MbapHeader(mbap_header), pdu);
1127        let adu_bytes = modbus_message
1128            .to_bytes()
1129            .expect("Failed to serialize ModbusMessage");
1130
1131        #[rustfmt::skip]
1132        let expected_adu: [u8; 11] = [
1133            0x12, 0x34, // Transaction ID
1134            0x00, 0x00, // Protocol ID
1135            0x00, 0x05, // Length (PDU length (FC + Data) + 1 byte Unit ID = (1 + 3) + 1 = 5)
1136            0x01,       // Unit ID
1137            0x03,       // Function Code (Read Holding Registers)
1138            0x00, 0x00, 0x00, // Data
1139        ];
1140
1141        assert_eq!(adu_bytes.as_slice(), &expected_adu);
1142    }
1143
1144    // --- Tests for FunctionCode::try_from ---
1145    /// Test case: `FunctionCode::try_from` with valid `u8` values.
1146    ///
1147    /// Verifies that known public function codes are correctly converted to their `FunctionCode` enum variants.
1148    /// Modbus Specification Reference: V1.1b3, Section 5.1 (Public Function Code Definition).
1149    #[test]
1150    fn test_function_code_try_from_valid() {
1151        assert_eq!(
1152            FunctionCode::try_from(0x01).unwrap(),
1153            FunctionCode::ReadCoils
1154        );
1155        assert_eq!(
1156            FunctionCode::try_from(0x08).unwrap(),
1157            FunctionCode::Diagnostics
1158        );
1159        assert_eq!(
1160            FunctionCode::try_from(0x2B).unwrap(),
1161            FunctionCode::EncapsulatedInterfaceTransport
1162        );
1163        assert_eq!(
1164            FunctionCode::try_from(0x18).unwrap(),
1165            FunctionCode::ReadFifoQueue
1166        );
1167        assert_eq!(
1168            FunctionCode::try_from(0x11).unwrap(),
1169            FunctionCode::ReportServerId
1170        );
1171    }
1172
1173    /// Test case: `FunctionCode::try_from` with invalid or reserved `u8` values.
1174    ///
1175    /// Verifies that `MbusError::UnsupportedFunction` is returned for unknown or reserved function code bytes.
1176    /// Modbus Specification Reference: V1.1b3, Section 5.1 (Public Function Code Definition).
1177    #[test]
1178    fn test_function_code_try_from_invalid() {
1179        let err = FunctionCode::try_from(0x00).expect_err("Should error for invalid FC 0x00");
1180        assert_eq!(err, MbusError::UnsupportedFunction(0x00));
1181
1182        let err =
1183            FunctionCode::try_from(0x09).expect_err("Should error for invalid FC 0x09 (reserved)");
1184        assert_eq!(err, MbusError::UnsupportedFunction(0x09));
1185
1186        let err = FunctionCode::try_from(0x64)
1187            .expect_err("Should error for invalid FC 0x64 (private range, not public)");
1188        assert_eq!(err, MbusError::UnsupportedFunction(0x64));
1189    }
1190
1191    // --- Round-trip tests (from_bytes -> to_bytes) ---
1192
1193    /// Test case: Round-trip serialization/deserialization for a PDU with data.
1194    ///
1195    /// Converts bytes to PDU and back to bytes, asserting equality with the original.
1196    /// Modbus Specification Reference: General Modbus PDU structure.
1197    #[test]
1198    fn test_pdu_round_trip_with_data() {
1199        let original_bytes = [0x01, 0x00, 0x00, 0x00, 0x0A]; // Read Coils
1200        let pdu = Pdu::from_bytes(&original_bytes).expect("from_bytes failed");
1201        let new_bytes = pdu.to_bytes().expect("to_bytes failed");
1202        assert_eq!(original_bytes.as_slice(), new_bytes.as_slice());
1203    }
1204
1205    /// Test case: Round-trip serialization/deserialization for a PDU with maximum data length.
1206    ///
1207    /// Converts bytes to PDU and back to bytes, asserting equality with the original.
1208    /// Modbus Specification Reference: V1.1b3, Section 4.1 (PDU Size).
1209    #[test]
1210    fn test_pdu_round_trip_max_data() {
1211        let mut original_bytes_vec: Vec<u8, 253> = Vec::new();
1212        let _ = original_bytes_vec.push(0x03); // Read Holding Registers
1213        for i in 0..252 {
1214            let _ = original_bytes_vec.push(i as u8);
1215        }
1216        let original_bytes = original_bytes_vec.as_slice();
1217
1218        let pdu = Pdu::from_bytes(original_bytes).expect("from_bytes failed");
1219        let new_bytes = pdu.to_bytes().expect("to_bytes failed");
1220        assert_eq!(original_bytes, new_bytes.as_slice());
1221    }
1222
1223    // --- Tests for ModbusMessage::to_ascii_bytes ---
1224
1225    /// Test case: `ModbusMessage::to_ascii_bytes` with a valid message.
1226    ///
1227    /// Verifies correct ASCII encoding and LRC calculation.
1228    /// Request: Slave 1, Read Coils (FC 01), Start 0, Qty 10.
1229    /// Binary: 01 01 00 00 00 0A
1230    /// LRC: -(01+01+0A) = -12 = F4
1231    /// ASCII: :01010000000AF4\r\n
1232    #[test]
1233    fn test_modbus_message_to_ascii_bytes_valid() {
1234        let slave_addr = SlaveAddress::new(1).unwrap();
1235        let mut data = Vec::new();
1236        data.extend_from_slice(&[0x00, 0x00, 0x00, 0x0A]).unwrap();
1237        let pdu = Pdu::new(FunctionCode::ReadCoils, data, 4);
1238        let message = ModbusMessage::new(AdditionalAddress::SlaveAddress(slave_addr), pdu);
1239
1240        let ascii_bytes = message
1241            .to_ascii_bytes()
1242            .expect("Failed to convert to ASCII");
1243
1244        let expected = b":01010000000AF4\r\n";
1245        assert_eq!(ascii_bytes.as_slice(), expected);
1246    }
1247
1248    /// Test case: `ModbusMessage::to_ascii_bytes` boundary check.
1249    ///
1250    /// Case 1: Data len 125. Total ASCII = 1 + (1+1+125+1)*2 + 2 = 1 + 256 + 2 = 259.
1251    /// This fits comfortably within MAX_ADU_FRAME_LEN (513).
1252    #[test]
1253    fn test_modbus_message_to_ascii_bytes_max_capacity() {
1254        let slave_addr = SlaveAddress::new(1).unwrap();
1255        let mut data = Vec::new();
1256        for _ in 0..125 {
1257            data.push(0xAA).unwrap();
1258        }
1259        let pdu = Pdu::new(FunctionCode::ReadHoldingRegisters, data, 125);
1260        let message = ModbusMessage::new(AdditionalAddress::SlaveAddress(slave_addr), pdu);
1261
1262        let ascii_bytes = message.to_ascii_bytes().expect("Should fit in buffer");
1263        assert_eq!(ascii_bytes.len(), 259);
1264    }
1265
1266    /// Test case: `ModbusMessage::to_ascii_bytes` with large payload.
1267    ///
1268    /// Case 2: Data len 126. Total ASCII = 1 + (1+1+126+1)*2 + 2 = 1 + 258 + 2 = 261.
1269    /// This should NOT fail with MAX_ADU_FRAME_LEN = 513.
1270    #[test]
1271    fn test_modbus_message_to_ascii_bytes_large_payload() {
1272        let slave_addr = SlaveAddress::new(1).unwrap();
1273        let mut data = Vec::new();
1274        for _ in 0..126 {
1275            data.push(0xAA).unwrap();
1276        }
1277        let pdu = Pdu::new(FunctionCode::ReadHoldingRegisters, data, 126);
1278        let message = ModbusMessage::new(AdditionalAddress::SlaveAddress(slave_addr), pdu);
1279
1280        let ascii_bytes = message.to_ascii_bytes().expect("Should fit in buffer");
1281        assert_eq!(ascii_bytes.len(), 261);
1282    }
1283
1284    // --- Tests for decompile_adu_frame ---
1285
1286    #[test]
1287    fn test_decompile_adu_frame_tcp_valid() {
1288        let frame = [
1289            0x12, 0x34, // TID
1290            0x00, 0x00, // PID
1291            0x00, 0x06, // Length
1292            0x01, // Unit ID
1293            0x03, // FC
1294            0x00, 0x01, 0x00, 0x02, // Data
1295        ];
1296        let msg = decompile_adu_frame(&frame, TransportType::StdTcp)
1297            .expect("Should decode valid TCP frame");
1298        assert_eq!(msg.function_code(), FunctionCode::ReadHoldingRegisters);
1299        if let AdditionalAddress::MbapHeader(header) = msg.additional_address {
1300            assert_eq!(header.transaction_id, 0x1234);
1301        } else {
1302            panic!("Expected MbapHeader");
1303        }
1304    }
1305
1306    #[test]
1307    fn test_decompile_adu_frame_tcp_invalid() {
1308        let frame = [0x00]; // Too short
1309        let err = decompile_adu_frame(&frame, TransportType::StdTcp).expect_err("Should fail");
1310        assert_eq!(err, MbusError::InvalidAduLength);
1311    }
1312
1313    #[test]
1314    fn test_decompile_adu_frame_rtu_valid() {
1315        // Frame: 01 03 00 6B 00 03 74 17 (CRC LE)
1316        let frame = [0x01, 0x03, 0x00, 0x6B, 0x00, 0x03, 0x74, 0x17];
1317        let msg = decompile_adu_frame(&frame, TransportType::StdSerial(SerialMode::Rtu))
1318            .expect("Valid RTU");
1319        assert_eq!(msg.function_code(), FunctionCode::ReadHoldingRegisters);
1320    }
1321
1322    #[test]
1323    fn test_decompile_adu_frame_rtu_too_short() {
1324        let frame = [0x01, 0x02, 0x03];
1325        let err = decompile_adu_frame(&frame, TransportType::StdSerial(SerialMode::Rtu))
1326            .expect_err("Too short");
1327        assert_eq!(err, MbusError::InvalidAduLength);
1328    }
1329
1330    #[test]
1331    fn test_decompile_adu_frame_rtu_crc_mismatch() {
1332        let frame = [0x01, 0x03, 0x00, 0x6B, 0x00, 0x03, 0x00, 0x00]; // Bad CRC
1333        let err = decompile_adu_frame(&frame, TransportType::StdSerial(SerialMode::Rtu))
1334            .expect_err("CRC mismatch");
1335        assert_eq!(err, MbusError::ChecksumError);
1336    }
1337
1338    #[test]
1339    fn test_decompile_adu_frame_ascii_valid() {
1340        // :010300000001FB\r\n
1341        let frame = b":010300000001FB\r\n";
1342        let msg = decompile_adu_frame(frame, TransportType::StdSerial(SerialMode::Ascii))
1343            .expect("Valid ASCII");
1344        assert_eq!(msg.function_code(), FunctionCode::ReadHoldingRegisters);
1345    }
1346
1347    #[test]
1348    fn test_decompile_adu_frame_ascii_too_short() {
1349        let frame = b":123\r\n";
1350        let err = decompile_adu_frame(frame, TransportType::StdSerial(SerialMode::Ascii))
1351            .expect_err("Too short");
1352        assert_eq!(err, MbusError::InvalidAduLength);
1353    }
1354
1355    #[test]
1356    fn test_decompile_adu_frame_ascii_missing_start() {
1357        let frame = b"010300000001FB\r\n";
1358        let err = decompile_adu_frame(frame, TransportType::StdSerial(SerialMode::Ascii))
1359            .expect_err("Missing start");
1360        assert_eq!(err, MbusError::BasicParseError);
1361    }
1362
1363    #[test]
1364    fn test_decompile_adu_frame_ascii_missing_end() {
1365        let frame = b":010300000001FB\r"; // Missing \n
1366        let err = decompile_adu_frame(frame, TransportType::StdSerial(SerialMode::Ascii))
1367            .expect_err("Missing end");
1368        assert_eq!(err, MbusError::BasicParseError);
1369    }
1370
1371    #[test]
1372    fn test_decompile_adu_frame_ascii_odd_hex() {
1373        let frame = b":010300000001F\r\n"; // Odd length hex
1374        let err = decompile_adu_frame(frame, TransportType::StdSerial(SerialMode::Ascii))
1375            .expect_err("Odd hex");
1376        assert_eq!(err, MbusError::BasicParseError);
1377    }
1378
1379    #[test]
1380    fn test_decompile_adu_frame_ascii_lrc_mismatch() {
1381        let frame = b":01030000000100\r\n"; // LRC 00 is wrong, should be FB
1382        let err = decompile_adu_frame(frame, TransportType::StdSerial(SerialMode::Ascii))
1383            .expect_err("LRC mismatch");
1384        assert_eq!(err, MbusError::ChecksumError);
1385    }
1386
1387    #[test]
1388    fn test_decompile_adu_frame_ascii_buffer_overflow() {
1389        // Construct a frame that decodes to 261 bytes.
1390        let mut frame = Vec::<u8, 600>::new();
1391        frame.push(b':').unwrap();
1392        for _ in 0..261 {
1393            frame.extend_from_slice(b"00").unwrap();
1394        }
1395        frame.extend_from_slice(b"\r\n").unwrap();
1396        let err = decompile_adu_frame(&frame, TransportType::StdSerial(SerialMode::Ascii))
1397            .expect_err("Buffer overflow");
1398        assert_eq!(err, MbusError::BufferTooSmall);
1399    }
1400
1401    // --- Tests for derive_length_from_bytes ---
1402
1403    #[test]
1404    fn test_derive_length_tcp() {
1405        // TCP frame requires minimum 6 bytes to read the length offset.
1406        let short_frame = [0x00, 0x01, 0x00, 0x00, 0x00];
1407        assert_eq!(
1408            derive_length_from_bytes(&short_frame, TransportType::StdTcp),
1409            None
1410        );
1411
1412        // TCP MBAP: TID(2) PID(2) LEN(2) = 0x0006. Total length should be 6 + 6 = 12.
1413        let full_frame = [
1414            0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x01, 0x03, 0x00, 0x00, 0x00, 0x01,
1415        ];
1416        assert_eq!(
1417            derive_length_from_bytes(&full_frame, TransportType::StdTcp),
1418            Some(12)
1419        );
1420
1421        // TCP MBAP with invalid Protocol ID should return usize::MAX to trigger garbage disposal.
1422        let garbage_frame = [
1423            0x00, 0x01, 0xAA, 0xBB, 0x00, 0x06, 0x01, 0x03, 0x00, 0x00, 0x00, 0x01,
1424        ];
1425        assert_eq!(
1426            derive_length_from_bytes(&garbage_frame, TransportType::StdTcp),
1427            Some(usize::MAX)
1428        );
1429    }
1430
1431    #[test]
1432    fn test_derive_length_rtu_fixed() {
1433        // FC 5 (Write Single Coil) is uniformly 8 bytes for requests and responses.
1434        let request = [0x01, 0x05, 0x00, 0x0A, 0xFF, 0x00, 0x00, 0x00];
1435        assert_eq!(
1436            derive_length_from_bytes(&request, TransportType::StdSerial(SerialMode::Rtu)),
1437            Some(8)
1438        );
1439    }
1440
1441    #[test]
1442    fn test_derive_length_rtu_dynamic() {
1443        // Read Holding Registers Response (FC 03)
1444        // Schema: Address(1) + FC(03) + ByteCount(2) + Data(0x12, 0x34) + CRC(2)
1445        let mut resp = [0x01, 0x03, 0x02, 0x12, 0x34, 0x00, 0x00];
1446        let crc = checksum::crc16(&resp[..5]);
1447        let crc_bytes = crc.to_le_bytes();
1448        resp[5] = crc_bytes[0];
1449        resp[6] = crc_bytes[1];
1450
1451        // Opportunistic CRC should instantly lock on an exact length of 7.
1452        assert_eq!(
1453            derive_length_from_bytes(&resp, TransportType::StdSerial(SerialMode::Rtu)),
1454            Some(7)
1455        );
1456
1457        // A partial frame (4 bytes) should predict up to 8 (max candidate comparison logic).
1458        assert_eq!(
1459            derive_length_from_bytes(&resp[..4], TransportType::StdSerial(SerialMode::Rtu)),
1460            Some(8)
1461        );
1462    }
1463
1464    #[test]
1465    fn test_derive_length_rtu_exception() {
1466        // Exception responses are universally 5 bytes.
1467        let exception = [0x01, 0x81, 0x02, 0x00, 0x00];
1468        assert_eq!(
1469            derive_length_from_bytes(&exception, TransportType::StdSerial(SerialMode::Rtu)),
1470            Some(5)
1471        );
1472    }
1473
1474    #[test]
1475    fn test_derive_length_rtu_forward_scan() {
1476        // Build a frame with unknown/custom Function Code (e.g. 0x44)
1477        let mut custom_frame = [0x01, 0x44, 0xAA, 0xBB, 0x00, 0x00];
1478        let crc = checksum::crc16(&custom_frame[..4]);
1479        let crc_bytes = crc.to_le_bytes();
1480        custom_frame[4] = crc_bytes[0];
1481        custom_frame[5] = crc_bytes[1];
1482
1483        assert_eq!(
1484            derive_length_from_bytes(&custom_frame, TransportType::StdSerial(SerialMode::Rtu)),
1485            Some(6)
1486        );
1487
1488        // Without the valid CRC, it will continuously scan forward but yield None if unmatched.
1489        assert_eq!(
1490            derive_length_from_bytes(
1491                &custom_frame[..4],
1492                TransportType::StdSerial(SerialMode::Rtu)
1493            ),
1494            None
1495        );
1496    }
1497
1498    #[test]
1499    fn test_derive_length_ascii() {
1500        let frame = b":010300000001FB\r\n";
1501        assert_eq!(
1502            derive_length_from_bytes(frame, TransportType::StdSerial(SerialMode::Ascii)),
1503            Some(17)
1504        );
1505
1506        let partial = b":010300000001F";
1507        assert_eq!(
1508            derive_length_from_bytes(partial, TransportType::StdSerial(SerialMode::Ascii)),
1509            None
1510        );
1511    }
1512}