Skip to main content

mbus_core/models/diagnostic/
model.rs

1//! Modbus Device Identification Models
2//!
3//! This module provides the data structures and types required for Function Code 43 (0x2B),
4//! specifically the Encapsulated Interface Transport for Read Device Identification (MEI Type 0x0E).
5//!
6//! It includes:
7//! - `DeviceIdentificationResponse`: The top-level response structure.
8//! - `DeviceIdObject`: Individual identification objects (e.g., Vendor Name, Product Code).
9//! - `ObjectId`: Strongly typed identifiers for basic, regular, and extended objects.
10//! - `DeviceIdObjectIterator`: A memory-efficient iterator for parsing objects from raw buffers.
11//!
12//! # Example
13//! ```
14//! # use mbus_core::models::diagnostic::{DeviceIdentificationResponse, ReadDeviceIdCode, ConformityLevel, ObjectId};
15//! # let resp = DeviceIdentificationResponse {
16//! #     read_device_id_code: ReadDeviceIdCode::Basic,
17//! #     conformity_level: ConformityLevel::BasicStreamAndIndividual,
18//! #     more_follows: false,
19//! #     next_object_id: ObjectId::from(0x00),
20//! #     objects_data: [0; 252],
21//! #     number_of_objects: 0,
22//! # };
23//! // Assuming a response has been received and parsed into `resp`
24//! for obj_result in resp.objects() {
25//!     let obj = obj_result.expect("Valid object");
26//!     println!("Object ID: {}, Value: {:?}", obj.object_id, obj.value);
27//! }
28//! ```
29
30use crate::{data_unit::common::MAX_PDU_DATA_LEN, errors::MbusError};
31use core::fmt;
32use heapless::Vec;
33
34/// Represents an object ID.
35#[derive(Debug, Clone, PartialEq)]
36pub struct DeviceIdObject {
37    /// The ID of the object.
38    pub object_id: ObjectId,
39    /// The value of the object.
40    pub value: Vec<u8, MAX_PDU_DATA_LEN>,
41}
42
43/// An iterator over the device identification objects.
44///
45/// This iterator performs lazy parsing of the `objects_data` buffer, ensuring
46/// that memory is only allocated for one object at a time during iteration.
47pub struct DeviceIdObjectIterator<'a> {
48    /// Reference to the raw byte buffer containing the objects.
49    pub(crate) data: &'a [u8],
50    /// Current byte offset within the data buffer.
51    offset: usize,
52    /// Current object count.
53    count: u8,
54    /// Total number of objects.
55    total: u8,
56}
57
58impl<'a> Iterator for DeviceIdObjectIterator<'a> {
59    type Item = Result<DeviceIdObject, MbusError>;
60
61    /// Advances the iterator and returns the next device ID object.
62    fn next(&mut self) -> Option<Self::Item> {
63        if self.count >= self.total {
64            return None;
65        }
66
67        // Parsing logic is handled internally in the iterator step
68        // We reuse the parsing logic from the original implementation but applied incrementally
69        self.parse_next()
70    }
71}
72
73impl<'a> DeviceIdObjectIterator<'a> {
74    /// Parses the next `DeviceIdObject` from the raw objects data buffer.
75    ///
76    /// Each object in the stream consists of:
77    /// - Object Id (1 byte)
78    /// - Object Length (1 byte)
79    /// - Object Value (N bytes)
80    fn parse_next(&mut self) -> Option<Result<DeviceIdObject, MbusError>> {
81        // Check if there is enough data for the 2-byte header (Id + Length)
82        if self.offset + 2 > self.data.len() {
83            return Some(Err(MbusError::InvalidPduLength));
84        }
85        let obj_id = ObjectId::from(self.data[self.offset]);
86        let obj_len = self.data[self.offset + 1] as usize;
87        self.offset += 2; // Move past the header
88
89        // Ensure the remaining data contains the full object value
90        if self.offset + obj_len > self.data.len() {
91            return Some(Err(MbusError::InvalidPduLength));
92        }
93
94        let mut value = Vec::new();
95        // Copy the object value into the heapless::Vec
96        if value
97            .extend_from_slice(&self.data[self.offset..self.offset + obj_len])
98            .is_err()
99        {
100            return Some(Err(MbusError::BufferTooSmall));
101        }
102
103        self.offset += obj_len;
104        self.count += 1;
105
106        Some(Ok(DeviceIdObject {
107            object_id: obj_id,
108            value,
109        }))
110    }
111}
112
113/// Represents a response to a Read Device Identification request (FC 43 / MEI 0E).
114#[derive(Debug, Clone, PartialEq)]
115pub struct DeviceIdentificationResponse {
116    /// The code defining the type of access.
117    pub read_device_id_code: ReadDeviceIdCode,
118    /// The conformity level of the response.
119    pub conformity_level: ConformityLevel,
120    /// Indicates if there are more objects to follow.
121    pub more_follows: bool,
122    /// The ID of the next object in the response.
123    pub next_object_id: ObjectId,
124    /// The raw data of the objects.
125    pub objects_data: [u8; MAX_PDU_DATA_LEN],
126    /// The number of objects returned in the response.
127    pub number_of_objects: u8,
128}
129
130impl DeviceIdentificationResponse {
131    /// Returns an iterator over the device identification objects.
132    pub fn objects(&self) -> DeviceIdObjectIterator<'_> {
133        DeviceIdObjectIterator {
134            data: &self.objects_data,
135            offset: 0,
136            count: 0,
137            total: self.number_of_objects,
138        }
139    }
140}
141
142/// Object IDs for Basic Device Identification.
143///
144/// These objects are mandatory for the Basic conformity level.
145/// Access type: Stream (ReadDeviceIdCode 0x01, 0x02, 0x03) or Individual (ReadDeviceIdCode 0x04).
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147#[repr(u8)]
148pub enum BasicObjectId {
149    /// Vendor Name (Mandatory). Object ID 0x00.
150    VendorName = 0x00,
151    /// Product Code (Mandatory). Object ID 0x01.
152    ProductCode = 0x01,
153    /// Major Minor Revision (Mandatory). Object ID 0x02.
154    MajorMinorRevision = 0x02,
155}
156
157impl TryFrom<u8> for BasicObjectId {
158    type Error = MbusError;
159
160    fn try_from(value: u8) -> Result<Self, Self::Error> {
161        match value {
162            0x00 => Ok(BasicObjectId::VendorName),
163            0x01 => Ok(BasicObjectId::ProductCode),
164            0x02 => Ok(BasicObjectId::MajorMinorRevision),
165            _ => Err(MbusError::InvalidAddress),
166        }
167    }
168}
169
170impl fmt::Display for BasicObjectId {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        match self {
173            BasicObjectId::VendorName => write!(f, "VendorName"),
174            BasicObjectId::ProductCode => write!(f, "ProductCode"),
175            BasicObjectId::MajorMinorRevision => write!(f, "MajorMinorRevision"),
176        }
177    }
178}
179
180/// Object IDs for Regular Device Identification.
181///
182/// These objects are optional but defined in the standard.
183/// Access type: Stream (ReadDeviceIdCode 0x02, 0x03) or Individual (ReadDeviceIdCode 0x04).
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185#[repr(u8)]
186pub enum RegularObjectId {
187    /// Vendor URL (Optional). Object ID 0x03.
188    VendorUrl = 0x03,
189    /// Product Name (Optional). Object ID 0x04.
190    ProductName = 0x04,
191    /// Model Name (Optional). Object ID 0x05.
192    ModelName = 0x05,
193    /// User Application Name (Optional). Object ID 0x06.
194    UserApplicationName = 0x06,
195}
196
197impl TryFrom<u8> for RegularObjectId {
198    type Error = MbusError;
199
200    fn try_from(value: u8) -> Result<Self, Self::Error> {
201        match value {
202            0x03 => Ok(RegularObjectId::VendorUrl),
203            0x04 => Ok(RegularObjectId::ProductName),
204            0x05 => Ok(RegularObjectId::ModelName),
205            0x06 => Ok(RegularObjectId::UserApplicationName),
206            _ => Err(MbusError::InvalidAddress),
207        }
208    }
209}
210
211impl fmt::Display for RegularObjectId {
212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213        match self {
214            RegularObjectId::VendorUrl => write!(f, "VendorUrl"),
215            RegularObjectId::ProductName => write!(f, "ProductName"),
216            RegularObjectId::ModelName => write!(f, "ModelName"),
217            RegularObjectId::UserApplicationName => write!(f, "UserApplicationName"),
218        }
219    }
220}
221
222/// Extended Object IDs.
223///
224/// Range 0x80 - 0xFF. These are private/vendor-specific.
225/// Access type: Stream (ReadDeviceIdCode 0x03) or Individual (ReadDeviceIdCode 0x04).
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
227pub struct ExtendedObjectId(u8);
228
229impl ExtendedObjectId {
230    /// Creates a new `ExtendedObjectId`.
231    ///
232    /// Returns `None` if the id is not in the range 0x80..=0xFF.
233    pub fn new(id: u8) -> Option<Self> {
234        if (0x80..=0xFF).contains(&id) {
235            Some(Self(id))
236        } else {
237            None
238        }
239    }
240
241    /// Returns the raw object ID.
242    pub fn value(&self) -> u8 {
243        self.0
244    }
245}
246
247/// Read Device ID Code (Function Code 43 / 14).
248///
249/// Defines the type of access requested.
250#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
251#[repr(u8)]
252pub enum ReadDeviceIdCode {
253    /// Sentinel default used before parsing a concrete code.
254    /// This value should not appear in a valid decoded request.
255    #[default]
256    Err,
257    /// Request to get the basic device identification (stream access).
258    Basic = 0x01,
259    /// Request to get the regular device identification (stream access).
260    Regular = 0x02,
261    /// Request to get the extended device identification (stream access).
262    Extended = 0x03,
263    /// Request to get one specific identification object (individual access).
264    Specific = 0x04,
265}
266
267impl TryFrom<u8> for ReadDeviceIdCode {
268    type Error = MbusError;
269
270    fn try_from(value: u8) -> Result<Self, Self::Error> {
271        match value {
272            0x01 => Ok(ReadDeviceIdCode::Basic),
273            0x02 => Ok(ReadDeviceIdCode::Regular),
274            0x03 => Ok(ReadDeviceIdCode::Extended),
275            0x04 => Ok(ReadDeviceIdCode::Specific),
276            _ => Err(MbusError::InvalidDeviceIdCode),
277        }
278    }
279}
280
281/// Conformity Level returned in the response.
282#[derive(Debug, Clone, Copy, PartialEq, Eq)]
283#[repr(u8)]
284pub enum ConformityLevel {
285    /// Basic identification (stream access only).
286    BasicStreamOnly = 0x01,
287    /// Regular identification (stream access only).
288    RegularStreamOnly = 0x02,
289    /// Extended identification (stream access only).
290    ExtendedStreamOnly = 0x03,
291    /// Basic identification (stream access and individual access).
292    BasicStreamAndIndividual = 0x81,
293    /// Regular identification (stream access and individual access).
294    RegularStreamAndIndividual = 0x82,
295    /// Extended identification (stream access and individual access).
296    ExtendedStreamAndIndividual = 0x83,
297}
298
299impl TryFrom<u8> for ConformityLevel {
300    type Error = MbusError;
301
302    fn try_from(value: u8) -> Result<Self, Self::Error> {
303        match value {
304            0x01 => Ok(ConformityLevel::BasicStreamOnly),
305            0x02 => Ok(ConformityLevel::RegularStreamOnly),
306            0x03 => Ok(ConformityLevel::ExtendedStreamOnly),
307            0x81 => Ok(ConformityLevel::BasicStreamAndIndividual),
308            0x82 => Ok(ConformityLevel::RegularStreamAndIndividual),
309            0x83 => Ok(ConformityLevel::ExtendedStreamAndIndividual),
310            _ => Err(MbusError::ParseError),
311        }
312    }
313}
314
315/// Represents any valid Modbus Device Identification Object ID.
316#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
317pub enum ObjectId {
318    /// Sentinel default used before parsing a concrete object identifier.
319    /// This value should not appear in a valid decoded response.
320    #[default]
321    Err,
322    /// Basic Device Identification Object IDs (0x00 - 0x02).
323    Basic(BasicObjectId),
324    /// Regular Device Identification Object IDs (0x03 - 0x06).
325    Regular(RegularObjectId),
326    /// Extended Device Identification Object IDs (0x80 - 0xFF).
327    Extended(ExtendedObjectId),
328    /// Reserved range (0x07 - 0x7F).
329    Reserved(u8),
330}
331
332impl From<u8> for ObjectId {
333    fn from(id: u8) -> Self {
334        if let Ok(basic) = BasicObjectId::try_from(id) {
335            ObjectId::Basic(basic)
336        } else if let Ok(regular) = RegularObjectId::try_from(id) {
337            ObjectId::Regular(regular)
338        } else if let Some(extended) = ExtendedObjectId::new(id) {
339            ObjectId::Extended(extended)
340        } else {
341            ObjectId::Reserved(id)
342        }
343    }
344}
345
346impl fmt::Display for ObjectId {
347    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348        match self {
349            ObjectId::Basic(id) => write!(f, "Basic({})", id),
350            ObjectId::Regular(id) => write!(f, "Regular({})", id),
351            ObjectId::Extended(id) => write!(f, "Extended({:#04X})", id.value()),
352            ObjectId::Reserved(id) => write!(f, "Reserved({:#04X})", id),
353            ObjectId::Err => write!(f, "Err (sentinel default)"),
354        }
355    }
356}
357
358impl From<ObjectId> for u8 {
359    fn from(oid: ObjectId) -> u8 {
360        match oid {
361            ObjectId::Basic(id) => id as u8,
362            ObjectId::Regular(id) => id as u8,
363            ObjectId::Extended(id) => id.value(),
364            ObjectId::Reserved(id) => id,
365            ObjectId::Err => 0, // Sentinel default path.
366        }
367    }
368}