import xml.etree.ElementTree as ET
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Union
import ingenialogger
from ingenialink import exceptions as exc
from ingenialink.constants import SINGLE_AXIS_MINIMUM_SUBNODES
from ingenialink.register import REG_ACCESS, REG_ADDRESS_TYPE, REG_DTYPE, Register
logger = ingenialogger.get_logger(__name__)
# Dictionary constants guide:
# Each constant has this structure: DICT_ORIGIN_END
# ORIGIN: The start point of the path
# END: The end point of the path
# ORIGIN: LABELS
DICT_LABELS = "./Labels"
DICT_LABELS_LABEL = f"{DICT_LABELS}/Label"
[docs]class DictionaryCategories:
"""Contains all categories from a Dictionary.
Args:
list_xdf_categories: List of Elements from xdf file
"""
def __init__(self, list_xdf_categories: List[ET.Element]) -> None:
self._list_xdf_categories = list_xdf_categories
self._cat_ids: List[str] = []
self._categories: Dict[str, Dict[str, str]] = {}
self.load_cat_ids()
[docs] def load_cat_ids(self) -> None:
"""Load category IDs from dictionary."""
for element in self._list_xdf_categories:
self._cat_ids.append(element.attrib["id"])
cat_element = element.find(DICT_LABELS_LABEL)
if cat_element is None:
logger.warning(
f"The element of the category {element.attrib['id']} could not be load"
)
continue
cat_id = cat_element.text
if cat_id is None:
logger.warning(f"The ID of the category {element.attrib['id']} could not be load")
continue
self._categories[element.attrib["id"]] = {"en_US": cat_id}
@property
def category_ids(self) -> List[str]:
"""Category IDs."""
return self._cat_ids
[docs] def labels(self, cat_id: str) -> Dict[str, str]:
"""Obtain labels for a certain category ID.
Args:
cat_id: Category ID
Returns:
Labels dictionary.
"""
return self._categories[cat_id]
[docs]class DictionaryErrors:
"""Errors for the dictionary.
Args:
list_xdf_errors: List of Elements from xdf file
"""
def __init__(self, list_xdf_errors: List[ET.Element]) -> None:
self._list_xdf_errors = list_xdf_errors
self._errors: Dict[int, List[Optional[str]]] = {}
self.load_errors()
[docs] def load_errors(self) -> None:
"""Load errors from dictionary."""
for element in self._list_xdf_errors:
label = element.find(DICT_LABELS_LABEL)
if label is None:
logger.warning(f"Could not load label of error {element.attrib['id']}")
continue
self._errors[int(element.attrib["id"], 16)] = [
element.attrib["id"],
element.attrib["affected_module"],
element.attrib["error_type"].capitalize(),
label.text,
]
@property
def errors(self) -> Dict[int, List[Optional[str]]]:
"""Get the errors dictionary.
Returns:
Errors dictionary.
"""
return self._errors
[docs]class Dictionary(ABC):
"""Ingenia dictionary Abstract Base Class.
Args:
dictionary_path: Dictionary file path.
Raises:
ILCreationError: If the dictionary could not be created.
"""
# Dictionary constants guide:
# Each constant has this structure: DICT_ORIGIN_END
# ORIGIN: The start point of the path
# END: The end point of the path
# ORIGIN: ROOT
DICT_ROOT = "."
DICT_ROOT_HEADER = f"{DICT_ROOT}/Header"
DICT_ROOT_VERSION = f"{DICT_ROOT_HEADER}/Version"
DICT_ROOT_BODY = f"{DICT_ROOT}/Body"
DICT_ROOT_DEVICE = f"{DICT_ROOT_BODY}/Device"
DICT_ROOT_CATEGORIES = f"{DICT_ROOT_DEVICE}/Categories"
DICT_ROOT_CATEGORY = f"{DICT_ROOT_CATEGORIES}/Category"
DICT_ROOT_ERRORS = f"{DICT_ROOT_BODY}/Errors"
DICT_ROOT_ERROR = f"{DICT_ROOT_ERRORS}/Error"
DICT_ROOT_AXES = f"{DICT_ROOT_DEVICE}/Axes"
DICT_ROOT_AXIS = f"{DICT_ROOT_AXES}/Axis"
DICT_ROOT_REGISTERS = f"{DICT_ROOT_DEVICE}/Registers"
DICT_ROOT_REGISTER = f"{DICT_ROOT_REGISTERS}/Register"
# ORIGIN: REGISTERS
DICT_REGISTERS = "./Registers"
DICT_REGISTERS_REGISTER = f"{DICT_REGISTERS}/Register"
# ORIGIN: RANGE
DICT_RANGE = "./Range"
# ORIGIN: ENUMERATIONS
DICT_ENUMERATIONS = "./Enumerations"
DICT_ENUMERATIONS_ENUMERATION = f"{DICT_ENUMERATIONS}/Enum"
DICT_IMAGE = "DriveImage"
DICT_MOCO_IMAGE_ATTRIB = "moco"
dtype_xdf_options = {
"float": REG_DTYPE.FLOAT,
"s8": REG_DTYPE.S8,
"u8": REG_DTYPE.U8,
"s16": REG_DTYPE.S16,
"u16": REG_DTYPE.U16,
"s32": REG_DTYPE.S32,
"u32": REG_DTYPE.U32,
"s64": REG_DTYPE.S64,
"u64": REG_DTYPE.U64,
"str": REG_DTYPE.STR,
}
access_xdf_options = {"r": REG_ACCESS.RO, "w": REG_ACCESS.WO, "rw": REG_ACCESS.RW}
address_type_xdf_options = {
"NVM": REG_ADDRESS_TYPE.NVM,
"NVM_NONE": REG_ADDRESS_TYPE.NVM_NONE,
"NVM_CFG": REG_ADDRESS_TYPE.NVM_CFG,
"NVM_LOCK": REG_ADDRESS_TYPE.NVM_LOCK,
"NVM_HW": REG_ADDRESS_TYPE.NVM_HW,
}
def __init__(self, dictionary_path: str) -> None:
self.path = dictionary_path
"""Path of the dictionary."""
self.version = "1"
"""Version of the dictionary."""
self.firmware_version: Optional[str] = None
"""Firmware version declared in the dictionary."""
self.product_code: Optional[int] = None
"""Product code declared in the dictionary."""
self.part_number: Optional[str] = None
"""Part number declared in the dictionary."""
self.revision_number: Optional[int] = None
"""Revision number declared in the dictionary."""
self.interface: Optional[str] = None
"""Interface declared in the dictionary."""
self.subnodes: int = SINGLE_AXIS_MINIMUM_SUBNODES
"""Number of subnodes in the dictionary."""
self.categories: Optional[DictionaryCategories] = None
"""Instance of all the categories in the dictionary."""
self.errors: Optional[DictionaryErrors] = None
"""Instance of all the errors in the dictionary."""
self._registers: List[Dict[str, Register]] = []
"""Instance of all the registers in the dictionary"""
self.image: Optional[str] = None
"""Drive's encoded image."""
self.moco_image: Optional[str] = None
"""Motion CORE encoded image. Only available when using a COM-KIT."""
self.read_dictionary()
[docs] def registers(self, subnode: int) -> Dict[str, Register]:
"""Gets the register dictionary to the targeted subnode.
Args:
subnode: Identifier for the subnode.
Returns:
Dictionary of all the registers for a subnode.
"""
return self._registers[subnode]
[docs] def read_dictionary(self) -> None:
"""Reads the dictionary file and initializes all its components."""
try:
with open(self.path, "r", encoding="utf-8") as xdf_file:
tree = ET.parse(xdf_file)
except FileNotFoundError:
raise FileNotFoundError(f"There is not any xdf file in the path: {self.path}")
root = tree.getroot()
device = root.find(self.DICT_ROOT_DEVICE)
if device is None:
raise exc.ILError(
f"Could not load the dictionary {self.path}. Device information is missing"
)
# Subnodes
if root.findall(self.DICT_ROOT_AXES):
self.subnodes = len(root.findall(self.DICT_ROOT_AXIS))
for _ in range(self.subnodes):
self._registers.append({})
# Categories
list_xdf_categories = root.findall(self.DICT_ROOT_CATEGORY)
self.categories = DictionaryCategories(list_xdf_categories)
# Errors
list_xdf_errors = root.findall(self.DICT_ROOT_ERROR)
self.errors = DictionaryErrors(list_xdf_errors)
# Version
version_node = root.find(self.DICT_ROOT_VERSION)
if version_node is not None and version_node.text is not None:
self.version = version_node.text
self.firmware_version = device.attrib.get("firmwareVersion")
product_code = device.attrib.get("ProductCode")
if product_code is not None and product_code.isdecimal():
self.product_code = int(product_code)
self.part_number = device.attrib.get("PartNumber")
revision_number = device.attrib.get("RevisionNumber")
if revision_number is not None and revision_number.isdecimal():
self.revision_number = int(revision_number)
self.interface = device.attrib.get("Interface")
if root.findall(self.DICT_ROOT_AXES):
# For each axis
for axis in root.findall(self.DICT_ROOT_AXIS):
for register in axis.findall(self.DICT_REGISTERS_REGISTER):
current_read_register = self._read_xdf_register(register)
if current_read_register:
self._add_register_list(current_read_register)
else:
for register in root.findall(self.DICT_ROOT_REGISTER):
current_read_register = self._read_xdf_register(register)
if current_read_register:
self._add_register_list(current_read_register)
try:
images = root.findall(self.DICT_IMAGE)
for image in images:
if image.text is not None and image.text.strip():
if (
"type" in image.attrib
and image.attrib["type"] == self.DICT_MOCO_IMAGE_ATTRIB
):
self.moco_image = image.text
else:
self.image = image.text
except AttributeError:
logger.error(f"Dictionary {Path(self.path).name} has no image section.")
# Closing xdf file
xdf_file.close()
def _read_xdf_register(self, register: ET.Element) -> Optional[Register]:
"""Reads a register from the dictionary and creates a Register instance.
Args:
register: Register instance from the dictionary.
Returns:
The current register which it has been reading
None: When at least a mandatory attribute is not in a xdf file
Raises:
KeyError: If the register doesn't have an identifier.
ValueError: If the register data type is invalid.
ValueError: If the register access type is invalid.
ValueError: If the register address type is invalid.
KeyError: If some attribute is missing.
"""
try:
identifier = register.attrib["id"]
except KeyError as ke:
logger.error(f"The register doesn't have an identifier. Error caught: {ke}")
return None
try:
units = register.attrib["units"]
cyclic = register.attrib.get("cyclic", "CONFIG")
# Data type
dtype_aux = register.attrib["dtype"]
dtype = None
if dtype_aux in self.dtype_xdf_options:
dtype = self.dtype_xdf_options[dtype_aux]
else:
raise ValueError(
f"The data type {dtype_aux} does not exist for the register: {identifier}"
)
# Access type
access_aux = register.attrib["access"]
access = None
if access_aux in self.access_xdf_options:
access = self.access_xdf_options[access_aux]
else:
raise ValueError(
f"The access type {access_aux} does not exist for the register: {identifier}"
)
# Address type
address_type_aux = register.attrib["address_type"]
if address_type_aux in self.address_type_xdf_options:
address_type = self.address_type_xdf_options[address_type_aux]
else:
raise ValueError(
f"The address type {address_type_aux} does not exist for the register: "
f"{identifier}"
)
subnode = int(register.attrib.get("subnode", 1))
storage = register.attrib.get("storage")
cat_id = register.attrib.get("cat_id")
internal_use = int(register.attrib.get("internal_use", 0))
# Labels
labels_elem = register.findall(DICT_LABELS_LABEL)
labels = {label.attrib["lang"]: str(label.text) for label in labels_elem}
# Range
range_elem = register.find(self.DICT_RANGE)
reg_range: Union[Tuple[None, None], Tuple[str, str]] = (None, None)
if range_elem is not None:
range_min = range_elem.attrib["min"]
range_max = range_elem.attrib["max"]
reg_range = (range_min, range_max)
# Enumerations
enums_elem = register.findall(self.DICT_ENUMERATIONS_ENUMERATION)
enums = {str(enum.text): int(enum.attrib["value"]) for enum in enums_elem}
current_read_register = Register(
dtype,
access,
identifier=identifier,
units=units,
cyclic=cyclic,
subnode=subnode,
storage=storage,
reg_range=reg_range,
labels=labels,
enums=enums,
cat_id=cat_id,
internal_use=internal_use,
address_type=address_type,
)
return current_read_register
except KeyError as ke:
logger.error(f"Register with ID {identifier} has not attribute {ke}")
return None
def _add_register_list(
self,
register: Register,
) -> None:
"""Adds the current read register into the _registers list
Args:
register: the current read register it will be instanced
"""
identifier = register.identifier
subnode = register.subnode
if identifier is None:
return
self._registers[subnode][identifier] = register