Henrik Wahlqvist 65c1d746a7 Copy from Volvo Cars local project
We don't transfer git history since it may contain proprietary data that
we cannot have in an open sources version.

Change-Id: I9586124c1720db69a76b9390e208e9f0ba3b86d4
2024-05-29 08:03:54 +02:00

583 lines
22 KiB
Python

# Copyright 2024 Volvo Car Corporation
# Licensed under Apache 2.0.
# -*- coding: utf-8 -*-
"""Module for a2l-file generation."""
import sys
import re
import logging
from string import Template
from pprint import pformat
from pybuild.problem_logger import ProblemLogger
from pybuild.types import a2l_type, a2l_range
LOG = logging.getLogger()
class A2l(ProblemLogger):
"""Class for a2l-file generation."""
def __init__(self, var_data_dict, prj_cfg):
"""Generate a2l-file from provided data dictionary.
Args:
var_data_dict (dict): dict defining all variables and parameters in a2l
Sample indata structure:
::
{
"function": "rVcAesSupM",
"vars": {
"rVcAesSupM_p_SupMonrSCTarDly": {
"var": {
"type": "Float32",
"cvc_type": "CVC_DISP"
},
"a2l_data": {
"unit": "kPa",
"description": "Low pass filtered supercharger target pressure",
"max": "300",
"min": "0",
"lsb": "1",
"offset": "0",
"bitmask": None,
"x_axis": None,
"y_axis": None,
},
"array": None
}
}
}
"""
super().__init__()
self._var_dd = var_data_dict
self._prj_cf = prj_cfg
self._axis_ref = None
self._axis_data = None
self._compu_meths = None
self._rec_layouts = None
self._fnc_outputs = None
self._fnc_inputs = None
self._fnc_locals = None
self._fnc_char = None
# generate the a2l string
self._gen_a2l()
def gen_a2l(self, filename):
"""Write a2l-data to file.
Args:
filename (str): Name of the generated a2l-file.
"""
with open(filename, 'w', encoding="utf-8") as a2l:
a2l.write(self._a2lstr)
self.debug('Generated %s', filename)
def _gen_a2l(self):
"""Generate an a2l-file based on the supplied data dictionary."""
self._gen_compu_methods()
# self.debug('_compu_meths')
# self.debug(pp.pformat(self._compu_meths))
self._find_axis_ref()
# self.debug("_axis_ref")
# self.debug(pp.pformat(self._axis_ref))
self._find_axis_data()
self._gen_record_layouts_data()
# self.debug("_axis_data")
# self.debug(pp.pformat(self._axis_data))
# self.debug("_rec_layouts")
# self.debug(pp.pformat(self._rec_layouts))
self._check_axis_ref()
output_meas = ''
output_char = ''
output_axis = ''
self._fnc_outputs = []
self._fnc_inputs = []
self._fnc_locals = []
self._fnc_char = []
for var, data in self._var_dd['vars'].items():
try:
cvc_type = data['var']['cvc_type']
if 'CVC_DISP' in cvc_type:
output_meas += self._gen_a2l_measurement_blk(var, data)
self._fnc_locals.append(var)
elif 'CVC_CAL' in cvc_type:
self._fnc_char.append(var)
srch = re.search('_[rcxyXY]$', var)
if srch is None:
output_char += self._gen_a2l_characteristic_blk(var, data)
else:
output_axis += self._gen_a2l_axis_pts_blk(var, data)
elif cvc_type == 'CVC_NVM':
output_meas += self._gen_a2l_measurement_blk(var, data)
self._fnc_locals.append(var)
elif cvc_type == 'CVC_IN':
self._outputs = None
self._inputs = None
self._fnc_inputs.append(var)
elif cvc_type == 'CVC_OUT':
self._fnc_outputs.append(var)
except TypeError:
self.warning("Warning: %s has no A2l-data", var)
except Exception as e:
self.critical("Unexpected error: %s", sys.exc_info()[0])
raise e
# generate COMPU_METHS
output_compu_m = ''
for k in self._compu_meths:
output_compu_m += self._gen_a2l_compu_metod_blk(k)
# generate FUNCTIONS
output_funcs = self._gen_a2l_function_blk()
# generate RECORD_LAYOUTS
output_recl = ''
for k in self._rec_layouts:
output_recl += self._gen_a2l_rec_layout_blk(k)
output = output_char + output_axis + output_meas + \
output_compu_m + output_funcs + output_recl
# self.debug(pp.pformat(self._var_dd))
# self.debug('Output:')
# self.debug(output)
self._a2lstr = output
def _find_axis_data(self):
"""Parse all variables and identify axis points.
TODO: Change this function to check for names with _r | _c
suffixes
"""
self._axis_data = {}
variable_names = self._var_dd['vars'].keys()
for name in variable_names:
x_nm = y_nm = None
if name + '_x' in variable_names:
x_nm = name + '_x'
if name + '_y' in variable_names:
y_nm = name + '_x'
if x_nm is not None or y_nm is not None:
self._axis_data[name] = (x_nm, y_nm)
def _find_axis_ref(self):
"""Parse all variables and identify which are defined as axis points."""
self._axis_ref = {}
for var, data in self._var_dd['vars'].items():
if data.get('a2l_data') is not None:
x_axis = data['a2l_data'].get('x_axis')
y_axis = data['a2l_data'].get('y_axis')
if x_axis is not None:
if x_axis in self._axis_ref:
self._axis_ref[x_axis]['used_in'].append((var, 'x'))
else:
self._axis_ref[x_axis] = {'used_in': [(var, 'x')]}
if y_axis is not None:
if y_axis in self._axis_ref:
self._axis_ref[y_axis]['used_in'].append((var, 'y'))
else:
self._axis_ref[y_axis] = {'used_in': [(var, 'y')]}
@classmethod
def _get_a2d_minmax(cls, a2d, ctype=None):
"""Get min max limits from a2l data.
Gives max limits if min/max limits are undefined.
"""
typelim = a2l_range(ctype)
minlim = a2d.get('min')
if minlim is None or minlim == '-':
minlim = typelim[0]
maxlim = a2d.get('max')
if maxlim is None or maxlim == '-':
maxlim = typelim[1]
return minlim, maxlim
def _check_axis_ref(self):
"""Check that the axis definitions are defined in the code."""
undef_axis = [ax for ax in self._axis_ref
if ax not in self._var_dd['vars']]
if undef_axis:
self.warning(f'Undefined axis {pformat(undef_axis)}')
def _gen_compu_methods(self):
"""Generate COMPU_METHOD data, and add it into the var_data_dict."""
self._compu_meths = {}
for var, data in self._var_dd['vars'].items():
a2d = data.get('a2l_data')
if a2d is not None:
lsb = self._calc_lsb(a2d['lsb'])
offset_str = str(a2d['offset'])
is_offset_num = bool(re.match('[0-9]', offset_str))
if is_offset_num:
offset = float(offset_str)
else:
offset = 0
key = (lsb, offset, a2d['unit'])
self._var_dd['vars'][var]['compu_meth'] = key
name = self._compu_key_2_name(key)
if key in self._compu_meths:
self._compu_meths[key]['vars'].append(var)
else:
self._compu_meths[key] = {'name': name,
'vars': [var],
'coeffs': self._get_coefs_str(lsb,
offset)}
def _compu_key_2_name(self, key):
"""Generate a COMPU_METHOD name from the keys in the name.
Args:
key (tuple): a list with compumethod keys (lsb, offset, unit)
"""
conversion_list = [(r'[\./]', '_'), ('%', 'percent'),
('-', 'None'), (r'\W', '_')]
name = f"{self._var_dd['function']}_{key[0]}_{key[1]}_{key[2]}"
for frm, to_ in conversion_list:
name = re.sub(frm, to_, name)
return name
@staticmethod
def _array_to_a2l_string(array):
"""Convert c-style array definitions to A2L MATRIX_DIM style."""
if not isinstance(array, list):
array = [array]
dims = [1, 1, 1]
for i, res in enumerate(array):
dims[i] = res
return f"MATRIX_DIM {dims[0]} {dims[1]} {dims[2]}"
@staticmethod
def _get_coefs_str(lsb, offset):
"""Calculate the a2l-coeffs from the lsb and offs fields.
The fields are defined in the a2l_data dictionary.
"""
return f"COEFFS 0 1 {offset} 0 0 {lsb}"
@staticmethod
def _calc_lsb(lsb):
"""Convert 2^-2, style lsbs to numericals."""
if isinstance(lsb, str):
if lsb == '-':
return 1
shift = re.match(r'(\d+)\^([\-+0-9]+)', lsb)
if shift is not None:
lsb_num = pow(int(shift.group(1)), int(shift.group(2)))
else:
lsb_num = float(lsb)
return lsb_num
return lsb
def _gen_record_layouts_data(self):
"""Generate record layouts."""
self._rec_layouts = {}
for var, data in self._var_dd['vars'].items():
if data.get('a2l_data') is not None:
a2l_unit = a2l_type(data['var']['type'])
# if calibration data has a suffix of _x of _y it is a axis_pts
srch = re.search('_[xyXY]$', var)
if srch is not None:
name = a2l_unit + "_X_INCR_DIRECT"
self._rec_layouts[name] = f"AXIS_PTS_X 1 {a2l_unit} INDEX_INCR DIRECT"
data['rec_layout'] = name
else:
name = a2l_type(data['var']['type']) + "_COL_DIRECT"
self._rec_layouts[name] = f"FNC_VALUES 1 {a2l_unit} COLUMN_DIR DIRECT"
data['rec_layout'] = name
def _get_inpq_data(self, inp_quant):
"""Get the necessary InputQuantity parameters."""
if inp_quant is not None:
if inp_quant in self._var_dd['vars']:
return inp_quant
return 'NO_INPUT_QUANTITY'
# Bosh template
_meas_tmplt = Template("""
/begin MEASUREMENT
$Name /* Name */
"$LongIdent" /* LongIdentifier */
$Datatype /* Datatype */
$Conversion /* Conversion */
1 /* Resolution */
0 /* Accuracy */
$LowerLimit /* LowerLimit */
$UpperLimit /* UpperLimit */
$OptionalData
ECU_ADDRESS 0x00000000
/end MEASUREMENT
""")
# Denso template
_meas_tmplt_nvm = Template("""
/begin MEASUREMENT
$Name /* Name */
"$LongIdent" /* LongIdentifier */
$Datatype /* Datatype */
$Conversion /* Conversion */
1 /* Resolution */
0 /* Accuracy */
$LowerLimit /* LowerLimit */
$UpperLimit /* UpperLimit */
$OptionalData
/end MEASUREMENT
""")
def _gen_a2l_measurement_blk(self, var_name, data):
"""Generate an a2l MEASUREMENT block."""
opt_data = 'READ_WRITE'
a2d = data.get('a2l_data')
if a2d is not None:
c_type = data['var']['type']
# if c_type == 'Bool':
# opt_data += '\n' + ' ' * 8 + "BIT_MASK 0x1"
if a2d.get('bitmask') is not None:
opt_data += '\n' + ' ' * 8 + "BIT_MASK %s" % a2d['bitmask']
if data.get('array'):
opt_data += '\n' + ' ' * 8 + \
self._array_to_a2l_string(data['array'])
ecu_supplier, _ = self._prj_cf.get_ecu_info()
if a2d.get('symbol'):
if ecu_supplier == 'Denso':
opt_data += '\n' + ' ' * 8 + 'SYMBOL_LINK "%s" %s' % (a2d['symbol'], a2d.get('symbol_offset'))
LOG.debug('This a2l is for Denso %s', opt_data)
elif ecu_supplier in ['RB', 'CSP', 'HI', 'ZC']:
var_name = a2d['symbol'] + '._' + var_name
LOG.debug('This a2l is for %s %s', ecu_supplier, var_name)
dtype = a2l_type(c_type)
minlim, maxlim = self._get_a2d_minmax(a2d, c_type)
conv = self._compu_meths[data['compu_meth']]['name']
if a2d.get('symbol') and ecu_supplier == 'Denso':
res = self._meas_tmplt_nvm.substitute(Name=var_name,
LongIdent=a2d['description'].replace('"', '\\"'),
Datatype=dtype,
Conversion=conv,
LowerLimit=minlim,
UpperLimit=maxlim,
OptionalData=opt_data)
else:
res = self._meas_tmplt.substitute(Name=var_name,
LongIdent=a2d['description'].replace('"', '\\"'),
Datatype=dtype,
Conversion=conv,
LowerLimit=minlim,
UpperLimit=maxlim,
OptionalData=opt_data)
return res
return None
_char_tmplt = Template("""
/begin CHARACTERISTIC
$Name /* Name */
"$LongIdent" /* LongIdentifier */
$Type /* Datatype */
0x00000000 /* address: $Name */
$Deposit /* Deposit */
0 /* MaxDiff */
$Conversion /* Conversion */
$LowerLimit /* LowerLimit */
$UpperLimit /* UpperLimit */$OptionalData
/end CHARACTERISTIC
""")
def _gen_a2l_characteristic_blk(self, var, data):
"""Generate an a2l CHARACTERISTIC block."""
opt_data = ''
a2d = data.get('a2l_data')
type_ = 'WRONG_TYPE'
if a2d is not None:
arr = data.get('array')
if arr is not None:
arr_dim = len(arr)
else:
arr_dim = 0
# Check is axis_pts are defined for the axis, if not make the
# type a VAL_BLK with matrix dimension, otherwise set
# a CURVE or MAP type
# If arr_dim is 0 the CHARACTERISTIC is a value
if arr_dim == 0:
type_ = 'VALUE'
elif arr_dim == 1:
x_axis_name = var + '_x'
# Check if axis variable is defined
if x_axis_name in self._var_dd['vars'].keys():
type_ = 'CURVE'
opt_data += self._gen_a2l_axis_desc_blk(self._get_inpq_data(a2d.get('x_axis')),
x_axis_name)
else:
type_ = 'VAL_BLK'
opt_data += self._array_to_a2l_string(data['array'])
elif arr_dim == 2:
x_axis_name = var + '_x'
y_axis_name = var + '_y'
# Check if axis variable is defined
nbr_def_axis = 0
if x_axis_name in self._var_dd['vars'].keys():
nbr_def_axis += 1
if y_axis_name in self._var_dd['vars'].keys():
nbr_def_axis += 1
if nbr_def_axis == 2:
type_ = 'MAP'
inpq_x = self._get_inpq_data(a2d['x_axis'])
opt_data += self._gen_a2l_axis_desc_blk(inpq_x, x_axis_name)
inpq_y = self._get_inpq_data(a2d['y_axis'])
opt_data += self._gen_a2l_axis_desc_blk(inpq_y, y_axis_name)
elif nbr_def_axis == 0:
type_ = 'VAL_BLK'
opt_data += self._array_to_a2l_string(data['array'])
else:
self.warning(
'MAP %s has only one AXIS_PTS defined, shall be none or two', var)
minlim, maxlim = self._get_a2d_minmax(a2d)
res = self._char_tmplt.substitute(Name=var,
LongIdent=a2d['description'].replace('"', '\\"'),
Type=type_,
Deposit=data['rec_layout'],
Conversion=self._compu_meths[data['compu_meth']]['name'],
LowerLimit=minlim,
UpperLimit=maxlim,
OptionalData=opt_data)
return res
self.warning("%s has no A2L-data", var)
return None
# Types ASCII CURVE MAP VAL_BLK VALUE
_axis_desc_tmplt = Template("""
/begin AXIS_DESCR
COM_AXIS /* Attribute */
$inp_quant /* InputQuantity */
$conv /* Conversion */
$maxaxispts /* MaxAxisPoints */
$minlim /* LowerLimit */
$maxlim /* UpperLimit */
AXIS_PTS_REF $axis_pts_ref
DEPOSIT ABSOLUTE
/end AXIS_DESCR""")
def _gen_a2l_axis_desc_blk(self, inp_quant, axis_pts_ref):
"""Generate an a2l AXIS_DESCR block.
TODO: Check that the AXIS_PTS_REF blocks are defined
"""
out = ''
inp_quant_txt = self._get_inpq_data(inp_quant)
axis_pts = self._var_dd['vars'][axis_pts_ref]
conv = self._compu_meths[axis_pts['compu_meth']]['name']
max_axis_pts = axis_pts['array'][0]
min_lim, max_lim = self._get_a2d_minmax(axis_pts['a2l_data'])
out += self._axis_desc_tmplt.substitute(inp_quant=inp_quant_txt,
conv=conv,
maxaxispts=max_axis_pts,
minlim=min_lim,
maxlim=max_lim,
axis_pts_ref=axis_pts_ref)
return out
_compu_meth_tmplt = Template("""
/begin COMPU_METHOD
$name /* Name */
"$longident" /* LongIdentifier */
RAT_FUNC /* ConversionType */
"$format" /* Format */
"$unit" /* Unit */
$coeffs
/end COMPU_METHOD
""")
def _gen_a2l_compu_metod_blk(self, key):
"""Generate an a2l COMPU_METHOD block."""
cmeth = self._compu_meths[key]
name = self._compu_key_2_name(key)
out = self._compu_meth_tmplt.substitute(name=name,
longident='',
format='%11.3',
unit=key[2],
coeffs=cmeth['coeffs'])
return out
_axis_tmplt = Template("""
/begin AXIS_PTS
$name /* Name */
"$longident" /* LongIdentifier */
0x00000000
NO_INPUT_QUANTITY /* InputQuantity */
$deposit /* Deposit */
0 /* MaxDiff */
$convert /* Conversion */
$max_ax_pts /* MaxAxisPoints */
$minlim /* LowerLimit */
$maxlim /* UpperLimit */
DEPOSIT ABSOLUTE
/end AXIS_PTS
""")
def _gen_a2l_axis_pts_blk(self, var, data):
"""Generate an a2l AXIS_PTS block."""
deposit = data['rec_layout']
conv = self._compu_meths[data['compu_meth']]['name']
max_axis_pts = data['array'][0]
min_lim, max_lim = self._get_a2d_minmax(data['a2l_data'])
out = self._axis_tmplt.substitute(name=var,
longident=data['a2l_data']['description'],
deposit=deposit,
convert=conv,
max_ax_pts=max_axis_pts,
minlim=min_lim,
maxlim=max_lim)
return out
_rec_layout_tmplt = Template("""
/begin RECORD_LAYOUT
$name /* Name */
$string
/end RECORD_LAYOUT
""")
def _gen_a2l_rec_layout_blk(self, key):
"""Generate an a2l AXIS_PTS block."""
string = self._rec_layouts[key]
out = self._rec_layout_tmplt.substitute(name=key,
string=string)
return out
def _gen_a2l_function_blk(self):
"""Generate an a2l FUNCTION block."""
out = '\n /begin FUNCTION\n'
out += f' {self._var_dd["function"]} /* Name */\n'
out += ' "" /* LongIdentifier */\n'
if self._fnc_char:
out += ' /begin DEF_CHARACTERISTIC\n'
for idf in self._fnc_char:
out += f' {idf} /* Identifier */\n'
out += ' /end DEF_CHARACTERISTIC\n'
if self._fnc_inputs:
out += ' /begin IN_MEASUREMENT\n'
for idf in self._fnc_inputs:
out += f' {idf} /* Identifier */\n'
out += ' /end IN_MEASUREMENT\n'
if self._fnc_locals:
out += ' /begin LOC_MEASUREMENT\n'
for idf in self._fnc_locals:
out += f' {idf} /* Identifier */\n'
out += ' /end LOC_MEASUREMENT\n'
if self._fnc_outputs:
out += ' /begin OUT_MEASUREMENT\n'
for idf in self._fnc_outputs:
out += f' {idf} /* Identifier */\n'
out += ' /end OUT_MEASUREMENT\n'
out += ' /end FUNCTION\n'
return out