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