
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
583 lines
22 KiB
Python
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 powertrain_build.problem_logger import ProblemLogger
|
|
from powertrain_build.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_cfg = 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'])
|
|
|
|
use_symbol_links = self._prj_cfg.get_code_generation_config(item='useA2lSymbolLinks')
|
|
if a2d.get('symbol'):
|
|
if use_symbol_links:
|
|
opt_data += '\n' + ' ' * 8 + 'SYMBOL_LINK "%s" %s' % (a2d['symbol'], a2d.get('symbol_offset'))
|
|
LOG.debug('This a2l is using SYMBOL_LINK for %s', opt_data)
|
|
else:
|
|
var_name = a2d['symbol'] + '._' + var_name
|
|
LOG.debug('This a2l is not using SYMBOL_LINK for %s', 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 use_symbol_links:
|
|
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
|