
We don't transfer git history since it may contain proprietary data that we cannot have in an open sources version. Change-Id: I9586124c1720db69a76b9390e208e9f0ba3b86d4
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 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
|