Skip to main content

pamoja_modbus/
request.rs

1//! Reading a request the way a device does.
2
3use crate::function::{Exception, Function};
4use crate::pdu::Pdu;
5use crate::response::{Coils, Registers};
6
7/// A request as a device reads it, checked against the limits the specification sets.
8///
9/// [`parse`](Request::parse) follows the order of the specification's state diagrams: a
10/// function the device does not serve is refused with
11/// [`IllegalFunction`](Exception::IllegalFunction), and a quantity, a byte count, or a coil
12/// value outside what the function allows with [`IllegalDataValue`](Exception::IllegalDataValue).
13/// Whether the addresses exist is the device's own question, answered with
14/// [`IllegalDataAddress`](Exception::IllegalDataAddress) once the request has parsed.
15///
16/// # Examples
17///
18/// ```
19/// use pamoja_modbus::{Exception, Pdu, Request};
20///
21/// let pdu = Pdu::read_holding_registers(0x006B, 3);
22/// assert_eq!(
23///     Request::parse(pdu.as_bytes()),
24///     Ok(Request::ReadHoldingRegisters { start: 0x006B, quantity: 3 })
25/// );
26///
27/// // 126 registers is one more than a reply can carry.
28/// let greedy = Pdu::read_holding_registers(0, 126);
29/// assert_eq!(Request::parse(greedy.as_bytes()), Err(Exception::IllegalDataValue));
30/// ```
31#[derive(Clone, Copy, Debug)]
32pub enum Request<'a> {
33    /// Read coils, function `0x01`.
34    ReadCoils {
35        /// The first coil's address.
36        start: u16,
37        /// How many coils, 1 to 2000.
38        quantity: u16,
39    },
40    /// Read discrete inputs, function `0x02`.
41    ReadDiscreteInputs {
42        /// The first input's address.
43        start: u16,
44        /// How many inputs, 1 to 2000.
45        quantity: u16,
46    },
47    /// Read holding registers, function `0x03`.
48    ReadHoldingRegisters {
49        /// The first register's address.
50        start: u16,
51        /// How many registers, 1 to 125.
52        quantity: u16,
53    },
54    /// Read input registers, function `0x04`.
55    ReadInputRegisters {
56        /// The first register's address.
57        start: u16,
58        /// How many registers, 1 to 125.
59        quantity: u16,
60    },
61    /// Write one coil, function `0x05`.
62    WriteSingleCoil {
63        /// The coil's address.
64        address: u16,
65        /// The state to write.
66        on: bool,
67    },
68    /// Write one holding register, function `0x06`.
69    WriteSingleRegister {
70        /// The register's address.
71        address: u16,
72        /// The value to write.
73        value: u16,
74    },
75    /// Write a run of coils, function `0x0F`.
76    WriteMultipleCoils {
77        /// The first coil's address.
78        start: u16,
79        /// The states to write, 1 to 1968 of them, in address order.
80        values: Coils<'a>,
81    },
82    /// Write a run of holding registers, function `0x10`.
83    WriteMultipleRegisters {
84        /// The first register's address.
85        start: u16,
86        /// The values to write, 1 to 123 of them, in address order.
87        values: Registers<'a>,
88    },
89}
90
91impl PartialEq for Request<'_> {
92    fn eq(&self, other: &Self) -> bool {
93        use Request::*;
94        match (self, other) {
95            (
96                ReadCoils {
97                    start: a,
98                    quantity: b,
99                },
100                ReadCoils {
101                    start: c,
102                    quantity: d,
103                },
104            )
105            | (
106                ReadDiscreteInputs {
107                    start: a,
108                    quantity: b,
109                },
110                ReadDiscreteInputs {
111                    start: c,
112                    quantity: d,
113                },
114            )
115            | (
116                ReadHoldingRegisters {
117                    start: a,
118                    quantity: b,
119                },
120                ReadHoldingRegisters {
121                    start: c,
122                    quantity: d,
123                },
124            )
125            | (
126                ReadInputRegisters {
127                    start: a,
128                    quantity: b,
129                },
130                ReadInputRegisters {
131                    start: c,
132                    quantity: d,
133                },
134            ) => a == c && b == d,
135            (WriteSingleCoil { address: a, on: b }, WriteSingleCoil { address: c, on: d }) => {
136                a == c && b == d
137            }
138            (
139                WriteSingleRegister {
140                    address: a,
141                    value: b,
142                },
143                WriteSingleRegister {
144                    address: c,
145                    value: d,
146                },
147            ) => a == c && b == d,
148            (
149                WriteMultipleCoils {
150                    start: a,
151                    values: b,
152                },
153                WriteMultipleCoils {
154                    start: c,
155                    values: d,
156                },
157            ) => a == c && b.eq(*d),
158            (
159                WriteMultipleRegisters {
160                    start: a,
161                    values: b,
162                },
163                WriteMultipleRegisters {
164                    start: c,
165                    values: d,
166                },
167            ) => a == c && b.eq(*d),
168            _ => false,
169        }
170    }
171}
172
173fn word(bytes: &[u8], at: usize) -> u16 {
174    u16::from_be_bytes([bytes[at], bytes[at + 1]])
175}
176
177impl<'a> Request<'a> {
178    /// Reads a request PDU, checking it the way the specification's state diagrams do.
179    ///
180    /// # Arguments
181    ///
182    /// * `pdu` - the request: a function code followed by its data.
183    ///
184    /// # Returns
185    ///
186    /// The request.
187    ///
188    /// # Errors
189    ///
190    /// [`Exception::IllegalFunction`] for an empty PDU or a function this type does not
191    /// name, and [`Exception::IllegalDataValue`] for a PDU too short for its function, a
192    /// quantity outside the function's range, a byte count that does not match the
193    /// quantity, or a single coil written with anything but `0x0000` or `0xFF00`.
194    pub fn parse(pdu: &'a [u8]) -> Result<Request<'a>, Exception> {
195        let (&code, data) = pdu.split_first().ok_or(Exception::IllegalFunction)?;
196        let function = Function::from_code(code).ok_or(Exception::IllegalFunction)?;
197        if data.len() < 4 {
198            return Err(Exception::IllegalDataValue);
199        }
200        let first = word(data, 0);
201        let second = word(data, 2);
202        let within = |quantity: u16, most: usize| {
203            if quantity == 0 || usize::from(quantity) > most {
204                Err(Exception::IllegalDataValue)
205            } else {
206                Ok(quantity)
207            }
208        };
209        let fixed = |request: Request<'a>| {
210            if data.len() == 4 {
211                Ok(request)
212            } else {
213                Err(Exception::IllegalDataValue)
214            }
215        };
216        match function {
217            Function::ReadCoils => fixed(Request::ReadCoils {
218                start: first,
219                quantity: within(second, Pdu::MAX_READ_BITS)?,
220            }),
221            Function::ReadDiscreteInputs => fixed(Request::ReadDiscreteInputs {
222                start: first,
223                quantity: within(second, Pdu::MAX_READ_BITS)?,
224            }),
225            Function::ReadHoldingRegisters => fixed(Request::ReadHoldingRegisters {
226                start: first,
227                quantity: within(second, Pdu::MAX_READ_REGISTERS)?,
228            }),
229            Function::ReadInputRegisters => fixed(Request::ReadInputRegisters {
230                start: first,
231                quantity: within(second, Pdu::MAX_READ_REGISTERS)?,
232            }),
233            Function::WriteSingleCoil => {
234                let on = match second {
235                    0xFF00 => true,
236                    0x0000 => false,
237                    _ => return Err(Exception::IllegalDataValue),
238                };
239                fixed(Request::WriteSingleCoil { address: first, on })
240            }
241            Function::WriteSingleRegister => fixed(Request::WriteSingleRegister {
242                address: first,
243                value: second,
244            }),
245            Function::WriteMultipleCoils => {
246                let quantity = within(second, Pdu::MAX_WRITE_COILS)?;
247                let bytes = Self::counted(data, usize::from(quantity).div_ceil(8))?;
248                Ok(Request::WriteMultipleCoils {
249                    start: first,
250                    values: Coils::over(bytes, usize::from(quantity)),
251                })
252            }
253            Function::WriteMultipleRegisters => {
254                let quantity = within(second, Pdu::MAX_WRITE_REGISTERS)?;
255                let bytes = Self::counted(data, usize::from(quantity) * 2)?;
256                Ok(Request::WriteMultipleRegisters {
257                    start: first,
258                    values: Registers::over(bytes),
259                })
260            }
261        }
262    }
263
264    // The bytes after a byte count, which has to be what the quantity needs and match what
265    // follows it.
266    fn counted(data: &'a [u8], needed: usize) -> Result<&'a [u8], Exception> {
267        match data.get(4) {
268            Some(&count) if usize::from(count) == needed && data.len() == 5 + needed => {
269                Ok(&data[5..])
270            }
271            _ => Err(Exception::IllegalDataValue),
272        }
273    }
274
275    /// Returns the function the request asks for.
276    pub fn function(&self) -> Function {
277        match self {
278            Request::ReadCoils { .. } => Function::ReadCoils,
279            Request::ReadDiscreteInputs { .. } => Function::ReadDiscreteInputs,
280            Request::ReadHoldingRegisters { .. } => Function::ReadHoldingRegisters,
281            Request::ReadInputRegisters { .. } => Function::ReadInputRegisters,
282            Request::WriteSingleCoil { .. } => Function::WriteSingleCoil,
283            Request::WriteSingleRegister { .. } => Function::WriteSingleRegister,
284            Request::WriteMultipleCoils { .. } => Function::WriteMultipleCoils,
285            Request::WriteMultipleRegisters { .. } => Function::WriteMultipleRegisters,
286        }
287    }
288
289    /// Returns the addresses the request reads or writes, as its first address and how many
290    /// follow, which a device checks against the addresses it has.
291    ///
292    /// # Returns
293    ///
294    /// The first address and the count.
295    pub fn span(&self) -> (u16, usize) {
296        match *self {
297            Request::ReadCoils { start, quantity }
298            | Request::ReadDiscreteInputs { start, quantity }
299            | Request::ReadHoldingRegisters { start, quantity }
300            | Request::ReadInputRegisters { start, quantity } => (start, usize::from(quantity)),
301            Request::WriteSingleCoil { address, .. }
302            | Request::WriteSingleRegister { address, .. } => (address, 1),
303            Request::WriteMultipleCoils { start, values } => (start, values.len()),
304            Request::WriteMultipleRegisters { start, values } => (start, values.len()),
305        }
306    }
307
308    /// Returns whether the request only reads.
309    pub fn reads(&self) -> bool {
310        matches!(
311            self,
312            Request::ReadCoils { .. }
313                | Request::ReadDiscreteInputs { .. }
314                | Request::ReadHoldingRegisters { .. }
315                | Request::ReadInputRegisters { .. }
316        )
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323
324    #[test]
325    fn the_specification_examples_parse_to_what_they_ask() {
326        // The request of each worked example in sections 6.1 to 6.6, 6.11, and 6.12.
327        let read_coils = Pdu::read_coils(0x0013, 0x0013);
328        assert_eq!(
329            Request::parse(read_coils.as_bytes()),
330            Ok(Request::ReadCoils {
331                start: 0x0013,
332                quantity: 0x0013
333            })
334        );
335        let inputs = Pdu::read_discrete_inputs(0x00C4, 0x0016);
336        assert_eq!(
337            Request::parse(inputs.as_bytes()).map(|request| request.span()),
338            Ok((0x00C4, 22))
339        );
340        let input_registers = Pdu::read_input_registers(0x0008, 1);
341        assert!(Request::parse(input_registers.as_bytes()).is_ok_and(|r| r.reads()));
342        let coil = Pdu::write_single_coil(0x00AC, true);
343        assert_eq!(
344            Request::parse(coil.as_bytes()),
345            Ok(Request::WriteSingleCoil {
346                address: 0x00AC,
347                on: true
348            })
349        );
350        let register = Pdu::write_single_register(0x0001, 0x0003);
351        assert_eq!(
352            Request::parse(register.as_bytes()).map(|r| r.function()),
353            Ok(Function::WriteSingleRegister)
354        );
355        let ten = [
356            true, false, true, true, false, false, true, true, true, false,
357        ];
358        let coils = Pdu::write_multiple_coils(0x0013, &ten).unwrap();
359        match Request::parse(coils.as_bytes()) {
360            Ok(Request::WriteMultipleCoils { start, values }) => {
361                assert_eq!(start, 0x0013);
362                assert!(values.eq(ten));
363            }
364            other => panic!("expected ten coils, got {other:?}"),
365        }
366        let registers = Pdu::write_multiple_registers(0x0001, &[0x000A, 0x0102]).unwrap();
367        match Request::parse(registers.as_bytes()) {
368            Ok(Request::WriteMultipleRegisters { start, values }) => {
369                assert_eq!(start, 0x0001);
370                assert!(values.eq([0x000A, 0x0102]));
371            }
372            other => panic!("expected two registers, got {other:?}"),
373        }
374    }
375
376    #[test]
377    fn a_quantity_outside_the_range_is_an_illegal_value() {
378        for pdu in [
379            Pdu::read_coils(0, 0),
380            Pdu::read_coils(0, 2001),
381            Pdu::read_discrete_inputs(0, 2001),
382            Pdu::read_holding_registers(0, 0),
383            Pdu::read_holding_registers(0, 126),
384            Pdu::read_input_registers(0, 126),
385        ] {
386            assert_eq!(
387                Request::parse(pdu.as_bytes()),
388                Err(Exception::IllegalDataValue),
389                "{:02x?}",
390                pdu.as_bytes()
391            );
392        }
393        assert!(Request::parse(Pdu::read_coils(0, 2000).as_bytes()).is_ok());
394        assert!(Request::parse(Pdu::read_holding_registers(0, 125).as_bytes()).is_ok());
395    }
396
397    #[test]
398    fn a_coil_value_other_than_on_or_off_is_an_illegal_value() {
399        // 6.5: only FF 00 and 00 00 are coil values; anything else does not affect the coil.
400        let half = Pdu::raw(0x05, &[0x00, 0xAC, 0x12, 0x34]).unwrap();
401        assert_eq!(
402            Request::parse(half.as_bytes()),
403            Err(Exception::IllegalDataValue)
404        );
405    }
406
407    #[test]
408    fn a_byte_count_that_does_not_match_is_an_illegal_value() {
409        // Two registers announced, three bytes counted.
410        let short = Pdu::raw(0x10, &[0x00, 0x01, 0x00, 0x02, 0x03, 0x00, 0x0A, 0x01]).unwrap();
411        assert_eq!(
412            Request::parse(short.as_bytes()),
413            Err(Exception::IllegalDataValue)
414        );
415        // Ten coils need two bytes.
416        let coils = Pdu::raw(0x0F, &[0x00, 0x13, 0x00, 0x0A, 0x01, 0xCD]).unwrap();
417        assert_eq!(
418            Request::parse(coils.as_bytes()),
419            Err(Exception::IllegalDataValue)
420        );
421        // A read with a byte too many.
422        let long = Pdu::raw(0x03, &[0x00, 0x00, 0x00, 0x01, 0x00]).unwrap();
423        assert_eq!(
424            Request::parse(long.as_bytes()),
425            Err(Exception::IllegalDataValue)
426        );
427    }
428
429    #[test]
430    fn an_unknown_function_is_an_illegal_function() {
431        let diagnostics = Pdu::raw(0x08, &[0x00, 0x00, 0xA5, 0x37]).unwrap();
432        assert_eq!(
433            Request::parse(diagnostics.as_bytes()),
434            Err(Exception::IllegalFunction)
435        );
436        assert_eq!(Request::parse(&[]), Err(Exception::IllegalFunction));
437        let truncated = Pdu::raw(0x03, &[0x00]).unwrap();
438        assert_eq!(
439            Request::parse(truncated.as_bytes()),
440            Err(Exception::IllegalDataValue)
441        );
442    }
443}