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.
Files changed (63) 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 +21 -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 +8 -6
  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/nameditemlist.py +2 -2
  25. odxtools/optionitem.py +79 -0
  26. odxtools/parameters/codedconstparameter.py +2 -3
  27. odxtools/readdiagcommconnector.py +79 -0
  28. odxtools/readparamvalue.py +52 -0
  29. odxtools/request.py +3 -3
  30. odxtools/response.py +8 -4
  31. odxtools/statemachine.py +3 -2
  32. odxtools/systemitem.py +23 -0
  33. odxtools/templates/diag_layer_container.odx-d.xml.jinja2 +4 -0
  34. odxtools/templates/ecu_config.odx-e.xml.jinja2 +38 -0
  35. odxtools/templates/flash.odx-f.xml.jinja2 +2 -2
  36. odxtools/templates/macros/printAudience.xml.jinja2 +3 -3
  37. odxtools/templates/macros/printChecksum.xml.jinja2 +1 -1
  38. odxtools/templates/macros/printCompuMethod.xml.jinja2 +2 -2
  39. odxtools/templates/macros/printConfigData.xml.jinja2 +39 -0
  40. odxtools/templates/macros/printConfigDataDictionarySpec.xml.jinja2 +22 -0
  41. odxtools/templates/macros/printConfigItems.xml.jinja2 +66 -0
  42. odxtools/templates/macros/printConfigRecord.xml.jinja2 +73 -0
  43. odxtools/templates/macros/printDOP.xml.jinja2 +5 -6
  44. odxtools/templates/macros/printDataRecord.xml.jinja2 +35 -0
  45. odxtools/templates/macros/printDiagCommDataConnector.xml.jinja2 +66 -0
  46. odxtools/templates/macros/printExpectedIdent.xml.jinja2 +1 -1
  47. odxtools/templates/macros/printFlashdata.xml.jinja2 +2 -2
  48. odxtools/templates/macros/printItemValue.xml.jinja2 +31 -0
  49. odxtools/templates/macros/printOwnIdent.xml.jinja2 +1 -1
  50. odxtools/templates/macros/printSecurity.xml.jinja2 +4 -4
  51. odxtools/templates/macros/printSegment.xml.jinja2 +1 -1
  52. odxtools/validbasevariant.py +62 -0
  53. odxtools/validityfor.py +16 -3
  54. odxtools/variantmatcher.py +4 -4
  55. odxtools/version.py +2 -2
  56. odxtools/writediagcommconnector.py +77 -0
  57. odxtools/writepdxfile.py +15 -0
  58. {odxtools-10.2.1.dist-info → odxtools-10.3.0.dist-info}/METADATA +2 -1
  59. {odxtools-10.2.1.dist-info → odxtools-10.3.0.dist-info}/RECORD +63 -39
  60. {odxtools-10.2.1.dist-info → odxtools-10.3.0.dist-info}/WHEEL +1 -1
  61. {odxtools-10.2.1.dist-info → odxtools-10.3.0.dist-info}/entry_points.txt +0 -0
  62. {odxtools-10.2.1.dist-info → odxtools-10.3.0.dist-info}/licenses/LICENSE +0 -0
  63. {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)
@@ -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, candidate_services: Iterable[DiagService]) -> list[Message]:
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, request: bytes) -> list[Message]:
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) -> 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))
@@ -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) -> bytes | None:
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, cast, overload, runtime_checkable
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 cast(T | None, self._item_dict.get(key, default))
162
+ return self._item_dict.get(key, default)
163
163
 
164
164
  def __eq__(self, other: object) -> bool:
165
165
  """