oxirs_modbus/protocol/
frame.rs

1//! Modbus frame parsing and serialization
2//!
3//! This module handles Modbus Application Data Unit (ADU) and
4//! Protocol Data Unit (PDU) structures.
5
6use crate::error::{ModbusError, ModbusResult};
7use bytes::{BufMut, BytesMut};
8
9/// Modbus function codes
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11#[repr(u8)]
12pub enum FunctionCode {
13    /// Read Coils (0x01)
14    ReadCoils = 0x01,
15    /// Read Discrete Inputs (0x02)
16    ReadDiscreteInputs = 0x02,
17    /// Read Holding Registers (0x03)
18    ReadHoldingRegisters = 0x03,
19    /// Read Input Registers (0x04)
20    ReadInputRegisters = 0x04,
21    /// Write Single Coil (0x05)
22    WriteSingleCoil = 0x05,
23    /// Write Single Register (0x06)
24    WriteSingleRegister = 0x06,
25    /// Write Multiple Coils (0x0F)
26    WriteMultipleCoils = 0x0F,
27    /// Write Multiple Registers (0x10)
28    WriteMultipleRegisters = 0x10,
29}
30
31impl FunctionCode {
32    /// Create from u8
33    pub fn from_u8(code: u8) -> ModbusResult<Self> {
34        match code {
35            0x01 => Ok(Self::ReadCoils),
36            0x02 => Ok(Self::ReadDiscreteInputs),
37            0x03 => Ok(Self::ReadHoldingRegisters),
38            0x04 => Ok(Self::ReadInputRegisters),
39            0x05 => Ok(Self::WriteSingleCoil),
40            0x06 => Ok(Self::WriteSingleRegister),
41            0x0F => Ok(Self::WriteMultipleCoils),
42            0x10 => Ok(Self::WriteMultipleRegisters),
43            _ => Err(ModbusError::InvalidFunctionCode(code)),
44        }
45    }
46
47    /// Convert to u8
48    pub fn as_u8(self) -> u8 {
49        self as u8
50    }
51}
52
53/// Modbus TCP Application Data Unit (ADU)
54#[derive(Debug, Clone)]
55pub struct ModbusTcpFrame {
56    /// Transaction identifier (for matching requests/responses)
57    pub transaction_id: u16,
58
59    /// Protocol identifier (always 0 for Modbus)
60    pub protocol_id: u16,
61
62    /// Unit identifier (slave address)
63    pub unit_id: u8,
64
65    /// Function code
66    pub function_code: FunctionCode,
67
68    /// Data payload
69    pub data: Vec<u8>,
70}
71
72impl ModbusTcpFrame {
73    /// Build a request frame for reading holding registers
74    pub fn read_holding_registers(
75        transaction_id: u16,
76        unit_id: u8,
77        start_addr: u16,
78        count: u16,
79    ) -> Self {
80        let mut data = BytesMut::with_capacity(4);
81        data.put_u16(start_addr);
82        data.put_u16(count);
83
84        Self {
85            transaction_id,
86            protocol_id: 0,
87            unit_id,
88            function_code: FunctionCode::ReadHoldingRegisters,
89            data: data.to_vec(),
90        }
91    }
92
93    /// Build a request frame for writing a single register
94    pub fn write_single_register(transaction_id: u16, unit_id: u8, addr: u16, value: u16) -> Self {
95        let mut data = BytesMut::with_capacity(4);
96        data.put_u16(addr);
97        data.put_u16(value);
98
99        Self {
100            transaction_id,
101            protocol_id: 0,
102            unit_id,
103            function_code: FunctionCode::WriteSingleRegister,
104            data: data.to_vec(),
105        }
106    }
107
108    /// Serialize frame to bytes for transmission
109    pub fn to_bytes(&self) -> Vec<u8> {
110        let length = 1 + 1 + self.data.len(); // unit_id + function_code + data
111        let mut bytes = BytesMut::with_capacity(7 + self.data.len());
112
113        // MBAP Header (7 bytes)
114        bytes.put_u16(self.transaction_id);
115        bytes.put_u16(self.protocol_id);
116        bytes.put_u16(length as u16);
117        bytes.put_u8(self.unit_id);
118
119        // PDU
120        bytes.put_u8(self.function_code.as_u8());
121        bytes.put(self.data.as_slice());
122
123        bytes.to_vec()
124    }
125
126    /// Parse response frame from bytes
127    pub fn from_bytes(bytes: &[u8]) -> ModbusResult<Self> {
128        if bytes.len() < 8 {
129            return Err(ModbusError::Io(std::io::Error::new(
130                std::io::ErrorKind::UnexpectedEof,
131                "Frame too short",
132            )));
133        }
134
135        let transaction_id = u16::from_be_bytes([bytes[0], bytes[1]]);
136        let protocol_id = u16::from_be_bytes([bytes[2], bytes[3]]);
137        let _length = u16::from_be_bytes([bytes[4], bytes[5]]);
138        let unit_id = bytes[6];
139        let function_code_byte = bytes[7];
140
141        // Check for exception response (high bit set)
142        if function_code_byte & 0x80 != 0 {
143            let original_function = function_code_byte & 0x7F;
144            let exception_code = if bytes.len() > 8 { bytes[8] } else { 0 };
145            return Err(ModbusError::ModbusException {
146                code: exception_code,
147                function: original_function,
148            });
149        }
150
151        let function_code = FunctionCode::from_u8(function_code_byte)?;
152        let data = bytes[8..].to_vec();
153
154        Ok(Self {
155            transaction_id,
156            protocol_id,
157            unit_id,
158            function_code,
159            data,
160        })
161    }
162
163    /// Extract register values from Read Holding Registers response
164    pub fn extract_registers(&self) -> ModbusResult<Vec<u16>> {
165        if self.data.is_empty() {
166            return Ok(Vec::new());
167        }
168
169        let byte_count = self.data[0] as usize;
170        if self.data.len() < 1 + byte_count {
171            return Err(ModbusError::Io(std::io::Error::new(
172                std::io::ErrorKind::UnexpectedEof,
173                "Incomplete register data",
174            )));
175        }
176
177        let mut registers = Vec::with_capacity(byte_count / 2);
178        let mut pos = 1;
179
180        while pos + 1 < self.data.len() && pos < 1 + byte_count {
181            let value = u16::from_be_bytes([self.data[pos], self.data[pos + 1]]);
182            registers.push(value);
183            pos += 2;
184        }
185
186        Ok(registers)
187    }
188}
189
190/// Modbus frame abstraction (TCP or RTU)
191#[derive(Debug, Clone)]
192pub enum ModbusFrame {
193    /// Modbus TCP frame
194    Tcp(ModbusTcpFrame),
195    /// Modbus RTU frame (to be implemented)
196    #[cfg(feature = "rtu")]
197    Rtu(ModbusRtuFrame),
198}
199
200/// Modbus RTU frame structure
201///
202/// Contains the PDU (Protocol Data Unit) plus unit identifier and CRC-16.
203#[cfg(feature = "rtu")]
204#[derive(Debug, Clone)]
205pub struct ModbusRtuFrame {
206    /// Unit identifier (slave address, 1-247)
207    pub unit_id: u8,
208    /// Modbus function code
209    pub function_code: FunctionCode,
210    /// Data payload (address, count, values)
211    pub data: Vec<u8>,
212    /// CRC-16 checksum (Modbus polynomial)
213    pub crc: u16,
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn test_function_code_conversion() {
222        assert_eq!(
223            FunctionCode::from_u8(0x03).unwrap(),
224            FunctionCode::ReadHoldingRegisters
225        );
226        assert_eq!(FunctionCode::ReadHoldingRegisters.as_u8(), 0x03);
227    }
228
229    #[test]
230    fn test_invalid_function_code() {
231        assert!(FunctionCode::from_u8(0xFF).is_err());
232    }
233
234    #[test]
235    fn test_build_read_holding_registers() {
236        let frame = ModbusTcpFrame::read_holding_registers(1, 1, 0, 10);
237        assert_eq!(frame.transaction_id, 1);
238        assert_eq!(frame.unit_id, 1);
239        assert_eq!(frame.function_code, FunctionCode::ReadHoldingRegisters);
240        assert_eq!(frame.data.len(), 4); // start_addr (2) + count (2)
241    }
242
243    #[test]
244    fn test_serialize_frame() {
245        let frame = ModbusTcpFrame::read_holding_registers(1, 1, 0, 10);
246        let bytes = frame.to_bytes();
247
248        // MBAP header: tid(2) + pid(2) + length(2) + unit(1) = 7 bytes
249        // PDU: function(1) + data(4) = 5 bytes
250        assert_eq!(bytes.len(), 12);
251
252        // Verify transaction ID
253        assert_eq!(u16::from_be_bytes([bytes[0], bytes[1]]), 1);
254
255        // Verify protocol ID (always 0)
256        assert_eq!(u16::from_be_bytes([bytes[2], bytes[3]]), 0);
257
258        // Verify function code
259        assert_eq!(bytes[7], 0x03);
260    }
261
262    #[test]
263    fn test_parse_response() {
264        // Simulate response: 5 registers with values [100, 200, 300, 400, 500]
265        let response = vec![
266            0x00, 0x01, // Transaction ID
267            0x00, 0x00, // Protocol ID
268            0x00, 0x0D, // Length (13 bytes)
269            0x01, // Unit ID
270            0x03, // Function code
271            0x0A, // Byte count (10 bytes = 5 registers)
272            0x00, 0x64, // Register 1: 100
273            0x00, 0xC8, // Register 2: 200
274            0x01, 0x2C, // Register 3: 300
275            0x01, 0x90, // Register 4: 400
276            0x01, 0xF4, // Register 5: 500
277        ];
278
279        let frame = ModbusTcpFrame::from_bytes(&response).unwrap();
280        assert_eq!(frame.transaction_id, 1);
281        assert_eq!(frame.function_code, FunctionCode::ReadHoldingRegisters);
282
283        let registers = frame.extract_registers().unwrap();
284        assert_eq!(registers, vec![100, 200, 300, 400, 500]);
285    }
286
287    #[test]
288    fn test_exception_response() {
289        // Exception response: function code | 0x80, exception code
290        let response = vec![
291            0x00, 0x01, // Transaction ID
292            0x00, 0x00, // Protocol ID
293            0x00, 0x03, // Length
294            0x01, // Unit ID
295            0x83, // Function code (0x03 | 0x80 = exception)
296            0x02, // Exception code (Illegal Data Address)
297        ];
298
299        let result = ModbusTcpFrame::from_bytes(&response);
300        assert!(result.is_err());
301
302        if let Err(ModbusError::ModbusException { code, function }) = result {
303            assert_eq!(code, 0x02);
304            assert_eq!(function, 0x03);
305        } else {
306            panic!("Expected ModbusException error");
307        }
308    }
309}