pamoja_modbus/pdu.rs
1//! The Modbus protocol data unit and the standard requests that build one.
2
3use crate::adu::Adu;
4use crate::error::ModbusError;
5
6/// A Modbus protocol data unit: a function code followed by its data.
7///
8/// The PDU is the part of a frame that is the same on every transport. On RTU it sits
9/// between the unit address and the CRC; wrap one with [`to_adu`](Pdu::to_adu) to get a
10/// frame ready for the wire.
11///
12/// The constructors build the standard requests so callers state intent ("read three
13/// holding registers") rather than packing bytes, encoding addresses and counts in the
14/// big-endian order Modbus uses. For a function code this crate does not name, [`raw`](Pdu::raw)
15/// carries arbitrary bytes through unchanged. The data is held in a fixed buffer, so a
16/// PDU needs no allocation.
17///
18/// # Examples
19///
20/// ```
21/// use pamoja_modbus::Pdu;
22///
23/// let pdu = Pdu::write_single_register(0x0001, 0x0003);
24/// assert_eq!(pdu.as_bytes(), &[0x06, 0x00, 0x01, 0x00, 0x03]);
25/// ```
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub struct Pdu {
28 bytes: [u8; Pdu::MAX_LEN],
29 len: usize,
30}
31
32impl Pdu {
33 /// The largest a Modbus RTU PDU may be, in bytes: the 256-byte ADU less the one-byte
34 /// address and the two-byte CRC.
35 pub const MAX_LEN: usize = 253;
36
37 /// The most registers a single write-multiple-registers request may carry.
38 pub const MAX_WRITE_REGISTERS: usize = 123;
39
40 /// The most coils a single write-multiple-coils request may carry.
41 pub const MAX_WRITE_COILS: usize = 1968;
42
43 // Builds a five-byte request: a function code and two 16-bit words. Read requests
44 // carry a starting address and a quantity; single-write requests carry an address
45 // and a value; the byte layout is identical.
46 fn pair(function: u8, first: u16, second: u16) -> Pdu {
47 let mut bytes = [0u8; Self::MAX_LEN];
48 bytes[0] = function;
49 bytes[1..3].copy_from_slice(&first.to_be_bytes());
50 bytes[3..5].copy_from_slice(&second.to_be_bytes());
51 Pdu { bytes, len: 5 }
52 }
53
54 /// Builds a read-coils request (function `0x01`).
55 ///
56 /// # Arguments
57 ///
58 /// * `start` - the address of the first coil to read.
59 /// * `count` - how many coils to read.
60 ///
61 /// # Returns
62 ///
63 /// The request PDU.
64 pub fn read_coils(start: u16, count: u16) -> Pdu {
65 Self::pair(0x01, start, count)
66 }
67
68 /// Builds a read-discrete-inputs request (function `0x02`).
69 ///
70 /// # Arguments
71 ///
72 /// * `start` - the address of the first discrete input to read.
73 /// * `count` - how many inputs to read.
74 ///
75 /// # Returns
76 ///
77 /// The request PDU.
78 pub fn read_discrete_inputs(start: u16, count: u16) -> Pdu {
79 Self::pair(0x02, start, count)
80 }
81
82 /// Builds a read-holding-registers request (function `0x03`).
83 ///
84 /// # Arguments
85 ///
86 /// * `start` - the address of the first holding register to read.
87 /// * `count` - how many registers to read.
88 ///
89 /// # Returns
90 ///
91 /// The request PDU.
92 pub fn read_holding_registers(start: u16, count: u16) -> Pdu {
93 Self::pair(0x03, start, count)
94 }
95
96 /// Builds a read-input-registers request (function `0x04`).
97 ///
98 /// # Arguments
99 ///
100 /// * `start` - the address of the first input register to read.
101 /// * `count` - how many registers to read.
102 ///
103 /// # Returns
104 ///
105 /// The request PDU.
106 pub fn read_input_registers(start: u16, count: u16) -> Pdu {
107 Self::pair(0x04, start, count)
108 }
109
110 /// Builds a write-single-coil request (function `0x05`).
111 ///
112 /// # Arguments
113 ///
114 /// * `address` - the address of the coil to write.
115 /// * `on` - the value to write: `true` drives the coil on, `false` off.
116 ///
117 /// # Returns
118 ///
119 /// The request PDU.
120 pub fn write_single_coil(address: u16, on: bool) -> Pdu {
121 Self::pair(0x05, address, if on { 0xFF00 } else { 0x0000 })
122 }
123
124 /// Builds a write-single-register request (function `0x06`).
125 ///
126 /// # Arguments
127 ///
128 /// * `address` - the address of the holding register to write.
129 /// * `value` - the 16-bit value to write.
130 ///
131 /// # Returns
132 ///
133 /// The request PDU.
134 pub fn write_single_register(address: u16, value: u16) -> Pdu {
135 Self::pair(0x06, address, value)
136 }
137
138 /// Builds a write-multiple-registers request (function `0x10`).
139 ///
140 /// # Arguments
141 ///
142 /// * `start` - the address of the first holding register to write.
143 /// * `values` - the 16-bit values to write to consecutive registers.
144 ///
145 /// # Returns
146 ///
147 /// The request PDU.
148 ///
149 /// # Errors
150 ///
151 /// Returns [`ModbusError::InvalidValueCount`] if `values` is empty or holds more than
152 /// [`MAX_WRITE_REGISTERS`](Pdu::MAX_WRITE_REGISTERS) values.
153 pub fn write_multiple_registers(start: u16, values: &[u16]) -> Result<Pdu, ModbusError> {
154 let quantity = values.len();
155 if quantity == 0 || quantity > Self::MAX_WRITE_REGISTERS {
156 return Err(ModbusError::InvalidValueCount);
157 }
158 let byte_count = quantity * 2;
159 let mut bytes = [0u8; Self::MAX_LEN];
160 bytes[0] = 0x10;
161 bytes[1..3].copy_from_slice(&start.to_be_bytes());
162 bytes[3..5].copy_from_slice(&(quantity as u16).to_be_bytes());
163 bytes[5] = byte_count as u8;
164 for (i, &value) in values.iter().enumerate() {
165 bytes[6 + i * 2..8 + i * 2].copy_from_slice(&value.to_be_bytes());
166 }
167 Ok(Pdu {
168 bytes,
169 len: 6 + byte_count,
170 })
171 }
172
173 /// Builds the reply a device sends to a read-holding-registers request.
174 ///
175 /// This is the answering half of [`read_holding_registers`](Pdu::read_holding_registers):
176 /// the function code, the byte count, then the registers big-endian. It lets a client
177 /// be written and tested against what a device sends without a device on the line.
178 ///
179 /// # Arguments
180 ///
181 /// * `values` - the register values the device reports, in address order.
182 ///
183 /// # Returns
184 ///
185 /// The reply PDU.
186 ///
187 /// # Errors
188 ///
189 /// Returns [`ModbusError::InvalidValueCount`] if `values` is empty or holds more than
190 /// [`MAX_WRITE_REGISTERS`](Pdu::MAX_WRITE_REGISTERS) values.
191 pub fn read_holding_registers_reply(values: &[u16]) -> Result<Pdu, ModbusError> {
192 Self::registers_reply(0x03, values)
193 }
194
195 /// Builds the reply a device sends to a read-input-registers request.
196 ///
197 /// This is the answering half of [`read_input_registers`](Pdu::read_input_registers).
198 ///
199 /// # Arguments
200 ///
201 /// * `values` - the register values the device reports, in address order.
202 ///
203 /// # Returns
204 ///
205 /// The reply PDU.
206 ///
207 /// # Errors
208 ///
209 /// Returns [`ModbusError::InvalidValueCount`] if `values` is empty or holds more than
210 /// [`MAX_WRITE_REGISTERS`](Pdu::MAX_WRITE_REGISTERS) values.
211 pub fn read_input_registers_reply(values: &[u16]) -> Result<Pdu, ModbusError> {
212 Self::registers_reply(0x04, values)
213 }
214
215 fn registers_reply(function: u8, values: &[u16]) -> Result<Pdu, ModbusError> {
216 let quantity = values.len();
217 if quantity == 0 || quantity > Self::MAX_WRITE_REGISTERS {
218 return Err(ModbusError::InvalidValueCount);
219 }
220 let byte_count = quantity * 2;
221 let mut bytes = [0u8; Self::MAX_LEN];
222 bytes[0] = function;
223 bytes[1] = byte_count as u8;
224 for (i, &value) in values.iter().enumerate() {
225 bytes[2 + i * 2..4 + i * 2].copy_from_slice(&value.to_be_bytes());
226 }
227 Ok(Pdu {
228 bytes,
229 len: 2 + byte_count,
230 })
231 }
232
233 /// Builds a write-multiple-coils request (function `0x0F`).
234 ///
235 /// The coils are packed into bytes least-significant bit first, the order Modbus
236 /// uses; any unused bits in the final byte are left zero.
237 ///
238 /// # Arguments
239 ///
240 /// * `start` - the address of the first coil to write.
241 /// * `values` - the coil states to write, one `bool` per coil.
242 ///
243 /// # Returns
244 ///
245 /// The request PDU.
246 ///
247 /// # Errors
248 ///
249 /// Returns [`ModbusError::InvalidValueCount`] if `values` is empty or holds more than
250 /// [`MAX_WRITE_COILS`](Pdu::MAX_WRITE_COILS) values.
251 pub fn write_multiple_coils(start: u16, values: &[bool]) -> Result<Pdu, ModbusError> {
252 let quantity = values.len();
253 if quantity == 0 || quantity > Self::MAX_WRITE_COILS {
254 return Err(ModbusError::InvalidValueCount);
255 }
256 let byte_count = quantity.div_ceil(8);
257 let mut bytes = [0u8; Self::MAX_LEN];
258 bytes[0] = 0x0F;
259 bytes[1..3].copy_from_slice(&start.to_be_bytes());
260 bytes[3..5].copy_from_slice(&(quantity as u16).to_be_bytes());
261 bytes[5] = byte_count as u8;
262 for (i, &on) in values.iter().enumerate() {
263 if on {
264 bytes[6 + i / 8] |= 1u8 << (i % 8);
265 }
266 }
267 Ok(Pdu {
268 bytes,
269 len: 6 + byte_count,
270 })
271 }
272
273 /// Builds a PDU from a raw function code and data, the escape hatch for function
274 /// codes this crate does not name.
275 ///
276 /// # Arguments
277 ///
278 /// * `function` - the function code byte.
279 /// * `data` - the bytes that follow it, used verbatim.
280 ///
281 /// # Returns
282 ///
283 /// The PDU.
284 ///
285 /// # Errors
286 ///
287 /// Returns [`ModbusError::FrameTooLong`] if the function code plus `data` would not
288 /// fit a PDU (more than [`MAX_LEN`](Pdu::MAX_LEN) bytes).
289 pub fn raw(function: u8, data: &[u8]) -> Result<Pdu, ModbusError> {
290 let len = 1 + data.len();
291 if len > Self::MAX_LEN {
292 return Err(ModbusError::FrameTooLong);
293 }
294 let mut bytes = [0u8; Self::MAX_LEN];
295 bytes[0] = function;
296 bytes[1..len].copy_from_slice(data);
297 Ok(Pdu { bytes, len })
298 }
299
300 /// Returns the function code, the first byte of the PDU.
301 ///
302 /// # Returns
303 ///
304 /// The function code.
305 pub fn function_code(&self) -> u8 {
306 self.bytes[0]
307 }
308
309 /// Returns the PDU bytes: the function code followed by its data.
310 ///
311 /// # Returns
312 ///
313 /// The PDU as a byte slice.
314 pub fn as_bytes(&self) -> &[u8] {
315 &self.bytes[..self.len]
316 }
317
318 /// Wraps this PDU into an RTU frame addressed to a unit, appending the CRC.
319 ///
320 /// # Arguments
321 ///
322 /// * `address` - the unit (slave) address the frame is for.
323 ///
324 /// # Returns
325 ///
326 /// The [`Adu`] ready to send.
327 pub fn to_adu(&self, address: u8) -> Adu {
328 Adu::assemble(address, self.as_bytes())
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 #[test]
337 fn read_holding_registers_matches_the_spec_example() {
338 let pdu = Pdu::read_holding_registers(0x006B, 3);
339 assert_eq!(pdu.as_bytes(), &[0x03, 0x00, 0x6B, 0x00, 0x03]);
340 assert_eq!(pdu.function_code(), 0x03);
341 }
342
343 #[test]
344 fn write_single_coil_encodes_on_and_off() {
345 assert_eq!(
346 Pdu::write_single_coil(0x00AC, true).as_bytes(),
347 &[0x05, 0x00, 0xAC, 0xFF, 0x00]
348 );
349 assert_eq!(
350 Pdu::write_single_coil(0x00AC, false).as_bytes(),
351 &[0x05, 0x00, 0xAC, 0x00, 0x00]
352 );
353 }
354
355 #[test]
356 fn write_single_register_matches_the_spec_example() {
357 assert_eq!(
358 Pdu::write_single_register(0x0001, 0x0003).as_bytes(),
359 &[0x06, 0x00, 0x01, 0x00, 0x03]
360 );
361 }
362
363 #[test]
364 fn write_multiple_registers_matches_the_spec_example() {
365 let pdu = Pdu::write_multiple_registers(0x0001, &[0x000A, 0x0102]).unwrap();
366 assert_eq!(
367 pdu.as_bytes(),
368 &[0x10, 0x00, 0x01, 0x00, 0x02, 0x04, 0x00, 0x0A, 0x01, 0x02]
369 );
370 }
371
372 #[test]
373 fn write_multiple_coils_packs_bits_lsb_first() {
374 // The spec's ten-coil example packs to 0xCD, 0x01.
375 let values = [
376 true, false, true, true, false, false, true, true, true, false,
377 ];
378 let pdu = Pdu::write_multiple_coils(0x0013, &values).unwrap();
379 assert_eq!(
380 pdu.as_bytes(),
381 &[0x0F, 0x00, 0x13, 0x00, 0x0A, 0x02, 0xCD, 0x01]
382 );
383 }
384
385 #[test]
386 fn an_empty_write_is_rejected() {
387 assert_eq!(
388 Pdu::write_multiple_registers(0, &[]),
389 Err(ModbusError::InvalidValueCount)
390 );
391 assert_eq!(
392 Pdu::write_multiple_coils(0, &[]),
393 Err(ModbusError::InvalidValueCount)
394 );
395 }
396
397 #[test]
398 fn an_oversized_write_is_rejected() {
399 let registers = [0u16; Pdu::MAX_WRITE_REGISTERS + 1];
400 assert_eq!(
401 Pdu::write_multiple_registers(0, ®isters),
402 Err(ModbusError::InvalidValueCount)
403 );
404 let coils = [false; Pdu::MAX_WRITE_COILS + 1];
405 assert_eq!(
406 Pdu::write_multiple_coils(0, &coils),
407 Err(ModbusError::InvalidValueCount)
408 );
409 }
410
411 #[test]
412 fn the_largest_write_still_fits_a_pdu() {
413 let registers = [0u16; Pdu::MAX_WRITE_REGISTERS];
414 let pdu = Pdu::write_multiple_registers(0, ®isters).unwrap();
415 assert!(pdu.as_bytes().len() <= Pdu::MAX_LEN);
416 }
417
418 #[test]
419 fn raw_carries_arbitrary_bytes() {
420 let pdu = Pdu::raw(0x2B, &[0x0E, 0x01, 0x00]).unwrap();
421 assert_eq!(pdu.as_bytes(), &[0x2B, 0x0E, 0x01, 0x00]);
422 }
423
424 #[test]
425 fn raw_rejects_an_oversized_pdu() {
426 let data = [0u8; Pdu::MAX_LEN];
427 assert_eq!(Pdu::raw(0x10, &data), Err(ModbusError::FrameTooLong));
428 }
429
430 #[test]
431 fn to_adu_appends_the_address_and_crc() {
432 let frame = Pdu::read_holding_registers(0x006B, 3).to_adu(0x11);
433 assert_eq!(
434 frame.as_bytes(),
435 &[0x11, 0x03, 0x00, 0x6B, 0x00, 0x03, 0x76, 0x87]
436 );
437 }
438 #[test]
439 fn a_read_registers_reply_is_the_frame_the_specification_shows() {
440 // The specification's worked example: three holding registers answered as the
441 // function code, a byte count of six, then the values big-endian.
442 let reply = Pdu::read_holding_registers_reply(&[0x022B, 0x0000, 0x0064])
443 .expect("three registers fit a reply");
444 assert_eq!(
445 reply.as_bytes(),
446 &[0x03, 0x06, 0x02, 0x2B, 0x00, 0x00, 0x00, 0x64]
447 );
448
449 let inputs = Pdu::read_input_registers_reply(&[1]).expect("one register fits");
450 assert_eq!(inputs.as_bytes(), &[0x04, 0x02, 0x00, 0x01]);
451 }
452
453 #[test]
454 fn a_reply_refuses_a_register_count_it_cannot_carry() {
455 assert_eq!(
456 Pdu::read_holding_registers_reply(&[]),
457 Err(ModbusError::InvalidValueCount)
458 );
459 let too_many = [0u16; Pdu::MAX_WRITE_REGISTERS + 1];
460 assert_eq!(
461 Pdu::read_holding_registers_reply(&too_many),
462 Err(ModbusError::InvalidValueCount)
463 );
464 }
465}