Skip to main content

rusty_modbus_codec/response/
device_id.rs

1//! Read Device Identification response (FC 0x2B, MEI type 0x0E, Spec V1.1b3 §6.21).
2
3use rusty_modbus_types::{DeviceIdCode, MeiType};
4
5use crate::error::DecodeError;
6
7fn valid_conformity_level(level: u8) -> bool {
8    matches!(level, 0x01 | 0x02 | 0x03 | 0x81 | 0x82 | 0x83)
9}
10
11/// A single identification object parsed from the response.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct DeviceIdObjectEntry<'buf> {
14    /// Object ID (0x00–0x06 standard, 0x80–0xFF vendor-specific).
15    pub id: u8,
16    /// Object value bytes (typically UTF-8 text).
17    pub value: &'buf [u8],
18}
19
20/// FC 0x2B / MEI 0x0E — Read Device Identification response.
21///
22/// Spec V1.1b3 §6.21. Object data is borrowed from the source buffer.
23/// Use [`objects`](Self::objects) to iterate over individual entries.
24#[derive(Debug)]
25pub struct ReadDeviceIdentificationResponse<'buf> {
26    /// The access code that was requested.
27    pub device_id_code: DeviceIdCode,
28    /// Conformity level of the device.
29    pub conformity_level: u8,
30    /// If true, more objects follow — send another request with `next_object_id`.
31    pub more_follows: bool,
32    /// Next object ID to request if `more_follows` is true.
33    pub next_object_id: u8,
34    /// Number of object entries in this response.
35    pub num_objects: u8,
36    /// Raw object data bytes (packed: `[obj_id, obj_len, value...]` repeated).
37    pub object_data: &'buf [u8],
38}
39
40impl<'buf> ReadDeviceIdentificationResponse<'buf> {
41    /// Decode from the data bytes following the function code.
42    ///
43    /// # Errors
44    ///
45    /// Returns `DecodeError` if the data is malformed.
46    pub fn decode(data: &'buf [u8]) -> Result<Self, DecodeError> {
47        if data.len() < 6 {
48            return Err(DecodeError::Truncated {
49                expected: 6,
50                actual: data.len(),
51            });
52        }
53        if data[0] != MeiType::ReadDeviceIdentification.code() {
54            return Err(DecodeError::UnknownMeiType(data[0]));
55        }
56        let device_id_code =
57            DeviceIdCode::from_raw(data[1]).ok_or(DecodeError::InvalidDeviceIdCode(data[1]))?;
58        let conformity_level = data[2];
59        if !valid_conformity_level(conformity_level) {
60            return Err(DecodeError::InvalidDeviceIdConformityLevel(
61                conformity_level,
62            ));
63        }
64        let more_follows = match data[3] {
65            0x00 => false,
66            0xFF => true,
67            value => return Err(DecodeError::InvalidDeviceIdMoreFollows(value)),
68        };
69        let next_object_id = data[4];
70        if !more_follows && next_object_id != 0 {
71            return Err(DecodeError::InvalidDeviceIdNextObjectId(next_object_id));
72        }
73        let num_objects = data[5];
74        if device_id_code == DeviceIdCode::Individual && num_objects != 1 {
75            return Err(DecodeError::InvalidDeviceIdObjectCount(num_objects));
76        }
77
78        // Validate that object data is well-formed.
79        let object_data = &data[6..];
80        let mut offset = 0;
81        for _ in 0..num_objects {
82            if offset + 2 > object_data.len() {
83                return Err(DecodeError::Truncated {
84                    expected: 6 + offset + 2,
85                    actual: data.len(),
86                });
87            }
88            let obj_len = object_data[offset + 1] as usize;
89            offset += 2 + obj_len;
90            if offset > object_data.len() {
91                return Err(DecodeError::Truncated {
92                    expected: 6 + offset,
93                    actual: data.len(),
94                });
95            }
96        }
97        if offset != object_data.len() {
98            return Err(DecodeError::LengthMismatch {
99                expected: 6 + offset,
100                actual: data.len(),
101            });
102        }
103
104        Ok(Self {
105            device_id_code,
106            conformity_level,
107            more_follows,
108            next_object_id,
109            num_objects,
110            object_data,
111        })
112    }
113
114    /// Iterate over the identification objects in this response.
115    #[must_use]
116    pub fn objects(&self) -> DeviceIdObjectIter<'buf> {
117        DeviceIdObjectIter {
118            data: self.object_data,
119            remaining: self.num_objects,
120        }
121    }
122}
123
124/// Iterator over device identification object entries.
125pub struct DeviceIdObjectIter<'buf> {
126    data: &'buf [u8],
127    remaining: u8,
128}
129
130impl<'buf> Iterator for DeviceIdObjectIter<'buf> {
131    type Item = DeviceIdObjectEntry<'buf>;
132
133    fn next(&mut self) -> Option<Self::Item> {
134        if self.remaining == 0 || self.data.len() < 2 {
135            return None;
136        }
137        let id = self.data[0];
138        let len = self.data[1] as usize;
139        if self.data.len() < 2 + len {
140            self.remaining = 0;
141            self.data = &[];
142            return None;
143        }
144        let value = &self.data[2..2 + len];
145        self.data = &self.data[2 + len..];
146        self.remaining -= 1;
147        Some(DeviceIdObjectEntry { id, value })
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    extern crate alloc;
154    use alloc::vec::Vec;
155
156    use super::*;
157
158    #[test]
159    fn decode_single_object_response() {
160        let data = [
161            0x0E, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x05, b'h', b'e', b'l', b'l', b'o',
162        ];
163        let resp = ReadDeviceIdentificationResponse::decode(&data).unwrap();
164        assert_eq!(resp.device_id_code, DeviceIdCode::BasicStream);
165        assert!(!resp.more_follows);
166        assert_eq!(resp.num_objects, 1);
167        let objs: Vec<_> = resp.objects().collect();
168        assert_eq!(objs[0].id, 0x00);
169        assert_eq!(objs[0].value, b"hello");
170    }
171
172    #[test]
173    fn decode_multiple_objects() {
174        let data = [
175            0x0E, 0x01, 0x01, 0x00, 0x00, 0x03, 0x00, 0x04, b't', b'e', b's', b't', 0x01, 0x03,
176            b'X', b'Y', b'Z', 0x02, 0x05, b'1', b'.', b'0', b'.', b'0',
177        ];
178        let resp = ReadDeviceIdentificationResponse::decode(&data).unwrap();
179        let objs: Vec<_> = resp.objects().collect();
180        assert_eq!(objs.len(), 3);
181        assert_eq!(objs[0].value, b"test");
182        assert_eq!(objs[1].value, b"XYZ");
183        assert_eq!(objs[2].value, b"1.0.0");
184    }
185
186    #[test]
187    fn decode_more_follows() {
188        let data = [0x0E, 0x01, 0x01, 0xFF, 0x02, 0x01, 0x00, 0x02, b'O', b'K'];
189        let resp = ReadDeviceIdentificationResponse::decode(&data).unwrap();
190        assert!(resp.more_follows);
191        assert_eq!(resp.next_object_id, 0x02);
192    }
193
194    #[test]
195    fn decode_truncated_header() {
196        let data = [0x0E, 0x01, 0x01];
197        assert!(matches!(
198            ReadDeviceIdentificationResponse::decode(&data),
199            Err(DecodeError::Truncated { .. })
200        ));
201    }
202
203    #[test]
204    fn decode_truncated_object() {
205        let data = [0x0E, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x05, b'h', b'i'];
206        assert!(matches!(
207            ReadDeviceIdentificationResponse::decode(&data),
208            Err(DecodeError::Truncated { .. })
209        ));
210    }
211
212    #[test]
213    fn decode_rejects_invalid_conformity_level() {
214        let data = [0x0E, 0x01, 0x04, 0x00, 0x00, 0x00];
215        assert!(matches!(
216            ReadDeviceIdentificationResponse::decode(&data),
217            Err(DecodeError::InvalidDeviceIdConformityLevel(0x04))
218        ));
219    }
220
221    #[test]
222    fn decode_rejects_invalid_more_follows_value() {
223        let data = [0x0E, 0x01, 0x01, 0x01, 0x00, 0x00];
224        assert!(matches!(
225            ReadDeviceIdentificationResponse::decode(&data),
226            Err(DecodeError::InvalidDeviceIdMoreFollows(0x01))
227        ));
228    }
229
230    #[test]
231    fn decode_rejects_next_object_id_without_more_follows() {
232        let data = [0x0E, 0x01, 0x01, 0x00, 0x02, 0x00];
233        assert!(matches!(
234            ReadDeviceIdentificationResponse::decode(&data),
235            Err(DecodeError::InvalidDeviceIdNextObjectId(0x02))
236        ));
237    }
238
239    #[test]
240    fn decode_rejects_individual_response_without_one_object() {
241        let data = [0x0E, 0x04, 0x81, 0x00, 0x00, 0x00];
242        assert!(matches!(
243            ReadDeviceIdentificationResponse::decode(&data),
244            Err(DecodeError::InvalidDeviceIdObjectCount(0))
245        ));
246    }
247
248    #[test]
249    fn object_iterator_stops_on_malformed_manual_response() {
250        let resp = ReadDeviceIdentificationResponse {
251            device_id_code: DeviceIdCode::BasicStream,
252            conformity_level: 0x01,
253            more_follows: false,
254            next_object_id: 0,
255            num_objects: 1,
256            object_data: &[0x00, 0x05, b'h', b'i'],
257        };
258
259        assert!(resp.objects().next().is_none());
260    }
261}