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