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