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;
5use crate::function::Exception;
6
7/// A Modbus protocol data unit: a function code followed by its data.
8///
9/// The PDU is the part of a frame that is the same on every transport. On RTU it sits
10/// between the unit address and the CRC; wrap one with [`to_adu`](Pdu::to_adu) to get a
11/// frame ready for the wire.
12///
13/// The constructors build the standard requests so callers state intent ("read three
14/// holding registers") rather than packing bytes, encoding addresses and counts in the
15/// big-endian order Modbus uses. For a function code this crate does not name, [`raw`](Pdu::raw)
16/// carries arbitrary bytes through unchanged. The data is held in a fixed buffer, so a
17/// PDU needs no allocation.
18///
19/// # Examples
20///
21/// ```
22/// use pamoja_modbus::{Pdu, Request};
23///
24/// // Set a pump controller's pressure setpoint, held in register 40, to 1200 kPa.
25/// let pdu = Pdu::write_single_register(40, 1200);
26///
27/// // The controller reads it back as the request it is.
28/// let request = Request::parse(pdu.as_bytes()).expect("a request the controller can serve");
29/// assert!(matches!(
30/// request,
31/// Request::WriteSingleRegister { address: 40, value: 1200 }
32/// ));
33/// ```
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub struct Pdu {
36 bytes: [u8; Pdu::MAX_LEN],
37 len: usize,
38}
39
40impl Pdu {
41 /// The largest a Modbus RTU PDU may be, in bytes: the 256-byte ADU less the one-byte
42 /// address and the two-byte CRC.
43 pub const MAX_LEN: usize = 253;
44
45 /// The most registers a single write-multiple-registers request may carry.
46 pub const MAX_WRITE_REGISTERS: usize = 123;
47
48 /// The most coils a single write-multiple-coils request may carry.
49 pub const MAX_WRITE_COILS: usize = 1968;
50
51 /// The most holding or input registers a single read may ask for, which fills a reply's
52 /// 250 data bytes.
53 pub const MAX_READ_REGISTERS: usize = 125;
54
55 /// The most coils or discrete inputs a single read may ask for.
56 pub const MAX_READ_BITS: usize = 2000;
57
58 // Builds a five-byte request: a function code and two 16-bit words. Read requests
59 // carry a starting address and a quantity; single-write requests carry an address
60 // and a value; the byte layout is identical.
61 fn pair(function: u8, first: u16, second: u16) -> Pdu {
62 let mut bytes = [0u8; Self::MAX_LEN];
63 bytes[0] = function;
64 bytes[1..3].copy_from_slice(&first.to_be_bytes());
65 bytes[3..5].copy_from_slice(&second.to_be_bytes());
66 Pdu { bytes, len: 5 }
67 }
68
69 /// Builds a read-coils request (function `0x01`).
70 ///
71 /// # Arguments
72 ///
73 /// * `start` - the address of the first coil to read.
74 /// * `count` - how many coils to read.
75 ///
76 /// # Returns
77 ///
78 /// The request PDU.
79 pub fn read_coils(start: u16, count: u16) -> Pdu {
80 Self::pair(0x01, start, count)
81 }
82
83 /// Builds a read-discrete-inputs request (function `0x02`).
84 ///
85 /// # Arguments
86 ///
87 /// * `start` - the address of the first discrete input to read.
88 /// * `count` - how many inputs to read.
89 ///
90 /// # Returns
91 ///
92 /// The request PDU.
93 pub fn read_discrete_inputs(start: u16, count: u16) -> Pdu {
94 Self::pair(0x02, start, count)
95 }
96
97 /// Builds a read-holding-registers request (function `0x03`).
98 ///
99 /// # Arguments
100 ///
101 /// * `start` - the address of the first holding register to read.
102 /// * `count` - how many registers to read.
103 ///
104 /// # Returns
105 ///
106 /// The request PDU.
107 pub fn read_holding_registers(start: u16, count: u16) -> Pdu {
108 Self::pair(0x03, start, count)
109 }
110
111 /// Builds a read-input-registers request (function `0x04`).
112 ///
113 /// # Arguments
114 ///
115 /// * `start` - the address of the first input register to read.
116 /// * `count` - how many registers to read.
117 ///
118 /// # Returns
119 ///
120 /// The request PDU.
121 pub fn read_input_registers(start: u16, count: u16) -> Pdu {
122 Self::pair(0x04, start, count)
123 }
124
125 /// Builds a write-single-coil request (function `0x05`).
126 ///
127 /// # Arguments
128 ///
129 /// * `address` - the address of the coil to write.
130 /// * `on` - the value to write: `true` drives the coil on, `false` off.
131 ///
132 /// # Returns
133 ///
134 /// The request PDU.
135 pub fn write_single_coil(address: u16, on: bool) -> Pdu {
136 Self::pair(0x05, address, if on { 0xFF00 } else { 0x0000 })
137 }
138
139 /// Builds a write-single-register request (function `0x06`).
140 ///
141 /// # Arguments
142 ///
143 /// * `address` - the address of the holding register to write.
144 /// * `value` - the 16-bit value to write.
145 ///
146 /// # Returns
147 ///
148 /// The request PDU.
149 pub fn write_single_register(address: u16, value: u16) -> Pdu {
150 Self::pair(0x06, address, value)
151 }
152
153 /// Builds a write-multiple-registers request (function `0x10`).
154 ///
155 /// # Arguments
156 ///
157 /// * `start` - the address of the first holding register to write.
158 /// * `values` - the 16-bit values to write to consecutive registers.
159 ///
160 /// # Returns
161 ///
162 /// The request PDU.
163 ///
164 /// # Errors
165 ///
166 /// Returns [`ModbusError::InvalidValueCount`] if `values` is empty or holds more than
167 /// [`MAX_WRITE_REGISTERS`](Pdu::MAX_WRITE_REGISTERS) values.
168 pub fn write_multiple_registers(start: u16, values: &[u16]) -> Result<Pdu, ModbusError> {
169 let quantity = values.len();
170 if quantity == 0 || quantity > Self::MAX_WRITE_REGISTERS {
171 return Err(ModbusError::InvalidValueCount);
172 }
173 let byte_count = quantity * 2;
174 let mut bytes = [0u8; Self::MAX_LEN];
175 bytes[0] = 0x10;
176 bytes[1..3].copy_from_slice(&start.to_be_bytes());
177 bytes[3..5].copy_from_slice(&(quantity as u16).to_be_bytes());
178 bytes[5] = byte_count as u8;
179 for (i, &value) in values.iter().enumerate() {
180 bytes[6 + i * 2..8 + i * 2].copy_from_slice(&value.to_be_bytes());
181 }
182 Ok(Pdu {
183 bytes,
184 len: 6 + byte_count,
185 })
186 }
187
188 /// Builds the reply a device sends to a read-holding-registers request.
189 ///
190 /// This is the answering half of [`read_holding_registers`](Pdu::read_holding_registers):
191 /// the function code, the byte count, then the registers big-endian. It lets a client
192 /// be written and tested against what a device sends without a device on the line.
193 ///
194 /// # Arguments
195 ///
196 /// * `values` - the register values the device reports, in address order.
197 ///
198 /// # Returns
199 ///
200 /// The reply PDU.
201 ///
202 /// # Errors
203 ///
204 /// Returns [`ModbusError::InvalidValueCount`] if `values` is empty or holds more than
205 /// [`MAX_READ_REGISTERS`](Pdu::MAX_READ_REGISTERS) values.
206 pub fn read_holding_registers_reply(values: &[u16]) -> Result<Pdu, ModbusError> {
207 Self::registers_reply(0x03, values)
208 }
209
210 /// Builds the reply a device sends to a read-input-registers request.
211 ///
212 /// This is the answering half of [`read_input_registers`](Pdu::read_input_registers).
213 ///
214 /// # Arguments
215 ///
216 /// * `values` - the register values the device reports, in address order.
217 ///
218 /// # Returns
219 ///
220 /// The reply PDU.
221 ///
222 /// # Errors
223 ///
224 /// Returns [`ModbusError::InvalidValueCount`] if `values` is empty or holds more than
225 /// [`MAX_READ_REGISTERS`](Pdu::MAX_READ_REGISTERS) values.
226 pub fn read_input_registers_reply(values: &[u16]) -> Result<Pdu, ModbusError> {
227 Self::registers_reply(0x04, values)
228 }
229
230 fn registers_reply(function: u8, values: &[u16]) -> Result<Pdu, ModbusError> {
231 let quantity = values.len();
232 if quantity == 0 || quantity > Self::MAX_READ_REGISTERS {
233 return Err(ModbusError::InvalidValueCount);
234 }
235 let byte_count = quantity * 2;
236 let mut bytes = [0u8; Self::MAX_LEN];
237 bytes[0] = function;
238 bytes[1] = byte_count as u8;
239 for (i, &value) in values.iter().enumerate() {
240 bytes[2 + i * 2..4 + i * 2].copy_from_slice(&value.to_be_bytes());
241 }
242 Ok(Pdu {
243 bytes,
244 len: 2 + byte_count,
245 })
246 }
247
248 /// Builds a write-multiple-coils request (function `0x0F`).
249 ///
250 /// The coils are packed into bytes least-significant bit first, the order Modbus
251 /// uses; any unused bits in the final byte are left zero.
252 ///
253 /// # Arguments
254 ///
255 /// * `start` - the address of the first coil to write.
256 /// * `values` - the coil states to write, one `bool` per coil.
257 ///
258 /// # Returns
259 ///
260 /// The request PDU.
261 ///
262 /// # Errors
263 ///
264 /// Returns [`ModbusError::InvalidValueCount`] if `values` is empty or holds more than
265 /// [`MAX_WRITE_COILS`](Pdu::MAX_WRITE_COILS) values.
266 pub fn write_multiple_coils(start: u16, values: &[bool]) -> Result<Pdu, ModbusError> {
267 let quantity = values.len();
268 if quantity == 0 || quantity > Self::MAX_WRITE_COILS {
269 return Err(ModbusError::InvalidValueCount);
270 }
271 let byte_count = quantity.div_ceil(8);
272 let mut bytes = [0u8; Self::MAX_LEN];
273 bytes[0] = 0x0F;
274 bytes[1..3].copy_from_slice(&start.to_be_bytes());
275 bytes[3..5].copy_from_slice(&(quantity as u16).to_be_bytes());
276 bytes[5] = byte_count as u8;
277 for (i, &on) in values.iter().enumerate() {
278 if on {
279 bytes[6 + i / 8] |= 1u8 << (i % 8);
280 }
281 }
282 Ok(Pdu {
283 bytes,
284 len: 6 + byte_count,
285 })
286 }
287
288 /// Builds the reply a device sends to a read-coils request: the function code, the byte
289 /// count, then the coils packed least-significant bit first.
290 ///
291 /// # Arguments
292 ///
293 /// * `values` - the coil states the device reports, in address order.
294 ///
295 /// # Returns
296 ///
297 /// The reply PDU.
298 ///
299 /// # Errors
300 ///
301 /// Returns [`ModbusError::InvalidValueCount`] if `values` is empty or holds more than
302 /// [`MAX_READ_BITS`](Pdu::MAX_READ_BITS) values.
303 pub fn read_coils_reply(values: &[bool]) -> Result<Pdu, ModbusError> {
304 Self::bits_reply(0x01, values)
305 }
306
307 /// Builds the reply a device sends to a read-discrete-inputs request.
308 ///
309 /// # Arguments
310 ///
311 /// * `values` - the input states the device reports, in address order.
312 ///
313 /// # Returns
314 ///
315 /// The reply PDU.
316 ///
317 /// # Errors
318 ///
319 /// Returns [`ModbusError::InvalidValueCount`] if `values` is empty or holds more than
320 /// [`MAX_READ_BITS`](Pdu::MAX_READ_BITS) values.
321 pub fn read_discrete_inputs_reply(values: &[bool]) -> Result<Pdu, ModbusError> {
322 Self::bits_reply(0x02, values)
323 }
324
325 fn bits_reply(function: u8, values: &[bool]) -> Result<Pdu, ModbusError> {
326 let quantity = values.len();
327 if quantity == 0 || quantity > Self::MAX_READ_BITS {
328 return Err(ModbusError::InvalidValueCount);
329 }
330 let byte_count = quantity.div_ceil(8);
331 let mut bytes = [0u8; Self::MAX_LEN];
332 bytes[0] = function;
333 bytes[1] = byte_count as u8;
334 for (i, &on) in values.iter().enumerate() {
335 if on {
336 bytes[2 + i / 8] |= 1u8 << (i % 8);
337 }
338 }
339 Ok(Pdu {
340 bytes,
341 len: 2 + byte_count,
342 })
343 }
344
345 /// Builds the reply a device sends to a write-multiple-coils request: the function code,
346 /// the starting address, and how many coils it wrote.
347 ///
348 /// # Arguments
349 ///
350 /// * `start` - the address of the first coil written.
351 /// * `quantity` - how many coils were written.
352 ///
353 /// # Returns
354 ///
355 /// The reply PDU.
356 pub fn write_multiple_coils_reply(start: u16, quantity: u16) -> Pdu {
357 Self::pair(0x0F, start, quantity)
358 }
359
360 /// Builds the reply a device sends to a write-multiple-registers request.
361 ///
362 /// # Arguments
363 ///
364 /// * `start` - the address of the first register written.
365 /// * `quantity` - how many registers were written.
366 ///
367 /// # Returns
368 ///
369 /// The reply PDU.
370 pub fn write_multiple_registers_reply(start: u16, quantity: u16) -> Pdu {
371 Self::pair(0x10, start, quantity)
372 }
373
374 /// Builds the exception a device answers with when it refuses a request: the request's
375 /// function code with its high bit set, then the exception code.
376 ///
377 /// # Arguments
378 ///
379 /// * `function` - the function code of the request refused.
380 /// * `exception` - why it was refused.
381 ///
382 /// # Returns
383 ///
384 /// The exception PDU.
385 pub fn exception(function: u8, exception: Exception) -> Pdu {
386 let mut bytes = [0u8; Self::MAX_LEN];
387 bytes[0] = function | 0x80;
388 bytes[1] = exception.code();
389 Pdu { bytes, len: 2 }
390 }
391
392 /// Builds a PDU from a raw function code and data, the escape hatch for function
393 /// codes this crate does not name.
394 ///
395 /// # Arguments
396 ///
397 /// * `function` - the function code byte.
398 /// * `data` - the bytes that follow it, used verbatim.
399 ///
400 /// # Returns
401 ///
402 /// The PDU.
403 ///
404 /// # Errors
405 ///
406 /// Returns [`ModbusError::FrameTooLong`] if the function code plus `data` would not
407 /// fit a PDU (more than [`MAX_LEN`](Pdu::MAX_LEN) bytes).
408 pub fn raw(function: u8, data: &[u8]) -> Result<Pdu, ModbusError> {
409 let len = 1 + data.len();
410 if len > Self::MAX_LEN {
411 return Err(ModbusError::FrameTooLong);
412 }
413 let mut bytes = [0u8; Self::MAX_LEN];
414 bytes[0] = function;
415 bytes[1..len].copy_from_slice(data);
416 Ok(Pdu { bytes, len })
417 }
418
419 /// Returns the function code, the first byte of the PDU.
420 ///
421 /// # Returns
422 ///
423 /// The function code.
424 pub fn function_code(&self) -> u8 {
425 self.bytes[0]
426 }
427
428 /// Returns the PDU bytes: the function code followed by its data.
429 ///
430 /// # Returns
431 ///
432 /// The PDU as a byte slice.
433 pub fn as_bytes(&self) -> &[u8] {
434 &self.bytes[..self.len]
435 }
436
437 /// Wraps this PDU into an RTU frame addressed to a unit, appending the CRC.
438 ///
439 /// # Arguments
440 ///
441 /// * `address` - the unit (slave) address the frame is for.
442 ///
443 /// # Returns
444 ///
445 /// The [`Adu`] ready to send.
446 pub fn to_adu(&self, address: u8) -> Adu {
447 Adu::assemble(address, self.as_bytes())
448 }
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454
455 #[test]
456 fn read_holding_registers_matches_the_spec_example() {
457 let pdu = Pdu::read_holding_registers(0x006B, 3);
458 assert_eq!(pdu.as_bytes(), &[0x03, 0x00, 0x6B, 0x00, 0x03]);
459 assert_eq!(pdu.function_code(), 0x03);
460 }
461
462 #[test]
463 fn write_single_coil_encodes_on_and_off() {
464 assert_eq!(
465 Pdu::write_single_coil(0x00AC, true).as_bytes(),
466 &[0x05, 0x00, 0xAC, 0xFF, 0x00]
467 );
468 assert_eq!(
469 Pdu::write_single_coil(0x00AC, false).as_bytes(),
470 &[0x05, 0x00, 0xAC, 0x00, 0x00]
471 );
472 }
473
474 #[test]
475 fn write_single_register_matches_the_spec_example() {
476 assert_eq!(
477 Pdu::write_single_register(0x0001, 0x0003).as_bytes(),
478 &[0x06, 0x00, 0x01, 0x00, 0x03]
479 );
480 }
481
482 #[test]
483 fn write_multiple_registers_matches_the_spec_example() {
484 let pdu = Pdu::write_multiple_registers(0x0001, &[0x000A, 0x0102]).unwrap();
485 assert_eq!(
486 pdu.as_bytes(),
487 &[0x10, 0x00, 0x01, 0x00, 0x02, 0x04, 0x00, 0x0A, 0x01, 0x02]
488 );
489 }
490
491 #[test]
492 fn write_multiple_coils_packs_bits_lsb_first() {
493 // The spec's ten-coil example packs to 0xCD, 0x01.
494 let values = [
495 true, false, true, true, false, false, true, true, true, false,
496 ];
497 let pdu = Pdu::write_multiple_coils(0x0013, &values).unwrap();
498 assert_eq!(
499 pdu.as_bytes(),
500 &[0x0F, 0x00, 0x13, 0x00, 0x0A, 0x02, 0xCD, 0x01]
501 );
502 }
503
504 #[test]
505 fn an_empty_write_is_rejected() {
506 assert_eq!(
507 Pdu::write_multiple_registers(0, &[]),
508 Err(ModbusError::InvalidValueCount)
509 );
510 assert_eq!(
511 Pdu::write_multiple_coils(0, &[]),
512 Err(ModbusError::InvalidValueCount)
513 );
514 }
515
516 #[test]
517 fn an_oversized_write_is_rejected() {
518 let registers = [0u16; Pdu::MAX_WRITE_REGISTERS + 1];
519 assert_eq!(
520 Pdu::write_multiple_registers(0, ®isters),
521 Err(ModbusError::InvalidValueCount)
522 );
523 let coils = [false; Pdu::MAX_WRITE_COILS + 1];
524 assert_eq!(
525 Pdu::write_multiple_coils(0, &coils),
526 Err(ModbusError::InvalidValueCount)
527 );
528 }
529
530 #[test]
531 fn the_largest_write_still_fits_a_pdu() {
532 let registers = [0u16; Pdu::MAX_WRITE_REGISTERS];
533 let pdu = Pdu::write_multiple_registers(0, ®isters).unwrap();
534 assert!(pdu.as_bytes().len() <= Pdu::MAX_LEN);
535 }
536
537 #[test]
538 fn raw_carries_arbitrary_bytes() {
539 let pdu = Pdu::raw(0x2B, &[0x0E, 0x01, 0x00]).unwrap();
540 assert_eq!(pdu.as_bytes(), &[0x2B, 0x0E, 0x01, 0x00]);
541 }
542
543 #[test]
544 fn raw_rejects_an_oversized_pdu() {
545 let data = [0u8; Pdu::MAX_LEN];
546 assert_eq!(Pdu::raw(0x10, &data), Err(ModbusError::FrameTooLong));
547 }
548
549 #[test]
550 fn to_adu_appends_the_address_and_crc() {
551 let frame = Pdu::read_holding_registers(0x006B, 3).to_adu(0x11);
552 assert_eq!(
553 frame.as_bytes(),
554 &[0x11, 0x03, 0x00, 0x6B, 0x00, 0x03, 0x76, 0x87]
555 );
556 }
557 #[test]
558 fn a_read_registers_reply_is_the_frame_the_specification_shows() {
559 // The specification's worked example: three holding registers answered as the
560 // function code, a byte count of six, then the values big-endian.
561 let reply = Pdu::read_holding_registers_reply(&[0x022B, 0x0000, 0x0064])
562 .expect("three registers fit a reply");
563 assert_eq!(
564 reply.as_bytes(),
565 &[0x03, 0x06, 0x02, 0x2B, 0x00, 0x00, 0x00, 0x64]
566 );
567
568 let inputs = Pdu::read_input_registers_reply(&[1]).expect("one register fits");
569 assert_eq!(inputs.as_bytes(), &[0x04, 0x02, 0x00, 0x01]);
570 }
571
572 #[test]
573 fn a_reply_refuses_a_register_count_it_cannot_carry() {
574 assert_eq!(
575 Pdu::read_holding_registers_reply(&[]),
576 Err(ModbusError::InvalidValueCount)
577 );
578 let too_many = [0u16; Pdu::MAX_READ_REGISTERS + 1];
579 assert_eq!(
580 Pdu::read_holding_registers_reply(&too_many),
581 Err(ModbusError::InvalidValueCount)
582 );
583 }
584
585 #[test]
586 fn a_read_of_125_registers_fills_the_reply() {
587 // 6.3: a read asks for 1 to 125 registers, and 125 is 250 data bytes, which with
588 // the function code and the byte count fills a reply.
589 let full = Pdu::read_holding_registers_reply(&[0xABCD; Pdu::MAX_READ_REGISTERS])
590 .expect("125 registers fit a reply");
591 assert_eq!(full.as_bytes().len(), 252);
592 assert_eq!(full.as_bytes()[1], 250);
593 }
594
595 #[test]
596 fn the_bit_replies_are_the_frames_the_specification_shows() {
597 // 6.1: coils 20 to 38 as the specification lists them, answered CD 6B 05.
598 let coils = [
599 true, false, true, true, false, false, true, true, true, true, false, true, false,
600 true, true, false, true, false, true,
601 ];
602 let reply = Pdu::read_coils_reply(&coils).expect("19 coils fit");
603 assert_eq!(reply.as_bytes(), &[0x01, 0x03, 0xCD, 0x6B, 0x05]);
604
605 // 6.2: discrete inputs 197 to 218, answered AC DB 35.
606 let inputs = [
607 false, false, true, true, false, true, false, true, true, true, false, true, true,
608 false, true, true, true, false, true, false, true, true,
609 ];
610 let reply = Pdu::read_discrete_inputs_reply(&inputs).expect("22 inputs fit");
611 assert_eq!(reply.as_bytes(), &[0x02, 0x03, 0xAC, 0xDB, 0x35]);
612
613 assert_eq!(
614 Pdu::read_coils_reply(&[]),
615 Err(ModbusError::InvalidValueCount)
616 );
617 let too_many = [false; Pdu::MAX_READ_BITS + 1];
618 assert_eq!(
619 Pdu::read_coils_reply(&too_many),
620 Err(ModbusError::InvalidValueCount)
621 );
622 }
623
624 #[test]
625 fn the_write_replies_and_the_exception_are_the_frames_the_specification_shows() {
626 // 6.11 and 6.12: a write of many answers with where it started and how many.
627 assert_eq!(
628 Pdu::write_multiple_coils_reply(0x0013, 0x000A).as_bytes(),
629 &[0x0F, 0x00, 0x13, 0x00, 0x0A]
630 );
631 assert_eq!(
632 Pdu::write_multiple_registers_reply(0x0001, 0x0002).as_bytes(),
633 &[0x10, 0x00, 0x01, 0x00, 0x02]
634 );
635 // Section 7: a read of an output that does not exist answers 81 02.
636 assert_eq!(
637 Pdu::exception(0x01, Exception::IllegalDataAddress).as_bytes(),
638 &[0x81, 0x02]
639 );
640 }
641}