Skip to main content

mechutil/
object_dictionary.rs

1//
2// Copyright (C) 2024 Automated Design Corp.. All Rights Reserved.
3// Created Date: 2024-10-23 06:12:47
4// -----
5// Last Modified: 2024-11-04 07:41:41
6// -----
7//
8//
9
10//! This module provides an implementation based upon the CANopen object dictionary
11//! or CAN over EtherCAT (CoE) systems. The object
12//! dictionary is the core mechanism for configuration and communication with
13//! CANopen devices. Each entry in the dictionary is represented by an index, which
14//! may contain sub-indices for more complex data structures such as arrays and
15//! records.
16//!
17//! The `MechObjectDictionary` struct serves as a collection of these entries, each
18//! described by its index, object type, access rights, data type, and value. The
19//! module supports serialization and deserialization to/from JSON to facilitate
20//! easy configuration management and device integration.
21//!
22//! Key Components:
23//! - `MechObjectDictionary`: The main container for the object dictionary entries.
24//! - `MechObjectDictionaryEntry`: Represents a single entry in the object dictionary.
25//! - `RegisterValue`: Enum representing either a single value or a set of sub-indices.
26//! - `AccessRights`: Enum describing the access permissions for an entry (ReadWrite, ReadOnly, WriteOnly).
27//! - `ObjectType`: Enum for the type of object (Array, Record, Variable).
28//!
29//! Features:
30//! - Create and manage object dictionary entries.
31//! - Support for both single values and arrays of sub-indices.
32//! - Serialization and deserialization to/from JSON for configuration persistence.
33//!
34//! Example Usage:
35//! ```no_run
36//! use mechutil::object_dictionary::{MechObjectDictionary, ObjectType, AccessRights, MechObjectDictionaryValue};
37//! use mechutil::register_value::MechCommandRegisterValue;
38//!
39//! let mut dictionary = MechObjectDictionary::new(
40//!     0x000401,  // Device Type (generic I/O device)
41//!     1000,      // Heartbeat interval of 1000 ms
42//!     12345,     // Vendor ID
43//!     67890,     // Product Code
44//! );
45//! let initial_value = MechCommandRegisterValue::new();  // Assume this creates a default value.
46//! dictionary.add_entry(0x1000, ObjectType::Array, "Example Object".to_string(), "uint32".to_string(), AccessRights::ReadWrite, true, 5, initial_value);
47//! ```
48//!
49//! In general, for custom functionality, the objects should be in the range 0x2000–0x5FFF, **Manufacturer-Specific Objects**.
50//! See the common_canopen_objects.md doc for CANOpen standard dictionary objects and their addresses.
51//!
52//!
53
54//
55// TODO:
56// - [] PDO-like features.
57// - - [] Register PDOs
58// - - [] Map registers to a PDO
59// - []  API for making required objects:
60// - - 0x1000 Device Type (Read Only)
61// - - 0x1001 Error Register (Read Only)
62// - - 0x1017 Producer Heartbeat Time (Const)
63// - - 0x1018 Identity Object (Read Only)
64//
65// - [] Look into this:
66// - - "In addition you need Communication Parameters and Mapping parameters for every PDO, but these are allowed to be constants.""
67
68use crate::register_value::MechCommandRegisterValue;
69use anyhow::anyhow;
70use serde::{Deserialize, Serialize};
71use std::collections::HashMap;
72use std::fs;
73
74/// Enum for access rights (read/write, read-only, write-only) for the object dictionary entry.
75#[derive(Clone, Debug, Serialize, Deserialize)]
76pub enum AccessRights {
77    ReadWrite,
78    Const,
79    ReadOnly,
80    WriteOnly,
81}
82
83/// Enum for the object type in the object dictionary entry (e.g., array, record, variable).
84#[derive(Clone, Debug, Serialize, Deserialize)]
85pub enum ObjectType {
86    /// A collection of homogeneous sub-indices, meaning all sub-indices within an Array must have the same data type.
87    Array,
88    /// A collection of heterogeneous sub-indices, meaning each sub-index can represent a different type of data
89    Record,
90    /// A single value. This object does not contain sub-indices.
91    Variable,
92}
93
94/// Represents an entry in the CANopen Object Dictionary.
95///
96/// This struct models the data, metadata, and access properties for a specific
97/// entry within a CANopen-compliant Object Dictionary, where each entry is
98/// identified by a unique 16-bit `index`.
99///
100/// ### Fields
101/// - `index`: The 16-bit address of the object in the Object Dictionary.
102/// - `object_type`: Specifies the type of object. This can be an `Array`, `Record`,
103///   or `Variable`, which determines if the object contains a list of values,
104///   a structured set of different data types, or a single value.
105/// - `name`: A descriptive name for the object, helping to identify its purpose
106///   or function within the device configuration.
107/// - `datatype`: The data type of the object as a `String`. This string represents
108///   the underlying data format (e.g., `"uint32"`, `"int16"`, `"float"`). The `datatype`
109///   string is informational, ensuring compatibility across systems without imposing
110///   a strict type. To enforce or interpret data types programmatically, use the
111///   appropriate `from_*` and `to_*` methods in `MechCommandRegisterValue`.
112/// - `access_rights`: Defines the access permissions for the entry, including `ReadOnly`,
113///   `WriteOnly`, `ReadWrite`, or `Const` (constant, unmodifiable data).
114/// - `mandatory`: Indicates whether the object is required for the device to be CANopen-compliant
115///   (`true` if mandatory) or is optional (`false`).
116/// - `value`: Stores the entry’s value(s). This field can either hold a single value or
117///   a collection of sub-indices, depending on the object type.
118///
119/// ### Example Usage
120/// ```no_run
121/// use mechutil::object_dictionary::{MechObjectDictionaryEntry, ObjectType, AccessRights, MechObjectDictionaryValue};
122/// use mechutil::register_value::MechCommandRegisterValue;
123///
124/// let entry = MechObjectDictionaryEntry {
125///     index: 0x1018,
126///     object_type: ObjectType::Record,
127///     name: "Identity Object".to_string(),
128///     datatype: "record".to_string(),
129///     access_rights: AccessRights::ReadOnly,
130///     mandatory: true,
131///     value: MechObjectDictionaryValue::SubIndices(vec![
132///         MechCommandRegisterValue::from_uint32(12345),  // Vendor ID
133///         MechCommandRegisterValue::from_uint32(67890),  // Product Code
134///         MechCommandRegisterValue::from_uint32(1),      // Revision Number
135///         MechCommandRegisterValue::from_uint32(12345678), // Serial Number
136///     ]),
137/// };
138/// ```
139#[derive(Clone, Debug, Serialize, Deserialize)]
140pub struct MechObjectDictionaryEntry {
141    pub index: u16,                       // 16-bit address of the object in the dictionary
142    pub object_type: ObjectType,          // Type of object (array, record, variable)
143    pub name: String,                     // Descriptive name of the object
144    pub datatype: String,                 // Data type of the variable
145    pub access_rights: AccessRights,      // Access rights (read/write, read-only, write-only)
146    pub mandatory: bool, // Indicates if the object is mandatory (true) or optional (false)
147    pub value: MechObjectDictionaryValue, // The value or sub-indices for the object
148}
149
150/// Defines the value storage for an entry in the CANopen Object Dictionary.
151///
152/// `MechObjectDictionaryValue` can store a single `MechCommandRegisterValue`
153/// or a vector of `MechCommandRegisterValue`s, representing an array or record
154/// structure with multiple sub-indices. This enum allows flexibility in how
155/// values are stored within the Object Dictionary:
156/// - `Single`: Used for entries that have a single value (e.g., device-specific parameters).
157/// - `SubIndices`: Used for entries that contain multiple values, typically
158///   when `object_type` is `Record` or `Array`.
159///
160/// ### Example Usage
161/// ```no_run
162/// use mechutil::object_dictionary::{MechObjectDictionaryValue};
163/// use mechutil::register_value::MechCommandRegisterValue;
164///
165/// // Single value example
166/// let single_value = MechObjectDictionaryValue::Single(MechCommandRegisterValue::from_int32(42));
167///
168/// // Array of values example
169/// let array_value = MechObjectDictionaryValue::SubIndices(vec![
170///     MechCommandRegisterValue::from_uint32(0),
171///     MechCommandRegisterValue::from_uint32(1),
172///     MechCommandRegisterValue::from_uint32(2),
173/// ]);
174/// ```
175#[derive(Clone, Debug, Serialize, Deserialize)]
176pub enum MechObjectDictionaryValue {
177    Single(MechCommandRegisterValue),
178    SubIndices(Vec<MechCommandRegisterValue>),
179}
180
181/// `MechObjectDictionary` is a structured collection of entries for a CANopen device's Object Dictionary.
182///
183/// This dictionary serves as the central data structure for defining and organizing device parameters,
184/// configuration settings, and runtime information according to CANopen standards. Each entry in the
185/// dictionary is mapped by its index and may represent a single value, a structured record, or an array of
186/// homogeneous elements.
187///
188/// ### Key Features
189/// - **Standard Entries**: The `new` method initializes the dictionary with commonly used CANopen objects
190///   (e.g., Device Type, Error Register, Producer Heartbeat Time, Identity Object).
191/// - **Customizable**: New entries can be added dynamically to the dictionary with specific properties
192///   like access rights and data types.
193/// - **Supports Single and Array Entries**: Entries can either hold a single value or a collection of sub-indices
194///   (e.g., for records or arrays).
195///
196/// ### Example Usage
197/// ```no_run
198/// use mechutil::object_dictionary::{MechObjectDictionary, ObjectType, AccessRights};
199/// use mechutil::register_value::MechCommandRegisterValue;
200///
201/// // Initialize a new object dictionary with standard entries and default values
202/// let mut dictionary = MechObjectDictionary::new(
203///     0x000401,  // Device Type (generic I/O device)
204///     1000,      // Heartbeat interval of 1000 ms
205///     12345,     // Vendor ID
206///     67890,     // Product Code
207/// );
208///
209/// // Adding a custom entry for a device-specific parameter (single value)
210/// let custom_value = MechCommandRegisterValue::from_int32(42);
211/// dictionary.add_value_entry(
212///     0x2000,
213///     ObjectType::Variable,
214///     "Custom Parameter".to_string(),
215///     "int32".to_string(),
216///     AccessRights::ReadWrite,
217///     false,
218///     custom_value,
219/// );
220///
221/// // Adding an array entry for logging error history (multiple sub-indices)
222/// let error_history_value = MechCommandRegisterValue::from_uint32(0);
223/// dictionary.add_array_entry(
224///     0x1003,
225///     ObjectType::Array,
226///     "Error History".to_string(),
227///     "uint32".to_string(),
228///     AccessRights::ReadWrite,
229///     false,
230///     5,   // Number of sub-indices for error codes
231///     error_history_value,
232/// );
233///
234/// // Reading the value of a single-value entry (e.g., Device Type)
235/// if let Ok(value) = dictionary.get_index_value(0x1000) {
236///     println!("Device Type: {:?}", value);
237/// }
238///
239/// // Reading a specific sub-index value from an array entry (e.g., Error History at index 1)
240/// if let Ok(error_code) = dictionary.get_sub_index_value(0x1003, 1) {
241///     println!("Error Code at sub-index 1: {:?}", error_code);
242/// }
243/// ```
244#[derive(Clone, Debug, Serialize, Deserialize)]
245pub struct MechObjectDictionary {
246    registers: HashMap<u16, MechObjectDictionaryEntry>, // Register index to entry mapping
247}
248
249impl MechObjectDictionary {
250    /// Creates a new `MechObjectDictionary` with standard default entries.
251    ///
252    /// This function initializes the dictionary with commonly used CANopen objects, including:
253    /// - `0x1000` Device Type (Read Only): Specifies the type of the device.
254    /// - `0x1001` Error Register (Read Only): Holds error flags for device status monitoring.
255    /// - `0x1017` Producer Heartbeat Time (Const): Sets the interval for heartbeat messages.
256    /// - `0x1018` Identity Object (Read Only): Contains identification information about the device.
257    ///
258    /// # Parameters
259    /// - `device_type`: The initial value for the `0x1000` Device Type entry.
260    /// - `heartbeat_time_interval_ms`: The interval in milliseconds for the `0x1017` Producer Heartbeat Time entry.
261    /// - `vendor_id`: The Vendor ID for the `0x1018:01` sub-index of the Identity Object.
262    /// - `product_code`: The Product Code for the `0x1018:02` sub-index of the Identity Object.
263    ///
264    /// # Example
265    /// ```no_run
266    /// use mechutil::object_dictionary::MechObjectDictionary;
267    /// let dictionary = MechObjectDictionary::new(
268    ///     0x2001,     // Device Type for a software device
269    ///     1000,      // Heartbeat interval of 1000 ms (1 second)
270    ///     12345,     // Vendor ID
271    ///     67890,     // Product Code
272    /// );
273    /// ```
274    pub fn new(
275        device_type: u32,
276        heartbeat_time_interval_ms: u16,
277        vendor_id: u32,
278        product_code: u32,
279    ) -> Self {
280        let mut dictionary = MechObjectDictionary {
281            registers: HashMap::new(),
282        };
283
284        // Default initial value with zero values for each register
285        let default_value = MechCommandRegisterValue::new();
286
287        // 0x1000 Device Type
288        dictionary.add_value_entry(
289            0x1000,
290            ObjectType::Variable,
291            "Device Type".to_string(),
292            "uint32".to_string(),
293            AccessRights::ReadOnly,
294            true,
295            MechCommandRegisterValue::from_uint32(device_type),
296        );
297
298        // 0x1001 Error Register
299        dictionary.add_value_entry(
300            0x1001,
301            ObjectType::Variable,
302            "Error Register".to_string(),
303            "uint8".to_string(),
304            AccessRights::ReadOnly,
305            true,
306            default_value.clone(),
307        );
308
309        // 0x1017 Producer Heartbeat Time
310        dictionary.add_value_entry(
311            0x1017,
312            ObjectType::Variable,
313            "Producer Heartbeat Time".to_string(),
314            "uint16".to_string(),
315            AccessRights::Const,
316            true,
317            MechCommandRegisterValue::from_uint16(heartbeat_time_interval_ms),
318        );
319
320        // 0x1018 Identity Object (Typically a Record with multiple sub-indices)
321        dictionary.add_array_entry(
322            0x1018,
323            ObjectType::Record,
324            "Identity Object".to_string(),
325            "record".to_string(),
326            AccessRights::ReadOnly,
327            true,
328            4, // Example: Identity Object might have 4 sub-indices for Vendor ID, Product Code, Revision Number, Serial Number
329            default_value,
330        );
331
332        let _ = dictionary.set_value(
333            0x1018,
334            Some(1),
335            MechCommandRegisterValue::from_uint32(vendor_id),
336        );
337        let _ = dictionary.set_value(
338            0x1018,
339            Some(2),
340            MechCommandRegisterValue::from_uint32(product_code),
341        );
342        // revision number
343        let _ = dictionary.set_value(0x1018, Some(3), MechCommandRegisterValue::from_uint32(0));
344        // serial number
345        let _ = dictionary.set_value(0x1018, Some(4), MechCommandRegisterValue::from_uint32(0));
346
347        return dictionary;
348    }
349
350    /// Adds a new entry to the object dictionary with an initial value.
351    /// If `num_sub_indices` is greater than 1, it creates sub-indices with the provided initial value;
352    /// otherwise, it creates a single value entry.
353    ///
354    /// # Parameters
355    /// - `index`: The register index.
356    /// - `object_type`: The type of the object (array, record, variable).
357    /// - `name`: The name of the object.
358    /// - `datatype`: The data type of the object.
359    /// - `access_rights`: The access rights for this object (read/write, read-only, write-only).
360    /// - `mandatory`: Whether the object is mandatory or optional.
361    /// - `num_indices`: The number of indices/sub-indices in this value. If 0 or 1, it creates a single value.
362    /// - `initial_value`: The initial value to set for the entry (either single or in each sub-index).
363    ///
364    /// # Example
365    /// ```
366    /// use mechutil::object_dictionary::MechObjectDictionary;
367    /// use mechutil::object_dictionary::AccessRights;
368    /// use mechutil::object_dictionary::ObjectType;
369    /// use mechutil::register_value::MechCommandRegisterValue;
370    /// let mut dictionary = MechObjectDictionary::new(
371    ///     0x000401,  // Device Type (generic I/O device)
372    ///     1000,      // Heartbeat interval of 1000 ms
373    ///     12345,     // Vendor ID
374    ///     67890,     // Product Code
375    /// );
376    /// let initial_value = MechCommandRegisterValue::new();  // Assume this creates a default value.
377    /// dictionary.add_entry(0x1000, ObjectType::Array, "Example Object".to_string(), "uint32".to_string(), AccessRights::ReadWrite, true, 5, initial_value);
378    /// ```
379    pub fn add_entry(
380        &mut self,
381        index: u16,
382        object_type: ObjectType,
383        name: String,
384        datatype: String,
385        access_rights: AccessRights,
386        mandatory: bool,
387        num_indices: usize,
388        initial_value: MechCommandRegisterValue,
389    ) {
390        let value = if num_indices <= 1 {
391            // Create a single entry with the provided initial value
392            MechObjectDictionaryValue::Single(initial_value)
393        } else {
394            // Create an array of sub-indices, each initialized with the provided initial value
395            // The first index of the array is used to store the size/number of values. So, we need to
396            // add an additional index to the num_indices.
397            MechObjectDictionaryValue::SubIndices(vec![initial_value; num_indices + 1])
398        };
399
400        let entry = MechObjectDictionaryEntry {
401            index,
402            object_type,
403            name,
404            datatype,
405            access_rights,
406            mandatory,
407            value,
408        };
409
410        self.registers.insert(index, entry);
411    }
412
413    /// Adds a new entry with a single value to the object dictionary.
414    /// This function is used for entries that do not have sub-indices and only require a single `MechCommandRegisterValue`.
415    ///
416    /// # Parameters
417    /// - `index`: The register index of the object.
418    /// - `object_type`: The type of the object (e.g., array, record, variable).
419    /// - `name`: A string representing the name of the object.
420    /// - `datatype`: The data type of the object.
421    /// - `access_rights`: Access permissions for this entry (read/write, read-only, write-only).
422    /// - `mandatory`: A boolean indicating whether the object is mandatory or optional.
423    /// - `initial_value`: The initial value to set for the entry.
424    ///
425    /// # Example
426    /// ```
427    /// use mechutil::object_dictionary::MechObjectDictionary;
428    /// use mechutil::object_dictionary::ObjectType;
429    /// use mechutil::object_dictionary::AccessRights;
430    /// use mechutil::register_value::MechCommandRegisterValue;
431    /// let mut dictionary = MechObjectDictionary::new(
432    ///     0x000401,  // Device Type (generic I/O device)
433    ///     1000,      // Heartbeat interval of 1000 ms
434    ///     12345,     // Vendor ID
435    ///     67890,     // Product Code
436    /// );
437    /// let initial_value = MechCommandRegisterValue::from_int32(12345);
438    /// dictionary.add_value_entry(
439    ///     0x1000,
440    ///     ObjectType::Variable,
441    ///     "Example Value".to_string(),
442    ///     "int32".to_string(),
443    ///     AccessRights::ReadWrite,
444    ///     true,
445    ///     initial_value,
446    /// );
447    /// ```
448    pub fn add_value_entry(
449        &mut self,
450        index: u16,
451        object_type: ObjectType,
452        name: String,
453        datatype: String,
454        access_rights: AccessRights,
455        mandatory: bool,
456        initial_value: MechCommandRegisterValue,
457    ) {
458        let value = MechObjectDictionaryValue::Single(initial_value);
459
460        let entry = MechObjectDictionaryEntry {
461            index,
462            object_type,
463            name,
464            datatype,
465            access_rights,
466            mandatory,
467            value,
468        };
469
470        self.registers.insert(index, entry);
471    }
472
473    /// Adds a new entry with multiple sub-indices to the object dictionary.
474    /// This function is used for entries that require an array of sub-indices, each initialized to `initial_value`.
475    ///
476    /// # Parameters
477    /// - `index`: The register index of the object.
478    /// - `object_type`: The type of the object (e.g., array, record, variable).
479    /// - `name`: A string representing the name of the object.
480    /// - `datatype`: The data type of the object.
481    /// - `access_rights`: Access permissions for this entry (read/write, read-only, write-only).
482    /// - `mandatory`: A boolean indicating whether the object is mandatory or optional.
483    /// - `num_sub_indices`: The number of sub-indices within the entry (each initialized with `initial_value`).
484    /// - `initial_value`: The initial value to set for each sub-index in the array.
485    ///
486    /// # Example
487    /// ```
488    /// use mechutil::object_dictionary::AccessRights;
489    /// use mechutil::object_dictionary::ObjectType;
490    /// use mechutil::register_value::MechCommandRegisterValue;
491    /// use mechutil::object_dictionary::MechObjectDictionary;
492    /// let mut dictionary = MechObjectDictionary::new(
493    ///     0x000401,  // Device Type (generic I/O device)
494    ///     1000,      // Heartbeat interval of 1000 ms
495    ///     12345,     // Vendor ID
496    ///     67890,     // Product Code
497    /// );
498    /// let initial_value = MechCommandRegisterValue::from_int32(0);
499    /// dictionary.add_array_entry(
500    ///     0x2000,
501    ///     ObjectType::Array,
502    ///     "Example Array".to_string(),
503    ///     "int32".to_string(),
504    ///     AccessRights::ReadWrite,
505    ///     true,
506    ///     5,
507    ///     initial_value,
508    /// );
509    /// ```    
510    pub fn add_array_entry(
511        &mut self,
512        index: u16,
513        object_type: ObjectType,
514        name: String,
515        datatype: String,
516        access_rights: AccessRights,
517        mandatory: bool,
518        num_sub_indices: usize,
519        initial_value: MechCommandRegisterValue,
520    ) {
521        // Create an array of sub-indices, each initialized with the provided initial value
522        // The first index of the array is used to store the size/number of values. So, we need to
523        // add an additional index to the num_indices.
524        let value = MechObjectDictionaryValue::SubIndices(vec![initial_value; num_sub_indices + 1]);
525
526        let entry = MechObjectDictionaryEntry {
527            index,
528            object_type,
529            name,
530            datatype,
531            access_rights,
532            mandatory,
533            value,
534        };
535
536        self.registers.insert(index, entry);
537
538        // Set the 0 index to the number of indices.
539        let _ = self.set_value(
540            index,
541            Some(0),
542            MechCommandRegisterValue::from_uint16(num_sub_indices as u16),
543        );
544    }
545
546    /// Checks if an entry exists in the object dictionary.
547    ///
548    /// # Parameters
549    /// - `index`: The register index.
550    ///
551    /// # Returns
552    /// - `true` if the index exists, otherwise `false`.
553    ///
554    /// # Example
555    /// ```no_run
556    /// use mechutil::{object_dictionary::{MechObjectDictionary, ObjectType,AccessRights}};
557    /// use mechutil::register_value::MechCommandRegisterValue;
558    /// let mut dictionary = MechObjectDictionary::new(
559    ///     0x000401,  // Device Type (generic I/O device)
560    ///     1000,      // Heartbeat interval of 1000 ms
561    ///     12345,     // Vendor ID
562    ///     67890,     // Product Code
563    /// );
564    /// let initial_value = MechCommandRegisterValue::new();  // Assume this creates a default value.
565    /// dictionary.add_entry(0x1000, ObjectType::Array, "Example Object".to_string(), "uint32".to_string(), AccessRights::ReadWrite, true, 5, initial_value);
566    /// assert!(dictionary.entry_exists(0x1000));  // Returns true
567    /// ```
568    pub fn entry_exists(&self, index: u16) -> bool {
569        self.registers.contains_key(&index)
570    }
571
572    /// Reads the value of an object dictionary entry without sub-indices.
573    ///
574    /// # Parameters
575    /// - `index`: The register index.
576    ///
577    /// # Returns
578    /// - `Result<&MechCommandRegisterValue, anyhow::Error>`: Returns the value if found and valid,
579    ///   or an error message if the entry does not exist or if it contains sub-indices.
580    pub fn get_index_value(&self, index: u16) -> Result<&MechCommandRegisterValue, anyhow::Error> {
581        let entry;
582        match self.registers.get(&index) {
583            Some(res) => entry = res,
584            None => return Err(anyhow!("Entry {} not found", index)),
585        }
586
587        // Check that this entry is a single value, not an array
588        match &entry.value {
589            MechObjectDictionaryValue::Single(value) => Ok(value),
590            MechObjectDictionaryValue::SubIndices(values) => {
591                // Return the value of the first index, which should be the length of this index.
592                match values.first() {
593                    Some(ret) => Ok(ret),
594                    None => {
595                        return Err(anyhow!("Index {} is empty.", index));
596                    }
597                }
598            }
599        }
600    }
601
602    /// Reads the value of a specific sub-index for an entry that has sub-indices.
603    ///
604    /// # Parameters
605    /// - `index`: The register index.
606    /// - `sub_index`: The sub-index to retrieve within the entry.
607    ///
608    /// # Returns
609    /// - `Result<&MechCommandRegisterValue, anyhow::Error>`: Returns the value at the specified sub-index if found and valid,
610    ///   or an error message if the entry does not exist, if it does not have sub-indices, or if the sub-index is out of bounds.
611    pub fn get_sub_index_value(
612        &self,
613        index: u16,
614        sub_index: u16,
615    ) -> Result<&MechCommandRegisterValue, anyhow::Error> {
616        let entry;
617        match self.registers.get(&index) {
618            Some(res) => entry = res,
619            None => return Err(anyhow!("Entry {} not found", index)),
620        }
621
622        // Check that this entry has sub-indices
623        match &entry.value {
624            MechObjectDictionaryValue::Single(_) => Err(anyhow!(
625                "Entry does not contain sub-indices; use `get_index_value` instead".to_string()
626            )),
627            MechObjectDictionaryValue::SubIndices(values) => match values.get(sub_index as usize) {
628                Some(ret) => return Ok(ret),
629                None => {
630                    return Err(anyhow!("Sub-index {} out of bounds", sub_index));
631                }
632            },
633        }
634    }
635
636    /// Retrieves all sub-index values of a specified entry in the Object Dictionary.
637    /// For convenience.
638    ///
639    /// This function is useful for obtaining the full list of values when an entry
640    /// contains multiple sub-indices (e.g., in array or record types).
641    ///
642    /// # Parameters
643    /// - `index`: The 16-bit register index of the object in the Object Dictionary.
644    ///
645    /// # Returns
646    /// - `Ok(Vec<MechCommandRegisterValue>)`: A vector containing all sub-index values
647    ///   if the specified entry exists and contains sub-indices.
648    /// - `Err(anyhow::Error)`: An error if the entry does not exist or does not contain sub-indices.
649    ///
650    /// # Errors
651    /// Returns an error if the entry:
652    /// - Does not exist in the Object Dictionary.
653    /// - Exists but does not have sub-indices.
654    ///
655    /// # Example
656    /// ```no_run
657    /// use mechutil::object_dictionary::MechObjectDictionary;
658    /// use mechutil::register_value::MechCommandRegisterValue;
659    ///
660    /// let dictionary = MechObjectDictionary::new(0x000401, 1000, 12345, 67890);
661    ///
662    /// // Assuming an array entry at index 0x2000 has been added to the dictionary.
663    /// let result = dictionary.get_all_sub_index_values(0x2000);
664    /// match result {
665    ///     Ok(values) => {
666    ///         for (i, value) in values.iter().enumerate() {
667    ///             println!("Sub-index {}: {:?}", i, value);
668    ///         }
669    ///     },
670    ///     Err(e) => println!("Error retrieving sub-index values: {}", e),
671    /// }
672    /// ```
673    pub fn get_all_sub_index_values(
674        &self,
675        index: u16,
676    ) -> Result<Vec<MechCommandRegisterValue>, anyhow::Error> {
677        let entry;
678        match self.registers.get(&index) {
679            Some(res) => entry = res,
680            None => return Err(anyhow!("Entry {} not found", index)),
681        }
682
683        // Check that this entry has sub-indices
684        match &entry.value {
685            MechObjectDictionaryValue::Single(_) => {
686                Err(anyhow!("Entry does not contain sub-indices!"))
687            }
688            MechObjectDictionaryValue::SubIndices(values) => {
689                return Ok(values.clone());
690            }
691        }
692    }
693
694    /// Retrieves a specified range of sub-index values for an entry in the Object Dictionary.
695    /// For convenience.
696    ///
697    /// This function provides a subset of sub-indices between `start` and `end` for entries
698    /// with multiple sub-indices (such as arrays). If `end` is negative, it includes all indices
699    /// from `start` to the last index.
700    ///
701    /// # Parameters
702    /// - `index`: The 16-bit register index of the object in the Object Dictionary.
703    /// - `start`: The starting sub-index (inclusive).
704    /// - `end`: The ending sub-index (inclusive). If `end` is negative, it defaults to the last sub-index.
705    ///
706    /// # Returns
707    /// - `Ok(Vec<MechCommandRegisterValue>)`: A vector containing values in the specified range,
708    ///   if the entry exists and contains sub-indices within the range.
709    /// - `Err(anyhow::Error)`: An error if the entry does not exist, does not contain sub-indices, or if
710    ///   the specified range is out of bounds.
711    ///
712    /// # Errors
713    /// Returns an error if:
714    /// - The entry does not exist or does not contain sub-indices.
715    /// - The specified `start` index is out of bounds.
716    /// - The specified `end` index is out of bounds.
717    ///
718    /// # Example
719    /// ```no_run
720    /// use mechutil::object_dictionary::MechObjectDictionary;
721    /// use mechutil::register_value::MechCommandRegisterValue;
722    ///
723    /// let dictionary = MechObjectDictionary::new(0x000401, 1000, 12345, 67890);
724    ///
725    /// // Assuming an array entry at index 0x2000 with multiple sub-indices has been added.
726    /// let result = dictionary.get_sub_index_range_values(0x2000, 1, 3);
727    /// match result {
728    ///     Ok(values) => {
729    ///         for (i, value) in values.iter().enumerate() {
730    ///             println!("Sub-index {}: {:?}", i + 1, value);
731    ///         }
732    ///     },
733    ///     Err(e) => println!("Error retrieving range of sub-index values: {}", e),
734    /// }
735    /// ```
736    pub fn get_sub_index_range_values(
737        &self,
738        index: u16,
739        start: u16,
740        end: i16,
741    ) -> Result<Vec<MechCommandRegisterValue>, anyhow::Error> {
742        let entry;
743        match self.registers.get(&index) {
744            Some(res) => entry = res,
745            None => return Err(anyhow!("Entry {} not found", index)),
746        }
747
748        // Check that this entry has sub-indices
749        match &entry.value {
750            MechObjectDictionaryValue::Single(_) => {
751                Err(anyhow!("Entry does not contain sub-indices!"))
752            }
753            MechObjectDictionaryValue::SubIndices(values) => {
754                if (start as usize) >= values.len() {
755                    return Err(anyhow!(
756                        "start {} out of range of values len {}",
757                        start,
758                        values.len()
759                    ));
760                }
761
762                let end_index;
763                if end < 0 {
764                    end_index = values.len() - 1;
765                } else if (end as usize) < values.len() - 1 {
766                    end_index = end as usize;
767                } else {
768                    return Err(anyhow!(
769                        "end {} out of range of values len {}",
770                        end,
771                        values.len()
772                    ));
773                }
774
775                return Ok(values[(start as usize)..=end_index].to_vec());
776            }
777        }
778    }
779
780    /// Sets the value of an object dictionary entry at a specific index. For use internally. Does not
781    /// take access rights into account.
782    ///
783    /// # Parameters
784    /// - `index`: The register index.
785    /// - `sub_index`: Optional sub-index for entries with multiple values (arrays).
786    /// - `new_value`: The new value to be set.
787    ///
788    /// # Returns
789    /// - `Result<(), anyhow::Error>`: Returns Ok if the value is set successfully, or an error message if setting is denied or if the entry does not exist.
790    pub fn set_value(
791        &mut self,
792        index: u16,
793        sub_index: Option<u16>,
794        new_value: MechCommandRegisterValue,
795    ) -> Result<(), anyhow::Error> {
796        // Get the entry for the specified index.
797
798        let entry;
799        match self.registers.get_mut(&index) {
800            Some(res) => {
801                entry = res;
802            }
803            None => {
804                log::debug!("Entry {} not found. Available entries:", index);
805                for key in self.registers.keys() {
806                    log::debug!("\t{}", key);
807                }
808
809                return Err(anyhow!("Entry {} not found.", index));
810            }
811        }
812
813        // Set the value based on whether the entry has sub-indices or is a single value.
814        match &mut entry.value {
815            MechObjectDictionaryValue::Single(value) => {
816                if sub_index.is_some() {
817                    return Err(anyhow!(
818                        "Sub-index provided, but entry {} is not an array",
819                        index
820                    ));
821                }
822                *value = new_value; // Directly set the single value
823            }
824            MechObjectDictionaryValue::SubIndices(values) => {
825                if let Some(sub_idx) = sub_index {
826                    // Ensure sub-index is within bounds
827                    if (sub_idx as usize) < values.len() {
828                        values[sub_idx as usize] = new_value;
829                    } else {
830                        return Err(anyhow!("Sub-index {} out of bounds", sub_idx));
831                    }
832                } else {
833                    return Err(anyhow!("Sub-index is required for array values"));
834                }
835            }
836        }
837
838        Ok(())
839    }
840
841    /// Sets the value of an object dictionary entry at a specific index. For use internally. Does not
842    /// take access rights into account. Only works for single-value entries.
843    ///
844    /// # Parameters
845    /// - `index`: The register index.
846    /// - `new_value`: The new value to be set.
847    ///
848    /// # Returns
849    /// - `Result<(), anyhow::Error>`: Returns Ok if the value is set successfully, or an error message if setting is denied or if the entry does not exist.
850    pub fn set_index_value(
851        &mut self,
852        index: u16,
853        new_value: MechCommandRegisterValue,
854    ) -> Result<(), anyhow::Error> {
855        // Get the entry for the specified index.
856        let entry;
857        match self.registers.get_mut(&index) {
858            Some(res) => entry = res,
859            None => return Err(anyhow!("Entry {} not found", index)),
860        }
861
862        // Set the value based on whether the entry has sub-indices or is a single value.
863        match &mut entry.value {
864            MechObjectDictionaryValue::Single(value) => {
865                *value = new_value; // Directly set the single value
866            }
867            MechObjectDictionaryValue::SubIndices(_) => {
868                return Err(anyhow!("Sub-index is required for array values"));
869            }
870        }
871
872        Ok(())
873    }
874
875    /// Sets the value of the sub-index an object dictionary entry at a specific index. For use internally. Does not
876    /// take access rights into account. Only works for Array or Record entries, not Single value entries.
877    ///
878    /// # Parameters
879    /// - `index`: The register index.
880    /// - `sub_index`: sub-index for entries with multiple values (arrays & records).
881    /// - `new_value`: The new value to be set.
882    ///
883    /// # Returns
884    /// - `Result<(), anyhow::Error>`: Returns Ok if the value is set successfully, or an error message if setting is denied or if the entry does not exist.
885    pub fn set_sub_index_value(
886        &mut self,
887        index: u16,
888        sub_index: u16,
889        new_value: MechCommandRegisterValue,
890    ) -> Result<(), anyhow::Error> {
891        // Get the entry for the specified index.
892        let entry;
893        match self.registers.get_mut(&index) {
894            Some(res) => entry = res,
895            None => return Err(anyhow!("Entry {} not found", index)),
896        }
897
898        // Set the value based on whether the entry has sub-indices or is a single value.
899        match &mut entry.value {
900            MechObjectDictionaryValue::Single(value) => {
901                return Err(anyhow!(
902                    "Sub-index provided, but entry {} is not an array",
903                    index
904                ));
905            }
906            MechObjectDictionaryValue::SubIndices(values) => {
907                if (sub_index as usize) < values.len() {
908                    values[sub_index as usize] = new_value;
909                } else {
910                    return Err(anyhow!("Sub-index {} out of bounds", sub_index));
911                }
912            }
913        }
914
915        Ok(())
916    }
917
918    /// Client/External-facing function to set the value of an object dictionary entry at a specific index, if allowed by access rights.
919    /// When taking requests from an external source, use this function to set the value of an Object Dictionary Entry.
920    ///
921    /// # Parameters
922    /// - `index`: The register index.
923    /// - `sub_index`: Optional sub-index for entries with multiple values (arrays).
924    /// - `new_value`: The new value to be set.
925    ///
926    /// # Returns
927    /// - `Result<(), anyhow::Error>`: Returns Ok if the value is set successfully, or an error message if setting is denied or if the entry does not exist.
928    pub fn set_value_external(
929        &mut self,
930        index: u16,
931        sub_index: Option<u16>,
932        new_value: MechCommandRegisterValue,
933    ) -> Result<(), anyhow::Error> {
934        // Get the entry for the specified index.
935        let entry;
936        match self.registers.get_mut(&index) {
937            Some(res) => entry = res,
938            None => return Err(anyhow!("Entry {} not found", index)),
939        }
940
941        // Check if the entry is writable based on its access rights.
942        match entry.access_rights {
943            AccessRights::ReadOnly | AccessRights::Const => {
944                return Err(anyhow!("Cannot set value: Entry is read-only"));
945            }
946            AccessRights::WriteOnly | AccessRights::ReadWrite => {}
947        }
948
949        return self.set_value(index, sub_index, new_value);
950    }
951
952    /// Loads the object dictionary from a JSON file.
953    ///
954    /// # Parameters
955    /// - `filename`: The path to the JSON file.
956    ///
957    /// # Returns
958    /// - `Result<(), Box<dyn std::error::Error>>`: Ok if successful, or an error otherwise.
959    ///
960    /// # Example
961    /// ```no_run
962    /// use mechutil::{object_dictionary::MechObjectDictionary};
963    /// let mut dictionary = MechObjectDictionary::new(
964    ///     0x000401,  // Device Type (generic I/O device)
965    ///     1000,      // Heartbeat interval of 1000 ms
966    ///     12345,     // Vendor ID
967    ///     67890,     // Product Code
968    /// );
969    /// dictionary.load_from_json("input.json").unwrap();
970    /// ```
971    pub fn load_from_json(&mut self, filename: &str) -> Result<(), Box<dyn std::error::Error>> {
972        let data = fs::read_to_string(filename)?;
973        let json_data: HashMap<u16, MechObjectDictionaryEntry> = serde_json::from_str(&data)?;
974
975        for (index, entry) in json_data {
976            self.registers.insert(index, entry);
977        }
978
979        Ok(())
980    }
981
982    /// Serializes the object dictionary to a JSON file.
983    ///
984    /// # Parameters
985    /// - `filename`: The path to the JSON file.
986    ///
987    /// # Returns
988    /// - `Result<(), Box<dyn std::error::Error>>`: Ok if successful, or an error otherwise.
989    ///
990    /// # Example
991    /// ```no_run
992    /// use mechutil::{object_dictionary::MechObjectDictionary};
993    /// let mut dictionary = MechObjectDictionary::new(
994    ///     0x000401,  // Device Type (generic I/O device)
995    ///     1000,      // Heartbeat interval of 1000 ms
996    ///     12345,     // Vendor ID
997    ///     67890,     // Product Code
998    /// );
999    /// dictionary.save_to_json("output.json").unwrap();
1000    /// ```
1001    pub fn save_to_json(&self, filename: &str) -> Result<(), Box<dyn std::error::Error>> {
1002        let json_data = serde_json::to_string_pretty(&self.registers)?;
1003        fs::write(filename, json_data)?;
1004        Ok(())
1005    }
1006}
1007
1008#[cfg(test)]
1009mod tests {
1010    use super::*;
1011
1012    #[test]
1013    fn test_create_and_check_entry() {
1014        let mut dictionary = MechObjectDictionary::new(0x2001, 1000, 0, 0);
1015
1016        // Create a new entry with 5 sub-indices
1017        dictionary.add_entry(
1018            0x1000,
1019            ObjectType::Array,
1020            "Example Object".to_string(),
1021            "uint32".to_string(),
1022            AccessRights::ReadWrite,
1023            true,
1024            5,
1025            MechCommandRegisterValue::from_uint32(5),
1026        );
1027
1028        // Check if the entry exists
1029        assert!(dictionary.entry_exists(0x1000));
1030
1031        // Check if an entry that hasn't been added doesn't exist
1032        assert!(!dictionary.entry_exists(0x2000));
1033    }
1034
1035    #[test]
1036    fn test_single_value_entry() {
1037        let mut dictionary = MechObjectDictionary::new(0x2001, 1000, 0, 0);
1038
1039        // Create a new entry with a single value (no sub-indices)
1040        dictionary.add_entry(
1041            0x2000,
1042            ObjectType::Variable,
1043            "Single Value Object".to_string(),
1044            "int16".to_string(),
1045            AccessRights::ReadOnly,
1046            false,
1047            1,
1048            MechCommandRegisterValue::from_int16(1),
1049        );
1050
1051        // Check if the entry exists
1052        assert!(dictionary.entry_exists(0x2000));
1053
1054        assert!(
1055            dictionary
1056                .set_index_value(0x2000, MechCommandRegisterValue::from_int16(1234))
1057                .is_ok()
1058        );
1059
1060        let val_check = dictionary.get_index_value(0x2000).unwrap();
1061        assert_eq!(val_check.to_int16().unwrap(), 1234);
1062    }
1063
1064    #[test]
1065    fn test_serialize_to_json() {
1066        let mut dictionary = MechObjectDictionary::new(0x2001, 1000, 0, 0);
1067
1068        // Add an entry
1069        dictionary.add_entry(
1070            0x1000,
1071            ObjectType::Array,
1072            "Test Object".to_string(),
1073            "uint32".to_string(),
1074            AccessRights::ReadWrite,
1075            true,
1076            3,
1077            MechCommandRegisterValue::from_int16(3),
1078        );
1079
1080        // Serialize to JSON
1081        let json_data = serde_json::to_string_pretty(&dictionary).unwrap();
1082
1083        // Check if JSON contains the index and the correct values
1084        assert!(json_data.contains("\"index\": 4096")); // 0x1000 in decimal is 4096
1085        assert!(json_data.contains("\"name\": \"Test Object\""));
1086        assert!(json_data.contains("\"access_rights\": \"ReadWrite\""));
1087    }
1088
1089    #[test]
1090    fn test_deserialize_from_json() {
1091        let json_data = r#"
1092        {
1093            "4096": {
1094                "index": 4096,
1095                "object_type": "Array",
1096                "name":  "Deserialized Object",
1097                "datatype": "uint32",
1098                "access_rights": "ReadWrite",
1099                "mandatory": true,
1100                "value": {
1101                    "SubIndices": [
1102                        {
1103                            "type_id": 7,
1104                            "value_len": 4,
1105                            "num_columns": 1,
1106                            "num_rows": 1,
1107                            "data": [1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
1108                        }
1109                    ]
1110                }
1111            }
1112        }
1113        "#;
1114
1115        let mut dictionary = MechObjectDictionary::new(0x2001, 1000, 0, 0);
1116
1117        // Deserialize from JSON
1118        let json_data: HashMap<u16, MechObjectDictionaryEntry> =
1119            serde_json::from_str(&json_data).unwrap();
1120
1121        // Check if the entry was deserialized correctly
1122        dictionary.registers = json_data;
1123        assert!(dictionary.entry_exists(0x1000));
1124        let entry = dictionary.registers.get(&0x1000).unwrap();
1125        assert_eq!(entry.name, "Deserialized Object");
1126        assert_eq!(entry.datatype, "uint32");
1127        assert!(matches!(
1128            entry.value,
1129            MechObjectDictionaryValue::SubIndices(_)
1130        ));
1131    }
1132}