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}