//
// Copyright (C) 2024 Automated Design Corp.. All Rights Reserved.
// Created Date: 2024-10-23 06:12:47
// -----
// Last Modified: 2024-11-04 07:41:41
// -----
//
//
//! This module provides an implementation based upon the CANopen object dictionary
//! or CAN over EtherCAT (CoE) systems. The object
//! dictionary is the core mechanism for configuration and communication with
//! CANopen devices. Each entry in the dictionary is represented by an index, which
//! may contain sub-indices for more complex data structures such as arrays and
//! records.
//!
//! The `MechObjectDictionary` struct serves as a collection of these entries, each
//! described by its index, object type, access rights, data type, and value. The
//! module supports serialization and deserialization to/from JSON to facilitate
//! easy configuration management and device integration.
//!
//! Key Components:
//! - `MechObjectDictionary`: The main container for the object dictionary entries.
//! - `MechObjectDictionaryEntry`: Represents a single entry in the object dictionary.
//! - `RegisterValue`: Enum representing either a single value or a set of sub-indices.
//! - `AccessRights`: Enum describing the access permissions for an entry (ReadWrite, ReadOnly, WriteOnly).
//! - `ObjectType`: Enum for the type of object (Array, Record, Variable).
//!
//! Features:
//! - Create and manage object dictionary entries.
//! - Support for both single values and arrays of sub-indices.
//! - Serialization and deserialization to/from JSON for configuration persistence.
//!
//! Example Usage:
//! ```no_run
//! use mechutil::object_dictionary::{MechObjectDictionary, ObjectType, AccessRights, MechObjectDictionaryValue};
//! use mechutil::register_value::MechCommandRegisterValue;
//!
//! let mut dictionary = MechObjectDictionary::new(
//! 0x000401, // Device Type (generic I/O device)
//! 1000, // Heartbeat interval of 1000 ms
//! 12345, // Vendor ID
//! 67890, // Product Code
//! );
//! let initial_value = MechCommandRegisterValue::new(); // Assume this creates a default value.
//! dictionary.add_entry(0x1000, ObjectType::Array, "Example Object".to_string(), "uint32".to_string(), AccessRights::ReadWrite, true, 5, initial_value);
//! ```
//!
//! In general, for custom functionality, the objects should be in the range 0x2000–0x5FFF, **Manufacturer-Specific Objects**.
//! See the common_canopen_objects.md doc for CANOpen standard dictionary objects and their addresses.
//!
//!
//
// TODO:
// - [] PDO-like features.
// - - [] Register PDOs
// - - [] Map registers to a PDO
// - [] API for making required objects:
// - - 0x1000 Device Type (Read Only)
// - - 0x1001 Error Register (Read Only)
// - - 0x1017 Producer Heartbeat Time (Const)
// - - 0x1018 Identity Object (Read Only)
//
// - [] Look into this:
// - - "In addition you need Communication Parameters and Mapping parameters for every PDO, but these are allowed to be constants.""
use crate::register_value::MechCommandRegisterValue;
use anyhow::anyhow;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
/// Enum for access rights (read/write, read-only, write-only) for the object dictionary entry.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum AccessRights {
ReadWrite,
Const,
ReadOnly,
WriteOnly,
}
/// Enum for the object type in the object dictionary entry (e.g., array, record, variable).
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ObjectType {
/// A collection of homogeneous sub-indices, meaning all sub-indices within an Array must have the same data type.
Array,
/// A collection of heterogeneous sub-indices, meaning each sub-index can represent a different type of data
Record,
/// A single value. This object does not contain sub-indices.
Variable,
}
/// Represents an entry in the CANopen Object Dictionary.
///
/// This struct models the data, metadata, and access properties for a specific
/// entry within a CANopen-compliant Object Dictionary, where each entry is
/// identified by a unique 16-bit `index`.
///
/// ### Fields
/// - `index`: The 16-bit address of the object in the Object Dictionary.
/// - `object_type`: Specifies the type of object. This can be an `Array`, `Record`,
/// or `Variable`, which determines if the object contains a list of values,
/// a structured set of different data types, or a single value.
/// - `name`: A descriptive name for the object, helping to identify its purpose
/// or function within the device configuration.
/// - `datatype`: The data type of the object as a `String`. This string represents
/// the underlying data format (e.g., `"uint32"`, `"int16"`, `"float"`). The `datatype`
/// string is informational, ensuring compatibility across systems without imposing
/// a strict type. To enforce or interpret data types programmatically, use the
/// appropriate `from_*` and `to_*` methods in `MechCommandRegisterValue`.
/// - `access_rights`: Defines the access permissions for the entry, including `ReadOnly`,
/// `WriteOnly`, `ReadWrite`, or `Const` (constant, unmodifiable data).
/// - `mandatory`: Indicates whether the object is required for the device to be CANopen-compliant
/// (`true` if mandatory) or is optional (`false`).
/// - `value`: Stores the entry’s value(s). This field can either hold a single value or
/// a collection of sub-indices, depending on the object type.
///
/// ### Example Usage
/// ```no_run
/// use mechutil::object_dictionary::{MechObjectDictionaryEntry, ObjectType, AccessRights, MechObjectDictionaryValue};
/// use mechutil::register_value::MechCommandRegisterValue;
///
/// let entry = MechObjectDictionaryEntry {
/// index: 0x1018,
/// object_type: ObjectType::Record,
/// name: "Identity Object".to_string(),
/// datatype: "record".to_string(),
/// access_rights: AccessRights::ReadOnly,
/// mandatory: true,
/// value: MechObjectDictionaryValue::SubIndices(vec![
/// MechCommandRegisterValue::from_uint32(12345), // Vendor ID
/// MechCommandRegisterValue::from_uint32(67890), // Product Code
/// MechCommandRegisterValue::from_uint32(1), // Revision Number
/// MechCommandRegisterValue::from_uint32(12345678), // Serial Number
/// ]),
/// };
/// ```
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MechObjectDictionaryEntry {
pub index: u16, // 16-bit address of the object in the dictionary
pub object_type: ObjectType, // Type of object (array, record, variable)
pub name: String, // Descriptive name of the object
pub datatype: String, // Data type of the variable
pub access_rights: AccessRights, // Access rights (read/write, read-only, write-only)
pub mandatory: bool, // Indicates if the object is mandatory (true) or optional (false)
pub value: MechObjectDictionaryValue, // The value or sub-indices for the object
}
/// Defines the value storage for an entry in the CANopen Object Dictionary.
///
/// `MechObjectDictionaryValue` can store a single `MechCommandRegisterValue`
/// or a vector of `MechCommandRegisterValue`s, representing an array or record
/// structure with multiple sub-indices. This enum allows flexibility in how
/// values are stored within the Object Dictionary:
/// - `Single`: Used for entries that have a single value (e.g., device-specific parameters).
/// - `SubIndices`: Used for entries that contain multiple values, typically
/// when `object_type` is `Record` or `Array`.
///
/// ### Example Usage
/// ```no_run
/// use mechutil::object_dictionary::{MechObjectDictionaryValue};
/// use mechutil::register_value::MechCommandRegisterValue;
///
/// // Single value example
/// let single_value = MechObjectDictionaryValue::Single(MechCommandRegisterValue::from_int32(42));
///
/// // Array of values example
/// let array_value = MechObjectDictionaryValue::SubIndices(vec![
/// MechCommandRegisterValue::from_uint32(0),
/// MechCommandRegisterValue::from_uint32(1),
/// MechCommandRegisterValue::from_uint32(2),
/// ]);
/// ```
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum MechObjectDictionaryValue {
Single(MechCommandRegisterValue),
SubIndices(Vec<MechCommandRegisterValue>),
}
/// `MechObjectDictionary` is a structured collection of entries for a CANopen device's Object Dictionary.
///
/// This dictionary serves as the central data structure for defining and organizing device parameters,
/// configuration settings, and runtime information according to CANopen standards. Each entry in the
/// dictionary is mapped by its index and may represent a single value, a structured record, or an array of
/// homogeneous elements.
///
/// ### Key Features
/// - **Standard Entries**: The `new` method initializes the dictionary with commonly used CANopen objects
/// (e.g., Device Type, Error Register, Producer Heartbeat Time, Identity Object).
/// - **Customizable**: New entries can be added dynamically to the dictionary with specific properties
/// like access rights and data types.
/// - **Supports Single and Array Entries**: Entries can either hold a single value or a collection of sub-indices
/// (e.g., for records or arrays).
///
/// ### Example Usage
/// ```no_run
/// use mechutil::object_dictionary::{MechObjectDictionary, ObjectType, AccessRights};
/// use mechutil::register_value::MechCommandRegisterValue;
///
/// // Initialize a new object dictionary with standard entries and default values
/// let mut dictionary = MechObjectDictionary::new(
/// 0x000401, // Device Type (generic I/O device)
/// 1000, // Heartbeat interval of 1000 ms
/// 12345, // Vendor ID
/// 67890, // Product Code
/// );
///
/// // Adding a custom entry for a device-specific parameter (single value)
/// let custom_value = MechCommandRegisterValue::from_int32(42);
/// dictionary.add_value_entry(
/// 0x2000,
/// ObjectType::Variable,
/// "Custom Parameter".to_string(),
/// "int32".to_string(),
/// AccessRights::ReadWrite,
/// false,
/// custom_value,
/// );
///
/// // Adding an array entry for logging error history (multiple sub-indices)
/// let error_history_value = MechCommandRegisterValue::from_uint32(0);
/// dictionary.add_array_entry(
/// 0x1003,
/// ObjectType::Array,
/// "Error History".to_string(),
/// "uint32".to_string(),
/// AccessRights::ReadWrite,
/// false,
/// 5, // Number of sub-indices for error codes
/// error_history_value,
/// );
///
/// // Reading the value of a single-value entry (e.g., Device Type)
/// if let Ok(value) = dictionary.get_index_value(0x1000) {
/// println!("Device Type: {:?}", value);
/// }
///
/// // Reading a specific sub-index value from an array entry (e.g., Error History at index 1)
/// if let Ok(error_code) = dictionary.get_sub_index_value(0x1003, 1) {
/// println!("Error Code at sub-index 1: {:?}", error_code);
/// }
/// ```
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MechObjectDictionary {
registers: HashMap<u16, MechObjectDictionaryEntry>, // Register index to entry mapping
}
impl MechObjectDictionary {
/// Creates a new `MechObjectDictionary` with standard default entries.
///
/// This function initializes the dictionary with commonly used CANopen objects, including:
/// - `0x1000` Device Type (Read Only): Specifies the type of the device.
/// - `0x1001` Error Register (Read Only): Holds error flags for device status monitoring.
/// - `0x1017` Producer Heartbeat Time (Const): Sets the interval for heartbeat messages.
/// - `0x1018` Identity Object (Read Only): Contains identification information about the device.
///
/// # Parameters
/// - `device_type`: The initial value for the `0x1000` Device Type entry.
/// - `heartbeat_time_interval_ms`: The interval in milliseconds for the `0x1017` Producer Heartbeat Time entry.
/// - `vendor_id`: The Vendor ID for the `0x1018:01` sub-index of the Identity Object.
/// - `product_code`: The Product Code for the `0x1018:02` sub-index of the Identity Object.
///
/// # Example
/// ```no_run
/// use mechutil::object_dictionary::MechObjectDictionary;
/// let dictionary = MechObjectDictionary::new(
/// 0x2001, // Device Type for a software device
/// 1000, // Heartbeat interval of 1000 ms (1 second)
/// 12345, // Vendor ID
/// 67890, // Product Code
/// );
/// ```
pub fn new(
device_type: u32,
heartbeat_time_interval_ms: u16,
vendor_id: u32,
product_code: u32,
) -> Self {
let mut dictionary = MechObjectDictionary {
registers: HashMap::new(),
};
// Default initial value with zero values for each register
let default_value = MechCommandRegisterValue::new();
// 0x1000 Device Type
dictionary.add_value_entry(
0x1000,
ObjectType::Variable,
"Device Type".to_string(),
"uint32".to_string(),
AccessRights::ReadOnly,
true,
MechCommandRegisterValue::from_uint32(device_type),
);
// 0x1001 Error Register
dictionary.add_value_entry(
0x1001,
ObjectType::Variable,
"Error Register".to_string(),
"uint8".to_string(),
AccessRights::ReadOnly,
true,
default_value.clone(),
);
// 0x1017 Producer Heartbeat Time
dictionary.add_value_entry(
0x1017,
ObjectType::Variable,
"Producer Heartbeat Time".to_string(),
"uint16".to_string(),
AccessRights::Const,
true,
MechCommandRegisterValue::from_uint16(heartbeat_time_interval_ms),
);
// 0x1018 Identity Object (Typically a Record with multiple sub-indices)
dictionary.add_array_entry(
0x1018,
ObjectType::Record,
"Identity Object".to_string(),
"record".to_string(),
AccessRights::ReadOnly,
true,
4, // Example: Identity Object might have 4 sub-indices for Vendor ID, Product Code, Revision Number, Serial Number
default_value,
);
let _ = dictionary.set_value(
0x1018,
Some(1),
MechCommandRegisterValue::from_uint32(vendor_id),
);
let _ = dictionary.set_value(
0x1018,
Some(2),
MechCommandRegisterValue::from_uint32(product_code),
);
// revision number
let _ = dictionary.set_value(0x1018, Some(3), MechCommandRegisterValue::from_uint32(0));
// serial number
let _ = dictionary.set_value(0x1018, Some(4), MechCommandRegisterValue::from_uint32(0));
return dictionary;
}
/// Adds a new entry to the object dictionary with an initial value.
/// If `num_sub_indices` is greater than 1, it creates sub-indices with the provided initial value;
/// otherwise, it creates a single value entry.
///
/// # Parameters
/// - `index`: The register index.
/// - `object_type`: The type of the object (array, record, variable).
/// - `name`: The name of the object.
/// - `datatype`: The data type of the object.
/// - `access_rights`: The access rights for this object (read/write, read-only, write-only).
/// - `mandatory`: Whether the object is mandatory or optional.
/// - `num_indices`: The number of indices/sub-indices in this value. If 0 or 1, it creates a single value.
/// - `initial_value`: The initial value to set for the entry (either single or in each sub-index).
///
/// # Example
/// ```
/// use mechutil::object_dictionary::MechObjectDictionary;
/// use mechutil::object_dictionary::AccessRights;
/// use mechutil::object_dictionary::ObjectType;
/// use mechutil::register_value::MechCommandRegisterValue;
/// let mut dictionary = MechObjectDictionary::new(
/// 0x000401, // Device Type (generic I/O device)
/// 1000, // Heartbeat interval of 1000 ms
/// 12345, // Vendor ID
/// 67890, // Product Code
/// );
/// let initial_value = MechCommandRegisterValue::new(); // Assume this creates a default value.
/// dictionary.add_entry(0x1000, ObjectType::Array, "Example Object".to_string(), "uint32".to_string(), AccessRights::ReadWrite, true, 5, initial_value);
/// ```
pub fn add_entry(
&mut self,
index: u16,
object_type: ObjectType,
name: String,
datatype: String,
access_rights: AccessRights,
mandatory: bool,
num_indices: usize,
initial_value: MechCommandRegisterValue,
) {
let value = if num_indices <= 1 {
// Create a single entry with the provided initial value
MechObjectDictionaryValue::Single(initial_value)
} else {
// Create an array of sub-indices, each initialized with the provided initial value
// The first index of the array is used to store the size/number of values. So, we need to
// add an additional index to the num_indices.
MechObjectDictionaryValue::SubIndices(vec![initial_value; num_indices + 1])
};
let entry = MechObjectDictionaryEntry {
index,
object_type,
name,
datatype,
access_rights,
mandatory,
value,
};
self.registers.insert(index, entry);
}
/// Adds a new entry with a single value to the object dictionary.
/// This function is used for entries that do not have sub-indices and only require a single `MechCommandRegisterValue`.
///
/// # Parameters
/// - `index`: The register index of the object.
/// - `object_type`: The type of the object (e.g., array, record, variable).
/// - `name`: A string representing the name of the object.
/// - `datatype`: The data type of the object.
/// - `access_rights`: Access permissions for this entry (read/write, read-only, write-only).
/// - `mandatory`: A boolean indicating whether the object is mandatory or optional.
/// - `initial_value`: The initial value to set for the entry.
///
/// # Example
/// ```
/// use mechutil::object_dictionary::MechObjectDictionary;
/// use mechutil::object_dictionary::ObjectType;
/// use mechutil::object_dictionary::AccessRights;
/// use mechutil::register_value::MechCommandRegisterValue;
/// let mut dictionary = MechObjectDictionary::new(
/// 0x000401, // Device Type (generic I/O device)
/// 1000, // Heartbeat interval of 1000 ms
/// 12345, // Vendor ID
/// 67890, // Product Code
/// );
/// let initial_value = MechCommandRegisterValue::from_int32(12345);
/// dictionary.add_value_entry(
/// 0x1000,
/// ObjectType::Variable,
/// "Example Value".to_string(),
/// "int32".to_string(),
/// AccessRights::ReadWrite,
/// true,
/// initial_value,
/// );
/// ```
pub fn add_value_entry(
&mut self,
index: u16,
object_type: ObjectType,
name: String,
datatype: String,
access_rights: AccessRights,
mandatory: bool,
initial_value: MechCommandRegisterValue,
) {
let value = MechObjectDictionaryValue::Single(initial_value);
let entry = MechObjectDictionaryEntry {
index,
object_type,
name,
datatype,
access_rights,
mandatory,
value,
};
self.registers.insert(index, entry);
}
/// Adds a new entry with multiple sub-indices to the object dictionary.
/// This function is used for entries that require an array of sub-indices, each initialized to `initial_value`.
///
/// # Parameters
/// - `index`: The register index of the object.
/// - `object_type`: The type of the object (e.g., array, record, variable).
/// - `name`: A string representing the name of the object.
/// - `datatype`: The data type of the object.
/// - `access_rights`: Access permissions for this entry (read/write, read-only, write-only).
/// - `mandatory`: A boolean indicating whether the object is mandatory or optional.
/// - `num_sub_indices`: The number of sub-indices within the entry (each initialized with `initial_value`).
/// - `initial_value`: The initial value to set for each sub-index in the array.
///
/// # Example
/// ```
/// use mechutil::object_dictionary::AccessRights;
/// use mechutil::object_dictionary::ObjectType;
/// use mechutil::register_value::MechCommandRegisterValue;
/// use mechutil::object_dictionary::MechObjectDictionary;
/// let mut dictionary = MechObjectDictionary::new(
/// 0x000401, // Device Type (generic I/O device)
/// 1000, // Heartbeat interval of 1000 ms
/// 12345, // Vendor ID
/// 67890, // Product Code
/// );
/// let initial_value = MechCommandRegisterValue::from_int32(0);
/// dictionary.add_array_entry(
/// 0x2000,
/// ObjectType::Array,
/// "Example Array".to_string(),
/// "int32".to_string(),
/// AccessRights::ReadWrite,
/// true,
/// 5,
/// initial_value,
/// );
/// ```
pub fn add_array_entry(
&mut self,
index: u16,
object_type: ObjectType,
name: String,
datatype: String,
access_rights: AccessRights,
mandatory: bool,
num_sub_indices: usize,
initial_value: MechCommandRegisterValue,
) {
// Create an array of sub-indices, each initialized with the provided initial value
// The first index of the array is used to store the size/number of values. So, we need to
// add an additional index to the num_indices.
let value = MechObjectDictionaryValue::SubIndices(vec![initial_value; num_sub_indices + 1]);
let entry = MechObjectDictionaryEntry {
index,
object_type,
name,
datatype,
access_rights,
mandatory,
value,
};
self.registers.insert(index, entry);
// Set the 0 index to the number of indices.
let _ = self.set_value(
index,
Some(0),
MechCommandRegisterValue::from_uint16(num_sub_indices as u16),
);
}
/// Checks if an entry exists in the object dictionary.
///
/// # Parameters
/// - `index`: The register index.
///
/// # Returns
/// - `true` if the index exists, otherwise `false`.
///
/// # Example
/// ```no_run
/// use mechutil::{object_dictionary::{MechObjectDictionary, ObjectType,AccessRights}};
/// use mechutil::register_value::MechCommandRegisterValue;
/// let mut dictionary = MechObjectDictionary::new(
/// 0x000401, // Device Type (generic I/O device)
/// 1000, // Heartbeat interval of 1000 ms
/// 12345, // Vendor ID
/// 67890, // Product Code
/// );
/// let initial_value = MechCommandRegisterValue::new(); // Assume this creates a default value.
/// dictionary.add_entry(0x1000, ObjectType::Array, "Example Object".to_string(), "uint32".to_string(), AccessRights::ReadWrite, true, 5, initial_value);
/// assert!(dictionary.entry_exists(0x1000)); // Returns true
/// ```
pub fn entry_exists(&self, index: u16) -> bool {
self.registers.contains_key(&index)
}
/// Reads the value of an object dictionary entry without sub-indices.
///
/// # Parameters
/// - `index`: The register index.
///
/// # Returns
/// - `Result<&MechCommandRegisterValue, anyhow::Error>`: Returns the value if found and valid,
/// or an error message if the entry does not exist or if it contains sub-indices.
pub fn get_index_value(&self, index: u16) -> Result<&MechCommandRegisterValue, anyhow::Error> {
let entry;
match self.registers.get(&index) {
Some(res) => entry = res,
None => return Err(anyhow!("Entry {} not found", index)),
}
// Check that this entry is a single value, not an array
match &entry.value {
MechObjectDictionaryValue::Single(value) => Ok(value),
MechObjectDictionaryValue::SubIndices(values) => {
// Return the value of the first index, which should be the length of this index.
match values.first() {
Some(ret) => Ok(ret),
None => {
return Err(anyhow!("Index {} is empty.", index));
}
}
}
}
}
/// Reads the value of a specific sub-index for an entry that has sub-indices.
///
/// # Parameters
/// - `index`: The register index.
/// - `sub_index`: The sub-index to retrieve within the entry.
///
/// # Returns
/// - `Result<&MechCommandRegisterValue, anyhow::Error>`: Returns the value at the specified sub-index if found and valid,
/// 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.
pub fn get_sub_index_value(
&self,
index: u16,
sub_index: u16,
) -> Result<&MechCommandRegisterValue, anyhow::Error> {
let entry;
match self.registers.get(&index) {
Some(res) => entry = res,
None => return Err(anyhow!("Entry {} not found", index)),
}
// Check that this entry has sub-indices
match &entry.value {
MechObjectDictionaryValue::Single(_) => Err(anyhow!(
"Entry does not contain sub-indices; use `get_index_value` instead".to_string()
)),
MechObjectDictionaryValue::SubIndices(values) => match values.get(sub_index as usize) {
Some(ret) => return Ok(ret),
None => {
return Err(anyhow!("Sub-index {} out of bounds", sub_index));
}
},
}
}
/// Retrieves all sub-index values of a specified entry in the Object Dictionary.
/// For convenience.
///
/// This function is useful for obtaining the full list of values when an entry
/// contains multiple sub-indices (e.g., in array or record types).
///
/// # Parameters
/// - `index`: The 16-bit register index of the object in the Object Dictionary.
///
/// # Returns
/// - `Ok(Vec<MechCommandRegisterValue>)`: A vector containing all sub-index values
/// if the specified entry exists and contains sub-indices.
/// - `Err(anyhow::Error)`: An error if the entry does not exist or does not contain sub-indices.
///
/// # Errors
/// Returns an error if the entry:
/// - Does not exist in the Object Dictionary.
/// - Exists but does not have sub-indices.
///
/// # Example
/// ```no_run
/// use mechutil::object_dictionary::MechObjectDictionary;
/// use mechutil::register_value::MechCommandRegisterValue;
///
/// let dictionary = MechObjectDictionary::new(0x000401, 1000, 12345, 67890);
///
/// // Assuming an array entry at index 0x2000 has been added to the dictionary.
/// let result = dictionary.get_all_sub_index_values(0x2000);
/// match result {
/// Ok(values) => {
/// for (i, value) in values.iter().enumerate() {
/// println!("Sub-index {}: {:?}", i, value);
/// }
/// },
/// Err(e) => println!("Error retrieving sub-index values: {}", e),
/// }
/// ```
pub fn get_all_sub_index_values(
&self,
index: u16,
) -> Result<Vec<MechCommandRegisterValue>, anyhow::Error> {
let entry;
match self.registers.get(&index) {
Some(res) => entry = res,
None => return Err(anyhow!("Entry {} not found", index)),
}
// Check that this entry has sub-indices
match &entry.value {
MechObjectDictionaryValue::Single(_) => {
Err(anyhow!("Entry does not contain sub-indices!"))
}
MechObjectDictionaryValue::SubIndices(values) => {
return Ok(values.clone());
}
}
}
/// Retrieves a specified range of sub-index values for an entry in the Object Dictionary.
/// For convenience.
///
/// This function provides a subset of sub-indices between `start` and `end` for entries
/// with multiple sub-indices (such as arrays). If `end` is negative, it includes all indices
/// from `start` to the last index.
///
/// # Parameters
/// - `index`: The 16-bit register index of the object in the Object Dictionary.
/// - `start`: The starting sub-index (inclusive).
/// - `end`: The ending sub-index (inclusive). If `end` is negative, it defaults to the last sub-index.
///
/// # Returns
/// - `Ok(Vec<MechCommandRegisterValue>)`: A vector containing values in the specified range,
/// if the entry exists and contains sub-indices within the range.
/// - `Err(anyhow::Error)`: An error if the entry does not exist, does not contain sub-indices, or if
/// the specified range is out of bounds.
///
/// # Errors
/// Returns an error if:
/// - The entry does not exist or does not contain sub-indices.
/// - The specified `start` index is out of bounds.
/// - The specified `end` index is out of bounds.
///
/// # Example
/// ```no_run
/// use mechutil::object_dictionary::MechObjectDictionary;
/// use mechutil::register_value::MechCommandRegisterValue;
///
/// let dictionary = MechObjectDictionary::new(0x000401, 1000, 12345, 67890);
///
/// // Assuming an array entry at index 0x2000 with multiple sub-indices has been added.
/// let result = dictionary.get_sub_index_range_values(0x2000, 1, 3);
/// match result {
/// Ok(values) => {
/// for (i, value) in values.iter().enumerate() {
/// println!("Sub-index {}: {:?}", i + 1, value);
/// }
/// },
/// Err(e) => println!("Error retrieving range of sub-index values: {}", e),
/// }
/// ```
pub fn get_sub_index_range_values(
&self,
index: u16,
start: u16,
end: i16,
) -> Result<Vec<MechCommandRegisterValue>, anyhow::Error> {
let entry;
match self.registers.get(&index) {
Some(res) => entry = res,
None => return Err(anyhow!("Entry {} not found", index)),
}
// Check that this entry has sub-indices
match &entry.value {
MechObjectDictionaryValue::Single(_) => {
Err(anyhow!("Entry does not contain sub-indices!"))
}
MechObjectDictionaryValue::SubIndices(values) => {
if (start as usize) >= values.len() {
return Err(anyhow!(
"start {} out of range of values len {}",
start,
values.len()
));
}
let end_index;
if end < 0 {
end_index = values.len() - 1;
} else if (end as usize) < values.len() - 1 {
end_index = end as usize;
} else {
return Err(anyhow!(
"end {} out of range of values len {}",
end,
values.len()
));
}
return Ok(values[(start as usize)..=end_index].to_vec());
}
}
}
/// Sets the value of an object dictionary entry at a specific index. For use internally. Does not
/// take access rights into account.
///
/// # Parameters
/// - `index`: The register index.
/// - `sub_index`: Optional sub-index for entries with multiple values (arrays).
/// - `new_value`: The new value to be set.
///
/// # Returns
/// - `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.
pub fn set_value(
&mut self,
index: u16,
sub_index: Option<u16>,
new_value: MechCommandRegisterValue,
) -> Result<(), anyhow::Error> {
// Get the entry for the specified index.
let entry;
match self.registers.get_mut(&index) {
Some(res) => {
entry = res;
}
None => {
log::debug!("Entry {} not found. Available entries:", index);
for key in self.registers.keys() {
log::debug!("\t{}", key);
}
return Err(anyhow!("Entry {} not found.", index));
}
}
// Set the value based on whether the entry has sub-indices or is a single value.
match &mut entry.value {
MechObjectDictionaryValue::Single(value) => {
if sub_index.is_some() {
return Err(anyhow!(
"Sub-index provided, but entry {} is not an array",
index
));
}
*value = new_value; // Directly set the single value
}
MechObjectDictionaryValue::SubIndices(values) => {
if let Some(sub_idx) = sub_index {
// Ensure sub-index is within bounds
if (sub_idx as usize) < values.len() {
values[sub_idx as usize] = new_value;
} else {
return Err(anyhow!("Sub-index {} out of bounds", sub_idx));
}
} else {
return Err(anyhow!("Sub-index is required for array values"));
}
}
}
Ok(())
}
/// Sets the value of an object dictionary entry at a specific index. For use internally. Does not
/// take access rights into account. Only works for single-value entries.
///
/// # Parameters
/// - `index`: The register index.
/// - `new_value`: The new value to be set.
///
/// # Returns
/// - `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.
pub fn set_index_value(
&mut self,
index: u16,
new_value: MechCommandRegisterValue,
) -> Result<(), anyhow::Error> {
// Get the entry for the specified index.
let entry;
match self.registers.get_mut(&index) {
Some(res) => entry = res,
None => return Err(anyhow!("Entry {} not found", index)),
}
// Set the value based on whether the entry has sub-indices or is a single value.
match &mut entry.value {
MechObjectDictionaryValue::Single(value) => {
*value = new_value; // Directly set the single value
}
MechObjectDictionaryValue::SubIndices(_) => {
return Err(anyhow!("Sub-index is required for array values"));
}
}
Ok(())
}
/// Sets the value of the sub-index an object dictionary entry at a specific index. For use internally. Does not
/// take access rights into account. Only works for Array or Record entries, not Single value entries.
///
/// # Parameters
/// - `index`: The register index.
/// - `sub_index`: sub-index for entries with multiple values (arrays & records).
/// - `new_value`: The new value to be set.
///
/// # Returns
/// - `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.
pub fn set_sub_index_value(
&mut self,
index: u16,
sub_index: u16,
new_value: MechCommandRegisterValue,
) -> Result<(), anyhow::Error> {
// Get the entry for the specified index.
let entry;
match self.registers.get_mut(&index) {
Some(res) => entry = res,
None => return Err(anyhow!("Entry {} not found", index)),
}
// Set the value based on whether the entry has sub-indices or is a single value.
match &mut entry.value {
MechObjectDictionaryValue::Single(value) => {
return Err(anyhow!(
"Sub-index provided, but entry {} is not an array",
index
));
}
MechObjectDictionaryValue::SubIndices(values) => {
if (sub_index as usize) < values.len() {
values[sub_index as usize] = new_value;
} else {
return Err(anyhow!("Sub-index {} out of bounds", sub_index));
}
}
}
Ok(())
}
/// Client/External-facing function to set the value of an object dictionary entry at a specific index, if allowed by access rights.
/// When taking requests from an external source, use this function to set the value of an Object Dictionary Entry.
///
/// # Parameters
/// - `index`: The register index.
/// - `sub_index`: Optional sub-index for entries with multiple values (arrays).
/// - `new_value`: The new value to be set.
///
/// # Returns
/// - `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.
pub fn set_value_external(
&mut self,
index: u16,
sub_index: Option<u16>,
new_value: MechCommandRegisterValue,
) -> Result<(), anyhow::Error> {
// Get the entry for the specified index.
let entry;
match self.registers.get_mut(&index) {
Some(res) => entry = res,
None => return Err(anyhow!("Entry {} not found", index)),
}
// Check if the entry is writable based on its access rights.
match entry.access_rights {
AccessRights::ReadOnly | AccessRights::Const => {
return Err(anyhow!("Cannot set value: Entry is read-only"));
}
AccessRights::WriteOnly | AccessRights::ReadWrite => {}
}
return self.set_value(index, sub_index, new_value);
}
/// Loads the object dictionary from a JSON file.
///
/// # Parameters
/// - `filename`: The path to the JSON file.
///
/// # Returns
/// - `Result<(), Box<dyn std::error::Error>>`: Ok if successful, or an error otherwise.
///
/// # Example
/// ```no_run
/// use mechutil::{object_dictionary::MechObjectDictionary};
/// let mut dictionary = MechObjectDictionary::new(
/// 0x000401, // Device Type (generic I/O device)
/// 1000, // Heartbeat interval of 1000 ms
/// 12345, // Vendor ID
/// 67890, // Product Code
/// );
/// dictionary.load_from_json("input.json").unwrap();
/// ```
pub fn load_from_json(&mut self, filename: &str) -> Result<(), Box<dyn std::error::Error>> {
let data = fs::read_to_string(filename)?;
let json_data: HashMap<u16, MechObjectDictionaryEntry> = serde_json::from_str(&data)?;
for (index, entry) in json_data {
self.registers.insert(index, entry);
}
Ok(())
}
/// Serializes the object dictionary to a JSON file.
///
/// # Parameters
/// - `filename`: The path to the JSON file.
///
/// # Returns
/// - `Result<(), Box<dyn std::error::Error>>`: Ok if successful, or an error otherwise.
///
/// # Example
/// ```no_run
/// use mechutil::{object_dictionary::MechObjectDictionary};
/// let mut dictionary = MechObjectDictionary::new(
/// 0x000401, // Device Type (generic I/O device)
/// 1000, // Heartbeat interval of 1000 ms
/// 12345, // Vendor ID
/// 67890, // Product Code
/// );
/// dictionary.save_to_json("output.json").unwrap();
/// ```
pub fn save_to_json(&self, filename: &str) -> Result<(), Box<dyn std::error::Error>> {
let json_data = serde_json::to_string_pretty(&self.registers)?;
fs::write(filename, json_data)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_and_check_entry() {
let mut dictionary = MechObjectDictionary::new(0x2001, 1000, 0, 0);
// Create a new entry with 5 sub-indices
dictionary.add_entry(
0x1000,
ObjectType::Array,
"Example Object".to_string(),
"uint32".to_string(),
AccessRights::ReadWrite,
true,
5,
MechCommandRegisterValue::from_uint32(5),
);
// Check if the entry exists
assert!(dictionary.entry_exists(0x1000));
// Check if an entry that hasn't been added doesn't exist
assert!(!dictionary.entry_exists(0x2000));
}
#[test]
fn test_single_value_entry() {
let mut dictionary = MechObjectDictionary::new(0x2001, 1000, 0, 0);
// Create a new entry with a single value (no sub-indices)
dictionary.add_entry(
0x2000,
ObjectType::Variable,
"Single Value Object".to_string(),
"int16".to_string(),
AccessRights::ReadOnly,
false,
1,
MechCommandRegisterValue::from_int16(1),
);
// Check if the entry exists
assert!(dictionary.entry_exists(0x2000));
assert!(
dictionary
.set_index_value(0x2000, MechCommandRegisterValue::from_int16(1234))
.is_ok()
);
let val_check = dictionary.get_index_value(0x2000).unwrap();
assert_eq!(val_check.to_int16().unwrap(), 1234);
}
#[test]
fn test_serialize_to_json() {
let mut dictionary = MechObjectDictionary::new(0x2001, 1000, 0, 0);
// Add an entry
dictionary.add_entry(
0x1000,
ObjectType::Array,
"Test Object".to_string(),
"uint32".to_string(),
AccessRights::ReadWrite,
true,
3,
MechCommandRegisterValue::from_int16(3),
);
// Serialize to JSON
let json_data = serde_json::to_string_pretty(&dictionary).unwrap();
// Check if JSON contains the index and the correct values
assert!(json_data.contains("\"index\": 4096")); // 0x1000 in decimal is 4096
assert!(json_data.contains("\"name\": \"Test Object\""));
assert!(json_data.contains("\"access_rights\": \"ReadWrite\""));
}
#[test]
fn test_deserialize_from_json() {
let json_data = r#"
{
"4096": {
"index": 4096,
"object_type": "Array",
"name": "Deserialized Object",
"datatype": "uint32",
"access_rights": "ReadWrite",
"mandatory": true,
"value": {
"SubIndices": [
{
"type_id": "UInt32",
"value_len": 4,
"num_columns": 1,
"num_rows": 1,
"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]
}
]
}
}
}
"#;
let mut dictionary = MechObjectDictionary::new(0x2001, 1000, 0, 0);
// Deserialize from JSON
let json_data: HashMap<u16, MechObjectDictionaryEntry> =
serde_json::from_str(&json_data).unwrap();
// Check if the entry was deserialized correctly
dictionary.registers = json_data;
assert!(dictionary.entry_exists(0x1000));
let entry = dictionary.registers.get(&0x1000).unwrap();
assert_eq!(entry.name, "Deserialized Object");
assert_eq!(entry.datatype, "uint32");
assert!(matches!(
entry.value,
MechObjectDictionaryValue::SubIndices(_)
));
}
}