from collections.abc import Iterator
from functools import cached_property
from typing import Optional, cast
from xml.etree import ElementTree
import ingenialogger
from ingenialink.canopen.dictionary import CanopenDictionary
from ingenialink.canopen.register import CanopenRegister
from ingenialink.constants import (
CANOPEN_ADDRESS_OFFSET,
CANOPEN_SUBNODE_0_ADDRESS_OFFSET,
MAP_ADDRESS_OFFSET,
)
from ingenialink.dictionary import (
CanOpenObject,
CanOpenObjectType,
DictionarySafetyModule,
DictionaryV2,
DictionaryV3,
Interface,
)
from ingenialink.enums.register import (
RegAccess,
RegAddressType,
RegCyclicType,
RegDtype,
)
from ingenialink.ethercat.register import EthercatRegister
from ingenialink.register import MonDistV3
logger = ingenialogger.get_logger(__name__)
[docs]
class EthercatDictionary(CanopenDictionary):
"""Base class for EtherCAT dictionaries."""
interface = Interface.ECAT
[docs]
class EthercatDictionaryV2(EthercatDictionary, DictionaryV2):
"""Contains all registers and information of a EtherCAT dictionary.
Args:
dictionary_path: Path to the Ingenia dictionary.
"""
@cached_property
def _monitoring_disturbance_objects(self) -> list["CanOpenObject"]:
monitoring_obj = CanOpenObject(
uid="MON_DATA",
idx=0x58B2,
object_type=CanOpenObjectType.RECORD,
registers=[
EthercatRegister(
identifier="MON_DATA_SUBINDEX_0",
units="none",
idx=0x58B2,
subidx=0x00,
pdo_access=RegCyclicType.CONFIG,
dtype=RegDtype.U8,
access=RegAccess.RW,
subnode=0,
labels={"en_US": "SubIndex 000"},
cat_id="MONITORING",
),
EthercatRegister(
identifier="MON_DATA_VALUE",
units="none",
idx=0x58B2,
subidx=0x01,
pdo_access=RegCyclicType.CONFIG,
dtype=RegDtype.BYTE_ARRAY_512,
access=RegAccess.RO,
subnode=0,
labels={"en_US": "Monitoring data"},
cat_id="MONITORING",
),
],
)
disturbance_obj = CanOpenObject(
uid="DIST_DATA",
idx=0x58B4,
object_type=CanOpenObjectType.RECORD,
registers=[
EthercatRegister(
identifier="DIST_DATA_SUBINDEX_0",
units="none",
idx=0x58B4,
subidx=0x00,
pdo_access=RegCyclicType.CONFIG,
dtype=RegDtype.U8,
access=RegAccess.RW,
subnode=0,
labels={"en_US": "SubIndex 000"},
cat_id="MONITORING",
),
EthercatRegister(
identifier="DIST_DATA_VALUE",
units="none",
idx=0x58B4,
subidx=0x01,
pdo_access=RegCyclicType.CONFIG,
dtype=RegDtype.BYTE_ARRAY_512,
access=RegAccess.WO,
subnode=0,
labels={"en_US": "Disturbance data"},
cat_id="MONITORING",
),
],
)
return [monitoring_obj, disturbance_obj]
@cached_property
def _monitoring_disturbance_registers(self) -> list[EthercatRegister]:
return [
cast("EthercatRegister", register)
for obj in self._monitoring_disturbance_objects
for register in obj.registers
]
@cached_property
def _safety_registers(self) -> list[EthercatRegister]:
return [
EthercatRegister(
identifier="FSOE_MANUF_SAFETY_ADDRESS",
idx=0x4193,
subidx=0x00,
pdo_access=RegCyclicType.CONFIG,
dtype=RegDtype.U16,
access=RegAccess.RW,
subnode=1,
labels={"en_US": "Safety address"},
cat_id="FSOE",
description="Contains the safety address",
units=None,
),
EthercatRegister(
identifier="MDP_CONFIGURED_MODULE_1",
idx=0xF030,
subidx=0x01,
pdo_access=RegCyclicType.CONFIG,
dtype=RegDtype.U32,
access=RegAccess.RW,
subnode=0,
labels={"en_US": "Configured module ident of the module 1"},
cat_id="MDP",
description="Configured module ident of the module 1",
units=None,
),
EthercatRegister(
identifier="FSOE_SAFE_INPUTS_MAP",
idx=0x46D2,
subidx=0x00,
pdo_access=RegCyclicType.CONFIG,
dtype=RegDtype.U16,
access=RegAccess.RW,
subnode=1,
cat_id="FSOE",
labels={"en_US": "Safe Inputs Map"},
description="Links the value read in the safe inputs to a Safety Function instance",
units=None,
enums={
"Disabled": 0,
"Safety Inputs linked to STO instance": 1,
"Safety Inputs linked to SS1 instance": 2,
},
),
EthercatRegister(
identifier="FSOE_SS1_TIME_TO_STO_1",
idx=0x6651,
subidx=0x01,
pdo_access=RegCyclicType.CONFIG,
dtype=RegDtype.U16,
access=RegAccess.RW,
subnode=1,
labels={"en_US": "SS1 Time to STO"},
cat_id="FSOE",
reg_range=(0, 10000),
description="Time that will take a SS1 function to transition to STO",
units="ms",
),
EthercatRegister(
identifier="FSOE_STO",
idx=0x6640,
subidx=0,
dtype=RegDtype.BOOL,
access=RegAccess.RO,
pdo_access=RegCyclicType.SAFETY_INPUT_OUTPUT,
subnode=1,
labels={"en_US": "STO Command"},
cat_id="FSOE",
description=(
"Safe Torque Off Safety function. "
"Commands the STO when writing. "
"Reports the STO status when reading."
),
units=None,
),
EthercatRegister(
identifier="FSOE_SS1_1",
idx=0x6650,
subidx=1,
dtype=RegDtype.BOOL,
access=RegAccess.RO,
pdo_access=RegCyclicType.SAFETY_INPUT_OUTPUT,
subnode=1,
labels={"en_US": "SS1 Command"},
cat_id="FSOE",
description=(
"SS1 Safety function.Commands the SS1 when writing. "
"Reports the SS1 status when reading."
),
units=None,
),
EthercatRegister(
identifier="FSOE_SAFE_INPUTS_VALUE",
idx=0x46D1,
subidx=0,
dtype=RegDtype.BOOL,
access=RegAccess.RO,
pdo_access=RegCyclicType.SAFETY_INPUT,
subnode=1,
labels={"en_US": "Safe Inputs Value"},
cat_id="FSOE",
description=(
"Value of the safe inputs combined with AND logic. Only updated in OP state"
),
units=None,
),
]
@cached_property
def _safety_modules(self) -> list[DictionarySafetyModule]:
def __module_ident(module_idx: int) -> int:
if self.product_code is None:
raise ValueError("Module ident cannot be calculated, product code missing.")
return (self.product_code & 0x7F00000) + module_idx
return [
DictionarySafetyModule(
uses_sra=False,
module_ident=__module_ident(0),
application_parameters=[
DictionarySafetyModule.ApplicationParameter(uid="FSOE_SAFE_INPUTS_MAP"),
DictionarySafetyModule.ApplicationParameter(uid="FSOE_SS1_TIME_TO_STO_1"),
],
),
DictionarySafetyModule(
uses_sra=True,
module_ident=__module_ident(1),
application_parameters=[
DictionarySafetyModule.ApplicationParameter(uid="FSOE_SAFE_INPUTS_MAP"),
DictionarySafetyModule.ApplicationParameter(uid="FSOE_SS1_TIME_TO_STO_1"),
],
),
]
def __create_pdo_map_assign(
self, idx: int, base_uid: str, base_label: str, n_elements: int
) -> CanOpenObject:
"""Generate PDO assignment registers.
Args:
idx (int): Index of register.
base_uid (str): Base unique identifier.
base_label (str): Base label.
n_elements (int): Number of elements of the pdo assign
Returns:
CanOpenObject: Object containing the registers for a pdo map assign.
"""
canopen_object = CanOpenObject(
uid=base_uid,
idx=idx,
object_type=CanOpenObjectType.ARRAY,
registers=list[CanopenRegister](
[
# Total register
EthercatRegister(
identifier=f"{base_uid}_TOTAL",
units="cnt",
subnode=0,
idx=idx,
subidx=0x00,
pdo_access=RegCyclicType.CONFIG,
dtype=RegDtype.U8,
access=RegAccess.RW,
address_type=RegAddressType.NVM_NONE,
labels={"en_US": "SubIndex 000"},
cat_id="COMMUNICATIONS",
)
]
+ [
# Element registers
EthercatRegister(
identifier=f"{base_uid}_{i}",
units="none",
subnode=0,
idx=idx,
subidx=i,
pdo_access=RegCyclicType.CONFIG,
dtype=RegDtype.U16,
access=RegAccess.RW,
address_type=RegAddressType.NVM_NONE,
labels={"en_US": f"{base_label} Element {i}"},
cat_id="COMMUNICATIONS",
)
for i in range(1, n_elements + 1)
],
),
)
for reg in canopen_object.registers:
reg.obj = canopen_object
return canopen_object
def __create_pdo_map(
self,
idx: int,
base_uid: str,
base_label: str,
n_elements: int,
read_only: bool = False,
subnode: int = 0,
) -> CanOpenObject:
"""Generate PDO map registers.
Args:
idx: Index of register.
base_uid: Base unique identifier.
base_label: Base label.
n_elements: Number of elements of the pdo map.
read_only: If True, the PDO map is read-only (default is False).
subnode: Subnode for the registers (default is 0).
Returns:
CanOpenObject: Object containing the registers for a pdo map.
"""
canopen_object = CanOpenObject(
uid=base_uid,
idx=idx,
object_type=CanOpenObjectType.RECORD,
registers=list[CanopenRegister](
[
# Total register
EthercatRegister(
identifier=f"{base_uid}_TOTAL",
units="none",
subnode=subnode,
idx=idx,
subidx=0x00,
pdo_access=RegCyclicType.CONFIG,
dtype=RegDtype.U8,
access=RegAccess.RO if read_only else RegAccess.RW,
address_type=RegAddressType.NVM_NONE,
labels={"en_US": "SubIndex 000"},
cat_id="COMMUNICATIONS",
)
]
+ [
# Element registers
EthercatRegister(
identifier=f"{base_uid}_{i}",
units="none",
subnode=subnode,
idx=idx,
subidx=i,
pdo_access=RegCyclicType.CONFIG,
dtype=RegDtype.U32,
access=RegAccess.RO if read_only else RegAccess.RW,
address_type=RegAddressType.NVM_NONE,
labels={"en_US": f"{base_label} Element {i}"},
cat_id="COMMUNICATIONS",
)
for i in range(1, n_elements + 1)
]
),
)
for reg in canopen_object.registers:
reg.obj = canopen_object
return canopen_object
def __create_pdo_objects(self) -> Iterator[CanOpenObject]:
# RPDO Assignments
yield self.__create_pdo_map_assign(
idx=0x1C12,
base_uid="ETG_COMMS_RPDO_ASSIGN",
base_label="RxPDO assign",
n_elements=3,
)
# RPDO Map 1
yield self.__create_pdo_map(
idx=0x1600,
base_uid="ETG_COMMS_RPDO_MAP1",
base_label="RxPDO Map 1",
n_elements=15,
)
# RPDO Map 2
yield self.__create_pdo_map(
idx=0x1601,
base_uid="ETG_COMMS_RPDO_MAP2",
base_label="RxPDO Map 2",
n_elements=15,
)
# RPDO Map 3
yield self.__create_pdo_map(
idx=0x1602,
base_uid="ETG_COMMS_RPDO_MAP3",
base_label="RxPDO Map 3",
n_elements=15,
)
# TPDO Assignments
yield self.__create_pdo_map_assign(
idx=0x1C13,
base_uid="ETG_COMMS_TPDO_ASSIGN",
base_label="TxPDO assign",
n_elements=3,
)
# TPDO Map
yield self.__create_pdo_map(
idx=0x1A00,
base_uid="ETG_COMMS_TPDO_MAP1",
base_label="TxPDO Map 1",
n_elements=15,
)
# TPDO Map
yield self.__create_pdo_map(
idx=0x1A01,
base_uid="ETG_COMMS_TPDO_MAP2",
base_label="TxPDO Map 2",
n_elements=15,
)
# TPDO Map
yield self.__create_pdo_map(
idx=0x1A02,
base_uid="ETG_COMMS_TPDO_MAP3",
base_label="TxPDO Map 3",
n_elements=15,
)
if self.is_safe:
# XDF V2 only supports phase I, where the pdo map is read-only
yield self.__create_pdo_map(
idx=0x1700,
base_uid="ETG_COMMS_RPDO_MAP256",
base_label="RxPDO Map 256",
n_elements=16,
read_only=True,
subnode=1,
)
yield self.__create_pdo_map(
idx=0x1B00,
base_uid="ETG_COMMS_TPDO_MAP256",
base_label="RxPDO Map 256",
n_elements=16,
read_only=True,
subnode=1,
)
@staticmethod
def __get_cia_offset(subnode: int) -> int:
"""Get the CiA offset for the register based on the subnode.
Args:
subnode: register subnode.
Returns:
The CiA offset for the register.
"""
return (
CANOPEN_SUBNODE_0_ADDRESS_OFFSET
if subnode == 0
else CANOPEN_ADDRESS_OFFSET + MAP_ADDRESS_OFFSET * (subnode - 1)
)
def _read_xdf_register(self, register: ElementTree.Element) -> Optional[EthercatRegister]:
current_read_register = super()._read_xdf_register(register)
if current_read_register is None:
return None
try:
idx = int(register.attrib["address"], 16) + self.__get_cia_offset(
current_read_register.subnode
)
subidx = 0x00
monitoring: Optional[MonDistV3] = None
if current_read_register.pdo_access != RegCyclicType.CONFIG:
address = idx - (
CANOPEN_ADDRESS_OFFSET
+ (MAP_ADDRESS_OFFSET * (current_read_register.subnode - 1))
)
monitoring = MonDistV3(
address=address,
subnode=current_read_register.subnode,
cyclic=current_read_register.pdo_access,
)
ethercat_register = EthercatRegister(
idx,
subidx,
current_read_register.dtype,
current_read_register.access,
identifier=current_read_register.identifier,
units=current_read_register.units,
pdo_access=current_read_register.pdo_access,
phy=current_read_register.phy,
subnode=current_read_register.subnode,
storage=current_read_register.storage,
reg_range=current_read_register.range,
labels=current_read_register.labels,
enums=current_read_register.enums,
cat_id=current_read_register.cat_id,
scat_id=current_read_register.scat_id,
internal_use=current_read_register.internal_use,
address_type=current_read_register.address_type,
bitfields=current_read_register.bitfields,
monitoring=monitoring,
description=current_read_register.description,
)
return ethercat_register
except KeyError as ke:
logger.error(
f"Register with ID {current_read_register.identifier} has not attribute {ke}"
)
return None
def _add_canopen_object(self, canopen_object: "CanOpenObject") -> None:
"""Adds Canopen object into the items list.
Args:
canopen_object: Canopen object to add.
"""
axis = canopen_object.registers[0].subnode
if axis not in self.items:
self.items[axis] = {}
self.items[axis][canopen_object.uid] = canopen_object
for reg in canopen_object.registers:
reg.obj = canopen_object
def _append_missing_registers(
self,
) -> None:
"""Append missing registers to the dictionary.
Mainly registers needed for Monitoring/Disturbance and PDOs.
"""
super()._append_missing_registers()
if self._DictionaryV2__MON_DIST_STATUS_REGISTER in self._registers[0]: # type: ignore[attr-defined]
for obj in self._monitoring_disturbance_objects:
self._add_canopen_object(obj)
if self.part_number in ["DEN-S-NET-E", "EVS-S-NET-E"]:
self.is_safe = True
for obj in self.__create_pdo_objects():
for register in obj.registers:
self._add_register_list(register)
subnode = obj.registers[0].subnode
if subnode not in self.items:
self.items[subnode] = {}
self.items[subnode][obj.uid] = obj
if self.is_safe:
for safety_submodule in self._safety_modules:
self.safety_modules[safety_submodule.module_ident] = safety_submodule
for register in self._safety_registers:
self._add_register_list(register)
[docs]
class EthercatDictionaryV3(EthercatDictionary, DictionaryV3):
"""Contains all registers and information of a EtherCAT dictionary.
Args:
dictionary_path: Path to the Ingenia dictionary.
"""