# Copyright 2024 Volvo Car Corporation # Licensed under Apache 2.0. """Python module used for handling zone controller specifications""" from ruamel.yaml import YAML from powertrain_build.lib import logger from powertrain_build.interface.base import BaseApplication LOGGER = logger.create_logger("base") class BadYamlFormat(Exception): """Exception to raise when in/out signal is not defined.""" def __init__(self, message): self.message = message class ZCAL(BaseApplication): """Zone controller abstraction layer""" def __repr__(self): """String representation of ZCAL""" return ( f"" ) def __init__(self, base_application): """Create the interface object Currently, there is no verification that the signal is used. TODO: Check if the signal is a set or a get somewhere. Maybe in here. Args: base_application (BaseApplication): Primary object of an interface Usually a raster, but can be an application or a model too. """ self.name = "" self.zc_translations = {} self.composition_spec = {} self.signal_names = {} self.e2e_sts_signals = set() self.update_bit_signals = set() self.clear_signal_names() self.interface_enums = set() self.base_application = base_application self.translations_files = [] self.device_domain = base_application.get_domain_mapping() self.signal_primitives_list = set() @staticmethod def read_translation_files(translation_files, keys=None): """ Searches translation files (yaml) for given keys at top level. Raises an error if conflicting configurations are found among the files. Return a dict containing the aggregated data found in the translation files for the keys provided in the function call. Args: translation_files (list): List of paths to files to search. keys (list): List of keys to search among translation files for (all present keys by default). Returns: specs (dict): Dictionary containing provided keys with the aggregated data stored under those keys in translation files. """ specs = {} for translation_file in translation_files: with open(translation_file, encoding="utf-8") as translation: yaml = YAML(typ='safe', pure=True) raw = yaml.load(translation) used_keys = raw.keys() if keys is None else keys for key in used_keys: new_elements = raw.get(key, None) if isinstance(new_elements, list): specs[key] = specs.get(key, []) + new_elements elif isinstance(new_elements, dict): specs[key] = specs.get(key, {}) if {**specs[key], **new_elements} != {**new_elements, **specs[key]}: LOGGER.error( "Conflicting configuration found for key '%s' among translation files: %s", key, translation_files, ) specs[key].update(new_elements) return specs def add_signals(self, signals, signal_type="insignal", *args): """Add signal names and properties Args: signals (list(Signals)): Signals to use signal_type (str): 'insignals' or 'outsignals' """ for signal in signals: if signal.name not in self.zc_translations: continue LOGGER.debug("Adding signal: %s", signal) for translation in self.zc_translations[signal.name]: signal_property = translation["property"] port_name = translation["port_name"] struct_name = translation["struct_name"] self.check_signal_property(port_name, struct_name, signal_property, signal_type) self.signal_names["zc"][signal_type].add(signal_property) self.signal_names["other"][signal_type].add(signal.name) for e2e_sts_signal_name in self.e2e_sts_signals: if e2e_sts_signal_name not in self.signal_names["other"]["insignals"]: LOGGER.warning("E2E check signal %s not used in any model.", e2e_sts_signal_name) else: self.signal_names["other"][signal_type].add(e2e_sts_signal_name) for update_bit_signal_name in self.update_bit_signals: if update_bit_signal_name not in self.signal_names["other"]["insignals"]: LOGGER.warning("Update bit signal %s not used in any model.", update_bit_signal_name) else: self.signal_names["other"][signal_type].add(update_bit_signal_name) LOGGER.debug("Registered signal names: %s", self.signal_names) def check_signal_property(self, port_name, struct_name, property_name, signal_type): """Check if we have only one signal written for the same property. Args: port_name (str): port name struct_name (str): signal struct name property_name (str): signal property signal_type (str): insignal or outsignal """ signal_primitive_spec = ".".join([port_name, struct_name, property_name, signal_type]) if signal_primitive_spec in self.signal_primitives_list and signal_type == "outsignal": error_msg = (f"You can't write {property_name} in {struct_name} as" f" {signal_type} since this primitive has been used.") raise Exception(error_msg) self.signal_primitives_list.add(signal_primitive_spec) def parse_definition(self, definition): """Parses all translation files and populates class interface data. Args: definition (list(Path)): Definition files """ raw = self.read_translation_files(definition) self.composition_spec = { key: raw[key] for key in ("port_interfaces", "data_types", "calls", "Diagnostics") if key in raw } ports_info = {} for port_name, port in raw.get("ports", {}).items(): signal_struct = port.get("element", {}) if signal_struct: for element_name, element_data in signal_struct.items(): self.populate_signal_translations(port_name, element_name, element_data) # Loop inside function to make sure direction is consistent ports_info[port_name] = { **self.get_port_info(signal_struct), "interface": port.get("interface") } else: raise Exception(f"Port {port_name} has no element.") self.composition_spec["ports"] = ports_info @staticmethod def get_port_info(signal_struct): """Extract port information from signal elements in port. Raises exception if signal elements are not exclusively sent in one direction. Args: signal_struct (dict): Signal dict containing list of signal elements Returns: port_info (dict): Dict containing port direction and if any elements should have an update bit associated with them. """ update_elements = set() direction = None for element_name, element_data in signal_struct.items(): for element in element_data: if "insignal" in element: temp_dir = "IN" elif "outsignal" in element: temp_dir = "OUT" else: raise BadYamlFormat(f"in/out signal for element in { element_name } is missing.") if direction is not None and direction != temp_dir: raise BadYamlFormat(f"Signal { element_name } has both in and out elements.") direction = temp_dir if element.get("updateBit", False): update_elements.add(element_name) port_info = {"direction": direction} if update_elements: port_info["enable_update"] = list(update_elements) return port_info def populate_signal_translations(self, port_name, element_name, element_data): """Populate class translations data. Args: port_name (str): Port name. element_name (str): Name of a port element. element_data (dict): Dict with signal structs to/from a port of an element. """ enumerations = self.base_application.enumerations for signal_definition in element_data: if "insignal" in signal_definition: signal_name = signal_definition["insignal"] base_signals = self.base_application.insignals elif "outsignal" in signal_definition: signal_name = signal_definition["outsignal"] base_signals = self.base_application.outsignals else: raise BadYamlFormat(f"in/out signal for { signal_name } is missing.") base_properties = None for base_signal in base_signals: if signal_name == base_signal.name: matching_base_signal = base_signal base_properties = self.base_application.get_signal_properties( matching_base_signal ) if base_properties is None: raise BadYamlFormat(f"in/out signal for { signal_name } is missing.") if base_properties["type"] in enumerations: if 'init' in signal_definition: init_value = signal_definition["init"] else: if enumerations[base_properties['type']]['default_value'] is not None: init_value = enumerations[base_properties['type']]['default_value'] else: LOGGER.warning('Initializing enumeration %s to "zero".', base_properties['type']) init_value = [ k for k, v in enumerations[base_properties['type']]['members'].items() if v == 0 ][0] else: init_value = signal_definition.get("init", 0) update_bit = signal_definition.get("updateBit", False) e2e_status = signal_definition.get("e2eStatus", False) group = signal_definition.get("group", element_name) translation = { "range": { "min": base_properties.get("min", "-"), "max": base_properties.get("max", "-") }, "offset": base_properties.get("offset", "-"), "factor": base_properties.get("factor", "-"), "property": signal_definition["property"], "init": init_value, "port_name": port_name, "struct_name": element_name, "variable_type": base_properties.get("type"), "description": base_properties.get("description"), "unit": base_properties.get("unit"), "debug": base_properties.get("debug", False), "dependability": e2e_status, "update_bit": update_bit } if signal_name not in self.zc_translations: self.zc_translations[signal_name] = [] self.zc_translations[signal_name].append(translation) if update_bit: update_bit_property = f"{element_name}UpdateBit" update_signal_name = f"yVc{group}_B_{update_bit_property}" if signal_name == update_signal_name: error_msg = f"Don't put updateBit status signals ({update_signal_name}) in yaml interface files." raise BadYamlFormat(error_msg) self.zc_translations[update_signal_name] = [ { "property": update_bit_property, "variable_type": "Bool", "property_interface_type": "Bool", "offset": "-", "factor": "-", "range": { "min": "-", "max": "-", }, "init": 1, "port_name": port_name, "struct_name": element_name, "description": f"Update bit signal for signal {element_name}.", "unit": None, "dependability": False, "update_bit": True } ] self.update_bit_signals.add(update_signal_name) if e2e_status: if "outsignal" in signal_definition: error_msg = "E2e status not expected for outsignals" raise BadYamlFormat(error_msg) e2e_sts_property = f"{element_name}E2eSts" e2e_sts_signal_name = f"sVc{group}_D_{e2e_sts_property}" if signal_name == e2e_sts_signal_name: error_msg = f"Don't put E2E status signals ({e2e_sts_signal_name}) in yaml interface files." raise BadYamlFormat(error_msg) if e2e_sts_signal_name not in self.zc_translations: self.zc_translations[e2e_sts_signal_name] = [ { "property": e2e_sts_property, "variable_type": "UInt8", "property_interface_type": "UInt8", "offset": 0, "factor": 1, "range": { "min": 0, "max": 255 }, "init": 255, "port_name": port_name, "struct_name": element_name, "description": f"E2E status code for E2E protected signal(s) {signal_name}.", "unit": None, "debug": False, "dependability": True, "update_bit": False } ] self.e2e_sts_signals.add(e2e_sts_signal_name) else: translation = self.zc_translations[e2e_sts_signal_name][0] translation["description"] = translation["description"][:-1] + f", {signal_name}." def clear_signal_names(self): """Clear signal names Clears defined signal names (but not signal properties). """ self.signal_names = { "zc": {"insignals": set(), "outsignals": set()}, "other": {"insignals": set(), "outsignals": set()} } def to_dict(self): """Method to generate dict to be saved as yaml Returns: spec (dict): Signalling specification """ spec = {"consumer": [], "producer": []} signal_roles = ["consumer", "producer"] signal_types = ["insignals", "outsignals"] for signal_role, signal_type in zip(signal_roles, signal_types): for signal_name in self.signal_names["other"][signal_type]: for signal_spec in self.zc_translations[signal_name]: spec[signal_role].append({**signal_spec, "variable": signal_name}) return spec