Skip to main content

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, &registers),
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, &registers).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}