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