powertrain-build 1.13.1__py3-none-any.whl → 1.13.3.dev3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. powertrain_build/__init__.py +40 -40
  2. powertrain_build/__main__.py +6 -6
  3. powertrain_build/a2l.py +582 -582
  4. powertrain_build/a2l_merge.py +650 -650
  5. powertrain_build/a2l_templates.py +717 -717
  6. powertrain_build/build.py +985 -985
  7. powertrain_build/build_defs.py +309 -309
  8. powertrain_build/build_proj_config.py +690 -690
  9. powertrain_build/check_interface.py +575 -575
  10. powertrain_build/cli.py +141 -141
  11. powertrain_build/config.py +542 -542
  12. powertrain_build/core.py +395 -395
  13. powertrain_build/core_dummy.py +343 -343
  14. powertrain_build/create_conversion_table.py +73 -73
  15. powertrain_build/dids.py +916 -916
  16. powertrain_build/dummy.py +157 -157
  17. powertrain_build/dummy_spm.py +252 -252
  18. powertrain_build/environmentcheck.py +52 -52
  19. powertrain_build/ext_dbg.py +255 -255
  20. powertrain_build/ext_var.py +327 -327
  21. powertrain_build/feature_configs.py +301 -301
  22. powertrain_build/gen_allsysteminfo.py +227 -227
  23. powertrain_build/gen_label_split.py +449 -449
  24. powertrain_build/handcode_replacer.py +124 -124
  25. powertrain_build/html_report.py +133 -133
  26. powertrain_build/interface/__init__.py +4 -4
  27. powertrain_build/interface/application.py +511 -511
  28. powertrain_build/interface/base.py +500 -500
  29. powertrain_build/interface/csp_api.py +490 -490
  30. powertrain_build/interface/device_proxy.py +677 -677
  31. powertrain_build/interface/ems.py +67 -67
  32. powertrain_build/interface/export_global_vars.py +121 -121
  33. powertrain_build/interface/generate_adapters.py +132 -132
  34. powertrain_build/interface/generate_hi_interface.py +87 -87
  35. powertrain_build/interface/generate_service.py +69 -69
  36. powertrain_build/interface/generate_wrappers.py +147 -147
  37. powertrain_build/interface/generation_utils.py +142 -142
  38. powertrain_build/interface/hal.py +194 -194
  39. powertrain_build/interface/model_yaml_verification.py +348 -348
  40. powertrain_build/interface/service.py +296 -296
  41. powertrain_build/interface/simulink.py +249 -249
  42. powertrain_build/interface/update_call_sources.py +180 -180
  43. powertrain_build/interface/update_model_yaml.py +186 -186
  44. powertrain_build/interface/zone_controller.py +362 -362
  45. powertrain_build/lib/__init__.py +4 -4
  46. powertrain_build/lib/helper_functions.py +127 -127
  47. powertrain_build/lib/logger.py +55 -55
  48. powertrain_build/matlab_scripts/CodeGen/BuildAutomationPyBuild.m +78 -78
  49. powertrain_build/matlab_scripts/CodeGen/Generate_A2L.m +154 -154
  50. powertrain_build/matlab_scripts/CodeGen/generateTLUnit.m +239 -239
  51. powertrain_build/matlab_scripts/CodeGen/getAsilClassification.m +28 -28
  52. powertrain_build/matlab_scripts/CodeGen/modelConfiguredForTL.m +28 -28
  53. powertrain_build/matlab_scripts/CodeGen/moveDefOutports.m +88 -88
  54. powertrain_build/matlab_scripts/CodeGen/parseCalMeasData.m +410 -410
  55. powertrain_build/matlab_scripts/CodeGen/parseCoreIdentifiers.m +139 -139
  56. powertrain_build/matlab_scripts/CodeGen/parseDIDs.m +141 -141
  57. powertrain_build/matlab_scripts/CodeGen/parseInPorts.m +106 -106
  58. powertrain_build/matlab_scripts/CodeGen/parseIncludeConfigs.m +25 -25
  59. powertrain_build/matlab_scripts/CodeGen/parseModelInfo.m +38 -38
  60. powertrain_build/matlab_scripts/CodeGen/parseNVM.m +81 -81
  61. powertrain_build/matlab_scripts/CodeGen/parseOutPorts.m +120 -120
  62. powertrain_build/matlab_scripts/CodeGen/parsePreProcBlks.m +23 -23
  63. powertrain_build/matlab_scripts/CodeGen/struct2JSON.m +128 -128
  64. powertrain_build/matlab_scripts/CodeGen/updateCodeSwConfig.m +31 -31
  65. powertrain_build/matlab_scripts/Init_PyBuild.m +91 -91
  66. powertrain_build/matlab_scripts/__init__.py +2 -2
  67. powertrain_build/matlab_scripts/helperFunctions/Get_Full_Name.m +46 -46
  68. powertrain_build/matlab_scripts/helperFunctions/Get_SrcLines.m +12 -12
  69. powertrain_build/matlab_scripts/helperFunctions/Init_Models.m +78 -78
  70. powertrain_build/matlab_scripts/helperFunctions/Init_Projects.m +67 -67
  71. powertrain_build/matlab_scripts/helperFunctions/Read_Units.m +34 -34
  72. powertrain_build/matlab_scripts/helperFunctions/SetProjectTimeSamples.m +26 -26
  73. powertrain_build/matlab_scripts/helperFunctions/Strip_Suffix.m +16 -16
  74. powertrain_build/matlab_scripts/helperFunctions/followLink.m +118 -118
  75. powertrain_build/matlab_scripts/helperFunctions/getCodeSwitches.m +50 -50
  76. powertrain_build/matlab_scripts/helperFunctions/getConsumerBlocks.m +30 -30
  77. powertrain_build/matlab_scripts/helperFunctions/getDefBlock.m +39 -39
  78. powertrain_build/matlab_scripts/helperFunctions/getDefOutport.m +58 -58
  79. powertrain_build/matlab_scripts/helperFunctions/getDstBlocks.m +19 -19
  80. powertrain_build/matlab_scripts/helperFunctions/getDstLines.m +13 -13
  81. powertrain_build/matlab_scripts/helperFunctions/getInterfaceSignals.m +37 -37
  82. powertrain_build/matlab_scripts/helperFunctions/getName.m +37 -37
  83. powertrain_build/matlab_scripts/helperFunctions/getPath.m +6 -6
  84. powertrain_build/matlab_scripts/helperFunctions/getProperValue.m +21 -21
  85. powertrain_build/matlab_scripts/helperFunctions/getSrcBlocks.m +19 -19
  86. powertrain_build/matlab_scripts/helperFunctions/getSrcLines.m +13 -13
  87. powertrain_build/matlab_scripts/helperFunctions/loadLibraries.m +10 -10
  88. powertrain_build/matlab_scripts/helperFunctions/loadjson.m +6 -6
  89. powertrain_build/matlab_scripts/helperFunctions/modifyEnumStructField.m +21 -21
  90. powertrain_build/matlab_scripts/helperFunctions/removeConfigDuplicates.m +31 -31
  91. powertrain_build/matlab_scripts/helperFunctions/sortSystemByClass.m +26 -26
  92. powertrain_build/matlab_scripts/helperFunctions/tl_getfast.m +89 -89
  93. powertrain_build/matlab_scripts/helperFunctions/topLevelSystem.m +20 -20
  94. powertrain_build/matlab_scripts/helperFunctions/updateModels.m +131 -131
  95. powertrain_build/memory_section.py +224 -224
  96. powertrain_build/nvm_def.py +729 -729
  97. powertrain_build/problem_logger.py +86 -86
  98. powertrain_build/pt_matlab.py +430 -430
  99. powertrain_build/pt_win32.py +144 -144
  100. powertrain_build/replace_compu_tab_ref.py +105 -105
  101. powertrain_build/rte_dummy.py +254 -254
  102. powertrain_build/sched_funcs.py +209 -207
  103. powertrain_build/signal.py +7 -7
  104. powertrain_build/signal_if_html_rep.py +221 -221
  105. powertrain_build/signal_if_html_rep_all.py +302 -302
  106. powertrain_build/signal_incons_html_rep.py +180 -180
  107. powertrain_build/signal_incons_html_rep_all.py +366 -366
  108. powertrain_build/signal_incons_html_rep_base.py +168 -168
  109. powertrain_build/signal_inconsistency_check.py +641 -641
  110. powertrain_build/signal_interfaces.py +864 -864
  111. powertrain_build/templates/Index_SigCheck_All.html +22 -22
  112. powertrain_build/templates/Index_SigIf_All.html +19 -19
  113. powertrain_build/types.py +218 -218
  114. powertrain_build/unit_configs.py +419 -419
  115. powertrain_build/user_defined_types.py +660 -660
  116. powertrain_build/versioncheck.py +66 -66
  117. powertrain_build/wrapper.py +512 -512
  118. powertrain_build/xlrd_csv.py +87 -87
  119. powertrain_build/zone_controller/__init__.py +4 -4
  120. powertrain_build/zone_controller/calibration.py +176 -176
  121. powertrain_build/zone_controller/composition_yaml.py +880 -878
  122. {powertrain_build-1.13.1.dist-info → powertrain_build-1.13.3.dev3.dist-info}/METADATA +100 -100
  123. powertrain_build-1.13.3.dev3.dist-info/RECORD +130 -0
  124. {powertrain_build-1.13.1.dist-info → powertrain_build-1.13.3.dev3.dist-info}/WHEEL +1 -1
  125. {powertrain_build-1.13.1.dist-info → powertrain_build-1.13.3.dev3.dist-info}/licenses/LICENSE +202 -202
  126. powertrain_build-1.13.3.dev3.dist-info/pbr.json +1 -0
  127. powertrain_build-1.13.1.dist-info/RECORD +0 -130
  128. powertrain_build-1.13.1.dist-info/pbr.json +0 -1
  129. {powertrain_build-1.13.1.dist-info → powertrain_build-1.13.3.dev3.dist-info}/entry_points.txt +0 -0
  130. {powertrain_build-1.13.1.dist-info → powertrain_build-1.13.3.dev3.dist-info}/licenses/AUTHORS +0 -0
  131. {powertrain_build-1.13.1.dist-info → powertrain_build-1.13.3.dev3.dist-info}/licenses/NOTICE +0 -0
  132. {powertrain_build-1.13.1.dist-info → powertrain_build-1.13.3.dev3.dist-info}/top_level.txt +0 -0
@@ -1,650 +1,650 @@
1
- # Copyright 2024 Volvo Car Corporation
2
- # Licensed under Apache 2.0.
3
-
4
- # -*- coding: utf-8 -*-
5
- """Module for merging of a2l-files."""
6
-
7
- import json
8
- import os
9
- import re
10
- from string import Template
11
-
12
- from powertrain_build.lib.helper_functions import deep_dict_update
13
- from powertrain_build.problem_logger import ProblemLogger
14
- from powertrain_build.a2l_templates import A2lProjectTemplate, A2lSilverTemplate
15
-
16
-
17
- class A2lMerge(ProblemLogger):
18
- """Class for merging of a2l-files."""
19
-
20
- def __init__(self, prj_cfg, ucfg, a2l_files_unit, a2l_files_gen):
21
- """Merge a2l-files based on provided project configuration.
22
-
23
- Removes symbols not included in the projects unit-config files.
24
-
25
- Args:
26
- prj_cfg (obj): Project config.
27
- ucfg (obj): Unit config.
28
- a2l_files_unit (list of str): Files to merge.
29
- a2l_files_gen (list of str): Files to merge.
30
- """
31
- super().__init__()
32
- self._prj_cfg = prj_cfg
33
- self._unit_cfg = ucfg
34
- self._per_unit_cfg = ucfg.get_per_unit_cfg()
35
- # generate the a2l string
36
- self._blks = {}
37
- self._removed_symbols = []
38
- self.a2l = ""
39
-
40
- # ----- Example blocks in a2l (TargetLink) -----
41
- #
42
- # /begin CHARACTERISTIC
43
- # cVc_B_SeriesHev /* Name */
44
- # "Series hybrid" /* LongIdentifier */
45
- # VALUE /* Type */
46
- # 0x00000000 /* address: cVc_B_SeriesHev */
47
- # UBYTE_COL_DIRECT /* Deposit */
48
- # 0 /* MaxDiff */
49
- # Scaling_3 /* Conversion */
50
- # 0 /* LowerLimit */
51
- # 1 /* UpperLimit */
52
- # /end CHARACTERISTIC
53
- #
54
- # Example of Bosch-nvm signal in nvm:
55
- #
56
- # /begin MEASUREMENT
57
- # nvm_list_32._sVcDclVu_D_Markow /* Name */
58
- # "No description given" /* LongIdentifier */
59
- # ULONG /* Datatype */
60
- # VcNvm_1_0_None /* Conversion */
61
- # 1 /* Resolution */
62
- # 0 /* Accuracy */
63
- # 0 /* LowerLimit */
64
- # 4294967295 /* UpperLimit */
65
- # READ_WRITE
66
- # MATRIX_DIM 152 1 1
67
- # ECU_ADDRESS 0x00000000
68
- # /end MEASUREMENT
69
- #
70
- # ----- Example blocks in a2l (Embedded Coder) -----
71
- #
72
- # /begin MEASUREMENT
73
- # /* Name */ sVcAesVe_md_VolmcOffs
74
- # /* Long identifier */ "Volumetric cylinder mass flow offset"
75
- # /* Data type */ FLOAT32_IEEE
76
- # /* Conversion method */ VcAesVe_CM_Float32_g_s
77
- # /* Resolution (Not used) */ 0
78
- # /* Accuracy (Not used) */ 0
79
- # /* Lower limit */ -100.0
80
- # /* Upper limit */ 100.0
81
- # ECU_ADDRESS 0x0000 /* @ECU_Address@sVcAesVe_md_VolmcOffs@ */
82
- # /end MEASUREMENT
83
- #
84
- # /begin CHARACTERISTIC
85
- # /* Name */ cVcAesVe_D_VolmcCmpSel
86
- # /* Long Identifier */ "Select compensation factor characterizing deviation from nominal voleff"
87
- # /* Type */ VALUE
88
- # /* ECU Address */ 0x0000 /* @ECU_Address@cVcAesVe_D_VolmcCmpSel@ */
89
- # /* Record Layout */ Scalar_UBYTE
90
- # /* Maximum Difference */ 0
91
- # /* Conversion Method */ VcAesVe_CM_uint8
92
- # /* Lower Limit */ 1.0
93
- # /* Upper Limit */ 3.0
94
- # /end CHARACTERISTIC
95
-
96
- self._block_finder = re.compile(r'(?:\s*\n)*' # Optional blank lines
97
- r'(\s*/begin (\w+)\s*' # begin <something> block
98
- r'\n\s*([\w.]+).*?\n' # label. (Bosch-nvm contains the .)
99
- r'.*?' # block definition
100
- r'/end\s+\2)', # end <something> block. Same something as before
101
- flags=re.M | re.DOTALL)
102
-
103
- self._tl_compu_method_parser = re.compile(
104
- r'(?:\s*\n)*(?P<compu_method>'
105
- r'\s*/begin COMPU_METHOD\s*\n'
106
- r'\s*(?P<name>\w*)\s*(/\* Name \*/)?\s*\n' # Name
107
- r'\s*"(?P<ID>.*?)"\s*(/\* LongIdentifier \*/.*?)\s*\n' # Long Identifier
108
- r'\s*(?P<conv_type>[A-Z_]*).*\s*\n' # ConversionType
109
- r'\s*"(?P<disp_format>.*)"\s*(/\* Format \*/)?\s*\n' # Format
110
- r'\s*"(?P<unit>.*?)"\s*(/\* Unit \*/)?\s*\n' # Unit
111
- r'\s*(?P<conversion>.*)\s*\n' # COEFFS
112
- r'(?P<indentation>\s*)/end COMPU_METHOD)', flags=re.M) # No DOTALL, so .* is [^\n]*
113
-
114
- # COMPU_METHOD parser that works with files generated by Embedded Coder
115
- self._ec_compu_method_parser = re.compile(
116
- r'(?:\s*\n)*(?P<compu_method>'
117
- r'\s*/begin COMPU_METHOD\s*\n'
118
- r'\s*(/\* Name of CompuMethod\s*\*/)?\s*(?P<name>\w*)\s*\n' # Name
119
- r'\s*(/\* Long identifier\s*\*/)?\s*"(?P<ID>.*?)"\s*\n' # Long Identifier
120
- r'\s*/\* Conversion Type\s*\*/\s*(?P<conv_type>[A-Z_]*)\s*\n' # ConversionType
121
- r'\s*(/\* Format\s*\*/)?\s*"(?P<disp_format>.*?)"\s*\n' # Format
122
- r'\s*(/\* Units\s*\*/)?\s*"(?P<unit>.*?)"\s*\n' # Unit
123
- r'\s*/\* Coefficients\s*\*/\s*(?P<conversion>.*?)\s*\n' # COEFFS
124
- r'(?P<indentation>\s*)/end COMPU_METHOD)', flags=re.M) # No DOTALL, so .* is [^\n]*
125
-
126
- self._expr_block_meas_kp_blob_parser = re.compile(
127
- r'\/begin\s+(?P<keyword>\w+)\s+(?P<class>\w+)\s*\n'
128
- r'\s*KP_BLOB\s+(?P<address>0x[0-9a-fA-F]+)\s*\n'
129
- r'\s*\/end\s+\1'
130
- )
131
-
132
- self._compu_methods = {}
133
- self._included_compu_methods = []
134
-
135
- self._tl_compu_method_template = Template(
136
- '$indentation/begin COMPU_METHOD\n'
137
- '$indentation $name /* Name */\n'
138
- '$indentation "$ID" /* LongIdentifier */\n'
139
- '$indentation $conv_type /* ConversionType */\n'
140
- '$indentation "$disp_format" /* Format */\n'
141
- '$indentation "$unit" /* Unit */\n'
142
- '$indentation $conversion\n'
143
- '$indentation/end COMPU_METHOD'
144
- )
145
-
146
- # COMPU_METHOD template that looks similar to COMPU_METHOD generated by Embedded Coder
147
- self._ec_compu_method_template = Template(
148
- '$indentation/begin COMPU_METHOD\n'
149
- '$indentation /* Name of CompuMethod */ $name\n'
150
- '$indentation /* Long identifier */ "$ID"\n'
151
- '$indentation /* Conversion Type */ $conv_type\n'
152
- '$indentation /* Format */ "$disp_format"\n'
153
- '$indentation /* Units */ "$unit"\n'
154
- '$indentation /* Coefficients */ $conversion\n'
155
- '$indentation/end COMPU_METHOD'
156
- )
157
-
158
- for filename in a2l_files_unit:
159
- removed_symbols = self._parse_unit(filename)
160
- self._removed_symbols.extend(removed_symbols)
161
- self.debug('Loaded %s', filename)
162
- for filename in a2l_files_gen:
163
- self._parse_gen(filename)
164
- self.debug('Loaded %s', filename)
165
-
166
- def _parse_unit(self, filename):
167
- """Parse the unit a2l-files and apply a filter to only active parameters."""
168
- self.debug('Processing %s', filename)
169
- with open(filename, 'r', encoding="ISO-8859-1") as a2lfp:
170
- a2ld = a2lfp.read()
171
- file_path_parts = os.path.split(filename)
172
- unit = file_path_parts[1].split('.')[0]
173
- base_path = file_path_parts[0]
174
- dcl_match = re.search(r'VcDcl[\w]+Mdl(__[\w]+)', base_path)
175
- if dcl_match is not None and 'Mdl' not in unit:
176
- # Hand coded model names including "__" will lead to this, due to name mismatch of .a2l and .json files.
177
- # E.g. VcDclPtrlMdl__denso:
178
- # 1. config_VcDclPtrlMdl__denso.json vs VcDclPtrlMdl__denso.a2l.
179
- # 1.1. Match: unit in self._per_unit_cfg.
180
- # 2. config_VcDclPtrl__denso.json vs VcDclPtrl.a2l.
181
- # 2.1. No match: unit not in self._per_unit_cfg.
182
- old_unit = unit
183
- unit = unit + dcl_match.group(1)
184
- self.info(
185
- 'Found unit %s with .a2l and .json file name mismatch. Using new unit name: %s',
186
- old_unit,
187
- unit
188
- )
189
-
190
- if unit in self._per_unit_cfg:
191
- u_conf = self._per_unit_cfg[unit]
192
- code_generator = u_conf['code_generator'] if 'code_generator' in u_conf else 'target_link'
193
- else:
194
- u_conf = {}
195
- code_generator = 'target_link'
196
-
197
- if code_generator == 'embedded_coder':
198
- blks = re.findall(r'(?:\s*\n)*(\s*/begin '
199
- r'(?!PROJECT|HEADER|MODULE|MOD_PAR|MOD_COMMON)(\w+)\s*(?:\n\s*)?'
200
- r'(?:/\*\s*[\w ]+\s*\*/\s*)?(\w+)([\[\d+\]]*).*?\n.*?/end\s+\2)',
201
- a2ld, flags=re.M | re.DOTALL)
202
- else:
203
- blks = re.findall(r'(?:\s*\n)*(\s*/begin (?!PROJECT|MODULE)(\w+)[\n\s]*'
204
- r'(\w+(?:\.\w+)?)([\[\d+\]]*).*?\n.*?/end\s+\2)', a2ld,
205
- flags=re.M | re.DOTALL)
206
-
207
- compu_method_translators = self._parse_compu_methods(a2ld, unit)
208
- unit_blks = {}
209
- removed_symbols = []
210
- if unit not in self._per_unit_cfg:
211
- # Handcoded a2l without json-files will lead to this.
212
- # Add json files for the handcoded a2l!
213
- # NOTE: Assuming TargetLink
214
- self.debug('%s is not in the units list. Looking for json.', unit)
215
- config_filename = os.path.join(
216
- self._prj_cfg.get_unit_cfg_deliv_dir(),
217
- f'config_{unit}.json')
218
- self.debug('Looking for %s', config_filename)
219
- if os.path.isfile(config_filename):
220
- with open(config_filename, 'r', encoding="utf-8") as config_file:
221
- u_conf = json.load(config_file)
222
- self._handle_config(
223
- code_generator, unit, u_conf, blks, unit_blks, removed_symbols, compu_method_translators
224
- )
225
- else:
226
- self.warning('%s does not have a unit_cfg json, '
227
- 'including all a2l-parameters', unit)
228
- for blk_def, type_, label, size in blks:
229
- if type_ == 'COMPU_METHOD':
230
- blk_def, label = self._replace_compu_method(blk_def, label, compu_method_translators)
231
- self.add_block_definition(unit_blks, type_, label, size, blk_def, compu_method_translators)
232
- else:
233
- self._handle_config(
234
- code_generator, unit, u_conf, blks, unit_blks, removed_symbols, compu_method_translators
235
- )
236
- deep_dict_update(self._blks, unit_blks)
237
- return removed_symbols
238
-
239
- def _handle_config(self, code_generator, unit, u_conf, blks, unit_blks, removed_symbols, compu_method_translators):
240
- """Merge all types of ram for the unit."""
241
- ram = u_conf['inports']
242
- ram.update(u_conf['outports'])
243
- ram.update(u_conf['local_vars'])
244
- # TODO: Function the variables and labels needs to be removed from
245
- # the FUNCTION block too
246
- for blk_def, type_, label, size in blks:
247
- remove_excluded_symbol = True
248
- inc = False
249
- if type_ == 'AXIS_PTS':
250
- if label in u_conf['calib_consts']:
251
- inc = True
252
- elif type_ == 'CHARACTERISTIC':
253
- if label in u_conf['calib_consts']:
254
- if label in [axis_label for _, axis_type, axis_label, _ in blks if axis_type == 'AXIS_PTS']:
255
- # AXIS_PTS can be used as CHARACTERISTC but not the other way around.
256
- # If there are duplicates, use the AXIS_PTS.
257
- self.debug('Will not add the block for CHARACTERISTC %s, but will keep it as a symbol,'
258
- ' since it exists as AXIS_PTS', label)
259
- remove_excluded_symbol = False
260
- inc = False
261
- else:
262
- inc = self._handle_axis_ptr_ref_config(u_conf, blk_def, unit)
263
- elif type_ == 'MEASUREMENT':
264
- if label in ram:
265
- key = label if size is None else label + size
266
- if label in u_conf['outports']:
267
- # This unit is producing the measurement.
268
- inc = True
269
- elif key in unit_blks.get(type_, {}):
270
- # This unit is not producing it, and it has already been added
271
- inc = False
272
- else:
273
- # This unit is not producing it, but it has not been added
274
- # Could be external signal, etc.
275
- inc = True
276
- elif type_ == 'COMPU_METHOD':
277
- inc = True
278
- blk_def, label = self._replace_compu_method(blk_def, label, compu_method_translators)
279
- else:
280
- inc = True
281
- if inc:
282
- self.add_block_definition(unit_blks, type_, label, size, blk_def, compu_method_translators)
283
- else:
284
- if remove_excluded_symbol:
285
- removed_symbols.append(label + size)
286
- self.debug('Did not include A2L-blk %s%s', label, size)
287
- if not self._unit_cfg.check_if_in_unit_cfg(unit, label):
288
- if type_ != 'COMPU_METHOD':
289
- self.warning('A2l block %s not in config json file for %s', label, unit)
290
-
291
- if 'FUNCTION' in unit_blks:
292
- unit_blks['FUNCTION'] = self._remove_symbols_from_func_blks(
293
- code_generator, unit_blks['FUNCTION'], removed_symbols
294
- )
295
-
296
- if 'GROUP' in unit_blks:
297
- unit_blks['GROUP'] = self._remove_symbols_from_grp_blks(unit_blks['GROUP'], removed_symbols)
298
-
299
- def _handle_axis_ptr_ref_config(self, u_conf, blk, unit):
300
- """Remove blocks referencing undefined blocks."""
301
- ref_re = re.compile(r'\s*AXIS_PTS_REF\s*([\w]*)')
302
- for axis_ptr_ref in ref_re.findall(blk):
303
- if axis_ptr_ref not in u_conf['calib_consts']:
304
- self.debug('Excluding due to %s missing in config', axis_ptr_ref)
305
- return False
306
- if not self._unit_cfg.check_if_in_unit_cfg(unit, axis_ptr_ref):
307
- self.debug('Excluding due to %s not active in config', axis_ptr_ref)
308
- return False
309
- return True
310
-
311
- def add_block_definition(self, unit_blks, type_, label, size, blk_def, compu_method_translators):
312
- """Add block definition to A2L-file."""
313
- size = '' if size is None else size
314
- blk_def = self._replace_conversions(blk_def, compu_method_translators)
315
- if type_ not in unit_blks:
316
- unit_blks[type_] = {}
317
- unit_blks[type_][label + size] = blk_def
318
-
319
- @staticmethod
320
- def _parse_func_blk(code_generator, fnc_blk):
321
- """Remove the unused symbols from the FUNCTION blocks in the A2L-file.
322
- Parse the FUNCTION block, TL or EC style based on code_generator.
323
- """
324
-
325
- if code_generator == 'target_link':
326
- pattern = r'\s*/begin\s+FUNCTION\s*?\n\s*(\w+).*?\n\s*"(.*?)".*?\n(.*)'
327
- else:
328
- pattern = r'\s*/begin\s+FUNCTION\s*?\n\s*.*\*/\s*(\w+).*?\n\s*.*\*/\s*"(.*?)".*?\n(.*)'
329
- res = re.match(pattern, fnc_blk, flags=re.M | re.DOTALL)
330
- fnc_name = res.group(1)
331
- long_id = res.group(2)
332
- fnc_dict = {
333
- 'fnc_name': fnc_name,
334
- 'long_id': long_id,
335
- 'body': {}
336
- }
337
- fnc_body = res.group(3)
338
- sb_res = re.findall(r'\s*/begin\s+(\w+[\[\d\]]*)\s*\n\s*'
339
- r'(.*?\n)\s*/end \1', fnc_body, flags=re.M | re.DOTALL)
340
- for sb_name, sub_blk in sb_res:
341
- symbols = set(re.findall(r'\s*(\w+(?:\.\w+)?[\[\d\]]*).*?\n', sub_blk, flags=re.M))
342
- fnc_dict['body'][sb_name] = symbols
343
- return fnc_dict
344
-
345
- @staticmethod
346
- def _parse_grp_blk(grp_blk):
347
- """Remove the unused symbols from the GROUP blocks in the A2L-file."""
348
- # parse the GROUP block
349
- res = re.match(r'\s*/begin\s+GROUP\s*?\n\s*.*\*/\s*(\w+).*?\n\s*.*\*/\s*"(.*?)".*?\n(.*)',
350
- grp_blk, flags=re.M | re.DOTALL)
351
- fnc_name = res.group(1)
352
- long_id = res.group(2)
353
- fnc_dict = {
354
- 'fnc_name': fnc_name,
355
- 'long_id': long_id,
356
- 'body': {}
357
- }
358
- fnc_body = res.group(3)
359
- sb_res = re.findall(r'\s*/begin\s+(\w+[\[\d\]]*)\s*\n\s*'
360
- r'(.*?\n)\s*/end \1', fnc_body, flags=re.M | re.DOTALL)
361
- for sb_name, sub_blk in sb_res:
362
- symbols = set(re.findall(r'\s*(\w+(?:\.\w+)?[\[\d\]]*).*?\n', sub_blk, flags=re.M))
363
- fnc_dict['body'][sb_name] = symbols
364
- return fnc_dict
365
-
366
- def _recursive_remove(self, a2l_dict, name):
367
- """Remove symbols from A2L dict (e.g. group or function)."""
368
- if name in a2l_dict:
369
- blk = a2l_dict[name]
370
- if 'SUB_FUNCTION' in blk:
371
- for sub_fnc in blk['SUB_FUNCTION']:
372
- if self._recursive_remove(a2l_dict, sub_fnc):
373
- blk['SUB_FUNCTION'] = blk['SUB_FUNCTION'] - set([sub_fnc])
374
- elif 'SUB_GROUP' in blk:
375
- for sub_grp in blk['SUB_GROUP']:
376
- if self._recursive_remove(a2l_dict, sub_grp):
377
- blk['SUB_GROUP'] = blk['SUB_GROUP'] - set([sub_grp])
378
- empty = True
379
- for key in blk:
380
- if blk[key]:
381
- empty = False
382
- break
383
- if empty:
384
- a2l_dict.pop(name)
385
- return True
386
- return False
387
-
388
- def _remove_symbols_from_func_blks(self, code_generator, fnc_blks, removed_symbols):
389
- """Remove the unused symbols from function blocks.
390
-
391
- If the function block is empty, it too will be removed.
392
- first iteration - remove all symbols that have been removed
393
- second iteration - recusively remover all functions without symbols
394
- """
395
- fnc_dict = {}
396
- for fnc_name, fnc_blk in fnc_blks.items():
397
- fnc_dict[fnc_name] = {}
398
- u_fnc_bdy = self._parse_func_blk(code_generator, fnc_blk)['body']
399
- sub_blk_types = set(u_fnc_bdy.keys()) - set(['SUB_FUNCTION'])
400
- for type_ in list(sub_blk_types):
401
- fnc_dict[fnc_name][type_] = u_fnc_bdy[type_] - set(removed_symbols)
402
- if 'SUB_FUNCTION' in u_fnc_bdy:
403
- fnc_dict[fnc_name]['SUB_FUNCTION'] = u_fnc_bdy['SUB_FUNCTION']
404
- # second iteration - remove empty FUNCTION blocks
405
- # TODO: Add functionality which parses the the function tree structures
406
- # And the run recursive remove on all tree roots.
407
- for fnc_name in fnc_blks.keys():
408
- self._recursive_remove(fnc_dict, fnc_name)
409
- # generate new function blocks
410
- new_fnc_blks = {}
411
- for fnc_name, fnc_data in fnc_dict.items():
412
- fnc_blk = f' /begin FUNCTION\n {fnc_name}\t/* Name */\n'
413
- fnc_blk += " \"\"\t/* LongIdentifier */\n"
414
- for sub_sec in sorted(fnc_data.keys()):
415
- sub_sec_data = fnc_data[sub_sec]
416
- if sub_sec_data:
417
- fnc_blk += f" /begin {sub_sec}\n"
418
- for param in sorted(sub_sec_data):
419
- fnc_blk += f" {param}\t/* Identifier */\n"
420
- fnc_blk += f" /end {sub_sec}\n"
421
- fnc_blk += " /end FUNCTION"
422
- new_fnc_blks[fnc_name] = fnc_blk
423
- return new_fnc_blks
424
-
425
- def _remove_symbols_from_grp_blks(self, grp_blks, removed_symbols):
426
- """Remove the unused symbols from group blocks.
427
-
428
- If the group block is empty, it too will be removed.
429
- first iteration - remove all symbols that have been removed
430
- second iteration - recusively remover all groups without symbols
431
- """
432
- grp_dict = {}
433
- for grp_name, grp_blk in grp_blks.items():
434
- grp_dict[grp_name] = {}
435
- u_grp_bdy = self._parse_grp_blk(grp_blk)['body']
436
- sub_blk_types = set(u_grp_bdy.keys()) - set(['SUB_GROUP'])
437
- for type_ in list(sub_blk_types):
438
- grp_dict[grp_name][type_] = u_grp_bdy[type_] - set(removed_symbols)
439
- if 'SUB_GROUP' in u_grp_bdy:
440
- grp_dict[grp_name]['SUB_GROUP'] = u_grp_bdy['SUB_GROUP']
441
- # second iteration - remove empty GROUP blocks
442
- # TODO: Add functionality which parses the the group tree structures
443
- # And the run recursive remove on all tree roots.
444
- for grp_name in grp_blks.keys():
445
- self._recursive_remove(grp_dict, grp_name)
446
- # generate new group blocks
447
- new_grp_blks = {}
448
- for grp_name, grp_data in grp_dict.items():
449
- grp_blk = f" /begin GROUP \n /* Name */ {grp_name}\n"
450
- grp_blk += " /* Long identifier */ \"\"\n"
451
- for sub_sec in sorted(grp_data.keys()):
452
- sub_sec_data = grp_data[sub_sec]
453
- if sub_sec_data:
454
- grp_blk += f" /begin {sub_sec}\n"
455
- for param in sorted(sub_sec_data):
456
- grp_blk += f" {param}\n"
457
- grp_blk += f" /end {sub_sec}\n"
458
- grp_blk += " /end GROUP"
459
- new_grp_blks[grp_name] = grp_blk
460
-
461
- return new_grp_blks
462
-
463
- def _parse_gen(self, filename):
464
- """Parse the generated a2l-files, without filter."""
465
- self.debug('parsing gen a2l: %s', filename)
466
- with open(filename, 'r', encoding="utf-8") as a2lfp:
467
- a2ld = a2lfp.read()
468
- for blk_def, type_, label in self._block_finder.findall(a2ld):
469
- self._blks.setdefault(type_, {}).setdefault(label, blk_def)
470
-
471
- @staticmethod
472
- def _replace_compu_method(blk_def, label, compu_method_translators):
473
- """Replace the compu method block and label."""
474
- for translator in compu_method_translators:
475
- if translator['old_compu_method'] == blk_def:
476
- return translator['new_compu_method'], translator['new_name']
477
- return blk_def, label
478
-
479
- def _store_compu_method(self, ID, conv_type, disp_format, unit, conversion, indentation, u_conf):
480
- """Stash compu methods that exists in the resulting a2l."""
481
- key = (ID, conv_type, disp_format, unit, conversion)
482
- if key in self._compu_methods:
483
- new_name = self._compu_methods[key]['name']
484
- new_compu_method = self._compu_methods[key]['method']
485
- else:
486
- new_name = 'Scaling_' + str(len(self._compu_methods))
487
- if 'code_generator' in u_conf and u_conf['code_generator'] == 'embedded_coder':
488
- new_compu_method = self._ec_compu_method_template.substitute(
489
- name=new_name,
490
- ID=ID,
491
- conv_type=conv_type,
492
- disp_format=disp_format,
493
- unit=unit,
494
- conversion=conversion,
495
- indentation=indentation
496
- )
497
- else:
498
- new_compu_method = self._tl_compu_method_template.substitute(
499
- name=new_name,
500
- ID=ID,
501
- conv_type=conv_type,
502
- disp_format=disp_format,
503
- unit=unit,
504
- conversion=conversion,
505
- indentation=indentation
506
- )
507
- self._compu_methods.update({key: {'name': new_name,
508
- 'method': new_compu_method}})
509
- return new_name, new_compu_method
510
-
511
- @staticmethod
512
- def _replace_conversions(blk_def, compu_method_translators):
513
- """Replace conversion identifiers in a2l block."""
514
- for translator in compu_method_translators:
515
- # The following check is faster than running the regex on the block.
516
- # It DOES give false positives, which is why the regex is used for substitution
517
- # and we do not immidiately return after one positive
518
- if translator['old_name'] in blk_def:
519
- blk_def = translator['regex'].sub(translator['replacement'], blk_def)
520
- return blk_def
521
-
522
- def _parse_compu_methods(self, blk_def, unit):
523
- """Replace compu methods to not overwrite any of them."""
524
- compu_method_translators = [] # Translators for one processed a2l file. Needs to be reset between files
525
- u_conf = self._per_unit_cfg[unit] if unit in self._per_unit_cfg else {}
526
- if 'code_generator' in u_conf and u_conf['code_generator'] == 'embedded_coder':
527
- for match in self._ec_compu_method_parser.finditer(blk_def):
528
- new_name, new_compu_method = self._store_compu_method(
529
- match['ID'],
530
- match['conv_type'],
531
- match['disp_format'],
532
- match['unit'],
533
- match['conversion'],
534
- match['indentation'],
535
- u_conf
536
- )
537
- compu_method_translators.append(
538
- {
539
- 'new_name': new_name,
540
- 'old_name': match['name'],
541
- 'regex': re.compile(
542
- r'(\s*)' # beginning
543
- r'\s*(/\* Conversion [Mm]ethod\s*\*/\s*)' # optional comment
544
- r'\b{name}\b' # word
545
- r'('
546
- r'\s*\n' # newline
547
- r')'.format(name=match['name']) # end of end-match
548
- ),
549
- 'replacement': r'\1\2{name}\3'.format(name=new_name),
550
- 'old_compu_method': match['compu_method'],
551
- 'new_compu_method': new_compu_method
552
- }
553
- )
554
- else:
555
- for match in self._tl_compu_method_parser.finditer(blk_def):
556
- new_name, new_compu_method = self._store_compu_method(
557
- match['ID'],
558
- match['conv_type'],
559
- match['disp_format'],
560
- match['unit'],
561
- match['conversion'],
562
- match['indentation'],
563
- u_conf
564
- )
565
- compu_method_translators.append(
566
- {
567
- 'new_name': new_name,
568
- 'old_name': match['name'],
569
- 'regex': re.compile(
570
- r'(\s*)' # beginning
571
- r'\b{name}\b' # word
572
- r'(' # start of end-match
573
- r'\s*(/\* Conversion \*/)?' # optional comment
574
- r'\s*\n' # newline
575
- r')'.format(name=match['name']) # end of end-match
576
- ),
577
- 'replacement': r'\1{name}\2'.format(name=new_name),
578
- 'old_compu_method': match['compu_method'],
579
- 'new_compu_method': new_compu_method
580
- }
581
- )
582
- return compu_method_translators
583
-
584
- def _patch_kp_blob(self, block):
585
- """Return updated measurement block text.
586
- Args:
587
- block (str): A2L text block
588
- Returns:
589
- a2l_text (str): A2L text without KP_BLOB.
590
- """
591
- ecu_address = '0x00000000'
592
- for match in self._expr_block_meas_kp_blob_parser.finditer(block):
593
- start, end = match.span()
594
- block = f'{block[:start]}ECU_ADDRESS {ecu_address}{block[end:]}'
595
- return block
596
-
597
- def merge(self, f_name, complete_a2l=False, silver_a2l=False):
598
- """Write merged a2l-file.
599
-
600
- Args:
601
- f_name (str): Output filename.
602
- """
603
- a2l = ''
604
- a2l_config = self._prj_cfg.get_a2l_cfg()
605
- for _, data in self._blks.items():
606
- for _, blk in data.items():
607
- if not a2l_config['allow_kp_blob']:
608
- blk = self._patch_kp_blob(blk)
609
- a2l += blk + '\n\n'
610
-
611
- if complete_a2l:
612
- events = []
613
- time_unit_10ms = '0x07'
614
- rasters = self._prj_cfg.get_units_raster_cfg()
615
- for xcp_id, evt_data in enumerate(rasters['SampleTimes'].items(), 1):
616
- events.append({
617
- 'time_cycle': '0x%02X' % int(evt_data[1] * 100),
618
- 'time_unit': time_unit_10ms,
619
- 'name': evt_data[0],
620
- 'channel_id': '0x%04X' % xcp_id
621
- })
622
- a2l_template = A2lProjectTemplate(
623
- a2l,
624
- a2l_config['asap2_version'],
625
- a2l_config['name'],
626
- events,
627
- a2l_config['ip_address'],
628
- a2l_config['ip_port']
629
- )
630
- a2l = a2l_template.render()
631
- elif silver_a2l:
632
- a2l_template = A2lSilverTemplate(a2l)
633
- a2l = a2l_template.render()
634
-
635
- self.a2l = a2l
636
-
637
- with open(f_name, 'w', encoding="ISO-8859-1") as ma2l:
638
- ma2l.write(a2l)
639
- self.info('Written the merged A2L-file %s', f_name)
640
-
641
- def get_characteristic_axis_data(self):
642
- """Get characteristic map axis data from merged a2l-file."""
643
- axis_data = {}
644
- for blk_def, type_, label in self._block_finder.findall(self.a2l):
645
- if type_ == "CHARACTERISTIC":
646
- axes = re.findall('AXIS_PTS_REF (.*)', blk_def)
647
- if label in axis_data:
648
- self.critical("Multiple CHARACTERISTIC for %s in merged a2l.", label)
649
- axis_data[label] = {'axes': axes}
650
- return axis_data
1
+ # Copyright 2024 Volvo Car Corporation
2
+ # Licensed under Apache 2.0.
3
+
4
+ # -*- coding: utf-8 -*-
5
+ """Module for merging of a2l-files."""
6
+
7
+ import json
8
+ import os
9
+ import re
10
+ from string import Template
11
+
12
+ from powertrain_build.lib.helper_functions import deep_dict_update
13
+ from powertrain_build.problem_logger import ProblemLogger
14
+ from powertrain_build.a2l_templates import A2lProjectTemplate, A2lSilverTemplate
15
+
16
+
17
+ class A2lMerge(ProblemLogger):
18
+ """Class for merging of a2l-files."""
19
+
20
+ def __init__(self, prj_cfg, ucfg, a2l_files_unit, a2l_files_gen):
21
+ """Merge a2l-files based on provided project configuration.
22
+
23
+ Removes symbols not included in the projects unit-config files.
24
+
25
+ Args:
26
+ prj_cfg (obj): Project config.
27
+ ucfg (obj): Unit config.
28
+ a2l_files_unit (list of str): Files to merge.
29
+ a2l_files_gen (list of str): Files to merge.
30
+ """
31
+ super().__init__()
32
+ self._prj_cfg = prj_cfg
33
+ self._unit_cfg = ucfg
34
+ self._per_unit_cfg = ucfg.get_per_unit_cfg()
35
+ # generate the a2l string
36
+ self._blks = {}
37
+ self._removed_symbols = []
38
+ self.a2l = ""
39
+
40
+ # ----- Example blocks in a2l (TargetLink) -----
41
+ #
42
+ # /begin CHARACTERISTIC
43
+ # cVc_B_SeriesHev /* Name */
44
+ # "Series hybrid" /* LongIdentifier */
45
+ # VALUE /* Type */
46
+ # 0x00000000 /* address: cVc_B_SeriesHev */
47
+ # UBYTE_COL_DIRECT /* Deposit */
48
+ # 0 /* MaxDiff */
49
+ # Scaling_3 /* Conversion */
50
+ # 0 /* LowerLimit */
51
+ # 1 /* UpperLimit */
52
+ # /end CHARACTERISTIC
53
+ #
54
+ # Example of Bosch-nvm signal in nvm:
55
+ #
56
+ # /begin MEASUREMENT
57
+ # nvm_list_32._sVcDclVu_D_Markow /* Name */
58
+ # "No description given" /* LongIdentifier */
59
+ # ULONG /* Datatype */
60
+ # VcNvm_1_0_None /* Conversion */
61
+ # 1 /* Resolution */
62
+ # 0 /* Accuracy */
63
+ # 0 /* LowerLimit */
64
+ # 4294967295 /* UpperLimit */
65
+ # READ_WRITE
66
+ # MATRIX_DIM 152 1 1
67
+ # ECU_ADDRESS 0x00000000
68
+ # /end MEASUREMENT
69
+ #
70
+ # ----- Example blocks in a2l (Embedded Coder) -----
71
+ #
72
+ # /begin MEASUREMENT
73
+ # /* Name */ sVcAesVe_md_VolmcOffs
74
+ # /* Long identifier */ "Volumetric cylinder mass flow offset"
75
+ # /* Data type */ FLOAT32_IEEE
76
+ # /* Conversion method */ VcAesVe_CM_Float32_g_s
77
+ # /* Resolution (Not used) */ 0
78
+ # /* Accuracy (Not used) */ 0
79
+ # /* Lower limit */ -100.0
80
+ # /* Upper limit */ 100.0
81
+ # ECU_ADDRESS 0x0000 /* @ECU_Address@sVcAesVe_md_VolmcOffs@ */
82
+ # /end MEASUREMENT
83
+ #
84
+ # /begin CHARACTERISTIC
85
+ # /* Name */ cVcAesVe_D_VolmcCmpSel
86
+ # /* Long Identifier */ "Select compensation factor characterizing deviation from nominal voleff"
87
+ # /* Type */ VALUE
88
+ # /* ECU Address */ 0x0000 /* @ECU_Address@cVcAesVe_D_VolmcCmpSel@ */
89
+ # /* Record Layout */ Scalar_UBYTE
90
+ # /* Maximum Difference */ 0
91
+ # /* Conversion Method */ VcAesVe_CM_uint8
92
+ # /* Lower Limit */ 1.0
93
+ # /* Upper Limit */ 3.0
94
+ # /end CHARACTERISTIC
95
+
96
+ self._block_finder = re.compile(r'(?:\s*\n)*' # Optional blank lines
97
+ r'(\s*/begin (\w+)\s*' # begin <something> block
98
+ r'\n\s*([\w.]+).*?\n' # label. (Bosch-nvm contains the .)
99
+ r'.*?' # block definition
100
+ r'/end\s+\2)', # end <something> block. Same something as before
101
+ flags=re.M | re.DOTALL)
102
+
103
+ self._tl_compu_method_parser = re.compile(
104
+ r'(?:\s*\n)*(?P<compu_method>'
105
+ r'\s*/begin COMPU_METHOD\s*\n'
106
+ r'\s*(?P<name>\w*)\s*(/\* Name \*/)?\s*\n' # Name
107
+ r'\s*"(?P<ID>.*?)"\s*(/\* LongIdentifier \*/.*?)\s*\n' # Long Identifier
108
+ r'\s*(?P<conv_type>[A-Z_]*).*\s*\n' # ConversionType
109
+ r'\s*"(?P<disp_format>.*)"\s*(/\* Format \*/)?\s*\n' # Format
110
+ r'\s*"(?P<unit>.*?)"\s*(/\* Unit \*/)?\s*\n' # Unit
111
+ r'\s*(?P<conversion>.*)\s*\n' # COEFFS
112
+ r'(?P<indentation>\s*)/end COMPU_METHOD)', flags=re.M) # No DOTALL, so .* is [^\n]*
113
+
114
+ # COMPU_METHOD parser that works with files generated by Embedded Coder
115
+ self._ec_compu_method_parser = re.compile(
116
+ r'(?:\s*\n)*(?P<compu_method>'
117
+ r'\s*/begin COMPU_METHOD\s*\n'
118
+ r'\s*(/\* Name of CompuMethod\s*\*/)?\s*(?P<name>\w*)\s*\n' # Name
119
+ r'\s*(/\* Long identifier\s*\*/)?\s*"(?P<ID>.*?)"\s*\n' # Long Identifier
120
+ r'\s*/\* Conversion Type\s*\*/\s*(?P<conv_type>[A-Z_]*)\s*\n' # ConversionType
121
+ r'\s*(/\* Format\s*\*/)?\s*"(?P<disp_format>.*?)"\s*\n' # Format
122
+ r'\s*(/\* Units\s*\*/)?\s*"(?P<unit>.*?)"\s*\n' # Unit
123
+ r'\s*/\* Coefficients\s*\*/\s*(?P<conversion>.*?)\s*\n' # COEFFS
124
+ r'(?P<indentation>\s*)/end COMPU_METHOD)', flags=re.M) # No DOTALL, so .* is [^\n]*
125
+
126
+ self._expr_block_meas_kp_blob_parser = re.compile(
127
+ r'\/begin\s+(?P<keyword>\w+)\s+(?P<class>\w+)\s*\n'
128
+ r'\s*KP_BLOB\s+(?P<address>0x[0-9a-fA-F]+)\s*\n'
129
+ r'\s*\/end\s+\1'
130
+ )
131
+
132
+ self._compu_methods = {}
133
+ self._included_compu_methods = []
134
+
135
+ self._tl_compu_method_template = Template(
136
+ '$indentation/begin COMPU_METHOD\n'
137
+ '$indentation $name /* Name */\n'
138
+ '$indentation "$ID" /* LongIdentifier */\n'
139
+ '$indentation $conv_type /* ConversionType */\n'
140
+ '$indentation "$disp_format" /* Format */\n'
141
+ '$indentation "$unit" /* Unit */\n'
142
+ '$indentation $conversion\n'
143
+ '$indentation/end COMPU_METHOD'
144
+ )
145
+
146
+ # COMPU_METHOD template that looks similar to COMPU_METHOD generated by Embedded Coder
147
+ self._ec_compu_method_template = Template(
148
+ '$indentation/begin COMPU_METHOD\n'
149
+ '$indentation /* Name of CompuMethod */ $name\n'
150
+ '$indentation /* Long identifier */ "$ID"\n'
151
+ '$indentation /* Conversion Type */ $conv_type\n'
152
+ '$indentation /* Format */ "$disp_format"\n'
153
+ '$indentation /* Units */ "$unit"\n'
154
+ '$indentation /* Coefficients */ $conversion\n'
155
+ '$indentation/end COMPU_METHOD'
156
+ )
157
+
158
+ for filename in a2l_files_unit:
159
+ removed_symbols = self._parse_unit(filename)
160
+ self._removed_symbols.extend(removed_symbols)
161
+ self.debug('Loaded %s', filename)
162
+ for filename in a2l_files_gen:
163
+ self._parse_gen(filename)
164
+ self.debug('Loaded %s', filename)
165
+
166
+ def _parse_unit(self, filename):
167
+ """Parse the unit a2l-files and apply a filter to only active parameters."""
168
+ self.debug('Processing %s', filename)
169
+ with open(filename, 'r', encoding="ISO-8859-1") as a2lfp:
170
+ a2ld = a2lfp.read()
171
+ file_path_parts = os.path.split(filename)
172
+ unit = file_path_parts[1].split('.')[0]
173
+ base_path = file_path_parts[0]
174
+ dcl_match = re.search(r'VcDcl[\w]+Mdl(__[\w]+)', base_path)
175
+ if dcl_match is not None and 'Mdl' not in unit:
176
+ # Hand coded model names including "__" will lead to this, due to name mismatch of .a2l and .json files.
177
+ # E.g. VcDclPtrlMdl__denso:
178
+ # 1. config_VcDclPtrlMdl__denso.json vs VcDclPtrlMdl__denso.a2l.
179
+ # 1.1. Match: unit in self._per_unit_cfg.
180
+ # 2. config_VcDclPtrl__denso.json vs VcDclPtrl.a2l.
181
+ # 2.1. No match: unit not in self._per_unit_cfg.
182
+ old_unit = unit
183
+ unit = unit + dcl_match.group(1)
184
+ self.info(
185
+ 'Found unit %s with .a2l and .json file name mismatch. Using new unit name: %s',
186
+ old_unit,
187
+ unit
188
+ )
189
+
190
+ if unit in self._per_unit_cfg:
191
+ u_conf = self._per_unit_cfg[unit]
192
+ code_generator = u_conf['code_generator'] if 'code_generator' in u_conf else 'target_link'
193
+ else:
194
+ u_conf = {}
195
+ code_generator = 'target_link'
196
+
197
+ if code_generator == 'embedded_coder':
198
+ blks = re.findall(r'(?:\s*\n)*(\s*/begin '
199
+ r'(?!PROJECT|HEADER|MODULE|MOD_PAR|MOD_COMMON)(\w+)\s*(?:\n\s*)?'
200
+ r'(?:/\*\s*[\w ]+\s*\*/\s*)?(\w+)([\[\d+\]]*).*?\n.*?/end\s+\2)',
201
+ a2ld, flags=re.M | re.DOTALL)
202
+ else:
203
+ blks = re.findall(r'(?:\s*\n)*(\s*/begin (?!PROJECT|MODULE)(\w+)[\n\s]*'
204
+ r'(\w+(?:\.\w+)?)([\[\d+\]]*).*?\n.*?/end\s+\2)', a2ld,
205
+ flags=re.M | re.DOTALL)
206
+
207
+ compu_method_translators = self._parse_compu_methods(a2ld, unit)
208
+ unit_blks = {}
209
+ removed_symbols = []
210
+ if unit not in self._per_unit_cfg:
211
+ # Handcoded a2l without json-files will lead to this.
212
+ # Add json files for the handcoded a2l!
213
+ # NOTE: Assuming TargetLink
214
+ self.debug('%s is not in the units list. Looking for json.', unit)
215
+ config_filename = os.path.join(
216
+ self._prj_cfg.get_unit_cfg_deliv_dir(),
217
+ f'config_{unit}.json')
218
+ self.debug('Looking for %s', config_filename)
219
+ if os.path.isfile(config_filename):
220
+ with open(config_filename, 'r', encoding="utf-8") as config_file:
221
+ u_conf = json.load(config_file)
222
+ self._handle_config(
223
+ code_generator, unit, u_conf, blks, unit_blks, removed_symbols, compu_method_translators
224
+ )
225
+ else:
226
+ self.warning('%s does not have a unit_cfg json, '
227
+ 'including all a2l-parameters', unit)
228
+ for blk_def, type_, label, size in blks:
229
+ if type_ == 'COMPU_METHOD':
230
+ blk_def, label = self._replace_compu_method(blk_def, label, compu_method_translators)
231
+ self.add_block_definition(unit_blks, type_, label, size, blk_def, compu_method_translators)
232
+ else:
233
+ self._handle_config(
234
+ code_generator, unit, u_conf, blks, unit_blks, removed_symbols, compu_method_translators
235
+ )
236
+ deep_dict_update(self._blks, unit_blks)
237
+ return removed_symbols
238
+
239
+ def _handle_config(self, code_generator, unit, u_conf, blks, unit_blks, removed_symbols, compu_method_translators):
240
+ """Merge all types of ram for the unit."""
241
+ ram = u_conf['inports']
242
+ ram.update(u_conf['outports'])
243
+ ram.update(u_conf['local_vars'])
244
+ # TODO: Function the variables and labels needs to be removed from
245
+ # the FUNCTION block too
246
+ for blk_def, type_, label, size in blks:
247
+ remove_excluded_symbol = True
248
+ inc = False
249
+ if type_ == 'AXIS_PTS':
250
+ if label in u_conf['calib_consts']:
251
+ inc = True
252
+ elif type_ == 'CHARACTERISTIC':
253
+ if label in u_conf['calib_consts']:
254
+ if label in [axis_label for _, axis_type, axis_label, _ in blks if axis_type == 'AXIS_PTS']:
255
+ # AXIS_PTS can be used as CHARACTERISTC but not the other way around.
256
+ # If there are duplicates, use the AXIS_PTS.
257
+ self.debug('Will not add the block for CHARACTERISTC %s, but will keep it as a symbol,'
258
+ ' since it exists as AXIS_PTS', label)
259
+ remove_excluded_symbol = False
260
+ inc = False
261
+ else:
262
+ inc = self._handle_axis_ptr_ref_config(u_conf, blk_def, unit)
263
+ elif type_ == 'MEASUREMENT':
264
+ if label in ram:
265
+ key = label if size is None else label + size
266
+ if label in u_conf['outports']:
267
+ # This unit is producing the measurement.
268
+ inc = True
269
+ elif key in unit_blks.get(type_, {}):
270
+ # This unit is not producing it, and it has already been added
271
+ inc = False
272
+ else:
273
+ # This unit is not producing it, but it has not been added
274
+ # Could be external signal, etc.
275
+ inc = True
276
+ elif type_ == 'COMPU_METHOD':
277
+ inc = True
278
+ blk_def, label = self._replace_compu_method(blk_def, label, compu_method_translators)
279
+ else:
280
+ inc = True
281
+ if inc:
282
+ self.add_block_definition(unit_blks, type_, label, size, blk_def, compu_method_translators)
283
+ else:
284
+ if remove_excluded_symbol:
285
+ removed_symbols.append(label + size)
286
+ self.debug('Did not include A2L-blk %s%s', label, size)
287
+ if not self._unit_cfg.check_if_in_unit_cfg(unit, label):
288
+ if type_ != 'COMPU_METHOD':
289
+ self.warning('A2l block %s not in config json file for %s', label, unit)
290
+
291
+ if 'FUNCTION' in unit_blks:
292
+ unit_blks['FUNCTION'] = self._remove_symbols_from_func_blks(
293
+ code_generator, unit_blks['FUNCTION'], removed_symbols
294
+ )
295
+
296
+ if 'GROUP' in unit_blks:
297
+ unit_blks['GROUP'] = self._remove_symbols_from_grp_blks(unit_blks['GROUP'], removed_symbols)
298
+
299
+ def _handle_axis_ptr_ref_config(self, u_conf, blk, unit):
300
+ """Remove blocks referencing undefined blocks."""
301
+ ref_re = re.compile(r'\s*AXIS_PTS_REF\s*([\w]*)')
302
+ for axis_ptr_ref in ref_re.findall(blk):
303
+ if axis_ptr_ref not in u_conf['calib_consts']:
304
+ self.debug('Excluding due to %s missing in config', axis_ptr_ref)
305
+ return False
306
+ if not self._unit_cfg.check_if_in_unit_cfg(unit, axis_ptr_ref):
307
+ self.debug('Excluding due to %s not active in config', axis_ptr_ref)
308
+ return False
309
+ return True
310
+
311
+ def add_block_definition(self, unit_blks, type_, label, size, blk_def, compu_method_translators):
312
+ """Add block definition to A2L-file."""
313
+ size = '' if size is None else size
314
+ blk_def = self._replace_conversions(blk_def, compu_method_translators)
315
+ if type_ not in unit_blks:
316
+ unit_blks[type_] = {}
317
+ unit_blks[type_][label + size] = blk_def
318
+
319
+ @staticmethod
320
+ def _parse_func_blk(code_generator, fnc_blk):
321
+ """Remove the unused symbols from the FUNCTION blocks in the A2L-file.
322
+ Parse the FUNCTION block, TL or EC style based on code_generator.
323
+ """
324
+
325
+ if code_generator == 'target_link':
326
+ pattern = r'\s*/begin\s+FUNCTION\s*?\n\s*(\w+).*?\n\s*"(.*?)".*?\n(.*)'
327
+ else:
328
+ pattern = r'\s*/begin\s+FUNCTION\s*?\n\s*.*\*/\s*(\w+).*?\n\s*.*\*/\s*"(.*?)".*?\n(.*)'
329
+ res = re.match(pattern, fnc_blk, flags=re.M | re.DOTALL)
330
+ fnc_name = res.group(1)
331
+ long_id = res.group(2)
332
+ fnc_dict = {
333
+ 'fnc_name': fnc_name,
334
+ 'long_id': long_id,
335
+ 'body': {}
336
+ }
337
+ fnc_body = res.group(3)
338
+ sb_res = re.findall(r'\s*/begin\s+(\w+[\[\d\]]*)\s*\n\s*'
339
+ r'(.*?\n)\s*/end \1', fnc_body, flags=re.M | re.DOTALL)
340
+ for sb_name, sub_blk in sb_res:
341
+ symbols = set(re.findall(r'\s*(\w+(?:\.\w+)?[\[\d\]]*).*?\n', sub_blk, flags=re.M))
342
+ fnc_dict['body'][sb_name] = symbols
343
+ return fnc_dict
344
+
345
+ @staticmethod
346
+ def _parse_grp_blk(grp_blk):
347
+ """Remove the unused symbols from the GROUP blocks in the A2L-file."""
348
+ # parse the GROUP block
349
+ res = re.match(r'\s*/begin\s+GROUP\s*?\n\s*.*\*/\s*(\w+).*?\n\s*.*\*/\s*"(.*?)".*?\n(.*)',
350
+ grp_blk, flags=re.M | re.DOTALL)
351
+ fnc_name = res.group(1)
352
+ long_id = res.group(2)
353
+ fnc_dict = {
354
+ 'fnc_name': fnc_name,
355
+ 'long_id': long_id,
356
+ 'body': {}
357
+ }
358
+ fnc_body = res.group(3)
359
+ sb_res = re.findall(r'\s*/begin\s+(\w+[\[\d\]]*)\s*\n\s*'
360
+ r'(.*?\n)\s*/end \1', fnc_body, flags=re.M | re.DOTALL)
361
+ for sb_name, sub_blk in sb_res:
362
+ symbols = set(re.findall(r'\s*(\w+(?:\.\w+)?[\[\d\]]*).*?\n', sub_blk, flags=re.M))
363
+ fnc_dict['body'][sb_name] = symbols
364
+ return fnc_dict
365
+
366
+ def _recursive_remove(self, a2l_dict, name):
367
+ """Remove symbols from A2L dict (e.g. group or function)."""
368
+ if name in a2l_dict:
369
+ blk = a2l_dict[name]
370
+ if 'SUB_FUNCTION' in blk:
371
+ for sub_fnc in blk['SUB_FUNCTION']:
372
+ if self._recursive_remove(a2l_dict, sub_fnc):
373
+ blk['SUB_FUNCTION'] = blk['SUB_FUNCTION'] - set([sub_fnc])
374
+ elif 'SUB_GROUP' in blk:
375
+ for sub_grp in blk['SUB_GROUP']:
376
+ if self._recursive_remove(a2l_dict, sub_grp):
377
+ blk['SUB_GROUP'] = blk['SUB_GROUP'] - set([sub_grp])
378
+ empty = True
379
+ for key in blk:
380
+ if blk[key]:
381
+ empty = False
382
+ break
383
+ if empty:
384
+ a2l_dict.pop(name)
385
+ return True
386
+ return False
387
+
388
+ def _remove_symbols_from_func_blks(self, code_generator, fnc_blks, removed_symbols):
389
+ """Remove the unused symbols from function blocks.
390
+
391
+ If the function block is empty, it too will be removed.
392
+ first iteration - remove all symbols that have been removed
393
+ second iteration - recusively remover all functions without symbols
394
+ """
395
+ fnc_dict = {}
396
+ for fnc_name, fnc_blk in fnc_blks.items():
397
+ fnc_dict[fnc_name] = {}
398
+ u_fnc_bdy = self._parse_func_blk(code_generator, fnc_blk)['body']
399
+ sub_blk_types = set(u_fnc_bdy.keys()) - set(['SUB_FUNCTION'])
400
+ for type_ in list(sub_blk_types):
401
+ fnc_dict[fnc_name][type_] = u_fnc_bdy[type_] - set(removed_symbols)
402
+ if 'SUB_FUNCTION' in u_fnc_bdy:
403
+ fnc_dict[fnc_name]['SUB_FUNCTION'] = u_fnc_bdy['SUB_FUNCTION']
404
+ # second iteration - remove empty FUNCTION blocks
405
+ # TODO: Add functionality which parses the the function tree structures
406
+ # And the run recursive remove on all tree roots.
407
+ for fnc_name in fnc_blks.keys():
408
+ self._recursive_remove(fnc_dict, fnc_name)
409
+ # generate new function blocks
410
+ new_fnc_blks = {}
411
+ for fnc_name, fnc_data in fnc_dict.items():
412
+ fnc_blk = f' /begin FUNCTION\n {fnc_name}\t/* Name */\n'
413
+ fnc_blk += " \"\"\t/* LongIdentifier */\n"
414
+ for sub_sec in sorted(fnc_data.keys()):
415
+ sub_sec_data = fnc_data[sub_sec]
416
+ if sub_sec_data:
417
+ fnc_blk += f" /begin {sub_sec}\n"
418
+ for param in sorted(sub_sec_data):
419
+ fnc_blk += f" {param}\t/* Identifier */\n"
420
+ fnc_blk += f" /end {sub_sec}\n"
421
+ fnc_blk += " /end FUNCTION"
422
+ new_fnc_blks[fnc_name] = fnc_blk
423
+ return new_fnc_blks
424
+
425
+ def _remove_symbols_from_grp_blks(self, grp_blks, removed_symbols):
426
+ """Remove the unused symbols from group blocks.
427
+
428
+ If the group block is empty, it too will be removed.
429
+ first iteration - remove all symbols that have been removed
430
+ second iteration - recusively remover all groups without symbols
431
+ """
432
+ grp_dict = {}
433
+ for grp_name, grp_blk in grp_blks.items():
434
+ grp_dict[grp_name] = {}
435
+ u_grp_bdy = self._parse_grp_blk(grp_blk)['body']
436
+ sub_blk_types = set(u_grp_bdy.keys()) - set(['SUB_GROUP'])
437
+ for type_ in list(sub_blk_types):
438
+ grp_dict[grp_name][type_] = u_grp_bdy[type_] - set(removed_symbols)
439
+ if 'SUB_GROUP' in u_grp_bdy:
440
+ grp_dict[grp_name]['SUB_GROUP'] = u_grp_bdy['SUB_GROUP']
441
+ # second iteration - remove empty GROUP blocks
442
+ # TODO: Add functionality which parses the the group tree structures
443
+ # And the run recursive remove on all tree roots.
444
+ for grp_name in grp_blks.keys():
445
+ self._recursive_remove(grp_dict, grp_name)
446
+ # generate new group blocks
447
+ new_grp_blks = {}
448
+ for grp_name, grp_data in grp_dict.items():
449
+ grp_blk = f" /begin GROUP \n /* Name */ {grp_name}\n"
450
+ grp_blk += " /* Long identifier */ \"\"\n"
451
+ for sub_sec in sorted(grp_data.keys()):
452
+ sub_sec_data = grp_data[sub_sec]
453
+ if sub_sec_data:
454
+ grp_blk += f" /begin {sub_sec}\n"
455
+ for param in sorted(sub_sec_data):
456
+ grp_blk += f" {param}\n"
457
+ grp_blk += f" /end {sub_sec}\n"
458
+ grp_blk += " /end GROUP"
459
+ new_grp_blks[grp_name] = grp_blk
460
+
461
+ return new_grp_blks
462
+
463
+ def _parse_gen(self, filename):
464
+ """Parse the generated a2l-files, without filter."""
465
+ self.debug('parsing gen a2l: %s', filename)
466
+ with open(filename, 'r', encoding="utf-8") as a2lfp:
467
+ a2ld = a2lfp.read()
468
+ for blk_def, type_, label in self._block_finder.findall(a2ld):
469
+ self._blks.setdefault(type_, {}).setdefault(label, blk_def)
470
+
471
+ @staticmethod
472
+ def _replace_compu_method(blk_def, label, compu_method_translators):
473
+ """Replace the compu method block and label."""
474
+ for translator in compu_method_translators:
475
+ if translator['old_compu_method'] == blk_def:
476
+ return translator['new_compu_method'], translator['new_name']
477
+ return blk_def, label
478
+
479
+ def _store_compu_method(self, ID, conv_type, disp_format, unit, conversion, indentation, u_conf):
480
+ """Stash compu methods that exists in the resulting a2l."""
481
+ key = (ID, conv_type, disp_format, unit, conversion)
482
+ if key in self._compu_methods:
483
+ new_name = self._compu_methods[key]['name']
484
+ new_compu_method = self._compu_methods[key]['method']
485
+ else:
486
+ new_name = 'Scaling_' + str(len(self._compu_methods))
487
+ if 'code_generator' in u_conf and u_conf['code_generator'] == 'embedded_coder':
488
+ new_compu_method = self._ec_compu_method_template.substitute(
489
+ name=new_name,
490
+ ID=ID,
491
+ conv_type=conv_type,
492
+ disp_format=disp_format,
493
+ unit=unit,
494
+ conversion=conversion,
495
+ indentation=indentation
496
+ )
497
+ else:
498
+ new_compu_method = self._tl_compu_method_template.substitute(
499
+ name=new_name,
500
+ ID=ID,
501
+ conv_type=conv_type,
502
+ disp_format=disp_format,
503
+ unit=unit,
504
+ conversion=conversion,
505
+ indentation=indentation
506
+ )
507
+ self._compu_methods.update({key: {'name': new_name,
508
+ 'method': new_compu_method}})
509
+ return new_name, new_compu_method
510
+
511
+ @staticmethod
512
+ def _replace_conversions(blk_def, compu_method_translators):
513
+ """Replace conversion identifiers in a2l block."""
514
+ for translator in compu_method_translators:
515
+ # The following check is faster than running the regex on the block.
516
+ # It DOES give false positives, which is why the regex is used for substitution
517
+ # and we do not immidiately return after one positive
518
+ if translator['old_name'] in blk_def:
519
+ blk_def = translator['regex'].sub(translator['replacement'], blk_def)
520
+ return blk_def
521
+
522
+ def _parse_compu_methods(self, blk_def, unit):
523
+ """Replace compu methods to not overwrite any of them."""
524
+ compu_method_translators = [] # Translators for one processed a2l file. Needs to be reset between files
525
+ u_conf = self._per_unit_cfg[unit] if unit in self._per_unit_cfg else {}
526
+ if 'code_generator' in u_conf and u_conf['code_generator'] == 'embedded_coder':
527
+ for match in self._ec_compu_method_parser.finditer(blk_def):
528
+ new_name, new_compu_method = self._store_compu_method(
529
+ match['ID'],
530
+ match['conv_type'],
531
+ match['disp_format'],
532
+ match['unit'],
533
+ match['conversion'],
534
+ match['indentation'],
535
+ u_conf
536
+ )
537
+ compu_method_translators.append(
538
+ {
539
+ 'new_name': new_name,
540
+ 'old_name': match['name'],
541
+ 'regex': re.compile(
542
+ r'(\s*)' # beginning
543
+ r'\s*(/\* Conversion [Mm]ethod\s*\*/\s*)' # optional comment
544
+ r'\b{name}\b' # word
545
+ r'('
546
+ r'\s*\n' # newline
547
+ r')'.format(name=match['name']) # end of end-match
548
+ ),
549
+ 'replacement': r'\1\2{name}\3'.format(name=new_name),
550
+ 'old_compu_method': match['compu_method'],
551
+ 'new_compu_method': new_compu_method
552
+ }
553
+ )
554
+ else:
555
+ for match in self._tl_compu_method_parser.finditer(blk_def):
556
+ new_name, new_compu_method = self._store_compu_method(
557
+ match['ID'],
558
+ match['conv_type'],
559
+ match['disp_format'],
560
+ match['unit'],
561
+ match['conversion'],
562
+ match['indentation'],
563
+ u_conf
564
+ )
565
+ compu_method_translators.append(
566
+ {
567
+ 'new_name': new_name,
568
+ 'old_name': match['name'],
569
+ 'regex': re.compile(
570
+ r'(\s*)' # beginning
571
+ r'\b{name}\b' # word
572
+ r'(' # start of end-match
573
+ r'\s*(/\* Conversion \*/)?' # optional comment
574
+ r'\s*\n' # newline
575
+ r')'.format(name=match['name']) # end of end-match
576
+ ),
577
+ 'replacement': r'\1{name}\2'.format(name=new_name),
578
+ 'old_compu_method': match['compu_method'],
579
+ 'new_compu_method': new_compu_method
580
+ }
581
+ )
582
+ return compu_method_translators
583
+
584
+ def _patch_kp_blob(self, block):
585
+ """Return updated measurement block text.
586
+ Args:
587
+ block (str): A2L text block
588
+ Returns:
589
+ a2l_text (str): A2L text without KP_BLOB.
590
+ """
591
+ ecu_address = '0x00000000'
592
+ for match in self._expr_block_meas_kp_blob_parser.finditer(block):
593
+ start, end = match.span()
594
+ block = f'{block[:start]}ECU_ADDRESS {ecu_address}{block[end:]}'
595
+ return block
596
+
597
+ def merge(self, f_name, complete_a2l=False, silver_a2l=False):
598
+ """Write merged a2l-file.
599
+
600
+ Args:
601
+ f_name (str): Output filename.
602
+ """
603
+ a2l = ''
604
+ a2l_config = self._prj_cfg.get_a2l_cfg()
605
+ for _, data in self._blks.items():
606
+ for _, blk in data.items():
607
+ if not a2l_config['allow_kp_blob']:
608
+ blk = self._patch_kp_blob(blk)
609
+ a2l += blk + '\n\n'
610
+
611
+ if complete_a2l:
612
+ events = []
613
+ time_unit_10ms = '0x07'
614
+ rasters = self._prj_cfg.get_units_raster_cfg()
615
+ for xcp_id, evt_data in enumerate(rasters['SampleTimes'].items(), 1):
616
+ events.append({
617
+ 'time_cycle': '0x%02X' % int(evt_data[1] * 100),
618
+ 'time_unit': time_unit_10ms,
619
+ 'name': evt_data[0],
620
+ 'channel_id': '0x%04X' % xcp_id
621
+ })
622
+ a2l_template = A2lProjectTemplate(
623
+ a2l,
624
+ a2l_config['asap2_version'],
625
+ a2l_config['name'],
626
+ events,
627
+ a2l_config['ip_address'],
628
+ a2l_config['ip_port']
629
+ )
630
+ a2l = a2l_template.render()
631
+ elif silver_a2l:
632
+ a2l_template = A2lSilverTemplate(a2l)
633
+ a2l = a2l_template.render()
634
+
635
+ self.a2l = a2l
636
+
637
+ with open(f_name, 'w', encoding="ISO-8859-1") as ma2l:
638
+ ma2l.write(a2l)
639
+ self.info('Written the merged A2L-file %s', f_name)
640
+
641
+ def get_characteristic_axis_data(self):
642
+ """Get characteristic map axis data from merged a2l-file."""
643
+ axis_data = {}
644
+ for blk_def, type_, label in self._block_finder.findall(self.a2l):
645
+ if type_ == "CHARACTERISTIC":
646
+ axes = re.findall('AXIS_PTS_REF (.*)', blk_def)
647
+ if label in axis_data:
648
+ self.critical("Multiple CHARACTERISTIC for %s in merged a2l.", label)
649
+ axis_data[label] = {'axes': axes}
650
+ return axis_data