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