Skip to main content

pamoja_modbus/
response.rs

1//! Reading values back out of a Modbus response PDU.
2
3use crate::error::ModbusError;
4use crate::function::Exception;
5
6/// A borrowed view over a response PDU, for reading the values a device returned.
7///
8/// A read response is a function code, a byte count, and then the data. [`registers`](Response::registers)
9/// and [`coils`](Response::coils) decode that data into the 16-bit words or the packed
10/// bits it represents; [`exception`](Response::exception) recognizes the alternative, a
11/// device that refused the request. The view borrows the PDU and copies nothing.
12///
13/// # Examples
14///
15/// ```
16/// use pamoja_modbus::{Pdu, Response};
17///
18/// // A soil probe's reply to a read of three registers: nitrogen, phosphorus, and
19/// // potassium, in milligrams per kilogram.
20/// let reply = Pdu::read_holding_registers_reply(&[555, 0, 100]).unwrap();
21/// let values: Vec<u16> = Response::new(reply.as_bytes()).registers().unwrap().collect();
22/// assert_eq!(values, [555, 0, 100]);
23/// ```
24#[derive(Clone, Copy, Debug)]
25pub struct Response<'a> {
26    pdu: &'a [u8],
27}
28
29impl<'a> Response<'a> {
30    /// Wraps a response PDU for reading.
31    ///
32    /// # Arguments
33    ///
34    /// * `pdu` - the response PDU, a function code followed by its data.
35    ///
36    /// # Returns
37    ///
38    /// The response view.
39    pub fn new(pdu: &'a [u8]) -> Self {
40        Response { pdu }
41    }
42
43    /// Returns the function code, the first byte of the PDU.
44    ///
45    /// # Returns
46    ///
47    /// The function code, or `0` if the PDU is empty.
48    pub fn function_code(&self) -> u8 {
49        self.pdu.first().copied().unwrap_or(0)
50    }
51
52    /// Returns the exception a device reported, if this is an exception response.
53    ///
54    /// # Returns
55    ///
56    /// The [`Exception`] if the function code's high bit is set and a defined exception
57    /// byte follows it, otherwise [`None`].
58    pub fn exception(&self) -> Option<Exception> {
59        if self.function_code() & 0x80 == 0 {
60            return None;
61        }
62        self.pdu.get(1).and_then(|&code| Exception::from_code(code))
63    }
64
65    // The data carried after the function code and byte-count header, validated so its
66    // length matches the declared byte count.
67    fn payload(&self) -> Result<&'a [u8], ModbusError> {
68        if self.pdu.len() < 2 {
69            return Err(ModbusError::MalformedResponse);
70        }
71        let byte_count = usize::from(self.pdu[1]);
72        let data = &self.pdu[2..];
73        if data.len() != byte_count {
74            return Err(ModbusError::MalformedResponse);
75        }
76        Ok(data)
77    }
78
79    /// Reads the 16-bit registers from a read-registers response.
80    ///
81    /// # Returns
82    ///
83    /// An iterator over the registers in order, each decoded from its big-endian pair.
84    ///
85    /// # Errors
86    ///
87    /// Returns [`ModbusError::MalformedResponse`] if the PDU is truncated, its declared
88    /// byte count does not match its data, or that data is not a whole number of registers.
89    pub fn registers(&self) -> Result<Registers<'a>, ModbusError> {
90        let data = self.payload()?;
91        if data.len() % 2 != 0 {
92            return Err(ModbusError::MalformedResponse);
93        }
94        Ok(Registers::over(data))
95    }
96
97    /// Reads the coils or discrete inputs from a read-bits response.
98    ///
99    /// The response packs the bits least-significant first; this unpacks exactly `count`
100    /// of them and ignores the padding in the final byte.
101    ///
102    /// # Arguments
103    ///
104    /// * `count` - how many bits to read, the quantity the request asked for.
105    ///
106    /// # Returns
107    ///
108    /// An iterator over `count` bits in order.
109    ///
110    /// # Errors
111    ///
112    /// Returns [`ModbusError::MalformedResponse`] if the PDU is truncated or its declared
113    /// byte count does not match the data or the requested `count`.
114    pub fn coils(&self, count: u16) -> Result<Coils<'a>, ModbusError> {
115        let data = self.payload()?;
116        if data.len() != usize::from(count).div_ceil(8) {
117            return Err(ModbusError::MalformedResponse);
118        }
119        Ok(Coils::over(data, usize::from(count)))
120    }
121}
122
123/// An iterator over the 16-bit registers of a read-registers response, or of a
124/// write-multiple-registers request.
125#[derive(Clone, Copy, Debug)]
126pub struct Registers<'a> {
127    data: &'a [u8],
128}
129
130impl<'a> Registers<'a> {
131    // Reads big-endian pairs out of bytes whose length the caller has checked is even.
132    pub(crate) fn over(data: &'a [u8]) -> Self {
133        Registers { data }
134    }
135}
136
137impl Iterator for Registers<'_> {
138    type Item = u16;
139
140    fn next(&mut self) -> Option<u16> {
141        if self.data.len() < 2 {
142            return None;
143        }
144        let value = u16::from_be_bytes([self.data[0], self.data[1]]);
145        self.data = &self.data[2..];
146        Some(value)
147    }
148
149    fn size_hint(&self) -> (usize, Option<usize>) {
150        let remaining = self.data.len() / 2;
151        (remaining, Some(remaining))
152    }
153}
154
155impl ExactSizeIterator for Registers<'_> {}
156
157/// An iterator over the bits of a read-coils or read-discrete-inputs response, or of a
158/// write-multiple-coils request.
159#[derive(Clone, Copy, Debug)]
160pub struct Coils<'a> {
161    data: &'a [u8],
162    index: usize,
163    remaining: usize,
164}
165
166impl<'a> Coils<'a> {
167    // Unpacks `count` bits, least significant first, from bytes the caller has checked hold
168    // at least that many.
169    pub(crate) fn over(data: &'a [u8], count: usize) -> Self {
170        Coils {
171            data,
172            index: 0,
173            remaining: count,
174        }
175    }
176}
177
178impl Iterator for Coils<'_> {
179    type Item = bool;
180
181    fn next(&mut self) -> Option<bool> {
182        if self.remaining == 0 {
183            return None;
184        }
185        let bit = (self.data[self.index / 8] >> (self.index % 8)) & 1;
186        self.index += 1;
187        self.remaining -= 1;
188        Some(bit != 0)
189    }
190
191    fn size_hint(&self) -> (usize, Option<usize>) {
192        (self.remaining, Some(self.remaining))
193    }
194}
195
196impl ExactSizeIterator for Coils<'_> {}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn registers_decode_in_order() {
204        let pdu = [0x03, 0x06, 0x02, 0x2B, 0x00, 0x00, 0x00, 0x64];
205        let values: [u16; 3] = {
206            let mut it = Response::new(&pdu).registers().unwrap();
207            [it.next().unwrap(), it.next().unwrap(), it.next().unwrap()]
208        };
209        assert_eq!(values, [0x022B, 0x0000, 0x0064]);
210    }
211
212    #[test]
213    fn registers_report_an_exact_length() {
214        let pdu = [0x03, 0x06, 0x02, 0x2B, 0x00, 0x00, 0x00, 0x64];
215        assert_eq!(Response::new(&pdu).registers().unwrap().len(), 3);
216    }
217
218    #[test]
219    fn registers_reject_a_byte_count_mismatch() {
220        // Byte count says six, but only two data bytes follow.
221        let pdu = [0x03, 0x06, 0x02, 0x2B];
222        assert_eq!(
223            Response::new(&pdu).registers().err(),
224            Some(ModbusError::MalformedResponse)
225        );
226    }
227
228    #[test]
229    fn coils_unpack_lsb_first_and_drop_padding() {
230        // Byte count one, data 0x05 is bits 1, 0, 1 in the low three positions.
231        let pdu = [0x01, 0x01, 0x05];
232        let bits: [bool; 3] = {
233            let mut it = Response::new(&pdu).coils(3).unwrap();
234            [it.next().unwrap(), it.next().unwrap(), it.next().unwrap()]
235        };
236        assert_eq!(bits, [true, false, true]);
237    }
238
239    #[test]
240    fn coils_reject_a_count_that_does_not_match_the_byte_count() {
241        let pdu = [0x01, 0x01, 0x05];
242        // Nine coils need two bytes, but only one is present.
243        assert_eq!(
244            Response::new(&pdu).coils(9).err(),
245            Some(ModbusError::MalformedResponse)
246        );
247    }
248
249    #[test]
250    fn an_exception_response_reads_as_an_exception() {
251        let pdu = [0x83, 0x02];
252        assert_eq!(
253            Response::new(&pdu).exception(),
254            Some(Exception::IllegalDataAddress)
255        );
256    }
257
258    #[test]
259    fn a_normal_response_has_no_exception() {
260        let pdu = [0x03, 0x02, 0x00, 0x64];
261        assert_eq!(Response::new(&pdu).exception(), None);
262    }
263}