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.
Files changed (74) hide show
  1. odxtools/cli/browse.py +4 -2
  2. odxtools/cli/compare.py +3 -3
  3. odxtools/compositecodec.py +1 -1
  4. odxtools/configdata.py +70 -0
  5. odxtools/configdatadictionaryspec.py +57 -0
  6. odxtools/configiditem.py +18 -0
  7. odxtools/configitem.py +85 -0
  8. odxtools/configrecord.py +146 -0
  9. odxtools/database.py +40 -0
  10. odxtools/dataiditem.py +18 -0
  11. odxtools/datarecord.py +132 -0
  12. odxtools/decodestate.py +1 -1
  13. odxtools/diagcommdataconnector.py +61 -0
  14. odxtools/diaglayers/diaglayer.py +9 -14
  15. odxtools/diagservice.py +10 -10
  16. odxtools/ecuconfig.py +89 -0
  17. odxtools/encryptcompressmethod.py +16 -3
  18. odxtools/externflashdata.py +21 -2
  19. odxtools/flashdata.py +40 -2
  20. odxtools/identvalue.py +16 -3
  21. odxtools/internflashdata.py +4 -0
  22. odxtools/isotp_state_machine.py +11 -11
  23. odxtools/itemvalue.py +77 -0
  24. odxtools/multipleecujob.py +178 -0
  25. odxtools/multipleecujobspec.py +142 -0
  26. odxtools/nameditemlist.py +2 -2
  27. odxtools/optionitem.py +79 -0
  28. odxtools/outputparam.py +1 -1
  29. odxtools/parameters/codedconstparameter.py +2 -3
  30. odxtools/readdiagcommconnector.py +79 -0
  31. odxtools/readparamvalue.py +52 -0
  32. odxtools/request.py +3 -3
  33. odxtools/response.py +8 -4
  34. odxtools/snrefcontext.py +2 -0
  35. odxtools/statemachine.py +3 -2
  36. odxtools/systemitem.py +23 -0
  37. odxtools/templates/diag_layer_container.odx-d.xml.jinja2 +4 -0
  38. odxtools/templates/ecu_config.odx-e.xml.jinja2 +38 -0
  39. odxtools/templates/flash.odx-f.xml.jinja2 +2 -2
  40. odxtools/templates/macros/printAdminData.xml.jinja2 +1 -1
  41. odxtools/templates/macros/printAudience.xml.jinja2 +3 -3
  42. odxtools/templates/macros/printChecksum.xml.jinja2 +1 -1
  43. odxtools/templates/macros/printCompuMethod.xml.jinja2 +2 -2
  44. odxtools/templates/macros/printConfigData.xml.jinja2 +39 -0
  45. odxtools/templates/macros/printConfigDataDictionarySpec.xml.jinja2 +22 -0
  46. odxtools/templates/macros/printConfigItems.xml.jinja2 +66 -0
  47. odxtools/templates/macros/printConfigRecord.xml.jinja2 +73 -0
  48. odxtools/templates/macros/printDOP.xml.jinja2 +5 -6
  49. odxtools/templates/macros/printDataRecord.xml.jinja2 +35 -0
  50. odxtools/templates/macros/printDiagCommDataConnector.xml.jinja2 +66 -0
  51. odxtools/templates/macros/printDiagDataDictionarySpec.xml.jinja2 +107 -0
  52. odxtools/templates/macros/printDiagLayer.xml.jinja2 +11 -97
  53. odxtools/templates/macros/printElementId.xml.jinja2 +2 -0
  54. odxtools/templates/macros/printExpectedIdent.xml.jinja2 +1 -1
  55. odxtools/templates/macros/printFlashdata.xml.jinja2 +2 -2
  56. odxtools/templates/macros/printFunctionalClass.xml.jinja2 +4 -4
  57. odxtools/templates/macros/printItemValue.xml.jinja2 +31 -0
  58. odxtools/templates/macros/printMultipleEcuJob.xml.jinja2 +77 -0
  59. odxtools/templates/macros/printOwnIdent.xml.jinja2 +1 -1
  60. odxtools/templates/macros/printSecurity.xml.jinja2 +4 -4
  61. odxtools/templates/macros/printSegment.xml.jinja2 +1 -1
  62. odxtools/templates/multiple-ecu-job-spec.odx-m.xml.jinja2 +51 -0
  63. odxtools/validbasevariant.py +62 -0
  64. odxtools/validityfor.py +16 -3
  65. odxtools/variantmatcher.py +4 -4
  66. odxtools/version.py +2 -2
  67. odxtools/writediagcommconnector.py +77 -0
  68. odxtools/writepdxfile.py +58 -27
  69. {odxtools-10.2.1.dist-info → odxtools-10.4.0.dist-info}/METADATA +2 -1
  70. {odxtools-10.2.1.dist-info → odxtools-10.4.0.dist-info}/RECORD +74 -45
  71. {odxtools-10.2.1.dist-info → odxtools-10.4.0.dist-info}/WHEEL +1 -1
  72. {odxtools-10.2.1.dist-info → odxtools-10.4.0.dist-info}/entry_points.txt +0 -0
  73. {odxtools-10.2.1.dist-info → odxtools-10.4.0.dist-info}/licenses/LICENSE +0 -0
  74. {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
@@ -21,7 +21,7 @@ class DecodeState:
21
21
  """Utility class to be used while decoding a message."""
22
22
 
23
23
  #: bytes to be decoded
24
- coded_message: bytes
24
+ coded_message: bytes | bytearray
25
25
 
26
26
  #: Absolute position of the origin
27
27
  #:
@@ -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)
@@ -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, DiagLayer)
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, candidate_services: Iterable[DiagService]) -> list[Message]:
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, request: bytes) -> list[Message]:
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) -> bytes:
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 b''
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) -> bytes:
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) -> bytes:
257
+ **kwargs: ParameterValue) -> bytearray:
258
258
  return self.negative_responses[response_index].encode(coded_request, **kwargs)
259
259
 
260
- def __call__(self, **kwargs: ParameterValue) -> bytes:
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
- value: str
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
- value = et_element.text or ""
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(value=value, value_type=value_type)
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] = {}
@@ -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
- value: str
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
- value = et_element.text or ""
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(value=value, value_type=value_type)
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())
@@ -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))