501 lines
17 KiB
Python
501 lines
17 KiB
Python
# Copyright 2024 Volvo Car Corporation
|
|
# Licensed under Apache 2.0.
|
|
|
|
# -*- coding: utf-8 -*-
|
|
"""Python module used for abstracting an application that should be interfacing others."""
|
|
from abc import abstractmethod
|
|
from ruamel.yaml import YAML
|
|
from powertrain_build.lib import logger
|
|
|
|
LOGGER = logger.create_logger('base')
|
|
|
|
|
|
def filter_signals(signals, domain):
|
|
""" Take a list of signals and remove all domains belonging to a domain
|
|
|
|
If the signal is part of the domain, it is not part of the resulting list
|
|
|
|
Arguments:
|
|
signals (list(Signal)): signals to filter
|
|
domain (Domain): domain that the signals should not be part of
|
|
"""
|
|
filtered_signals = []
|
|
for signal in signals:
|
|
if signal.name not in domain.signals:
|
|
filtered_signals.append(signal)
|
|
return filtered_signals
|
|
|
|
|
|
class MultipleProducersError(Exception):
|
|
"""Error when setting a producer and there already exists one"""
|
|
def __init__(self, signal, old_producer, new_producer):
|
|
"""Set error message
|
|
|
|
Args:
|
|
signal (Signal): Signal object
|
|
old_producer (BaseApplication): Producer already registered
|
|
new_producer (BaseApplication): Producer attempted to be registered
|
|
"""
|
|
super().__init__()
|
|
self.message = (f"{signal.name}:"
|
|
f" Attempting to set producer {new_producer}"
|
|
f" when {old_producer} is already set")
|
|
|
|
|
|
class BaseApplication:
|
|
"""Base application to build other adapters on"""
|
|
name = str()
|
|
_signals = None
|
|
node = str() # Used to calculate interface
|
|
|
|
read_strategies = {
|
|
"Always",
|
|
"OnChanged",
|
|
"OnUpdated"
|
|
}
|
|
|
|
def __repr__(self):
|
|
"""String representation for logging and debugging
|
|
|
|
Returns:
|
|
repr (string): Name of the application, the number of insignals and outsignals
|
|
"""
|
|
return f"<{self.name}" \
|
|
f" insignals:{len(self.insignals)}" \
|
|
f" outsignals:{len(self.outsignals)}>"
|
|
|
|
def parse_signals(self):
|
|
"""API interface to read all signals in any child object"""
|
|
self._get_signals()
|
|
|
|
@property
|
|
def insignals(self):
|
|
""" Insignals to the raster.
|
|
Calculated as all read ports - all written ports
|
|
|
|
Returns:
|
|
signals (list): List of Signal objects.
|
|
"""
|
|
if self._insignals is None:
|
|
self._get_signals()
|
|
return [self._signals[port] for port in self._insignals - self._outsignals]
|
|
|
|
@property
|
|
def outsignals(self):
|
|
""" All outports.
|
|
Since we might consume some of the signals that should also be sent elsewhere,
|
|
we do not remove internally consumed signals.
|
|
|
|
Returns:
|
|
signals (list): List of Signal objects.
|
|
"""
|
|
if self._outsignals is None:
|
|
self._get_signals()
|
|
return [self._signals[port] for port in self._outsignals]
|
|
|
|
@property
|
|
def signals(self):
|
|
"""API interface property in any child object
|
|
|
|
All cached signals.
|
|
If no cache exists, reads all signals and save to cache.
|
|
|
|
Returns:
|
|
signals (list): Signal objects
|
|
"""
|
|
if self._signals is None:
|
|
self.parse_signals()
|
|
return self._signals.values()
|
|
|
|
@abstractmethod
|
|
def _get_signals(self):
|
|
"""Stub to implement in child object"""
|
|
|
|
@abstractmethod
|
|
def get_signal_properties(self, signal):
|
|
"""Stub to implement in child object
|
|
|
|
Ideally, this should be moved to the signal.
|
|
Currently, getting the properties depends on how we read and define the signals.
|
|
"""
|
|
|
|
@abstractmethod
|
|
def parse_definition(self, definition):
|
|
"""Stub for parsing a defintion after the object has been initialized.
|
|
|
|
Raises NotImplementedError if called without being implemented.
|
|
|
|
Args:
|
|
definition: Definition of the Application. Type depends on the application.
|
|
"""
|
|
raise NotImplementedError('This is a stub')
|
|
|
|
|
|
class Signal:
|
|
"""Signal object
|
|
|
|
The signal should behave the same way independently of where we define it.
|
|
"""
|
|
def __repr__(self):
|
|
"""String representation for logging and debugging
|
|
|
|
Returns:
|
|
repr (string): Name of the application, the number of insignals and outsignals
|
|
"""
|
|
return (f"<{self.name} in {self.applications}"
|
|
f" producer:{self.producer}"
|
|
f" consumers:{self.consumers}>")
|
|
|
|
def __init__(self, name, application):
|
|
"""Define base properties of the signal object
|
|
|
|
The application object is used to read properties of a signal.
|
|
TODO: Do this when we define the signal and add properties known in other
|
|
systems when we encounter them.
|
|
|
|
Args:
|
|
name (string): Signal name
|
|
application (BaseApplication): Application defining the signal
|
|
"""
|
|
self.name = name
|
|
self.applications = {} # Add applications to a dict to prevent duplicates
|
|
if application is not None:
|
|
self.applications[application.name] = application
|
|
self._consumers = set()
|
|
self._producer = None
|
|
|
|
def add_application(self, application):
|
|
"""Add an application to find properties from
|
|
|
|
Args:
|
|
application (BaseApplication): Application to read properties from
|
|
"""
|
|
if application.name in self.applications:
|
|
return
|
|
self.applications[application.name] = application
|
|
|
|
@property
|
|
def consumers(self):
|
|
"""Get all consumers of a signal
|
|
|
|
Returns:
|
|
consumers (set): All consumers of a signal
|
|
"""
|
|
if isinstance(self._consumers, set):
|
|
return self._consumers
|
|
return set()
|
|
|
|
@consumers.setter
|
|
def consumers(self, consumers):
|
|
"""Set consumers of a signal
|
|
|
|
If the consumers is a list or set, iterate over each consumer
|
|
Otherwise, add the consumer to the set of consumers
|
|
|
|
Args:
|
|
consumers (list/set/string): consumer(s) of a signal
|
|
"""
|
|
if isinstance(consumers, (list, set)):
|
|
for consumer in consumers:
|
|
self._consumers.add(consumer)
|
|
else:
|
|
self._consumers.add(consumers)
|
|
|
|
@property
|
|
def producer(self):
|
|
"""Get the producer of a signal
|
|
|
|
Since we have some strange signals with multiple producers,
|
|
such as counters for dep, this returns a set.
|
|
Returns:
|
|
producer (set): Producer(s) of a signal
|
|
"""
|
|
if isinstance(self._producer, set):
|
|
return self._producer
|
|
return set()
|
|
|
|
@producer.setter
|
|
def producer(self, producer):
|
|
"""Set producer of a signal
|
|
|
|
Args:
|
|
producer (string/set): Name of the producer
|
|
"""
|
|
if isinstance(producer, set):
|
|
self._producer = producer
|
|
else:
|
|
self._producer = {producer}
|
|
|
|
def set_producer(self, producer):
|
|
"""Set producer of a signal
|
|
|
|
If there already is a registered producer of the signal,
|
|
raise MultipleProducersError
|
|
|
|
This can be expected and force_producer can be called to override this.
|
|
That must be explicit in each instance.
|
|
|
|
Args:
|
|
producer (string): Name of the producer
|
|
application (BaseApplication): Application defining the signal. Optional
|
|
"""
|
|
if isinstance(producer, set):
|
|
if self._producer is not None and producer - self._producer:
|
|
raise MultipleProducersError(self, self._producer, producer)
|
|
self.producer = producer
|
|
else:
|
|
if self._producer is not None \
|
|
and isinstance(producer, str) \
|
|
and producer not in self._producer:
|
|
raise MultipleProducersError(self, self._producer, producer)
|
|
self.producer = {producer}
|
|
|
|
def force_producer(self, producer):
|
|
"""Forcefully update add producers of a signal
|
|
|
|
This is needed since we have some signals that are written by multiple model
|
|
Args:
|
|
producers (string): Producer of a signal
|
|
application (BaseApplication): Application defining the signal. Optional
|
|
"""
|
|
|
|
self._producer.add(producer)
|
|
|
|
@property
|
|
def properties(self):
|
|
"""Properties of a signal
|
|
|
|
Currently not homogenized.
|
|
Therefore we read the properties from the application that defined the signal.
|
|
|
|
Returns:
|
|
properties (dict): properties of a signal
|
|
"""
|
|
properties = {}
|
|
for application in self.applications.values():
|
|
LOGGER.debug('Getting properties for %s from %s', self.name, application.name)
|
|
application_properties = application.get_signal_properties(self)
|
|
LOGGER.debug(application_properties)
|
|
for key, value in application_properties.items():
|
|
LOGGER.debug('Looking at %s: %s', key, value)
|
|
if key in properties and value != properties[key]:
|
|
LOGGER.debug('Signal %s already has %s with value %s, ignoring %s from %s',
|
|
self.name, key, properties[key], value, application.name)
|
|
continue
|
|
properties[key] = value
|
|
return properties
|
|
|
|
|
|
class Interface:
|
|
"""Interface between two objects"""
|
|
def __repr__(self):
|
|
"""String representation for logging and debugging
|
|
|
|
Returns:
|
|
repr (string): Name of the interface, and the length of received and transmitted signals
|
|
"""
|
|
return (f"<{self.name}"
|
|
f" a->b:{len(self.get_directional_signals(self.current, self.corresponding))}"
|
|
f" b->a:{len(self.get_directional_signals(self.corresponding, self.current))}>")
|
|
|
|
def debug(self):
|
|
"""Debug an interface object to stdout"""
|
|
LOGGER.info('name: %s', self.name)
|
|
for signal in self.get_directional_signals(self.current, self.corresponding):
|
|
LOGGER.info('insignal: %s', signal)
|
|
for signal in self.get_directional_signals(self.corresponding, self.current):
|
|
LOGGER.info('outsignal: %s', signal)
|
|
|
|
def __init__(self, current, corresponding):
|
|
"""Create the interface object
|
|
|
|
Args:
|
|
current (BaseApplication): Primary object of an interface
|
|
corresponding (BaseApplication): Secondary object of an interface
|
|
"""
|
|
self.name = current.name + '_' + corresponding.name
|
|
self.current = current
|
|
self.corresponding = corresponding
|
|
|
|
@staticmethod
|
|
def get_directional_signals(producer, consumer):
|
|
"""Get signals going from producer to consumer
|
|
|
|
Args:
|
|
producer (BaseApplication): producer of the signals
|
|
consumer (BaseApplication): consumer of the signals
|
|
Returns:
|
|
signals (list): Signals sent from producer and received in consumer
|
|
"""
|
|
outsignals = {signal.name: signal for signal in producer.outsignals}
|
|
signals = []
|
|
for signal in consumer.insignals:
|
|
if signal.name in outsignals:
|
|
signal.set_producer(outsignals[signal.name].producer)
|
|
signal.add_application(producer)
|
|
signal.add_application(consumer)
|
|
signals.append(signal)
|
|
return signals
|
|
|
|
def get_produced_signals(self, producer_name):
|
|
"""Get signals going from producer to consumer
|
|
|
|
This function can be used if you are lacking some objects
|
|
|
|
Args:
|
|
consumer_name (string): name of the consumer of the signals
|
|
Returns:
|
|
signals (list): Signals sent from producer and received in consumer
|
|
"""
|
|
if producer_name == self.current.name:
|
|
consumer = self.corresponding
|
|
producer = self.current
|
|
elif producer_name == self.corresponding.name:
|
|
consumer = self.current
|
|
producer = self.corresponding
|
|
else:
|
|
LOGGER.error('%s not in [%s, %s]',
|
|
producer_name,
|
|
self.current.name,
|
|
self.corresponding.name)
|
|
return self.get_directional_signals(producer, consumer)
|
|
|
|
def get_consumed_signals(self, consumer_name):
|
|
"""Get signals going from producer to consumer
|
|
|
|
This function can be used if you are lacking some objects
|
|
|
|
Args:
|
|
consumer_name (string): name of the consumer of the signals
|
|
Returns:
|
|
signals (list): Signals sent from producer and received in consumer
|
|
"""
|
|
if consumer_name == self.current.name:
|
|
consumer = self.current
|
|
producer = self.corresponding
|
|
elif consumer_name == self.corresponding.name:
|
|
consumer = self.corresponding
|
|
producer = self.current
|
|
else:
|
|
LOGGER.error('%s not in [%s, %s]',
|
|
consumer_name,
|
|
self.current.name,
|
|
self.corresponding.name)
|
|
return self.get_directional_signals(producer, consumer)
|
|
|
|
|
|
class Domain:
|
|
"""Domain with interacting interfaces"""
|
|
def __repr__(self):
|
|
"""String representation for logging and debugging
|
|
|
|
Returns:
|
|
repr (string): Name of the domain, and all clients for that domain
|
|
"""
|
|
return f"<{self.name}: {self.clients}>"
|
|
|
|
def __init__(self):
|
|
"""Initialize the object"""
|
|
self.name = ''
|
|
self.signals = {}
|
|
self.clients = set()
|
|
self._clients = {}
|
|
|
|
def set_name(self, name):
|
|
"""Set the name of the domain
|
|
|
|
Args:
|
|
name (string): Name of the domain
|
|
"""
|
|
self.name = name
|
|
|
|
def add_interface(self, interface):
|
|
"""Add an interface to a domain
|
|
|
|
Args:
|
|
interface (Interface): Interface object
|
|
"""
|
|
self._process_interface(interface)
|
|
|
|
def _process_interface(self, interface):
|
|
"""Process interface to add signals to the domain
|
|
|
|
Args:
|
|
interface (Interface): Interface object
|
|
"""
|
|
for signal in interface.get_directional_signals(interface.current, interface.corresponding):
|
|
self._process_signal(signal.name, interface.current, interface.corresponding)
|
|
for signal in interface.get_directional_signals(interface.corresponding, interface.current):
|
|
self._process_signal(signal.name, interface.corresponding, interface.current)
|
|
|
|
def _process_signal(self, signal_name, producer, consumer):
|
|
"""Process signal to add to the domain
|
|
|
|
Args:
|
|
signal_name (string): Name of the signal
|
|
consumer (BaseApplication): Consumer application of the signal
|
|
producer (BaseApplication): Producer application of the signal
|
|
"""
|
|
if signal_name not in self.signals:
|
|
self.signals[signal_name] = Signal(signal_name, producer)
|
|
self.signals[signal_name].consumers = consumer.name
|
|
self.signals[signal_name].add_application(producer)
|
|
self.signals[signal_name].add_application(consumer)
|
|
signal = self.signals[signal_name]
|
|
if producer.name not in self.clients:
|
|
self.clients.add(producer.name)
|
|
self._clients[producer.name] = {'producer': [], 'consumer': []}
|
|
self._clients[producer.name]['producer'].append(signal)
|
|
if consumer.name not in self.clients:
|
|
self.clients.add(producer.name)
|
|
self._clients[producer.name] = {'producer': [], 'consumer': []}
|
|
self._clients[producer.name]['consumer'].append(signal)
|
|
signal.consumers = consumer.name
|
|
try:
|
|
signal.producer = producer.name
|
|
except MultipleProducersError as mpe:
|
|
LOGGER.debug(mpe.message)
|
|
|
|
def create_groups(self):
|
|
"""Create groups of signals going from each producer
|
|
|
|
Returns:
|
|
signal_groups (dict): Signal groups
|
|
"""
|
|
signal_groups = {}
|
|
for signal in self.signals.values():
|
|
# Producer is always a set, to handle pass-through signals
|
|
for producer in signal.producer - set(signal_groups.keys()):
|
|
signal_groups[producer] = []
|
|
for producer in signal.producer:
|
|
signal_groups[producer].append(signal)
|
|
return signal_groups
|
|
|
|
def create_selective_groups(self, a_names, b_names):
|
|
"""Create groups for the a_list communicating with the b_names
|
|
|
|
Returns:
|
|
signal_groups (dict): Signal groups
|
|
"""
|
|
signal_groups = {name: {'consumer': [], 'producer': []} for name in a_names}
|
|
for signal in self.signals.values():
|
|
for producer in set(signal.producer):
|
|
if producer in a_names and signal.consumers & b_names:
|
|
signal_groups[producer]['producer'].append(signal)
|
|
for consumer in signal.consumers:
|
|
if consumer in a_names and set(signal.producer) & a_names:
|
|
signal_groups[consumer]['consumer'].append(signal)
|
|
return signal_groups
|
|
|
|
@staticmethod
|
|
def to_yaml(spec, output):
|
|
"""Writes spec to yaml file
|
|
|
|
Args:
|
|
spec (dict): data for the yaml
|
|
output (Path): file to write to
|
|
"""
|
|
with open(output, 'w', encoding="utf-8") as yaml_file:
|
|
yaml = YAML()
|
|
yaml.dump(spec, yaml_file)
|