odxtools 10.2.1__py3-none-any.whl → 10.4.0__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.
- odxtools/cli/browse.py +4 -2
- odxtools/cli/compare.py +3 -3
- odxtools/compositecodec.py +1 -1
- odxtools/configdata.py +70 -0
- odxtools/configdatadictionaryspec.py +57 -0
- odxtools/configiditem.py +18 -0
- odxtools/configitem.py +85 -0
- odxtools/configrecord.py +146 -0
- odxtools/database.py +40 -0
- odxtools/dataiditem.py +18 -0
- odxtools/datarecord.py +132 -0
- odxtools/decodestate.py +1 -1
- odxtools/diagcommdataconnector.py +61 -0
- odxtools/diaglayers/diaglayer.py +9 -14
- odxtools/diagservice.py +10 -10
- odxtools/ecuconfig.py +89 -0
- odxtools/encryptcompressmethod.py +16 -3
- odxtools/externflashdata.py +21 -2
- odxtools/flashdata.py +40 -2
- odxtools/identvalue.py +16 -3
- odxtools/internflashdata.py +4 -0
- odxtools/isotp_state_machine.py +11 -11
- odxtools/itemvalue.py +77 -0
- odxtools/multipleecujob.py +178 -0
- odxtools/multipleecujobspec.py +142 -0
- odxtools/nameditemlist.py +2 -2
- odxtools/optionitem.py +79 -0
- odxtools/outputparam.py +1 -1
- odxtools/parameters/codedconstparameter.py +2 -3
- odxtools/readdiagcommconnector.py +79 -0
- odxtools/readparamvalue.py +52 -0
- odxtools/request.py +3 -3
- odxtools/response.py +8 -4
- odxtools/snrefcontext.py +2 -0
- odxtools/statemachine.py +3 -2
- odxtools/systemitem.py +23 -0
- odxtools/templates/diag_layer_container.odx-d.xml.jinja2 +4 -0
- odxtools/templates/ecu_config.odx-e.xml.jinja2 +38 -0
- odxtools/templates/flash.odx-f.xml.jinja2 +2 -2
- odxtools/templates/macros/printAdminData.xml.jinja2 +1 -1
- odxtools/templates/macros/printAudience.xml.jinja2 +3 -3
- odxtools/templates/macros/printChecksum.xml.jinja2 +1 -1
- odxtools/templates/macros/printCompuMethod.xml.jinja2 +2 -2
- odxtools/templates/macros/printConfigData.xml.jinja2 +39 -0
- odxtools/templates/macros/printConfigDataDictionarySpec.xml.jinja2 +22 -0
- odxtools/templates/macros/printConfigItems.xml.jinja2 +66 -0
- odxtools/templates/macros/printConfigRecord.xml.jinja2 +73 -0
- odxtools/templates/macros/printDOP.xml.jinja2 +5 -6
- odxtools/templates/macros/printDataRecord.xml.jinja2 +35 -0
- odxtools/templates/macros/printDiagCommDataConnector.xml.jinja2 +66 -0
- odxtools/templates/macros/printDiagDataDictionarySpec.xml.jinja2 +107 -0
- odxtools/templates/macros/printDiagLayer.xml.jinja2 +11 -97
- odxtools/templates/macros/printElementId.xml.jinja2 +2 -0
- odxtools/templates/macros/printExpectedIdent.xml.jinja2 +1 -1
- odxtools/templates/macros/printFlashdata.xml.jinja2 +2 -2
- odxtools/templates/macros/printFunctionalClass.xml.jinja2 +4 -4
- odxtools/templates/macros/printItemValue.xml.jinja2 +31 -0
- odxtools/templates/macros/printMultipleEcuJob.xml.jinja2 +77 -0
- odxtools/templates/macros/printOwnIdent.xml.jinja2 +1 -1
- odxtools/templates/macros/printSecurity.xml.jinja2 +4 -4
- odxtools/templates/macros/printSegment.xml.jinja2 +1 -1
- odxtools/templates/multiple-ecu-job-spec.odx-m.xml.jinja2 +51 -0
- odxtools/validbasevariant.py +62 -0
- odxtools/validityfor.py +16 -3
- odxtools/variantmatcher.py +4 -4
- odxtools/version.py +2 -2
- odxtools/writediagcommconnector.py +77 -0
- odxtools/writepdxfile.py +58 -27
- {odxtools-10.2.1.dist-info → odxtools-10.4.0.dist-info}/METADATA +2 -1
- {odxtools-10.2.1.dist-info → odxtools-10.4.0.dist-info}/RECORD +74 -45
- {odxtools-10.2.1.dist-info → odxtools-10.4.0.dist-info}/WHEEL +1 -1
- {odxtools-10.2.1.dist-info → odxtools-10.4.0.dist-info}/entry_points.txt +0 -0
- {odxtools-10.2.1.dist-info → odxtools-10.4.0.dist-info}/licenses/LICENSE +0 -0
- {odxtools-10.2.1.dist-info → odxtools-10.4.0.dist-info}/top_level.txt +0 -0
odxtools/datarecord.py
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
2
|
+
import re
|
3
|
+
from dataclasses import dataclass, field
|
4
|
+
from typing import Any, cast
|
5
|
+
from xml.etree import ElementTree
|
6
|
+
|
7
|
+
from bincopy import BinFile
|
8
|
+
|
9
|
+
from .datafile import Datafile
|
10
|
+
from .dataformatselection import DataformatSelection
|
11
|
+
from .element import NamedElement
|
12
|
+
from .exceptions import odxraise, odxrequire
|
13
|
+
from .identvalue import IdentValue
|
14
|
+
from .odxdoccontext import OdxDocContext
|
15
|
+
from .odxlink import OdxLinkDatabase, OdxLinkId
|
16
|
+
from .snrefcontext import SnRefContext
|
17
|
+
from .specialdatagroup import SpecialDataGroup
|
18
|
+
from .utils import dataclass_fields_asdict
|
19
|
+
|
20
|
+
|
21
|
+
@dataclass(kw_only=True)
|
22
|
+
class DataRecord(NamedElement):
|
23
|
+
rule: str | None = None
|
24
|
+
key: str | None = None
|
25
|
+
data_id: IdentValue | None = None
|
26
|
+
sdgs: list[SpecialDataGroup] = field(default_factory=list)
|
27
|
+
|
28
|
+
# at most one of the following two attributes is not None
|
29
|
+
datafile: Datafile | None = None
|
30
|
+
data: str | None = None
|
31
|
+
|
32
|
+
dataformat: DataformatSelection
|
33
|
+
|
34
|
+
@property
|
35
|
+
def dataset(self) -> BinFile | bytearray:
|
36
|
+
if self.datafile is not None:
|
37
|
+
db = odxrequire(self._database)
|
38
|
+
if db is None:
|
39
|
+
return bytearray()
|
40
|
+
|
41
|
+
datafile = odxrequire(self.datafile)
|
42
|
+
if datafile is None:
|
43
|
+
return bytearray()
|
44
|
+
|
45
|
+
aux_file = odxrequire(db.auxiliary_files.get(datafile.value))
|
46
|
+
if aux_file is None:
|
47
|
+
return bytearray()
|
48
|
+
|
49
|
+
data_str = aux_file.read().decode()
|
50
|
+
aux_file.seek(0)
|
51
|
+
elif self.data is not None:
|
52
|
+
data_str = self.data
|
53
|
+
else:
|
54
|
+
odxraise("No data specified for DATA-RECORD")
|
55
|
+
return bytearray()
|
56
|
+
|
57
|
+
if self.dataformat in (DataformatSelection.INTEL_HEX, DataformatSelection.MOTOROLA_S):
|
58
|
+
bf = BinFile()
|
59
|
+
|
60
|
+
# remove white space and empty lines
|
61
|
+
bf.add("\n".join([re.sub(r"\s", "", x) for x in data_str.splitlines() if x.strip()]))
|
62
|
+
|
63
|
+
return bf
|
64
|
+
elif self.dataformat == DataformatSelection.BINARY:
|
65
|
+
return bytearray.fromhex(re.sub(r"\s", "", data_str, flags=re.MULTILINE))
|
66
|
+
|
67
|
+
# user defined formats are not possible here
|
68
|
+
odxraise(f"Unsupported data format {self.dataformat.value}")
|
69
|
+
return bytearray()
|
70
|
+
|
71
|
+
@property
|
72
|
+
def blob(self) -> bytearray:
|
73
|
+
"""Computes the binary data blob that ought to be send to the ECU.
|
74
|
+
|
75
|
+
i.e., this property stitches together the data of all
|
76
|
+
segments.
|
77
|
+
|
78
|
+
Note that, in order to reduce memory usage, this property is
|
79
|
+
not computed when instanting the data record object, but at
|
80
|
+
run time when it is accessed.
|
81
|
+
"""
|
82
|
+
|
83
|
+
if isinstance(self.dataset, BinFile):
|
84
|
+
return cast(bytearray, self.dataset.as_binary())
|
85
|
+
|
86
|
+
return self.dataset
|
87
|
+
|
88
|
+
@staticmethod
|
89
|
+
def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "DataRecord":
|
90
|
+
|
91
|
+
kwargs = dataclass_fields_asdict(NamedElement.from_et(et_element, context))
|
92
|
+
|
93
|
+
rule = et_element.findtext("RULE")
|
94
|
+
key = et_element.findtext("KEY")
|
95
|
+
data_id = None
|
96
|
+
if (did_elem := et_element.find("DATA-ID")) is not None:
|
97
|
+
data_id = IdentValue.from_et(did_elem, context)
|
98
|
+
sdgs = [SpecialDataGroup.from_et(sdge, context) for sdge in et_element.iterfind("SDGS/SDG")]
|
99
|
+
datafile = None
|
100
|
+
if (df_elem := et_element.find("DATA-FILE")) is not None:
|
101
|
+
datafile = Datafile.from_et(df_elem, context)
|
102
|
+
data = et_element.findtext("DATA")
|
103
|
+
|
104
|
+
dataformat_str = odxrequire(et_element.attrib.get("DATAFORMAT"))
|
105
|
+
try:
|
106
|
+
dataformat = DataformatSelection(dataformat_str)
|
107
|
+
except ValueError:
|
108
|
+
dataformat = cast(DataformatSelection, None)
|
109
|
+
odxraise(f"Encountered unknown data format selection '{dataformat_str}'")
|
110
|
+
|
111
|
+
return DataRecord(
|
112
|
+
rule=rule,
|
113
|
+
key=key,
|
114
|
+
data_id=data_id,
|
115
|
+
sdgs=sdgs,
|
116
|
+
datafile=datafile,
|
117
|
+
data=data,
|
118
|
+
dataformat=dataformat,
|
119
|
+
**kwargs)
|
120
|
+
|
121
|
+
def _build_odxlinks(self) -> dict[OdxLinkId, Any]:
|
122
|
+
odxlinks: dict[OdxLinkId, Any] = {}
|
123
|
+
|
124
|
+
return odxlinks
|
125
|
+
|
126
|
+
def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
|
127
|
+
pass
|
128
|
+
|
129
|
+
def _resolve_snrefs(self, context: SnRefContext) -> None:
|
130
|
+
# this is slightly hacky because we only remember the
|
131
|
+
# applicable ODX database and do not resolve any SNREFs here
|
132
|
+
self._database = odxrequire(context.database)
|
odxtools/decodestate.py
CHANGED
@@ -0,0 +1,61 @@
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
2
|
+
from dataclasses import dataclass
|
3
|
+
from typing import Any
|
4
|
+
from xml.etree import ElementTree
|
5
|
+
|
6
|
+
from .exceptions import odxrequire
|
7
|
+
from .odxdoccontext import OdxDocContext
|
8
|
+
from .odxlink import OdxLinkDatabase, OdxLinkId
|
9
|
+
from .readdiagcommconnector import ReadDiagCommConnector
|
10
|
+
from .snrefcontext import SnRefContext
|
11
|
+
from .utils import read_hex_binary
|
12
|
+
from .writediagcommconnector import WriteDiagCommConnector
|
13
|
+
|
14
|
+
|
15
|
+
@dataclass(kw_only=True)
|
16
|
+
class DiagCommDataConnector:
|
17
|
+
uncompressed_size: int
|
18
|
+
source_start_address: int
|
19
|
+
read_diag_comm_connector: ReadDiagCommConnector | None = None
|
20
|
+
write_diag_comm_connector: WriteDiagCommConnector | None = None
|
21
|
+
|
22
|
+
@staticmethod
|
23
|
+
def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "DiagCommDataConnector":
|
24
|
+
uncompressed_size = int(odxrequire(et_element.findtext("UNCOMPRESSED-SIZE")))
|
25
|
+
source_start_address = odxrequire(read_hex_binary(et_element.find("SOURCE-START-ADDRESS")))
|
26
|
+
|
27
|
+
read_diag_comm_connector = None
|
28
|
+
if (rdcc_elem := et_element.find("READ-DIAG-COMM-CONNECTOR")) is not None:
|
29
|
+
read_diag_comm_connector = ReadDiagCommConnector.from_et(rdcc_elem, context)
|
30
|
+
|
31
|
+
write_diag_comm_connector = None
|
32
|
+
if (wdcc_elem := et_element.find("WRITE-DIAG-COMM-CONNECTOR")) is not None:
|
33
|
+
write_diag_comm_connector = WriteDiagCommConnector.from_et(wdcc_elem, context)
|
34
|
+
|
35
|
+
return DiagCommDataConnector(
|
36
|
+
uncompressed_size=uncompressed_size,
|
37
|
+
source_start_address=source_start_address,
|
38
|
+
read_diag_comm_connector=read_diag_comm_connector,
|
39
|
+
write_diag_comm_connector=write_diag_comm_connector)
|
40
|
+
|
41
|
+
def _build_odxlinks(self) -> dict[OdxLinkId, Any]:
|
42
|
+
odxlinks = {}
|
43
|
+
|
44
|
+
if self.read_diag_comm_connector is not None:
|
45
|
+
odxlinks.update(self.read_diag_comm_connector._build_odxlinks())
|
46
|
+
if self.write_diag_comm_connector is not None:
|
47
|
+
odxlinks.update(self.write_diag_comm_connector._build_odxlinks())
|
48
|
+
|
49
|
+
return odxlinks
|
50
|
+
|
51
|
+
def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
|
52
|
+
if self.read_diag_comm_connector is not None:
|
53
|
+
self.read_diag_comm_connector._resolve_odxlinks(odxlinks)
|
54
|
+
if self.write_diag_comm_connector is not None:
|
55
|
+
self.write_diag_comm_connector._resolve_odxlinks(odxlinks)
|
56
|
+
|
57
|
+
def _resolve_snrefs(self, context: SnRefContext) -> None:
|
58
|
+
if self.read_diag_comm_connector is not None:
|
59
|
+
self.read_diag_comm_connector._resolve_snrefs(context)
|
60
|
+
if self.write_diag_comm_connector is not None:
|
61
|
+
self.write_diag_comm_connector._resolve_snrefs(context)
|
odxtools/diaglayers/diaglayer.py
CHANGED
@@ -98,14 +98,7 @@ class DiagLayer:
|
|
98
98
|
if self.import_refs:
|
99
99
|
imported_links: dict[OdxLinkId, Any] = {}
|
100
100
|
for import_ref in self.import_refs:
|
101
|
-
imported_dl = odxlinks.resolve(import_ref
|
102
|
-
|
103
|
-
odxassert(
|
104
|
-
imported_dl.variant_type == DiagLayerType.ECU_SHARED_DATA,
|
105
|
-
f"Tried to import references from diagnostic layer "
|
106
|
-
f"'{imported_dl.short_name}' of type {imported_dl.variant_type.value}. "
|
107
|
-
f"Only ECU-SHARED-DATA layers may be referenced using the "
|
108
|
-
f"IMPORT-REF mechanism")
|
101
|
+
imported_dl = odxlinks.resolve(import_ref)
|
109
102
|
|
110
103
|
# TODO: ensure that the imported diagnostic layer has
|
111
104
|
# not been referenced in any PARENT-REF of the current
|
@@ -352,7 +345,7 @@ class DiagLayer:
|
|
352
345
|
# corresponding request for `decode_response()`.)
|
353
346
|
request_prefix = b''
|
354
347
|
if s.request is not None:
|
355
|
-
request_prefix = s.request.coded_const_prefix()
|
348
|
+
request_prefix = bytes(s.request.coded_const_prefix())
|
356
349
|
prefixes = [request_prefix]
|
357
350
|
gnrs = getattr(self, "global_negative_responses", [])
|
358
351
|
prefixes += [
|
@@ -383,7 +376,7 @@ class DiagLayer:
|
|
383
376
|
else:
|
384
377
|
cast(list[DiagService], sub_tree[-1]).append(service)
|
385
378
|
|
386
|
-
def _find_services_for_uds(self, message: bytes) -> list[DiagService]:
|
379
|
+
def _find_services_for_uds(self, message: bytes | bytearray) -> list[DiagService]:
|
387
380
|
prefix_tree = self._prefix_tree
|
388
381
|
|
389
382
|
# Find matching service(s) in prefix tree
|
@@ -398,7 +391,8 @@ class DiagLayer:
|
|
398
391
|
possible_services += cast(list[DiagService], prefix_tree[-1])
|
399
392
|
return possible_services
|
400
393
|
|
401
|
-
def _decode(self, message: bytes
|
394
|
+
def _decode(self, message: bytes | bytearray,
|
395
|
+
candidate_services: Iterable[DiagService]) -> list[Message]:
|
402
396
|
decoded_messages: list[Message] = []
|
403
397
|
|
404
398
|
for service in candidate_services:
|
@@ -420,7 +414,7 @@ class DiagLayer:
|
|
420
414
|
|
421
415
|
decoded_messages.append(
|
422
416
|
Message(
|
423
|
-
coded_message=message,
|
417
|
+
coded_message=bytes(message),
|
424
418
|
service=service,
|
425
419
|
coding_object=gnr,
|
426
420
|
param_dict=decoded_gnr))
|
@@ -437,12 +431,13 @@ class DiagLayer:
|
|
437
431
|
|
438
432
|
return decoded_messages
|
439
433
|
|
440
|
-
def decode(self, message: bytes) -> list[Message]:
|
434
|
+
def decode(self, message: bytes | bytearray) -> list[Message]:
|
441
435
|
candidate_services = self._find_services_for_uds(message)
|
442
436
|
|
443
437
|
return self._decode(message, candidate_services)
|
444
438
|
|
445
|
-
def decode_response(self, response: bytes
|
439
|
+
def decode_response(self, response: bytes | bytearray,
|
440
|
+
request: bytes | bytearray) -> list[Message]:
|
446
441
|
candidate_services = self._find_services_for_uds(request)
|
447
442
|
if candidate_services is None:
|
448
443
|
raise DecodeError(f"Couldn't find corresponding service for request {request.hex()}.")
|
odxtools/diagservice.py
CHANGED
@@ -179,13 +179,13 @@ class DiagService(DiagComm):
|
|
179
179
|
|
180
180
|
self.request.print_free_parameters_info()
|
181
181
|
|
182
|
-
def decode_message(self, raw_message: bytes) -> Message:
|
182
|
+
def decode_message(self, raw_message: bytes | bytearray) -> Message:
|
183
183
|
request_prefix = b''
|
184
184
|
candidate_coding_objects: list[Request | Response] = [
|
185
185
|
*self.positive_responses, *self.negative_responses
|
186
186
|
]
|
187
187
|
if self.request is not None:
|
188
|
-
request_prefix = self.request.coded_const_prefix()
|
188
|
+
request_prefix = bytes(self.request.coded_const_prefix())
|
189
189
|
candidate_coding_objects.append(self.request)
|
190
190
|
|
191
191
|
coding_objects: list[Request | Response] = []
|
@@ -199,7 +199,7 @@ class DiagService(DiagComm):
|
|
199
199
|
try:
|
200
200
|
result_list.append(
|
201
201
|
Message(
|
202
|
-
coded_message=raw_message,
|
202
|
+
coded_message=bytes(raw_message),
|
203
203
|
service=self,
|
204
204
|
coding_object=coding_object,
|
205
205
|
param_dict=coding_object.decode(raw_message)))
|
@@ -221,7 +221,7 @@ class DiagService(DiagComm):
|
|
221
221
|
|
222
222
|
return result_list[0]
|
223
223
|
|
224
|
-
def encode_request(self, **kwargs: ParameterValue) ->
|
224
|
+
def encode_request(self, **kwargs: ParameterValue) -> bytearray:
|
225
225
|
"""Prepare an array of bytes ready to be send over the wire
|
226
226
|
for the request of this service.
|
227
227
|
"""
|
@@ -229,7 +229,7 @@ class DiagService(DiagComm):
|
|
229
229
|
# encoding are specified (parameters which have a default are
|
230
230
|
# optional)
|
231
231
|
if self.request is None:
|
232
|
-
return
|
232
|
+
return bytearray()
|
233
233
|
|
234
234
|
missing_params = {x.short_name
|
235
235
|
for x in self.request.required_parameters}.difference(kwargs.keys())
|
@@ -245,18 +245,18 @@ class DiagService(DiagComm):
|
|
245
245
|
return self.request.encode(**kwargs)
|
246
246
|
|
247
247
|
def encode_positive_response(self,
|
248
|
-
coded_request: bytes,
|
248
|
+
coded_request: bytes | bytearray,
|
249
249
|
response_index: int = 0,
|
250
|
-
**kwargs: ParameterValue) ->
|
250
|
+
**kwargs: ParameterValue) -> bytearray:
|
251
251
|
# TODO: Should the user decide the positive response or what are the differences?
|
252
252
|
return self.positive_responses[response_index].encode(coded_request, **kwargs)
|
253
253
|
|
254
254
|
def encode_negative_response(self,
|
255
|
-
coded_request: bytes,
|
255
|
+
coded_request: bytes | bytearray,
|
256
256
|
response_index: int = 0,
|
257
|
-
**kwargs: ParameterValue) ->
|
257
|
+
**kwargs: ParameterValue) -> bytearray:
|
258
258
|
return self.negative_responses[response_index].encode(coded_request, **kwargs)
|
259
259
|
|
260
|
-
def __call__(self, **kwargs: ParameterValue) ->
|
260
|
+
def __call__(self, **kwargs: ParameterValue) -> bytearray:
|
261
261
|
"""Encode a request."""
|
262
262
|
return self.encode_request(**kwargs)
|
odxtools/ecuconfig.py
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
2
|
+
from dataclasses import dataclass
|
3
|
+
from typing import TYPE_CHECKING, Any
|
4
|
+
from xml.etree import ElementTree
|
5
|
+
|
6
|
+
from .additionalaudience import AdditionalAudience
|
7
|
+
from .configdata import ConfigData
|
8
|
+
from .configdatadictionaryspec import ConfigDataDictionarySpec
|
9
|
+
from .nameditemlist import NamedItemList
|
10
|
+
from .odxcategory import OdxCategory
|
11
|
+
from .odxdoccontext import OdxDocContext
|
12
|
+
from .odxlink import OdxLinkDatabase, OdxLinkId
|
13
|
+
from .snrefcontext import SnRefContext
|
14
|
+
from .utils import dataclass_fields_asdict
|
15
|
+
|
16
|
+
if TYPE_CHECKING:
|
17
|
+
from .database import Database
|
18
|
+
|
19
|
+
|
20
|
+
@dataclass(kw_only=True)
|
21
|
+
class EcuConfig(OdxCategory):
|
22
|
+
config_datas: NamedItemList[ConfigData]
|
23
|
+
additional_audiences: NamedItemList[AdditionalAudience]
|
24
|
+
config_data_dictionary_spec: ConfigDataDictionarySpec | None
|
25
|
+
|
26
|
+
@staticmethod
|
27
|
+
def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "EcuConfig":
|
28
|
+
|
29
|
+
base_obj = OdxCategory.from_et(et_element, context)
|
30
|
+
kwargs = dataclass_fields_asdict(base_obj)
|
31
|
+
|
32
|
+
config_datas = NamedItemList([
|
33
|
+
ConfigData.from_et(el, context)
|
34
|
+
for el in et_element.iterfind("CONFIG-DATAS/CONFIG-DATA")
|
35
|
+
])
|
36
|
+
additional_audiences = NamedItemList([
|
37
|
+
AdditionalAudience.from_et(el, context)
|
38
|
+
for el in et_element.iterfind("ADDITIONAL-AUDIENCES/ADDITIONAL-AUDIENCE")
|
39
|
+
])
|
40
|
+
config_data_dictionary_spec = None
|
41
|
+
if (cdd_elem := et_element.find("CONFIG-DATA-DICTIONARY-SPEC")) is not None:
|
42
|
+
config_data_dictionary_spec = ConfigDataDictionarySpec.from_et(cdd_elem, context)
|
43
|
+
|
44
|
+
return EcuConfig(
|
45
|
+
config_datas=config_datas,
|
46
|
+
additional_audiences=additional_audiences,
|
47
|
+
config_data_dictionary_spec=config_data_dictionary_spec,
|
48
|
+
**kwargs)
|
49
|
+
|
50
|
+
def _build_odxlinks(self) -> dict[OdxLinkId, Any]:
|
51
|
+
odxlinks = super()._build_odxlinks()
|
52
|
+
|
53
|
+
for config_data in self.config_datas:
|
54
|
+
odxlinks.update(config_data._build_odxlinks())
|
55
|
+
|
56
|
+
for additional_audience in self.additional_audiences:
|
57
|
+
odxlinks.update(additional_audience._build_odxlinks())
|
58
|
+
|
59
|
+
if self.config_data_dictionary_spec is not None:
|
60
|
+
odxlinks.update(self.config_data_dictionary_spec._build_odxlinks())
|
61
|
+
|
62
|
+
return odxlinks
|
63
|
+
|
64
|
+
def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
|
65
|
+
super()._resolve_odxlinks(odxlinks)
|
66
|
+
|
67
|
+
for config_data in self.config_datas:
|
68
|
+
config_data._resolve_odxlinks(odxlinks)
|
69
|
+
|
70
|
+
for additional_audiences in self.additional_audiences:
|
71
|
+
additional_audiences._resolve_odxlinks(odxlinks)
|
72
|
+
|
73
|
+
if self.config_data_dictionary_spec is not None:
|
74
|
+
self.config_data_dictionary_spec._resolve_odxlinks(odxlinks)
|
75
|
+
|
76
|
+
def _finalize_init(self, database: "Database", odxlinks: OdxLinkDatabase) -> None:
|
77
|
+
super()._finalize_init(database, odxlinks)
|
78
|
+
|
79
|
+
def _resolve_snrefs(self, context: SnRefContext) -> None:
|
80
|
+
super()._resolve_snrefs(context)
|
81
|
+
|
82
|
+
for config_data in self.config_datas:
|
83
|
+
config_data._resolve_snrefs(context)
|
84
|
+
|
85
|
+
for additional_audiences in self.additional_audiences:
|
86
|
+
additional_audiences._resolve_snrefs(context)
|
87
|
+
|
88
|
+
if self.config_data_dictionary_spec is not None:
|
89
|
+
self.config_data_dictionary_spec._resolve_snrefs(context)
|
@@ -6,17 +6,26 @@ from .encryptcompressmethodtype import EncryptCompressMethodType
|
|
6
6
|
from .exceptions import odxraise, odxrequire
|
7
7
|
from .odxdoccontext import OdxDocContext
|
8
8
|
from .odxlink import OdxLinkDatabase, OdxLinkId
|
9
|
+
from .odxtypes import AtomicOdxType, DataType
|
9
10
|
from .snrefcontext import SnRefContext
|
10
11
|
|
11
12
|
|
12
13
|
@dataclass(kw_only=True)
|
13
14
|
class EncryptCompressMethod:
|
14
|
-
|
15
|
+
value_raw: str
|
15
16
|
value_type: EncryptCompressMethodType
|
16
17
|
|
18
|
+
@property
|
19
|
+
def value(self) -> AtomicOdxType:
|
20
|
+
return self._value
|
21
|
+
|
22
|
+
@property
|
23
|
+
def data_type(self) -> DataType:
|
24
|
+
return self._data_type
|
25
|
+
|
17
26
|
@staticmethod
|
18
27
|
def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "EncryptCompressMethod":
|
19
|
-
|
28
|
+
value_raw = et_element.text or ""
|
20
29
|
|
21
30
|
value_type_str = odxrequire(et_element.attrib.get("TYPE"))
|
22
31
|
try:
|
@@ -25,7 +34,11 @@ class EncryptCompressMethod:
|
|
25
34
|
value_type = cast(EncryptCompressMethodType, None)
|
26
35
|
odxraise(f"Encountered unknown addressing type '{value_type_str}'")
|
27
36
|
|
28
|
-
return EncryptCompressMethod(
|
37
|
+
return EncryptCompressMethod(value_raw=value_raw, value_type=value_type)
|
38
|
+
|
39
|
+
def __post_init__(self) -> None:
|
40
|
+
self._data_type = DataType(self.value_type.value)
|
41
|
+
self._value = self._data_type.from_string(self.value_raw.strip())
|
29
42
|
|
30
43
|
def _build_odxlinks(self) -> dict[OdxLinkId, Any]:
|
31
44
|
odxlinks: dict[OdxLinkId, Any] = {}
|
odxtools/externflashdata.py
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# SPDX-License-Identifier: MIT
|
2
2
|
from dataclasses import dataclass
|
3
|
-
from typing import Any
|
3
|
+
from typing import IO, Any
|
4
4
|
from xml.etree import ElementTree
|
5
5
|
|
6
6
|
from .datafile import Datafile
|
7
|
-
from .exceptions import odxrequire
|
7
|
+
from .exceptions import odxraise, odxrequire
|
8
8
|
from .flashdata import Flashdata
|
9
9
|
from .odxdoccontext import OdxDocContext
|
10
10
|
from .odxlink import OdxLinkDatabase, OdxLinkId
|
@@ -16,6 +16,21 @@ from .utils import dataclass_fields_asdict
|
|
16
16
|
class ExternFlashdata(Flashdata):
|
17
17
|
datafile: Datafile
|
18
18
|
|
19
|
+
@property
|
20
|
+
def data_str(self) -> str:
|
21
|
+
if self._database is None:
|
22
|
+
odxraise("No database object specified")
|
23
|
+
return ""
|
24
|
+
|
25
|
+
aux_file: IO[bytes] = odxrequire(self._database.auxiliary_files.get(self.datafile.value))
|
26
|
+
if aux_file is None:
|
27
|
+
return ""
|
28
|
+
|
29
|
+
result = aux_file.read().decode()
|
30
|
+
aux_file.seek(0)
|
31
|
+
|
32
|
+
return result
|
33
|
+
|
19
34
|
@staticmethod
|
20
35
|
def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "ExternFlashdata":
|
21
36
|
kwargs = dataclass_fields_asdict(Flashdata.from_et(et_element, context))
|
@@ -32,3 +47,7 @@ class ExternFlashdata(Flashdata):
|
|
32
47
|
|
33
48
|
def _resolve_snrefs(self, context: SnRefContext) -> None:
|
34
49
|
super()._resolve_snrefs(context)
|
50
|
+
|
51
|
+
# this is slightly hacky because we only remember the
|
52
|
+
# applicable ODX database and do not resolve any SNREFs here
|
53
|
+
self._database = context.database
|
odxtools/flashdata.py
CHANGED
@@ -1,12 +1,16 @@
|
|
1
1
|
# SPDX-License-Identifier: MIT
|
2
|
+
import re
|
2
3
|
from dataclasses import dataclass
|
3
|
-
from typing import Any
|
4
|
+
from typing import Any, cast
|
4
5
|
from xml.etree import ElementTree
|
5
6
|
|
7
|
+
from bincopy import BinFile
|
8
|
+
|
6
9
|
from .dataformat import Dataformat
|
10
|
+
from .dataformatselection import DataformatSelection
|
7
11
|
from .element import IdentifiableElement
|
8
12
|
from .encryptcompressmethod import EncryptCompressMethod
|
9
|
-
from .exceptions import odxrequire
|
13
|
+
from .exceptions import odxassert, odxraise, odxrequire
|
10
14
|
from .odxdoccontext import OdxDocContext
|
11
15
|
from .odxlink import OdxLinkDatabase, OdxLinkId
|
12
16
|
from .snrefcontext import SnRefContext
|
@@ -20,6 +24,40 @@ class Flashdata(IdentifiableElement):
|
|
20
24
|
dataformat: Dataformat
|
21
25
|
encrypt_compress_method: EncryptCompressMethod | None = None
|
22
26
|
|
27
|
+
@property
|
28
|
+
def data_str(self) -> str:
|
29
|
+
raise NotImplementedError(f"The .data_str property has not been implemented "
|
30
|
+
f"by the {type(self).__name__} class")
|
31
|
+
|
32
|
+
@property
|
33
|
+
def dataset(self) -> BinFile | bytearray | None:
|
34
|
+
data_str = self.data_str
|
35
|
+
if self.dataformat.selection in (DataformatSelection.INTEL_HEX,
|
36
|
+
DataformatSelection.MOTOROLA_S):
|
37
|
+
bf = BinFile()
|
38
|
+
|
39
|
+
# remove white space and empty lines
|
40
|
+
bf.add("\n".join([re.sub(r"\s", "", x) for x in data_str.splitlines() if x.strip()]))
|
41
|
+
|
42
|
+
return bf
|
43
|
+
elif self.dataformat.selection == DataformatSelection.BINARY:
|
44
|
+
return bytearray.fromhex(re.sub(r"\s", "", data_str, flags=re.MULTILINE))
|
45
|
+
else:
|
46
|
+
odxassert(self.dataformat.selection == DataformatSelection.USER_DEFINED,
|
47
|
+
f"Unsupported data format {self.dataformat.selection}")
|
48
|
+
return None
|
49
|
+
|
50
|
+
@property
|
51
|
+
def blob(self) -> bytearray:
|
52
|
+
ds = self.dataset
|
53
|
+
if isinstance(ds, BinFile):
|
54
|
+
return cast(bytearray, ds.as_binary())
|
55
|
+
elif isinstance(ds, bytearray):
|
56
|
+
return ds
|
57
|
+
|
58
|
+
odxraise("USER-DEFINED flash data cannot be interpreted on the odxtools level")
|
59
|
+
return bytearray()
|
60
|
+
|
23
61
|
@staticmethod
|
24
62
|
def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "Flashdata":
|
25
63
|
|
odxtools/identvalue.py
CHANGED
@@ -5,6 +5,7 @@ from xml.etree import ElementTree
|
|
5
5
|
from .exceptions import odxraise
|
6
6
|
from .identvaluetype import IdentValueType
|
7
7
|
from .odxdoccontext import OdxDocContext
|
8
|
+
from .odxtypes import AtomicOdxType, DataType
|
8
9
|
|
9
10
|
|
10
11
|
@dataclass(kw_only=True)
|
@@ -13,15 +14,23 @@ class IdentValue:
|
|
13
14
|
Corresponds to IDENT-VALUE.
|
14
15
|
"""
|
15
16
|
|
16
|
-
|
17
|
+
value_raw: str
|
17
18
|
|
18
19
|
# note that the spec says this attribute is named "TYPE", but in
|
19
20
|
# python, "type" is a build-in function...
|
20
21
|
value_type: IdentValueType
|
21
22
|
|
23
|
+
@property
|
24
|
+
def value(self) -> AtomicOdxType:
|
25
|
+
return self._value
|
26
|
+
|
27
|
+
@property
|
28
|
+
def data_type(self) -> DataType:
|
29
|
+
return self._data_type
|
30
|
+
|
22
31
|
@staticmethod
|
23
32
|
def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "IdentValue":
|
24
|
-
|
33
|
+
value_raw = et_element.text or ""
|
25
34
|
|
26
35
|
try:
|
27
36
|
value_type = IdentValueType(et_element.attrib["TYPE"])
|
@@ -29,4 +38,8 @@ class IdentValue:
|
|
29
38
|
odxraise(f"Cannot parse IDENT-VALUE-TYPE: {e}")
|
30
39
|
value_type = None
|
31
40
|
|
32
|
-
return IdentValue(
|
41
|
+
return IdentValue(value_raw=value_raw, value_type=value_type)
|
42
|
+
|
43
|
+
def __post_init__(self) -> None:
|
44
|
+
self._data_type = DataType(self.value_type.value)
|
45
|
+
self._value = self._data_type.from_string(self.value_raw.strip())
|
odxtools/internflashdata.py
CHANGED
@@ -15,6 +15,10 @@ from .utils import dataclass_fields_asdict
|
|
15
15
|
class InternFlashdata(Flashdata):
|
16
16
|
data: str
|
17
17
|
|
18
|
+
@property
|
19
|
+
def data_str(self) -> str:
|
20
|
+
return self.data
|
21
|
+
|
18
22
|
@staticmethod
|
19
23
|
def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "InternFlashdata":
|
20
24
|
kwargs = dataclass_fields_asdict(Flashdata.from_et(et_element, context))
|