Henrik Wahlqvist fda739bc13 Refactor usage of ecu_supplier
Removed most of the decisions based on ecu_supplier and made it more
granular using individual configuration.
Current project types will keep their config by adding templates in the
BaseConfig.json file.
Some usecases were kept for legacy reasons.

Change-Id: I3d6199713006489baff0bf73751596770fd1f968
2024-09-17 08:25:46 +00:00

678 lines
27 KiB
Python

# Copyright 2024 Volvo Car Corporation
# Licensed under Apache 2.0.
"""Python module used for reading device proxy arxml:s"""
from ruamel.yaml import YAML
import enum
from powertrain_build.interface.base import BaseApplication, Signal
from powertrain_build.lib import logger
LOGGER = logger.create_logger("device_proxy")
class MissingDevice(Exception):
"""Exception to raise when device is missing"""
def __init__(self, dp):
self.message = f"Device proxy {dp} missing from deviceDomains.json"
class BadYamlFormat(Exception):
"""Exception to raise when in/out signal is not defined."""
def __init__(self, message):
self.message = message
class DPAL(BaseApplication):
"""Device Proxy abstraction layer"""
dp_position = enum.Enum(
"Position",
names=[
"domain",
"property",
"variable_type",
"property_interface_type",
"property_manifest_type",
"offset",
"factor",
"default",
"length",
"min",
"max",
"enum",
"init",
"description",
"unit",
"group",
"strategy",
"debug",
"dependability",
"port_name"
],
)
def __repr__(self):
"""String representation of DPAL"""
return (
f"<DPAL {self.name}"
f" app_side insignals: {len(self.signal_names['other']['insignals'])}"
f" app_side outsignals: {len(self.signal_names['other']['outsignals'])}>"
)
def __init__(self, base_application):
"""Create the interface object
Args:
base_application (BaseApplication): Primary object of an interface
Usually a raster, but can be an application or a model too.
"""
self.name = ""
self.dp_translations = {}
# We do not care about domain when looking from a project perspective,
# we only care when generating manifests for csp.
self.domain_filter = None
self.signal_names = {
"dp": {"insignals": set(), "outsignals": set()},
"other": {"insignals": set(), "outsignals": set()},
}
self.e2e_sts_signals = set()
self.base_application = base_application
self.translations_files = []
self.device_domain = base_application.get_domain_mapping()
self.signal_primitives_list = []
def clear_signal_names(self):
"""Clear signal names
Clears defined signal names (but not signal properties).
"""
self.signal_names = {
"dp": {"insignals": set(), "outsignals": set()},
"other": {"insignals": set(), "outsignals": set()},
}
def add_signals(self, signals, signal_type="insignal", properties=[]):
"""Add signal names and properties
Args:
signals (list(Signals)): Signals to use
signal_type (str): 'insignals' or 'outsignals'
properties (list(str)): signal definition properties, default = []
"""
opposite = {"insignals": "outsignals", "outsignals": "insignals"}
dp_type = opposite[signal_type]
for signal in signals:
LOGGER.debug("Adding signal: %s", signal)
temp_set = set()
for translation in self.dp_translations.get(signal.name, []):
temp_list = list(translation)
domain = translation[self.dp_position.domain.value]
group = translation[self.dp_position.group.value]
dp_signal = translation[self.dp_position.property.value]
self.check_signal_property(domain, group, dp_signal, signal_type)
self.signal_names["dp"][dp_type].add(dp_signal)
for enum_property in properties:
LOGGER.debug("Modifying property: %s", enum_property)
value = signal.properties[enum_property["source"]]
if value == "-":
value = enum_property["default"]
temp_list[
self.dp_position[enum_property["destination"]].value
] = value
temp_set.add(tuple(temp_list))
self.dp_translations[signal.name] = temp_set
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)
self.signal_names["other"][signal_type].add(e2e_sts_signal_name)
self.check_groups()
LOGGER.debug("Registered signal names: %s", self.signal_names)
def check_signal_property(self, domain, group, property_name, signal_type):
"""Check if we have only one signal written for the same property.
Args:
domain (str): signal domain
group (str): signal group
property_name (str): signal property
signal_type (str): 'insignals' or 'outsignals'
"""
primitive_value = ""
for value in [domain, group, property_name]:
if value:
if primitive_value == "":
primitive_value = value
else:
primitive_value = primitive_value + '.' + value
if primitive_value == "":
raise Exception("The primitive does not contain any value!")
directional_primitive = f"{primitive_value}.{signal_type}"
self.check_property(directional_primitive, signal_type)
def check_property(self, property_spec, signal_type):
"""Check if we have only one signal written for the same property.
Args:
property_spec (str): property specification
signal_type (str): 'insignals' or 'outsignals'
"""
if property_spec in self.signal_primitives_list:
error_msg = (f"You can't write {property_spec} as "
f"{signal_type} since this primitive has been used."
" Run model_yaml_verification to identify exact models.")
raise Exception(error_msg)
self.signal_primitives_list.append(property_spec)
def check_groups(self):
"""Check and crash if signal group contains both produces and consumes signals."""
groups = {}
for signal_name, signal_specs in self.dp_translations.items():
if signal_name in self.signal_names["other"]['insignals']:
consumed = True
elif signal_name in self.signal_names["other"]['outsignals']:
consumed = False
else:
continue
for signal_spec in signal_specs:
group = signal_spec[self.dp_position.group.value]
if group is None:
continue
domain = signal_spec[self.dp_position.domain.value]
key = (domain, group)
if key not in groups:
groups[key] = {"consumed": consumed,
"signals": set()}
groups[key]["signals"].add(signal_name)
assert consumed == groups[key]["consumed"], \
f"Signal group {group} for {domain} contains both consumed and produced signals"
@staticmethod
def read_translation(translation_file):
"""Read specification of the format:
service:
interface:
properties:
- endpoint_name:
- signal: name
property: name
- signal: name
property: name
hal:
hal_name:
- primitive_endpoint:
- insignal: name
hal_name:
- struct_endpoint:
- insignal: name1
property: member1
- insignal: name2
property: member2
ecm:
- signal: name
signals:
tvrl:
- signal: name
property: can_name
offset: offset
factor: scaling
Args:
translation_file (Path): file with specs
Returns:
yaml_data (dict): Loaded YAML data as dict, empty if not found
"""
if not translation_file.is_file():
LOGGER.warning("No file found for %s", translation_file)
return {}
with open(translation_file, encoding="utf-8") as translation:
yaml = YAML(typ='safe', pure=True)
raw = yaml.load(translation)
return raw
def parse_group_definitions(self, signal_groups):
"""Parse group definitions.
Args:
signal_groups (dict): parsed yaml file.
"""
for dp_name, group_definitions in signal_groups.items():
for group in group_definitions:
port_name = None
if 'portname' in group:
port_name = group.pop('portname')
for group_name, signals in group.items():
self.parse_signal_definitions({dp_name: signals}, group_name, port_name)
def parse_signal_definitions(self, signals_definition, group=None, port_name=None):
"""Parse signal definitions.
Args:
signals_definition (dict): parsed yaml file.
group (str): Name of signal group, if signal belongs to a group.
port_name (str): Name of signal port, if there is one.
"""
enumerations = self.base_application.enumerations
for dp_name, dp_specification in signals_definition.items():
for specification in dp_specification:
in_out_signal = [key for key in specification.keys() if 'signal' in key]
base_signal = None
signal_name = None
if "in" in in_out_signal[0]:
for signal in self.base_application.insignals:
if signal.name == specification["insignal"]:
base_signal = signal
signal_name = signal.name
elif "out" in in_out_signal[0]:
for signal in self.base_application.outsignals:
if signal.name == specification["outsignal"]:
base_signal = signal
signal_name = signal.name
else:
raise BadYamlFormat(f"in/out signal for {dp_name} is missing.")
if base_signal is None:
continue
base_properties = self.base_application.get_signal_properties(
base_signal
)
if base_properties["type"] in enumerations:
underlying_data_type = enumerations[base_properties['type']]['underlying_data_type']
interface_type = underlying_data_type
manifest_type = underlying_data_type
if 'init' not in specification:
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 = specification.get("init", 0)
else:
interface_type = base_properties["type"]
manifest_type = base_properties["type"]
init_value = specification.get("init", 0)
if "out" in in_out_signal[0] and "strategy" in specification:
LOGGER.warning('Cannot set read strategy for outsignal %s, using "Always".', signal_name)
strategy = "Always"
else:
strategy = specification.get("strategy", "Always")
if strategy not in self.read_strategies:
LOGGER.warning('Invalid strategy %s, using "Always" instead.', strategy)
strategy = "Always"
if group is not None and specification.get("portname", None) is not None:
raise BadYamlFormat(f"Port name should be on group level not signal level: {dp_name}")
port_name_tmp = port_name if port_name is not None else specification.get("portname", None)
is_safe_signal = specification.get("dependability", False)
if signal_name not in self.dp_translations:
self.dp_translations[signal_name] = set()
domain = self._get_domain(dp_name)
self.dp_translations[signal_name].add(
(
"enum_0", # read from this tuple using the dp_position enum. Enum starts at 1 though.
domain,
specification["property"],
specification.get("type"),
interface_type,
manifest_type,
specification.get("offset"),
specification.get("factor"),
specification.get("default"),
specification.get("length"),
specification.get("min"),
specification.get("max"),
specification.get("enum"),
init_value,
specification.get("description"),
specification.get("unit"),
group,
strategy,
specification.get("debug", False),
is_safe_signal,
port_name_tmp
)
)
enable_e2e_sts = self.base_application.pybuild['build_cfg'].get_enable_end_to_end_status_signals()
if enable_e2e_sts and is_safe_signal and group is not None:
e2e_sts_property = f"{group}E2eSts"
e2e_sts_signal_name = f"sVc{domain}_D_{e2e_sts_property}"
if signal_name == e2e_sts_signal_name:
raise BadYamlFormat(f"Don't put E2E status signals ({signal_name}) in yaml interface files.")
if e2e_sts_signal_name not in self.dp_translations:
self.dp_translations[e2e_sts_signal_name] = set()
self.dp_translations[e2e_sts_signal_name].add(
(
"enum_0", # read from this tuple using the dp_position enum. Enum starts at 1 though.
domain,
e2e_sts_property,
"UInt8",
"UInt8",
"UInt8",
0,
1,
None,
None,
0,
255,
None,
255,
f"E2E status code for E2E protected signal (group) {signal_name}.",
None,
group,
strategy,
False,
is_safe_signal,
port_name_tmp
)
)
self.e2e_sts_signals.add(e2e_sts_signal_name)
def parse_definition(self, definition):
"""Parses all definition files
Args:
definition (list(Path)): Definition files
"""
for translation in definition:
raw = self.read_translation(translation)
self.parse_signal_definitions(raw.get("signals", {}))
self.parse_group_definitions(raw.get("signal_groups", {}))
def get_signal_properties(self, signal):
"""Get signal properties for signal
Calls self.base_application to get signal properties
Args:
signal (Signal): Signal to get properties for
"""
self.base_application.get_signal_properties(signal)
def _get_signals(self):
"""Read signals"""
self.parse_definition(self.translations_files)
def _get_domain(self, device_proxy):
"""Get domain for device proxy
Args:
device_proxy (str): Name of device proxy
Returns:
domain (str): Name of the domain
"""
if device_proxy not in self.device_domain:
raise MissingDevice(device_proxy)
return self.device_domain[device_proxy]
def _allow_domain(self, domain):
"""Check if device proxy is in current domain_filter
If there is no filter, the device is seen as part of the filter
Args:
domain (str): Name of the domain
Returns:
filtered (bool): True if device is not filtered away
"""
return self.domain_filter is None or domain in self.domain_filter
def get_signals(self, signal_type="insignals"):
"""Get signals to and from a dp abstraction
If it is set to False, we look at the application side.
Args:
signal_type (str): insignals or outsignals
Returns:
signals (list): Signals in the interface
"""
signal_names = self.signal_names["other"][signal_type]
signals = []
for name in self._allowed_names(signal_names):
signals.append(Signal(name, self))
return signals
@property
def insignals(self):
""" Signals going to the device proxy. """
return self.get_signals("insignals")
@property
def outsignals(self):
""" Signals sent from the device proxy. """
return self.get_signals("outsignals")
def dp_spec_to_dict(self, signal_spec, signal_name):
"""Convert signal specification to dict.
Args:
signal_spec (tuple): Signal specification
signal_name (str): Signal name
Returns:
signal_spec (dict): Signal specification
"""
return {
"variable": signal_name,
"variable_type": signal_spec[self.dp_position.variable_type.value],
"property_type": signal_spec[self.dp_position.property_interface_type.value],
"domain": signal_spec[self.dp_position.domain.value],
"default": signal_spec[self.dp_position.default.value],
"length": signal_spec[self.dp_position.length.value],
"property": signal_spec[self.dp_position.property.value],
"offset": signal_spec[self.dp_position.offset.value],
"factor": signal_spec[self.dp_position.factor.value],
"range": {
"min": signal_spec[self.dp_position.min.value],
"max": signal_spec[self.dp_position.max.value],
},
"init": signal_spec[self.dp_position.init.value],
"description": signal_spec[self.dp_position.description.value],
"unit": signal_spec[self.dp_position.unit.value],
"group": signal_spec[self.dp_position.group.value],
"strategy": signal_spec[self.dp_position.strategy.value],
"debug": signal_spec[self.dp_position.debug.value],
"dependability": signal_spec[self.dp_position.dependability.value],
"port_name": signal_spec[self.dp_position.port_name.value]
}
@classmethod
def dp_spec_for_manifest(cls, signal_spec):
"""Convert signal specification to dict for a signal manifest.
Args:
signal_spec (tuple): Signal specification
Returns:
signal_spec (dict): Signal specification
"""
spec = {
"name": signal_spec[cls.dp_position.property.value],
"type": signal_spec[cls.dp_position.property_manifest_type.value],
}
for key, value in {
"default": cls.dp_position.default.value,
"length": cls.dp_position.length.value,
"enum": cls.dp_position.enum.value,
"description": cls.dp_position.description.value,
"unit": cls.dp_position.unit.value,
}.items():
if signal_spec[value] is not None:
spec[key] = signal_spec[value]
if (
signal_spec[cls.dp_position.min.value] is not None
and signal_spec[cls.dp_position.max.value] is not None
and cls.dp_position.enum.value is not None
):
spec["range"] = {
"min": signal_spec[cls.dp_position.min.value],
"max": signal_spec[cls.dp_position.max.value],
}
return spec
def to_dict(self):
"""Method to generate dict to be saved as yaml
Returns:
spec (dict): Signalling specification
"""
spec = {"consumer": [], "producer": []}
for signal_name, signal_spec in self._allowed_names_and_specifications(
self.signal_names["other"]["insignals"]):
spec['consumer'].append(
self.dp_spec_to_dict(signal_spec, signal_name)
)
for signal_name, signal_spec in self._allowed_names_and_specifications(
self.signal_names["other"]["outsignals"]):
spec['producer'].append(
self.dp_spec_to_dict(signal_spec, signal_name)
)
return spec
def to_manifest(self, client_name):
"""Method to generate dict to be saved as yaml
Args:
client_name (str): Name of the client in signal comm
Returns:
spec (dict): Signal manifest for using a Device proxy
"""
manifest = {"name": client_name}
manifest["consumes"] = self.insignals_dp_manifest(client_name)
manifest["produces"] = self.outsignals_dp_manifest(client_name)
manifest = self.cleanup_dp_manifest(manifest)
if "consumes" not in manifest and "produces" not in manifest:
return None
return {"signal_info": {"version": 0.2, "clients": [manifest]}}
def _generator(self, signal_names, unique_names=False):
"""Iterate over signals for allowed devices
If unique_names is True, the iterator does not yield the same signal twice
if unique_names is False, it yields each allowed signal spec with the signal name
Args:
signal_names (list): allowed signals
Yields:
name (str): Name of the signal
specification (dict): signal specification for allowed device
"""
for signal_name, specifications in (
(name, spec) for name, spec in self.dp_translations.items()
if name in signal_names):
for specification in (
spec for spec in specifications
if self._allow_domain(spec[self.dp_position.domain.value])):
if unique_names:
yield signal_name, specification
break
yield signal_name, specification
def _allowed_names(self, signal_names):
""" Iterate over signal names for allowed devices
Args:
signal_names (list): allowed signals
Yields:
name (str): Signal name
"""
for name, _ in self._generator(signal_names, unique_names=True):
yield name
def _allowed_specifications(self, signal_names):
""" Iterate over signal specifications for allowed devices
Args:
signal_names (list): allowed signals
Yields:
specification (dict): Specification for a signal for an allowed device
"""
for _, spec in self._generator(signal_names, unique_names=False):
yield spec
def _allowed_names_and_specifications(self, signal_names):
""" Iterate over signal specifications for allowed devices
Args:
signal_names (list): allowed signals
Yields:
name (str): Signal name
specification (dict): Specification for the signal for an allowed device
"""
for name, spec in self._generator(signal_names, unique_names=False):
yield name, spec
def insignals_dp_manifest(self, client_name):
""" Create consumes part of manifest for reading signals from device proxies
Args:
client_name (str): Name of the client in signal comm
"""
consumes = [{"name": client_name, "signal_groups": []}]
signal_names = self.signal_names["other"]["insignals"]
consumed_groups = set()
for signal_spec in self._allowed_specifications(signal_names):
group = signal_spec[self.dp_position.group.value]
if group is not None:
consumed_groups.add(group)
else:
consumes[0]["signal_groups"].append(
{"name": signal_spec[self.dp_position.property.value]}
)
for group in consumed_groups:
consumes[0]["signal_groups"].append(
{"name": group}
)
return consumes
def outsignals_dp_manifest(self, client_name):
""" Update manifests for writing signals to device proxies
Args:
client_name (str): Name of the client in signal comm
"""
produces = [{"name": client_name, "signals": [], "signal_groups": []}]
signal_names = self.signal_names["other"]["outsignals"]
group_signals = {}
for signal_spec in self._allowed_specifications(signal_names):
group = signal_spec[self.dp_position.group.value]
if group is not None:
if group not in group_signals:
group_signals[group] = []
group_signals[group].append(
self.dp_spec_for_manifest(signal_spec)
)
else:
produces[0]["signals"].append(
self.dp_spec_for_manifest(signal_spec)
)
for group_name, signals in group_signals.items():
produces[0]["signal_groups"].append(
{"name": group_name,
"signals": list(signals)}
)
return produces
@staticmethod
def cleanup_dp_manifest(manifest):
""" Remove empty device proxies.
Args:
manifest (dict): Device proxy configurations
"""
if not manifest["produces"][0]["signal_groups"]:
del manifest["produces"][0]["signal_groups"]
if not manifest["produces"][0]["signals"]:
del manifest["produces"][0]["signals"]
if list(manifest["produces"][0].keys()) == ["name"]:
del manifest["produces"]
if not manifest["consumes"][0]["signal_groups"]:
del manifest["consumes"]
return manifest