odxtools 10.2.1__py3-none-any.whl → 10.3.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 +21 -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 +8 -6
- 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/nameditemlist.py +2 -2
- odxtools/optionitem.py +79 -0
- 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/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/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/printExpectedIdent.xml.jinja2 +1 -1
- odxtools/templates/macros/printFlashdata.xml.jinja2 +2 -2
- odxtools/templates/macros/printItemValue.xml.jinja2 +31 -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/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 +15 -0
- {odxtools-10.2.1.dist-info → odxtools-10.3.0.dist-info}/METADATA +2 -1
- {odxtools-10.2.1.dist-info → odxtools-10.3.0.dist-info}/RECORD +63 -39
- {odxtools-10.2.1.dist-info → odxtools-10.3.0.dist-info}/WHEEL +1 -1
- {odxtools-10.2.1.dist-info → odxtools-10.3.0.dist-info}/entry_points.txt +0 -0
- {odxtools-10.2.1.dist-info → odxtools-10.3.0.dist-info}/licenses/LICENSE +0 -0
- {odxtools-10.2.1.dist-info → odxtools-10.3.0.dist-info}/top_level.txt +0 -0
@@ -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
@@ -352,7 +352,7 @@ class DiagLayer:
|
|
352
352
|
# corresponding request for `decode_response()`.)
|
353
353
|
request_prefix = b''
|
354
354
|
if s.request is not None:
|
355
|
-
request_prefix = s.request.coded_const_prefix()
|
355
|
+
request_prefix = bytes(s.request.coded_const_prefix())
|
356
356
|
prefixes = [request_prefix]
|
357
357
|
gnrs = getattr(self, "global_negative_responses", [])
|
358
358
|
prefixes += [
|
@@ -383,7 +383,7 @@ class DiagLayer:
|
|
383
383
|
else:
|
384
384
|
cast(list[DiagService], sub_tree[-1]).append(service)
|
385
385
|
|
386
|
-
def _find_services_for_uds(self, message: bytes) -> list[DiagService]:
|
386
|
+
def _find_services_for_uds(self, message: bytes | bytearray) -> list[DiagService]:
|
387
387
|
prefix_tree = self._prefix_tree
|
388
388
|
|
389
389
|
# Find matching service(s) in prefix tree
|
@@ -398,7 +398,8 @@ class DiagLayer:
|
|
398
398
|
possible_services += cast(list[DiagService], prefix_tree[-1])
|
399
399
|
return possible_services
|
400
400
|
|
401
|
-
def _decode(self, message: bytes
|
401
|
+
def _decode(self, message: bytes | bytearray,
|
402
|
+
candidate_services: Iterable[DiagService]) -> list[Message]:
|
402
403
|
decoded_messages: list[Message] = []
|
403
404
|
|
404
405
|
for service in candidate_services:
|
@@ -420,7 +421,7 @@ class DiagLayer:
|
|
420
421
|
|
421
422
|
decoded_messages.append(
|
422
423
|
Message(
|
423
|
-
coded_message=message,
|
424
|
+
coded_message=bytes(message),
|
424
425
|
service=service,
|
425
426
|
coding_object=gnr,
|
426
427
|
param_dict=decoded_gnr))
|
@@ -437,12 +438,13 @@ class DiagLayer:
|
|
437
438
|
|
438
439
|
return decoded_messages
|
439
440
|
|
440
|
-
def decode(self, message: bytes) -> list[Message]:
|
441
|
+
def decode(self, message: bytes | bytearray) -> list[Message]:
|
441
442
|
candidate_services = self._find_services_for_uds(message)
|
442
443
|
|
443
444
|
return self._decode(message, candidate_services)
|
444
445
|
|
445
|
-
def decode_response(self, response: bytes
|
446
|
+
def decode_response(self, response: bytes | bytearray,
|
447
|
+
request: bytes | bytearray) -> list[Message]:
|
446
448
|
candidate_services = self._find_services_for_uds(request)
|
447
449
|
if candidate_services is None:
|
448
450
|
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))
|
odxtools/isotp_state_machine.py
CHANGED
@@ -44,7 +44,7 @@ class IsoTpStateMachine:
|
|
44
44
|
self._telegram_data: list[bytearray | None] = [None] * len(can_rx_ids)
|
45
45
|
self._telegram_last_rx_fragment_idx = [0] * len(can_rx_ids)
|
46
46
|
|
47
|
-
def decode_rx_frame(self, rx_id: int, data: bytes) -> Iterable[tuple[int, bytes]]:
|
47
|
+
def decode_rx_frame(self, rx_id: int, data: bytes | bytearray) -> Iterable[tuple[int, bytes]]:
|
48
48
|
"""Handle the ISO-TP state transitions caused by a CAN frame.
|
49
49
|
|
50
50
|
E.g., add some data to a telegram, etc. Returns a generator of
|
@@ -67,7 +67,7 @@ class IsoTpStateMachine:
|
|
67
67
|
self.on_single_frame(telegram_idx, data[1:1 + telegram_len])
|
68
68
|
self.on_telegram_complete(telegram_idx, data[1:1 + telegram_len])
|
69
69
|
|
70
|
-
yield (rx_id, data[1:1 + telegram_len])
|
70
|
+
yield (rx_id, bytes(data[1:1 + telegram_len]))
|
71
71
|
|
72
72
|
elif frame_type == IsoTp.FRAME_TYPE_FIRST:
|
73
73
|
frame_type, telegram_len = bitstruct.unpack("u4u12", data)
|
@@ -105,7 +105,7 @@ class IsoTpStateMachine:
|
|
105
105
|
self.on_sequence_error(telegram_idx, expected_segment_idx, rx_segment_idx)
|
106
106
|
elif len(telegram_data) == n:
|
107
107
|
self.on_telegram_complete(telegram_idx, telegram_data)
|
108
|
-
yield (rx_id, telegram_data)
|
108
|
+
yield (rx_id, bytes(telegram_data))
|
109
109
|
|
110
110
|
elif frame_type == IsoTp.FRAME_TYPE_FLOW_CONTROL:
|
111
111
|
frame_type, flow_control_flag = bitstruct.unpack("u4u4", data)
|
@@ -185,7 +185,7 @@ class IsoTpStateMachine:
|
|
185
185
|
"""
|
186
186
|
return self._can_rx_ids[telegram_idx]
|
187
187
|
|
188
|
-
def telegram_data(self, telegram_idx: int) ->
|
188
|
+
def telegram_data(self, telegram_idx: int) -> bytearray | None:
|
189
189
|
"""Given a Telegram index, returns the data received for this telegram
|
190
190
|
so far.
|
191
191
|
|
@@ -196,16 +196,16 @@ class IsoTpStateMachine:
|
|
196
196
|
##############
|
197
197
|
# Callbacks
|
198
198
|
##############
|
199
|
-
def on_single_frame(self, telegram_idx: int, frame_payload: bytes) -> None:
|
199
|
+
def on_single_frame(self, telegram_idx: int, frame_payload: bytes | bytearray) -> None:
|
200
200
|
"""Callback method for when an ISO-TP message of type "single frame" has been received"""
|
201
201
|
pass
|
202
202
|
|
203
|
-
def on_first_frame(self, telegram_idx: int, frame_payload: bytes) -> None:
|
203
|
+
def on_first_frame(self, telegram_idx: int, frame_payload: bytes | bytearray) -> None:
|
204
204
|
"""Callback method for when an ISO-TP message of type "first frame" has been received"""
|
205
205
|
pass
|
206
206
|
|
207
207
|
def on_consecutive_frame(self, telegram_idx: int, segment_idx: int,
|
208
|
-
frame_payload: bytes) -> None:
|
208
|
+
frame_payload: bytes | bytearray) -> None:
|
209
209
|
"""Callback method for when an ISO-TP message of type "consecutive frame" has been received"""
|
210
210
|
pass
|
211
211
|
|
@@ -221,7 +221,7 @@ class IsoTpStateMachine:
|
|
221
221
|
"""Method called when a frame exhibiting an unknown frame type has been received"""
|
222
222
|
pass
|
223
223
|
|
224
|
-
def on_telegram_complete(self, telegram_idx: int, telegram_payload: bytes) -> None:
|
224
|
+
def on_telegram_complete(self, telegram_idx: int, telegram_payload: bytes | bytearray) -> None:
|
225
225
|
"""Method called when an ISO-TP telegram has been fully received"""
|
226
226
|
pass
|
227
227
|
|
@@ -264,7 +264,7 @@ class IsoTpActiveDecoder(IsoTpStateMachine):
|
|
264
264
|
"""
|
265
265
|
return self._can_tx_ids[telegram_idx]
|
266
266
|
|
267
|
-
def on_single_frame(self, telegram_idx: int, frame_payload: bytes) -> None:
|
267
|
+
def on_single_frame(self, telegram_idx: int, frame_payload: bytes | bytearray) -> None:
|
268
268
|
# send ACK
|
269
269
|
# rx_id = self.can_rx_id(telegram_idx)
|
270
270
|
tx_id = self.can_tx_id(telegram_idx)
|
@@ -283,7 +283,7 @@ class IsoTpActiveDecoder(IsoTpStateMachine):
|
|
283
283
|
|
284
284
|
super().on_first_frame(telegram_idx, frame_payload)
|
285
285
|
|
286
|
-
def on_first_frame(self, telegram_idx: int, frame_payload: bytes) -> None:
|
286
|
+
def on_first_frame(self, telegram_idx: int, frame_payload: bytes | bytearray) -> None:
|
287
287
|
# send ACK
|
288
288
|
# rx_id = self.can_rx_id(telegram_idx)
|
289
289
|
tx_id = self.can_tx_id(telegram_idx)
|
@@ -306,7 +306,7 @@ class IsoTpActiveDecoder(IsoTpStateMachine):
|
|
306
306
|
super().on_first_frame(telegram_idx, frame_payload)
|
307
307
|
|
308
308
|
def on_consecutive_frame(self, telegram_idx: int, segment_idx: int,
|
309
|
-
frame_payload: bytes) -> None:
|
309
|
+
frame_payload: bytes | bytearray) -> None:
|
310
310
|
num_received = self._frames_received[telegram_idx]
|
311
311
|
if num_received is None:
|
312
312
|
# consequtive frame received before a first frame.
|
odxtools/itemvalue.py
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
2
|
+
from dataclasses import dataclass, field
|
3
|
+
from typing import Any
|
4
|
+
from xml.etree import ElementTree
|
5
|
+
|
6
|
+
from .audience import Audience
|
7
|
+
from .odxdoccontext import OdxDocContext
|
8
|
+
from .odxlink import OdxLinkDatabase, OdxLinkId
|
9
|
+
from .snrefcontext import SnRefContext
|
10
|
+
from .specialdatagroup import SpecialDataGroup
|
11
|
+
from .text import Text
|
12
|
+
|
13
|
+
|
14
|
+
@dataclass(kw_only=True)
|
15
|
+
class ItemValue:
|
16
|
+
"""This class represents a ITEM-VALUE."""
|
17
|
+
|
18
|
+
phys_constant_value: str | None
|
19
|
+
meaning: Text | None = None
|
20
|
+
key: str | None = None
|
21
|
+
rule: str | None = None
|
22
|
+
description: Text | None = None
|
23
|
+
sdgs: list[SpecialDataGroup] = field(default_factory=list)
|
24
|
+
audience: Audience | None = None
|
25
|
+
|
26
|
+
@staticmethod
|
27
|
+
def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "ItemValue":
|
28
|
+
phys_constant_value = et_element.findtext("PHYS-CONSTANT-VALUE")
|
29
|
+
|
30
|
+
meaning = None
|
31
|
+
if (meaning_elem := et_element.find("MEANING")) is not None:
|
32
|
+
meaning = Text.from_et(meaning_elem, context)
|
33
|
+
|
34
|
+
key = et_element.findtext("KEY")
|
35
|
+
rule = et_element.findtext("RULE")
|
36
|
+
|
37
|
+
description = None
|
38
|
+
if (description_elem := et_element.find("DESCRIPTION")) is not None:
|
39
|
+
description = Text.from_et(description_elem, context)
|
40
|
+
|
41
|
+
sdgs = [SpecialDataGroup.from_et(sdge, context) for sdge in et_element.iterfind("SDGS/SDG")]
|
42
|
+
|
43
|
+
audience = None
|
44
|
+
if (aud_elem := et_element.find("AUDIENCE")) is not None:
|
45
|
+
audience = Audience.from_et(aud_elem, context)
|
46
|
+
|
47
|
+
return ItemValue(
|
48
|
+
phys_constant_value=phys_constant_value,
|
49
|
+
meaning=meaning,
|
50
|
+
key=key,
|
51
|
+
rule=rule,
|
52
|
+
description=description,
|
53
|
+
sdgs=sdgs,
|
54
|
+
audience=audience,
|
55
|
+
)
|
56
|
+
|
57
|
+
def _build_odxlinks(self) -> dict[OdxLinkId, Any]:
|
58
|
+
result = {}
|
59
|
+
|
60
|
+
if self.audience is not None:
|
61
|
+
result.update(self.audience._build_odxlinks())
|
62
|
+
for sdg in self.sdgs:
|
63
|
+
result.update(sdg._build_odxlinks())
|
64
|
+
|
65
|
+
return result
|
66
|
+
|
67
|
+
def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
|
68
|
+
if self.audience is not None:
|
69
|
+
self.audience._resolve_odxlinks(odxlinks)
|
70
|
+
for sdg in self.sdgs:
|
71
|
+
sdg._resolve_odxlinks(odxlinks)
|
72
|
+
|
73
|
+
def _resolve_snrefs(self, context: SnRefContext) -> None:
|
74
|
+
if self.audience is not None:
|
75
|
+
self.audience._resolve_snrefs(context)
|
76
|
+
for sdg in self.sdgs:
|
77
|
+
sdg._resolve_snrefs(context)
|
odxtools/nameditemlist.py
CHANGED
@@ -4,7 +4,7 @@ import typing
|
|
4
4
|
from collections.abc import Collection, Iterable
|
5
5
|
from copy import deepcopy
|
6
6
|
from keyword import iskeyword
|
7
|
-
from typing import Any, SupportsIndex, TypeVar,
|
7
|
+
from typing import Any, SupportsIndex, TypeVar, overload, runtime_checkable
|
8
8
|
|
9
9
|
from .exceptions import odxraise
|
10
10
|
|
@@ -159,7 +159,7 @@ class ItemAttributeList(list[T]):
|
|
159
159
|
return super().__getitem__(key)
|
160
160
|
return default
|
161
161
|
else:
|
162
|
-
return
|
162
|
+
return self._item_dict.get(key, default)
|
163
163
|
|
164
164
|
def __eq__(self, other: object) -> bool:
|
165
165
|
"""
|