Skip to main content

rustmod_core/frame/
tcp.rs

1use crate::encoding::{Reader, Writer};
2use crate::{DecodeError, EncodeError, UnitId};
3
4/// Size of the MBAP header in bytes (transaction ID + protocol ID + length + unit ID).
5pub const MBAP_HEADER_LEN: usize = 7;
6
7/// Modbus Application Protocol (MBAP) header used in Modbus TCP framing.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub struct MbapHeader {
10    pub transaction_id: u16,
11    pub protocol_id: u16,
12    /// Length includes unit-id byte + PDU length.
13    pub length: u16,
14    pub unit_id: UnitId,
15}
16
17impl MbapHeader {
18    pub fn encode(&self, w: &mut Writer<'_>) -> Result<(), EncodeError> {
19        w.write_be_u16(self.transaction_id)?;
20        w.write_be_u16(self.protocol_id)?;
21        w.write_be_u16(self.length)?;
22        w.write_u8(self.unit_id.as_u8())?;
23        Ok(())
24    }
25
26    pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
27        let transaction_id = r.read_be_u16()?;
28        let protocol_id = r.read_be_u16()?;
29        let length = r.read_be_u16()?;
30        let unit_id = UnitId::new(r.read_u8()?);
31
32        if protocol_id != 0 {
33            return Err(DecodeError::InvalidValue);
34        }
35        if length < 1 {
36            return Err(DecodeError::InvalidLength);
37        }
38
39        Ok(Self {
40            transaction_id,
41            protocol_id,
42            length,
43            unit_id,
44        })
45    }
46}
47
48/// Encode a complete Modbus TCP frame (MBAP header + PDU) into the writer.
49pub fn encode_frame(
50    w: &mut Writer<'_>,
51    transaction_id: u16,
52    unit_id: UnitId,
53    pdu: &[u8],
54) -> Result<(), EncodeError> {
55    let pdu_len_u16: u16 = pdu
56        .len()
57        .try_into()
58        .map_err(|_| EncodeError::ValueOutOfRange)?;
59    let length = pdu_len_u16
60        .checked_add(1)
61        .ok_or(EncodeError::ValueOutOfRange)?;
62
63    let header = MbapHeader {
64        transaction_id,
65        protocol_id: 0,
66        length,
67        unit_id,
68    };
69    header.encode(w)?;
70    w.write_all(pdu)?;
71    Ok(())
72}
73
74/// Decode a complete Modbus TCP frame, returning the MBAP header and PDU slice.
75pub fn decode_frame<'a>(r: &mut Reader<'a>) -> Result<(MbapHeader, &'a [u8]), DecodeError> {
76    let header = MbapHeader::decode(r)?;
77    let pdu_len = usize::from(header.length - 1);
78    let pdu = r.read_exact(pdu_len)?;
79    Ok((header, pdu))
80}
81
82#[cfg(test)]
83mod tests {
84    use super::{decode_frame, encode_frame, MbapHeader};
85    use crate::encoding::{Reader, Writer};
86    use crate::{DecodeError, UnitId};
87
88    #[test]
89    fn mbap_roundtrip() {
90        let mut buf = [0u8; 32];
91        let mut w = Writer::new(&mut buf);
92        encode_frame(&mut w, 1, UnitId::new(2), &[0x03, 0x00, 0x6B, 0x00, 0x03]).unwrap();
93
94        let mut r = Reader::new(w.as_written());
95        let (header, pdu) = decode_frame(&mut r).unwrap();
96        assert_eq!(
97            header,
98            MbapHeader {
99                transaction_id: 1,
100                protocol_id: 0,
101                length: 6,
102                unit_id: UnitId::new(2),
103            }
104        );
105        assert_eq!(pdu, &[0x03, 0x00, 0x6B, 0x00, 0x03]);
106    }
107
108    #[test]
109    fn rejects_non_zero_protocol_id() {
110        let bytes = [0x00, 0x01, 0x00, 0x01, 0x00, 0x02, 0x01, 0x03];
111        let mut r = Reader::new(&bytes);
112        assert_eq!(decode_frame(&mut r).unwrap_err(), DecodeError::InvalidValue);
113    }
114}