modbus_relay/
modbus.rs

1use std::sync::Arc;
2
3use tracing::{debug, trace};
4
5use crate::{FrameErrorKind, RelayError, RtuTransport, errors::FrameError};
6
7/// Calculates the CRC16 checksum for Modbus RTU communication using a lookup table for high performance.
8///
9/// This function computes the CRC16-Modbus checksum for the provided data frame.
10/// It uses a precomputed lookup table to optimize performance by eliminating
11/// bitwise calculations within the inner loop.
12///
13/// # Arguments
14///
15/// * `data` - A slice of bytes representing the data frame for which the CRC is to be computed.
16///
17/// # Returns
18///
19/// The computed 16-bit CRC as a `u16` value.
20fn calc_crc16(data: &[u8]) -> u16 {
21    // Precomputed CRC16 lookup table for polynomial 0xA001 (Modbus standard)
22    const CRC16_TABLE: [u16; 256] = [
23        0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241, 0xC601, 0x06C0, 0x0780,
24        0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440, 0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1,
25        0xCE81, 0x0E40, 0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841, 0xD801,
26        0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40, 0x1E00, 0xDEC1, 0xDF81, 0x1F40,
27        0xDD01, 0x1DC0, 0x1C80, 0xDC41, 0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680,
28        0xD641, 0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040, 0xF001, 0x30C0,
29        0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240, 0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501,
30        0x35C0, 0x3480, 0xF441, 0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41,
31        0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840, 0x2800, 0xE8C1, 0xE981,
32        0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41, 0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1,
33        0xEC81, 0x2C40, 0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640, 0x2200,
34        0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041, 0xA001, 0x60C0, 0x6180, 0xA141,
35        0x6300, 0xA3C1, 0xA281, 0x6240, 0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480,
36        0xA441, 0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41, 0xAA01, 0x6AC0,
37        0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840, 0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01,
38        0x7BC0, 0x7A80, 0xBA41, 0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40,
39        0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640, 0x7200, 0xB2C1, 0xB381,
40        0x7340, 0xB101, 0x71C0, 0x7080, 0xB041, 0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0,
41        0x5280, 0x9241, 0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440, 0x9C01,
42        0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40, 0x5A00, 0x9AC1, 0x9B81, 0x5B40,
43        0x9901, 0x59C0, 0x5880, 0x9841, 0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81,
44        0x4A40, 0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41, 0x4400, 0x84C1,
45        0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641, 0x8201, 0x42C0, 0x4380, 0x8341, 0x4100,
46        0x81C1, 0x8081, 0x4040,
47    ];
48
49    let mut crc: u16 = 0xFFFF; // Initialize CRC to 0xFFFF as per Modbus standard
50
51    for &byte in data {
52        // XOR the lower byte of the CRC with the current byte and find the lookup table index
53        let index = ((crc ^ byte as u16) & 0x00FF) as usize;
54        // Update the CRC by shifting right and XORing with the table value
55        crc = (crc >> 8) ^ CRC16_TABLE[index];
56    }
57
58    crc
59}
60
61/// Estimates the expected size of a Modbus RTU response frame based on the function code and quantity.
62///
63/// # Arguments
64///
65/// * `function` - The Modbus function code.
66/// * `quantity` - The number of coils or registers involved.
67///
68/// # Returns
69///
70/// The estimated size of the response frame in bytes.
71pub fn guess_response_size(function: u8, quantity: u16) -> usize {
72    match function {
73        0x01 | 0x02 => {
74            // Read Coils / Read Discrete Inputs
75            // Each coil status is one bit; calculate the number of data bytes required
76            let data_bytes = (quantity as usize).div_ceil(8); // Round up to the nearest whole byte
77            // Response size: Address(1) + Function(1) + Byte Count(1) + Data + CRC(2)
78            1 + 1 + 1 + data_bytes + 2
79        }
80        0x03 | 0x04 => {
81            // Read Holding Registers / Read Input Registers
82            // Each register is two bytes
83            let data_bytes = (quantity as usize) * 2;
84            // Response size: Address(1) + Function(1) + Byte Count(1) + Data + CRC(2)
85            1 + 1 + 1 + data_bytes + 2
86        }
87        0x05 | 0x06 => {
88            // Write Single Coil / Write Single Register
89            // Response size: Address(1) + Function(1) + Address(2) + Value(2) + CRC(2)
90            1 + 1 + 2 + 2 + 2
91        }
92        0x0F | 0x10 => {
93            // Write Multiple Coils / Write Multiple Registers
94            // Response size: Address(1) + Function(1) + Address(2) + Quantity(2) + CRC(2)
95            1 + 1 + 2 + 2 + 2
96        }
97        _ => {
98            // Default maximum size for unknown function codes
99            256
100        }
101    }
102}
103
104/// Extracts a 16-bit unsigned integer from a Modbus RTU request frame starting at the specified index.
105///
106/// This function attempts to retrieve two consecutive bytes from the provided request slice,
107/// starting at the given index, and converts them into a `u16` value using big-endian byte order.
108/// If the request slice is too short to contain the required bytes, it returns a `RelayError`
109/// indicating an invalid frame format.
110///
111/// # Arguments
112///
113/// * `request` - A slice of bytes representing the Modbus RTU request frame.
114/// * `start` - The starting index within the request slice from which to extract the `u16` value.
115///
116/// # Returns
117///
118/// A `Result` containing the extracted `u16` value if successful, or a `RelayError` if the request
119/// slice is too short.
120///
121/// # Errors
122///
123/// Returns a `RelayError` with `FrameErrorKind::InvalidFormat` if the request slice does not contain
124/// enough bytes to extract a `u16` value starting at the specified index.
125fn get_u16_from_request(request: &[u8], start: usize) -> Result<u16, RelayError> {
126    request
127        .get(start..start + 2)
128        .map(|bytes| u16::from_be_bytes([bytes[0], bytes[1]]))
129        .ok_or_else(|| {
130            RelayError::frame(
131                FrameErrorKind::InvalidFormat,
132                "Request too short for register quantity".to_string(),
133                Some(request.to_vec()),
134            )
135        })
136}
137
138/// Extracts the quantity of coils or registers from a Modbus RTU request frame based on the function code.
139///
140/// This function determines the quantity of coils or registers involved in a Modbus RTU request
141/// by examining the function code and extracting the appropriate bytes from the request frame.
142/// For read functions (0x01 to 0x04) and write multiple functions (0x0F, 0x10), it extracts a 16-bit
143/// unsigned integer from bytes 4 and 5 of the request frame. For write single functions (0x05, 0x06),
144/// it returns a fixed quantity of 1. For other function codes, it defaults to a quantity of 1.
145///
146/// # Arguments
147///
148/// * `function_code` - The Modbus function code.
149/// * `request` - A slice of bytes representing the Modbus RTU request frame.
150///
151/// # Returns
152///
153/// A `Result` containing the extracted quantity as a `u16` value if successful, or a `RelayError` if the request
154/// slice is too short or the function code is invalid.
155///
156/// # Errors
157///
158/// Returns a `RelayError` with `FrameErrorKind::InvalidFormat` if the request slice does not contain
159/// enough bytes to extract the quantity for the specified function code.
160pub fn get_quantity(function_code: u8, request: &[u8]) -> Result<u16, RelayError> {
161    match function_code {
162        // For read functions (0x01 to 0x04) and write multiple functions (0x0F, 0x10),
163        // extract the quantity from bytes 4 and 5 of the request frame.
164        0x01..=0x04 | 0x0F | 0x10 => get_u16_from_request(request, 4),
165
166        // For write single functions (0x05, 0x06), the quantity is always 1.
167        0x05 | 0x06 => Ok(1),
168
169        // For other function codes, default the quantity to 1.
170        _ => Ok(1),
171    }
172}
173
174pub struct ModbusProcessor {
175    transport: Arc<RtuTransport>,
176}
177
178impl ModbusProcessor {
179    pub fn new(transport: Arc<RtuTransport>) -> Self {
180        Self { transport }
181    }
182
183    /// Processes a Modbus TCP request by converting it to Modbus RTU, sending it over the transport,
184    /// and then converting the RTU response back to Modbus TCP format.
185    ///
186    /// # Arguments
187    ///
188    /// * `transaction_id` - The Modbus TCP transaction ID.
189    /// * `unit_id` - The Modbus unit ID (slave address).
190    /// * `pdu` - The Protocol Data Unit from the Modbus TCP request.
191    ///
192    /// # Returns
193    ///
194    /// A `Result` containing the Modbus TCP response as a vector of bytes, or a `RelayError`.
195    pub async fn process_request(
196        &self,
197        transaction_id: [u8; 2],
198        unit_id: u8,
199        pdu: &[u8],
200        trace_frames: bool,
201    ) -> Result<Vec<u8>, RelayError> {
202        // Build RTU request frame: [Unit ID][PDU][CRC16]
203        let mut rtu_request = Vec::with_capacity(1 + pdu.len() + 2); // Unit ID + PDU + CRC16
204        rtu_request.push(unit_id);
205        rtu_request.extend_from_slice(pdu);
206
207        // Calculate CRC16 checksum and append to the request
208        let crc = calc_crc16(&rtu_request);
209        rtu_request.extend_from_slice(&crc.to_le_bytes()); // Append CRC16 in little-endian
210
211        if trace_frames {
212            trace!(
213                "Sending RTU request: unit_id=0x{:02X}, function=0x{:02X}, data={:02X?}, crc=0x{:04X}",
214                unit_id,
215                pdu.first().copied().unwrap_or(0),
216                &pdu[1..],
217                crc
218            );
219        }
220
221        // Estimate the expected RTU response size
222        let function_code = pdu.first().copied().unwrap_or(0);
223        let quantity = get_quantity(function_code, &rtu_request)?;
224
225        let expected_response_size = guess_response_size(function_code, quantity);
226
227        // Allocate buffer for RTU response
228        let mut rtu_response = vec![0u8; expected_response_size];
229
230        // Execute RTU transaction
231        let rtu_len = match self
232            .transport
233            .transaction(&rtu_request, &mut rtu_response)
234            .await
235        {
236            Ok(len) => {
237                if len < 5 {
238                    // Minimum RTU response size: Unit ID(1) + Function(1) + Data(1) + CRC(2)
239                    return Err(RelayError::frame(
240                        FrameErrorKind::TooShort,
241                        format!("RTU response too short: {} bytes", len),
242                        Some(rtu_response[..len].to_vec()),
243                    ));
244                }
245                len
246            }
247            Err(e) => {
248                debug!("Transport transaction error: {:?}", e);
249
250                // Prepare Modbus exception response with exception code 0x0B (Gateway Path Unavailable)
251                let exception_code = 0x0B;
252                let mut exception_response = Vec::with_capacity(9);
253                exception_response.extend_from_slice(&transaction_id);
254                exception_response.extend_from_slice(&[0x00, 0x00]); // Protocol ID
255                exception_response.extend_from_slice(&[0x00, 0x03]); // Length (Unit ID + Function + Exception Code)
256                exception_response.push(unit_id);
257                exception_response.push(function_code | 0x80); // Exception function code
258                exception_response.push(exception_code);
259
260                return Ok(exception_response);
261            }
262        };
263
264        // Truncate the buffer to the actual response length
265        rtu_response.truncate(rtu_len);
266
267        // Verify the CRC16 checksum of the RTU response
268        let expected_crc = calc_crc16(&rtu_response[..rtu_len - 2]);
269        let received_crc =
270            u16::from_le_bytes([rtu_response[rtu_len - 2], rtu_response[rtu_len - 1]]);
271        if expected_crc != received_crc {
272            return Err(RelayError::Frame(FrameError::Crc {
273                calculated: expected_crc,
274                received: received_crc,
275                frame_hex: hex::encode(&rtu_response[..rtu_len - 2]),
276            }));
277        }
278
279        // Remove CRC from RTU response
280        rtu_response.truncate(rtu_len - 2);
281
282        // Verify that the unit ID in the response matches
283        if rtu_response[0] != unit_id {
284            return Err(RelayError::frame(
285                FrameErrorKind::InvalidUnitId,
286                format!(
287                    "Unexpected unit ID in RTU response: expected=0x{:02X}, received=0x{:02X}",
288                    unit_id, rtu_response[0]
289                ),
290                Some(rtu_response.clone()),
291            ));
292        }
293
294        // Convert RTU response to Modbus TCP response
295        let tcp_length = rtu_response.len() as u16; // Length of Unit ID + PDU
296        let mut tcp_response = Vec::with_capacity(7 + rtu_response.len()); // MBAP Header(7) + PDU
297        tcp_response.extend_from_slice(&transaction_id); // Transaction ID
298        tcp_response.extend_from_slice(&[0x00, 0x00]); // Protocol ID
299        tcp_response.extend_from_slice(&tcp_length.to_be_bytes()); // Length field
300        tcp_response.extend_from_slice(&rtu_response); // Unit ID + PDU
301
302        Ok(tcp_response)
303    }
304}