pamoja_modbus/adu.rs
1//! The Modbus RTU application data unit: the frame that goes on the wire.
2
3use crate::crc::crc16;
4use crate::error::ModbusError;
5use crate::function::Exception;
6use crate::response::Response;
7
8/// A Modbus RTU frame: a unit address, a PDU, and a trailing CRC.
9///
10/// This is the complete unit of bytes an RTU transmitter puts on the bus and a receiver
11/// pulls off it. [`from_pdu`](Adu::from_pdu) builds one to send by appending the CRC;
12/// [`parse`](Adu::parse) reads one received, verifying the CRC so a frame corrupted in
13/// transit never reaches the application. The frame lives in a fixed buffer, so neither
14/// path allocates.
15///
16/// # Examples
17///
18/// ```
19/// use pamoja_modbus::{Adu, Function, Pdu};
20///
21/// // A read of three holding registers, addressed to unit 17.
22/// let pdu = Pdu::read_holding_registers(107, 3);
23/// let frame = Adu::from_pdu(17, pdu.as_bytes()).unwrap();
24/// assert_eq!(frame.address(), 17);
25/// assert_eq!(frame.function_code(), Function::ReadHoldingRegisters.code());
26///
27/// // A receiver checks the same bytes against the CRC they carry, and finds the PDU.
28/// let received = Adu::parse(frame.as_bytes()).unwrap();
29/// assert_eq!(received.pdu(), pdu.as_bytes());
30/// ```
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub struct Adu {
33 bytes: [u8; Adu::MAX_LEN],
34 len: usize,
35}
36
37impl Adu {
38 /// The largest a Modbus RTU frame may be, in bytes.
39 pub const MAX_LEN: usize = 256;
40
41 // The smallest valid frame: address, function code, and the two CRC bytes.
42 const MIN_LEN: usize = 4;
43
44 // Lays an address, a PDU, and the CRC into a frame. The caller guarantees the PDU
45 // fits, which every internal caller does by construction.
46 pub(crate) fn assemble(address: u8, pdu: &[u8]) -> Adu {
47 let len = 1 + pdu.len() + 2;
48 let mut bytes = [0u8; Self::MAX_LEN];
49 bytes[0] = address;
50 bytes[1..1 + pdu.len()].copy_from_slice(pdu);
51 let crc = crc16(&bytes[..1 + pdu.len()]);
52 bytes[1 + pdu.len()..len].copy_from_slice(&crc.to_le_bytes());
53 Adu { bytes, len }
54 }
55
56 /// Builds a frame for a unit address and PDU, appending the CRC.
57 ///
58 /// # Arguments
59 ///
60 /// * `address` - the unit (slave) address the frame is for.
61 /// * `pdu` - the protocol data unit: a function code followed by its data.
62 ///
63 /// # Returns
64 ///
65 /// The frame ready to send.
66 ///
67 /// # Errors
68 ///
69 /// Returns [`ModbusError::FrameTooLong`] if `pdu` is longer than a PDU may be, so the
70 /// frame would exceed [`MAX_LEN`](Adu::MAX_LEN) bytes.
71 pub fn from_pdu(address: u8, pdu: &[u8]) -> Result<Adu, ModbusError> {
72 if 1 + pdu.len() + 2 > Self::MAX_LEN {
73 return Err(ModbusError::FrameTooLong);
74 }
75 Ok(Self::assemble(address, pdu))
76 }
77
78 /// Parses a received frame, verifying its CRC.
79 ///
80 /// # Arguments
81 ///
82 /// * `bytes` - the raw frame as it came off the wire, CRC included.
83 ///
84 /// # Returns
85 ///
86 /// The validated frame.
87 ///
88 /// # Errors
89 ///
90 /// Returns [`ModbusError::FrameTooShort`] if `bytes` is shorter than a valid frame,
91 /// [`ModbusError::FrameTooLong`] if it is longer than [`MAX_LEN`](Adu::MAX_LEN), or
92 /// [`ModbusError::CrcMismatch`] if the trailing CRC does not match the contents.
93 pub fn parse(bytes: &[u8]) -> Result<Adu, ModbusError> {
94 if bytes.len() < Self::MIN_LEN {
95 return Err(ModbusError::FrameTooShort);
96 }
97 if bytes.len() > Self::MAX_LEN {
98 return Err(ModbusError::FrameTooLong);
99 }
100 let split = bytes.len() - 2;
101 let expected = crc16(&bytes[..split]);
102 let found = u16::from_le_bytes([bytes[split], bytes[split + 1]]);
103 if expected != found {
104 return Err(ModbusError::CrcMismatch { expected, found });
105 }
106 let mut buffer = [0u8; Self::MAX_LEN];
107 buffer[..bytes.len()].copy_from_slice(bytes);
108 Ok(Adu {
109 bytes: buffer,
110 len: bytes.len(),
111 })
112 }
113
114 /// Returns the unit address, the first byte of the frame.
115 ///
116 /// # Returns
117 ///
118 /// The unit (slave) address.
119 pub fn address(&self) -> u8 {
120 self.bytes[0]
121 }
122
123 /// Returns the function code, the first byte of the PDU.
124 ///
125 /// # Returns
126 ///
127 /// The function code. An exception response has its high bit set.
128 pub fn function_code(&self) -> u8 {
129 self.bytes[1]
130 }
131
132 /// Returns the PDU: the frame without its address and CRC.
133 ///
134 /// # Returns
135 ///
136 /// The protocol data unit as a byte slice.
137 pub fn pdu(&self) -> &[u8] {
138 &self.bytes[1..self.len - 2]
139 }
140
141 /// Returns the whole frame, CRC included, ready for the wire.
142 ///
143 /// # Returns
144 ///
145 /// The frame as a byte slice.
146 pub fn as_bytes(&self) -> &[u8] {
147 &self.bytes[..self.len]
148 }
149
150 /// Returns the exception a device reported, if this frame is an exception response.
151 ///
152 /// # Returns
153 ///
154 /// The [`Exception`] if the function code's high bit is set and an exception byte
155 /// follows it, otherwise [`None`] (including for a defined-but-unknown exception code).
156 pub fn exception(&self) -> Option<Exception> {
157 self.response().exception()
158 }
159
160 /// Returns a reader over this frame's PDU for decoding a response.
161 ///
162 /// # Returns
163 ///
164 /// A [`Response`] borrowing the PDU.
165 pub fn response(&self) -> Response<'_> {
166 Response::new(self.pdu())
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn from_pdu_then_parse_round_trips() {
176 let frame = Adu::from_pdu(0x11, &[0x03, 0x00, 0x6B, 0x00, 0x03]).unwrap();
177 let parsed = Adu::parse(frame.as_bytes()).unwrap();
178 assert_eq!(parsed.address(), 0x11);
179 assert_eq!(parsed.function_code(), 0x03);
180 assert_eq!(parsed.pdu(), &[0x03, 0x00, 0x6B, 0x00, 0x03]);
181 }
182
183 #[test]
184 fn parse_accepts_the_spec_request_frame() {
185 let parsed = Adu::parse(&[0x11, 0x03, 0x00, 0x6B, 0x00, 0x03, 0x76, 0x87]).unwrap();
186 assert_eq!(parsed.address(), 0x11);
187 assert_eq!(parsed.pdu(), &[0x03, 0x00, 0x6B, 0x00, 0x03]);
188 }
189
190 #[test]
191 fn parse_rejects_a_corrupt_crc() {
192 let result = Adu::parse(&[0x11, 0x03, 0x00, 0x6B, 0x00, 0x03, 0x00, 0x00]);
193 assert_eq!(
194 result,
195 Err(ModbusError::CrcMismatch {
196 expected: 0x8776,
197 found: 0x0000
198 })
199 );
200 }
201
202 #[test]
203 fn parse_rejects_a_short_frame() {
204 assert_eq!(
205 Adu::parse(&[0x11, 0x03, 0x76]),
206 Err(ModbusError::FrameTooShort)
207 );
208 }
209
210 #[test]
211 fn parse_rejects_an_oversized_frame() {
212 let frame = [0u8; Adu::MAX_LEN + 1];
213 assert_eq!(Adu::parse(&frame), Err(ModbusError::FrameTooLong));
214 }
215
216 #[test]
217 fn an_exception_response_surfaces_its_code() {
218 // Read holding registers (0x03) refused with illegal data address (0x02).
219 let frame = Adu::from_pdu(0x11, &[0x83, 0x02]).unwrap();
220 let parsed = Adu::parse(frame.as_bytes()).unwrap();
221 assert_eq!(parsed.exception(), Some(Exception::IllegalDataAddress));
222 }
223
224 #[test]
225 fn a_normal_response_has_no_exception() {
226 let frame = Adu::from_pdu(0x11, &[0x03, 0x02, 0x00, 0x64]).unwrap();
227 assert_eq!(frame.exception(), None);
228 }
229
230 #[test]
231 fn a_maximum_length_frame_round_trips() {
232 // The largest PDU is 253 bytes; with the address and CRC that is the 256-byte
233 // maximum RTU frame.
234 let pdu = [0xAB; 253];
235 let frame = Adu::from_pdu(0x01, &pdu).unwrap();
236 assert_eq!(frame.as_bytes().len(), Adu::MAX_LEN);
237 let parsed = Adu::parse(frame.as_bytes()).unwrap();
238 assert_eq!(parsed.pdu(), &pdu[..]);
239 }
240
241 #[test]
242 fn an_exception_bit_without_a_code_is_not_an_exception() {
243 // A reply whose function code has the exception bit set but carries no exception
244 // byte must read as no exception rather than panic.
245 let frame = Adu::from_pdu(0x11, &[0x83]).unwrap();
246 let parsed = Adu::parse(frame.as_bytes()).unwrap();
247 assert_eq!(parsed.exception(), None);
248 }
249}