Module object_dict

Module object_dict 

Source
Expand description

Object Dictionary

§Objects Overview

The object dictionary is the main mechanism of configuration and communication for a node. For example, SDO access is performed on sub objects, which are identified by the 16-bit object ID of their parent object, and an 8-bit sub index. Objects come in three varieties:

  • VAR: A single variable of any type (accessed at sub index 0)
  • ARRAY: An array of sub-objects, all with the same type. Sub-index 0 is a u8 containing the size of the array. Sub indices 1-N contain the array values.
  • RECORD: A collection of sub-objects of heterogenous types. Sub-index 0 contains the highest implemented sub index.

The set of data types which are be stored are defined by the DataType enum.

The object dictionary is generated at build time using the zencan-build crate, based on the device config TOML file. A goal of zencan is to minimize the amount of generated code, so the generated code primarily instantiates the types defined here.

§Object Storage

Most objects implement their own storage, and are statically allocated. However, it is possible to register object handlers at run-time, which may store data in any way they wish, and perform whatever logic is required upon access to the object. The objects must be declared as application_callback objects at build time, so that a CallbackObject is inserted into the object dictionary as a placeholder to store the run-time provided object.

§The ObjectAccess trait

Any struct which implements the ObjectAccess trait can be used to represent an object in the dictionary. For simple data objects, the object can be defined in TOML and a type implementing this trait will be created for it during code generation. Additionally, accessor methods will be defined for accessing the sub objects directly.

For more complex logic, custom objects can be implemented by implementing the ObjectAccess trait. A more ergonomic way to implement this trait is to implement the ProvidesSubObjects trait, and implement the sub objects individually by implementing the SubObjectAccess trait. Any object which implements ProvidesSubObjects will also get an ObjectAccess implementation.

§SubObject implementations

Most sub objects can be implemented using one of the following existing types:

§Example Custom Object Implementation

use zencan_node::object_dict::{ConstField, ScalarField, ProvidesSubObjects, SubObjectAccess};
use zencan_node::common::objects::{ObjectCode, SubInfo};
use zencan_node::common::sdo::AbortCode;
// Example external API used to access a value for a sub field
struct ExternalApi {}

impl ExternalApi {
    pub fn get_value(&self) -> f32 {
        42.0
    }
    pub fn set_value(&self, value: f32) {
        // TODO: Do something with the value
    }
}

struct ExternalSubObject {
    external_api: &'static ExternalApi,
}

impl ExternalSubObject {
    pub fn new(external_api: &'static ExternalApi) -> Self {
        Self { external_api }
    }
}

impl SubObjectAccess for ExternalSubObject {
    fn read(&self, offset: usize, buf: &mut [u8]) -> Result<usize, AbortCode> {
        let value_bytes = self.external_api.get_value().to_le_bytes();
        if offset < value_bytes.len() {
            let read_len = buf.len().min(value_bytes.len() - offset);
            buf[..read_len].copy_from_slice(&value_bytes[offset..offset + read_len]);
            Ok(read_len)
        } else {
            Ok(0)
        }
    }

    fn read_size(&self) -> usize {
        4
    }

    fn write(&self, data: &[u8]) -> Result<(), AbortCode> {
        if data.len() == 4 {
            let value = f32::from_le_bytes(data.try_into().unwrap());
            self.external_api.set_value(value);
            Ok(())
        } else if data.len() < 4 {
            Err(AbortCode::DataTypeMismatchLengthLow)
        } else if data.len() > 4 {
            Err(AbortCode::DataTypeMismatchLengthHigh)
        } else {
            let value = f32::from_le_bytes(data.try_into().unwrap());
            self.external_api.set_value(value);
            Ok(())
        }
    }
}

struct CustomObject {
    stored_field: ScalarField<u32>,
    external_field: ExternalSubObject,
}

impl CustomObject {
    pub fn new(external_api: &'static ExternalApi) -> Self {
        Self {
            external_field: ExternalSubObject::new(external_api),
            stored_field: ScalarField::<u32>::new(0),
        }
    }
}

impl ProvidesSubObjects for CustomObject {
    fn get_sub_object(&self, sub: u8) -> Option<(SubInfo, &dyn SubObjectAccess)> {
        match sub {
            // Sub 0 returns the highest sub index on the object
            0 => Some((
                SubInfo::MAX_SUB_NUMBER,
                const { &ConstField::new(3u8.to_le_bytes()) },
            )),
            // Sub 1 returns the u32 field stored in the object, implemented using ScalarField<u32>
            1 => Some((SubInfo::new_u32().rw_access().persist(true), &self.stored_field)),
            // Sub 2 returns a custom sub object which accesses the external API
            2 => Some((SubInfo::new_f32().rw_access().persist(false), &self.external_field)),
            _ => None,
        }
    }

    fn object_code(&self) -> ObjectCode {
        ObjectCode::Record
    }
}

§Object threading support

All object must be Sync and Send, to allow for access from any thread. This is implemented using the critical_section crate. All objects support ObjectAccess::read and ObjectAccess::write, which allow for atomic access of objects. For small objects, the SDO server will access objects using a single read or write call, buffering the data for segmented or block transfers, ensuring atomic access. However, if the size of the transfer is larger than the SDO buffer (currently fixed at 889 bytes, but likely to become adjustable in the future) then the SDO server is unable to buffer all of the data. For reading data, this will result in multiple calls to read with no guarantees that the data will not change in between, so it is possible that a client can get a “torn read”. For writing data to an object, the partial write API is used, and has similar concerns.

§Object flags for TPDO event triggering

Some objects support event flags, which can be set via ObjectAccess::set_event_flag. These are used to trigger TPDO transmission.

Structs§

ByteField
A sub object which contains a fixed-size byte array
CallbackObject
OD placeholder for an object which will have a handler registered at runtime
CallbackSubObject
A handler-backed sub-object for runtime registered implementation
ConstByteRefField
A sub-object implementation which is backed by a static byte slice
ConstField
A struct for a constant sub object whose value never changes
NullTermByteField
A byte field which supports storing short values using null termination to indicate size
ODEntry
Represents one item in the in-memory table of objects
ObjectFlagSync
A struct used for synchronizing the A/B event flags of all objects, which are used for triggering PDO events
ObjectFlags
Stores an event flag for each sub object in an object
ScalarField
A sub object which contains a single scalar value of type T, which is a standard rust type

Traits§

ObjectAccess
A trait for accessing objects
ObjectFlagAccess
Trait for accessing object flags
ProvidesSubObjects
A trait for structs which represent Objects to implement
SubObjectAccess
Allow transparent byte level access to a sub object

Functions§

find_object
Lookup an object from the Object dictionary table
find_object_entry
Lookup an entry from the object dictionary table