Skip to main content

wireforge_app/modbus/
parser.rs

1//! Modbus TCP packet parser.
2
3use super::types::ModbusFunction;
4
5const MBAP_HEADER_LEN: usize = 7;
6
7/// Zero-copy Modbus TCP packet parser.
8pub struct ModbusTcpPacket<'a> { buf: &'a [u8] }
9
10impl<'a> ModbusTcpPacket<'a> {
11    pub fn new(buf: &'a [u8]) -> Option<Self> {
12        if buf.len() < MBAP_HEADER_LEN { return None; }
13        Some(Self { buf })
14    }
15
16    pub fn transaction_id(&self) -> u16 { u16::from_be_bytes([self.buf[0], self.buf[1]]) }
17    pub fn protocol_id(&self) -> u16 { u16::from_be_bytes([self.buf[2], self.buf[3]]) }
18    pub fn length(&self) -> u16 { u16::from_be_bytes([self.buf[4], self.buf[5]]) }
19    pub fn unit_id(&self) -> u8 { self.buf[6] }
20    pub fn function_code(&self) -> u8 { self.buf.get(7).copied().unwrap_or(0) }
21    pub fn pdu_data(&self) -> &'a [u8] { &self.buf[MBAP_HEADER_LEN..] }
22
23    pub fn function(&self) -> ModbusFunction<'a> {
24        let pdu = self.pdu_data();
25        if pdu.is_empty() { return ModbusFunction::Unknown { function_code: 0, data: &[] }; }
26        let fc = pdu[0];
27        let data = &pdu[1..];
28        match fc {
29            1 => ModbusFunction::ReadCoils {
30                starting_address: self._u16(data, 0), quantity: self._u16(data, 2),
31            },
32            2 => ModbusFunction::ReadDiscreteInputs {
33                starting_address: self._u16(data, 0), quantity: self._u16(data, 2),
34            },
35            3 => {
36                if data.len() >= 4 && data.len() >= 5 + data[2] as usize {
37                    // Response format checked by caller
38                    ModbusFunction::ReadHoldingRegisters { starting_address: self._u16(data, 0), quantity: self._u16(data, 2) }
39                } else {
40                    ModbusFunction::ReadHoldingRegisters { starting_address: self._u16(data, 0), quantity: self._u16(data, 2) }
41                }
42            }
43            4 => ModbusFunction::ReadInputRegisters {
44                starting_address: self._u16(data, 0), quantity: self._u16(data, 2),
45            },
46            5 => ModbusFunction::WriteSingleCoil {
47                output_address: self._u16(data, 0),
48                value: data.len() >= 3 && data[2] == 0xFF,
49            },
50            6 => ModbusFunction::WriteSingleRegister {
51                register_address: self._u16(data, 0), value: self._u16(data, 2),
52            },
53            15 => ModbusFunction::WriteMultipleCoils {
54                starting_address: self._u16(data, 0), quantity: self._u16(data, 2),
55                byte_count: *data.get(4).unwrap_or(&0), values: &data[5..],
56            },
57            16 => ModbusFunction::WriteMultipleRegisters {
58                starting_address: self._u16(data, 0), quantity: self._u16(data, 2),
59                byte_count: *data.get(4).unwrap_or(&0), values: &data[5..],
60            },
61            _ => ModbusFunction::Unknown { function_code: fc, data },
62        }
63    }
64
65    fn _u16(&self, data: &[u8], off: usize) -> u16 {
66        if off + 2 <= data.len() { u16::from_be_bytes([data[off], data[off+1]]) } else { 0 }
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn parse_read_coils() {
76        let data = &[0x00u8, 0x01, 0x00, 0x00, 0x00, 0x06, 0x01, 0x01, 0x00, 0x00, 0x00, 0x0A];
77        // transaction=1, proto=0, len=6, unit=1, fc=1(read coils), addr=0, qty=10
78        let pkt = ModbusTcpPacket::new(data).unwrap();
79        assert_eq!(pkt.transaction_id(), 1);
80        assert_eq!(pkt.function_code(), 1);
81        match pkt.function() {
82            ModbusFunction::ReadCoils { starting_address, quantity } => {
83                assert_eq!(starting_address, 0); assert_eq!(quantity, 10);
84            }
85            _ => panic!("expected ReadCoils"),
86        }
87    }
88
89    #[test]
90    fn parse_read_holding_registers_response() {
91        // Function 3 response: 3 registers [0x0001, 0x0002, 0x0003]
92        let data = &[0x00u8, 0x01, 0x00, 0x00, 0x00, 0x09, 0x01, 0x03, 0x06,
93            0x00, 0x01, 0x00, 0x02, 0x00, 0x03];
94        let pkt = ModbusTcpPacket::new(data).unwrap();
95        assert_eq!(pkt.function_code(), 3);
96    }
97
98    #[test]
99    fn parse_write_single_register() {
100        let data = &[0x00u8, 0x01, 0x00, 0x00, 0x00, 0x06, 0x01, 0x06, 0x00, 0x01, 0x00, 0x2A];
101        let pkt = ModbusTcpPacket::new(data).unwrap();
102        match pkt.function() {
103            ModbusFunction::WriteSingleRegister { register_address, value } => {
104                assert_eq!(register_address, 1); assert_eq!(value, 42);
105            }
106            _ => panic!("expected WriteSingleRegister"),
107        }
108    }
109
110    #[test]
111    fn parse_too_short() {
112        assert!(ModbusTcpPacket::new(&[]).is_none());
113        assert!(ModbusTcpPacket::new(&[0u8; 6]).is_none());
114        assert!(ModbusTcpPacket::new(&[0u8; 7]).is_some());
115    }
116}