odxtools 10.2.0__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 (68) 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 +39 -17
  15. odxtools/diaglayers/ecuvariant.py +12 -19
  16. odxtools/diaglayers/hierarchyelement.py +5 -5
  17. odxtools/diaglayers/protocol.py +14 -0
  18. odxtools/diagservice.py +10 -10
  19. odxtools/ecuconfig.py +89 -0
  20. odxtools/encryptcompressmethod.py +16 -3
  21. odxtools/externflashdata.py +21 -2
  22. odxtools/flashdata.py +40 -2
  23. odxtools/identvalue.py +16 -3
  24. odxtools/internflashdata.py +4 -0
  25. odxtools/isotp_state_machine.py +11 -11
  26. odxtools/itemvalue.py +77 -0
  27. odxtools/nameditemlist.py +2 -2
  28. odxtools/odxlink.py +4 -2
  29. odxtools/optionitem.py +79 -0
  30. odxtools/parameters/codedconstparameter.py +2 -3
  31. odxtools/readdiagcommconnector.py +79 -0
  32. odxtools/readparamvalue.py +52 -0
  33. odxtools/request.py +3 -3
  34. odxtools/response.py +8 -4
  35. odxtools/statemachine.py +3 -2
  36. odxtools/subcomponentparamconnector.py +1 -1
  37. odxtools/systemitem.py +23 -0
  38. odxtools/templates/diag_layer_container.odx-d.xml.jinja2 +4 -0
  39. odxtools/templates/ecu_config.odx-e.xml.jinja2 +38 -0
  40. odxtools/templates/flash.odx-f.xml.jinja2 +2 -2
  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/printExpectedIdent.xml.jinja2 +1 -1
  52. odxtools/templates/macros/printFlashdata.xml.jinja2 +2 -2
  53. odxtools/templates/macros/printItemValue.xml.jinja2 +31 -0
  54. odxtools/templates/macros/printOwnIdent.xml.jinja2 +1 -1
  55. odxtools/templates/macros/printSecurity.xml.jinja2 +4 -4
  56. odxtools/templates/macros/printSegment.xml.jinja2 +1 -1
  57. odxtools/validbasevariant.py +62 -0
  58. odxtools/validityfor.py +16 -3
  59. odxtools/variantmatcher.py +4 -4
  60. odxtools/version.py +2 -2
  61. odxtools/writediagcommconnector.py +77 -0
  62. odxtools/writepdxfile.py +15 -0
  63. {odxtools-10.2.0.dist-info → odxtools-10.3.0.dist-info}/METADATA +2 -1
  64. {odxtools-10.2.0.dist-info → odxtools-10.3.0.dist-info}/RECORD +68 -44
  65. {odxtools-10.2.0.dist-info → odxtools-10.3.0.dist-info}/WHEEL +1 -1
  66. {odxtools-10.2.0.dist-info → odxtools-10.3.0.dist-info}/entry_points.txt +0 -0
  67. {odxtools-10.2.0.dist-info → odxtools-10.3.0.dist-info}/licenses/LICENSE +0 -0
  68. {odxtools-10.2.0.dist-info → odxtools-10.3.0.dist-info}/top_level.txt +0 -0
odxtools/cli/browse.py CHANGED
@@ -255,8 +255,10 @@ def encode_message_from_string_values(
255
255
  print(f"The value specified for parameter {inner_param_sn} is not a string")
256
256
  continue
257
257
 
258
- typed_dict[inner_param_sn] = _convert_string_to_odx_type(
259
- inner_param_value, inner_param.physical_type.base_data_type)
258
+ if isinstance(inner_param,
259
+ ParameterWithDOP) and inner_param.physical_type is not None:
260
+ typed_dict[inner_param_sn] = _convert_string_to_odx_type(
261
+ inner_param_value, inner_param.physical_type.base_data_type)
260
262
  parameter_values[parameter.short_name] = typed_dict
261
263
  else:
262
264
  if not isinstance(parameter_value, str):
odxtools/cli/compare.py CHANGED
@@ -425,10 +425,10 @@ class Comparison(Display):
425
425
  changed_parameters_of_service=services_with_param_changes)
426
426
  dl1_service_names = [service.short_name for service in dl1.services]
427
427
 
428
- dl1_request_prefixes: list[bytes | None] = [
428
+ dl1_request_prefixes: list[bytes | bytearray | None] = [
429
429
  None if s.request is None else s.request.coded_const_prefix() for s in dl1.services
430
430
  ]
431
- dl2_request_prefixes: list[bytes | None] = [
431
+ dl2_request_prefixes: list[bytes | bytearray | None] = [
432
432
  None if s.request is None else s.request.coded_const_prefix() for s in dl2.services
433
433
  ]
434
434
 
@@ -436,7 +436,7 @@ class Comparison(Display):
436
436
  for service1 in dl1.services:
437
437
 
438
438
  # check for added diagnostic services
439
- rq_prefix: bytes
439
+ rq_prefix: bytes | bytearray
440
440
  if service1.request is not None:
441
441
  rq_prefix = service1.request.coded_const_prefix()
442
442
 
@@ -79,7 +79,7 @@ def composite_codec_get_free_parameters(codec: CompositeCodec) -> list[Parameter
79
79
 
80
80
 
81
81
  def composite_codec_get_coded_const_prefix(codec: CompositeCodec,
82
- request_prefix: bytes = b'') -> bytes:
82
+ request_prefix: bytes = b'') -> bytearray:
83
83
  encode_state = EncodeState(coded_message=bytearray(), triggering_request=request_prefix)
84
84
 
85
85
  for param in codec.parameters:
odxtools/configdata.py ADDED
@@ -0,0 +1,70 @@
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 .configrecord import ConfigRecord
7
+ from .element import NamedElement
8
+ from .nameditemlist import NamedItemList
9
+ from .odxdoccontext import OdxDocContext
10
+ from .odxlink import OdxLinkDatabase, OdxLinkId
11
+ from .snrefcontext import SnRefContext
12
+ from .specialdatagroup import SpecialDataGroup
13
+ from .utils import dataclass_fields_asdict
14
+ from .validbasevariant import ValidBaseVariant
15
+
16
+
17
+ @dataclass(kw_only=True)
18
+ class ConfigData(NamedElement):
19
+ """This class represents a CONFIG-DATA."""
20
+ valid_base_variants: list[ValidBaseVariant]
21
+ config_records: NamedItemList[ConfigRecord]
22
+ sdgs: list[SpecialDataGroup] = field(default_factory=list)
23
+
24
+ @staticmethod
25
+ def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "ConfigData":
26
+ kwargs = dataclass_fields_asdict(NamedElement.from_et(et_element, context))
27
+
28
+ valid_base_variants = [
29
+ ValidBaseVariant.from_et(vbv_elem, context)
30
+ for vbv_elem in et_element.iterfind("VALID-BASE-VARIANTS/VALID-BASE-VARIANT")
31
+ ]
32
+ config_records = NamedItemList([
33
+ ConfigRecord.from_et(cr_elem, context)
34
+ for cr_elem in et_element.iterfind("CONFIG-RECORDS/CONFIG-RECORD")
35
+ ])
36
+ sdgs = [SpecialDataGroup.from_et(sdge, context) for sdge in et_element.iterfind("SDGS/SDG")]
37
+
38
+ return ConfigData(
39
+ valid_base_variants=valid_base_variants,
40
+ config_records=config_records,
41
+ sdgs=sdgs,
42
+ **kwargs)
43
+
44
+ def _build_odxlinks(self) -> dict[OdxLinkId, Any]:
45
+ result = {}
46
+
47
+ for valid_base_variant in self.valid_base_variants:
48
+ result.update(valid_base_variant._build_odxlinks())
49
+ for config_record in self.config_records:
50
+ result.update(config_record._build_odxlinks())
51
+ for sdg in self.sdgs:
52
+ result.update(sdg._build_odxlinks())
53
+
54
+ return result
55
+
56
+ def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
57
+ for valid_base_variant in self.valid_base_variants:
58
+ valid_base_variant._resolve_odxlinks(odxlinks)
59
+ for config_record in self.config_records:
60
+ config_record._resolve_odxlinks(odxlinks)
61
+ for sdg in self.sdgs:
62
+ sdg._resolve_odxlinks(odxlinks)
63
+
64
+ def _resolve_snrefs(self, context: SnRefContext) -> None:
65
+ for valid_base_variant in self.valid_base_variants:
66
+ valid_base_variant._resolve_snrefs(context)
67
+ for config_record in self.config_records:
68
+ config_record._resolve_snrefs(context)
69
+ for sdg in self.sdgs:
70
+ sdg._resolve_snrefs(context)
@@ -0,0 +1,57 @@
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 .dataobjectproperty import DataObjectProperty
7
+ from .nameditemlist import NamedItemList
8
+ from .odxdoccontext import OdxDocContext
9
+ from .odxlink import OdxLinkDatabase, OdxLinkId
10
+ from .snrefcontext import SnRefContext
11
+ from .unitspec import UnitSpec
12
+
13
+
14
+ @dataclass(kw_only=True)
15
+ class ConfigDataDictionarySpec:
16
+ data_object_props: NamedItemList[DataObjectProperty] = field(default_factory=NamedItemList)
17
+ unit_spec: UnitSpec | None = None
18
+
19
+ @staticmethod
20
+ def from_et(et_element: ElementTree.Element,
21
+ context: OdxDocContext) -> "ConfigDataDictionarySpec":
22
+ data_object_props = NamedItemList([
23
+ DataObjectProperty.from_et(dop_element, context)
24
+ for dop_element in et_element.iterfind("DATA-OBJECT-PROPS/DATA-OBJECT-PROP")
25
+ ])
26
+
27
+ if (spec_elem := et_element.find("UNIT-SPEC")) is not None:
28
+ unit_spec = UnitSpec.from_et(spec_elem, context)
29
+ else:
30
+ unit_spec = None
31
+
32
+ return ConfigDataDictionarySpec(
33
+ data_object_props=data_object_props,
34
+ unit_spec=unit_spec,
35
+ )
36
+
37
+ def _build_odxlinks(self) -> dict[OdxLinkId, Any]:
38
+ odxlinks = {}
39
+
40
+ for data_object_prop in self.data_object_props:
41
+ odxlinks.update(data_object_prop._build_odxlinks())
42
+ if self.unit_spec is not None:
43
+ odxlinks.update(self.unit_spec._build_odxlinks())
44
+
45
+ return odxlinks
46
+
47
+ def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
48
+ for data_object_prop in self.data_object_props:
49
+ data_object_prop._resolve_odxlinks(odxlinks)
50
+ if self.unit_spec is not None:
51
+ self.unit_spec._resolve_odxlinks(odxlinks)
52
+
53
+ def _resolve_snrefs(self, context: SnRefContext) -> None:
54
+ for data_object_prop in self.data_object_props:
55
+ data_object_prop._resolve_snrefs(context)
56
+ if self.unit_spec is not None:
57
+ self.unit_spec._resolve_snrefs(context)
@@ -0,0 +1,18 @@
1
+ # SPDX-License-Identifier: MIT
2
+ from dataclasses import dataclass
3
+ from xml.etree import ElementTree
4
+
5
+ from .configitem import ConfigItem
6
+ from .odxdoccontext import OdxDocContext
7
+ from .utils import dataclass_fields_asdict
8
+
9
+
10
+ @dataclass(kw_only=True)
11
+ class ConfigIdItem(ConfigItem):
12
+ """This class represents a CONFIG-ID-ITEM."""
13
+
14
+ @staticmethod
15
+ def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "ConfigIdItem":
16
+ kwargs = dataclass_fields_asdict(ConfigItem.from_et(et_element, context))
17
+
18
+ return ConfigIdItem(**kwargs)
odxtools/configitem.py ADDED
@@ -0,0 +1,85 @@
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 .dopbase import DopBase
7
+ from .element import NamedElement
8
+ from .exceptions import odxrequire
9
+ from .odxdoccontext import OdxDocContext
10
+ from .odxlink import OdxLinkDatabase, OdxLinkId, OdxLinkRef, resolve_snref
11
+ from .snrefcontext import SnRefContext
12
+ from .specialdatagroup import SpecialDataGroup
13
+ from .utils import dataclass_fields_asdict
14
+
15
+
16
+ @dataclass(kw_only=True)
17
+ class ConfigItem(NamedElement):
18
+ """This class represents a CONFIG-ITEM.
19
+
20
+ CONFIG-ITEM is the base class for CONFIG-ID-ITEM, DATA-ID-ITEM,
21
+ OPTION-ITEM, and SYSTEM-ITEM.
22
+ """
23
+ byte_position: int | None = None
24
+ bit_position: int | None = None
25
+
26
+ # according to the spec exactly one of the following two
27
+ # attributes is not None...x
28
+ data_object_prop_ref: OdxLinkRef | None = None
29
+ data_object_prop_snref: str | None = None
30
+ sdgs: list[SpecialDataGroup] = field(default_factory=list)
31
+
32
+ @property
33
+ def data_object_prop(self) -> DopBase:
34
+ return self._data_object_prop
35
+
36
+ @staticmethod
37
+ def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "ConfigItem":
38
+ kwargs = dataclass_fields_asdict(NamedElement.from_et(et_element, context))
39
+
40
+ byte_position = None
41
+ if (byte_pos_elem := et_element.findtext("BYTE-POSITION")) is not None:
42
+ byte_position = int(byte_pos_elem)
43
+
44
+ bit_position = None
45
+ if (bit_pos_elem := et_element.findtext("BIT-POSITION")) is not None:
46
+ bit_position = int(bit_pos_elem)
47
+
48
+ data_object_prop_ref = OdxLinkRef.from_et(et_element.find("DATA-OBJECT-PROP-REF"), context)
49
+ data_object_prop_snref = None
50
+ if (data_object_prop_snref_elem := et_element.find("DATA-OBJECT-PROP-SNREF")) is not None:
51
+ data_object_prop_snref = odxrequire(
52
+ data_object_prop_snref_elem.attrib.get("SHORT-NAME"))
53
+ sdgs = [SpecialDataGroup.from_et(sdge, context) for sdge in et_element.iterfind("SDGS/SDG")]
54
+
55
+ return ConfigItem(
56
+ byte_position=byte_position,
57
+ bit_position=bit_position,
58
+ data_object_prop_ref=data_object_prop_ref,
59
+ data_object_prop_snref=data_object_prop_snref,
60
+ sdgs=sdgs,
61
+ **kwargs)
62
+
63
+ def _build_odxlinks(self) -> dict[OdxLinkId, Any]:
64
+ result = {}
65
+
66
+ for sdg in self.sdgs:
67
+ result.update(sdg._build_odxlinks())
68
+
69
+ return result
70
+
71
+ def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
72
+ if self.data_object_prop_ref is not None:
73
+ self._data_object_prop = odxlinks.resolve(self.data_object_prop_ref, DopBase)
74
+
75
+ for sdg in self.sdgs:
76
+ sdg._resolve_odxlinks(odxlinks)
77
+
78
+ def _resolve_snrefs(self, context: SnRefContext) -> None:
79
+ if self.data_object_prop_snref is not None:
80
+ ddds = odxrequire(context.diag_layer).diag_data_dictionary_spec
81
+ self._data_object_prop = resolve_snref(self.data_object_prop_snref,
82
+ ddds.all_data_object_properties, DopBase)
83
+
84
+ for sdg in self.sdgs:
85
+ sdg._resolve_snrefs(context)
@@ -0,0 +1,146 @@
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 .configiditem import ConfigIdItem
8
+ from .dataiditem import DataIdItem
9
+ from .datarecord import DataRecord
10
+ from .diagcommdataconnector import DiagCommDataConnector
11
+ from .element import NamedElement
12
+ from .exceptions import odxrequire
13
+ from .identvalue import IdentValue
14
+ from .nameditemlist import NamedItemList
15
+ from .odxdoccontext import OdxDocContext
16
+ from .odxlink import OdxLinkDatabase, OdxLinkId
17
+ from .optionitem import OptionItem
18
+ from .snrefcontext import SnRefContext
19
+ from .specialdatagroup import SpecialDataGroup
20
+ from .systemitem import SystemItem
21
+ from .utils import dataclass_fields_asdict
22
+
23
+
24
+ @dataclass(kw_only=True)
25
+ class ConfigRecord(NamedElement):
26
+ """This class represents a CONFIG-RECORD."""
27
+ config_id_item: ConfigIdItem | None = None
28
+ diag_comm_data_connectors: list[DiagCommDataConnector] = field(default_factory=list)
29
+ config_id: IdentValue | None = None
30
+ data_records: NamedItemList[DataRecord] = field(default_factory=NamedItemList)
31
+ audience: Audience | None = None
32
+ system_items: NamedItemList[SystemItem] = field(default_factory=NamedItemList)
33
+ data_id_item: DataIdItem | None = None
34
+ option_items: NamedItemList[OptionItem] = field(default_factory=NamedItemList)
35
+ default_data_record_snref: str | None = None
36
+ sdgs: list[SpecialDataGroup] = field(default_factory=list)
37
+
38
+ @staticmethod
39
+ def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "ConfigRecord":
40
+ kwargs = dataclass_fields_asdict(NamedElement.from_et(et_element, context))
41
+
42
+ config_id_item = None
43
+ if (cid_elem := et_element.find("CONFIG-ID-ITEM")) is not None:
44
+ config_id_item = ConfigIdItem.from_et(cid_elem, context)
45
+ diag_comm_data_connectors = [
46
+ DiagCommDataConnector.from_et(dcdc_elem, context) for dcdc_elem in et_element.iterfind(
47
+ "DIAG-COMM-DATA-CONNECTORS/DIAG-COMM-DATA-CONNECTOR")
48
+ ]
49
+ config_id = None
50
+ if (cid_elem := et_element.find("CONFIG-ID")) is not None:
51
+ config_id = IdentValue.from_et(cid_elem, context)
52
+ data_records = NamedItemList([
53
+ DataRecord.from_et(dr_elem, context)
54
+ for dr_elem in et_element.iterfind("DATA-RECORDS/DATA-RECORD")
55
+ ])
56
+ audience = None
57
+ if (aud_elem := et_element.find("AUDIENCE")) is not None:
58
+ audience = Audience.from_et(aud_elem, context)
59
+ system_items = NamedItemList([
60
+ SystemItem.from_et(si_elem, context)
61
+ for si_elem in et_element.iterfind("SYSTEM-ITEMS/SYSTEM-ITEM")
62
+ ])
63
+ data_id_item = None
64
+ if (dii_elem := et_element.find("DATA-ID-ITEM")) is not None:
65
+ data_id_item = DataIdItem.from_et(dii_elem, context)
66
+ option_items = NamedItemList([
67
+ OptionItem.from_et(si_elem, context)
68
+ for si_elem in et_element.iterfind("OPTION-ITEMS/OPTION-ITEM")
69
+ ])
70
+ default_data_record_snref = None
71
+ if (default_data_record_snref_elem :=
72
+ et_element.find("DEFAULT-DATA-RECORD-SNREF")) is not None:
73
+ default_data_record_snref = odxrequire(
74
+ default_data_record_snref_elem.attrib.get("SHORT-NAME"))
75
+ sdgs = [SpecialDataGroup.from_et(sdge, context) for sdge in et_element.iterfind("SDGS/SDG")]
76
+
77
+ return ConfigRecord(
78
+ config_id_item=config_id_item,
79
+ diag_comm_data_connectors=diag_comm_data_connectors,
80
+ config_id=config_id,
81
+ data_records=data_records,
82
+ audience=audience,
83
+ system_items=system_items,
84
+ data_id_item=data_id_item,
85
+ option_items=option_items,
86
+ default_data_record_snref=default_data_record_snref,
87
+ sdgs=sdgs,
88
+ **kwargs)
89
+
90
+ def _build_odxlinks(self) -> dict[OdxLinkId, Any]:
91
+ result = {}
92
+
93
+ if self.config_id_item is not None:
94
+ result.update(self.config_id_item._build_odxlinks())
95
+ for diag_comm_data_connector in self.diag_comm_data_connectors:
96
+ result.update(diag_comm_data_connector._build_odxlinks())
97
+ for data_record in self.data_records:
98
+ result.update(data_record._build_odxlinks())
99
+ if self.audience is not None:
100
+ result.update(self.audience._build_odxlinks())
101
+ for system_item in self.system_items:
102
+ result.update(system_item._build_odxlinks())
103
+ if self.data_id_item is not None:
104
+ result.update(self.data_id_item._build_odxlinks())
105
+ for option_item in self.option_items:
106
+ result.update(option_item._build_odxlinks())
107
+ for sdg in self.sdgs:
108
+ result.update(sdg._build_odxlinks())
109
+
110
+ return result
111
+
112
+ def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
113
+ if self.config_id_item is not None:
114
+ self.config_id_item._resolve_odxlinks(odxlinks)
115
+ for diag_comm_data_connector in self.diag_comm_data_connectors:
116
+ diag_comm_data_connector._resolve_odxlinks(odxlinks)
117
+ for data_record in self.data_records:
118
+ data_record._resolve_odxlinks(odxlinks)
119
+ if self.audience is not None:
120
+ self.audience._resolve_odxlinks(odxlinks)
121
+ for system_item in self.system_items:
122
+ system_item._resolve_odxlinks(odxlinks)
123
+ if self.data_id_item is not None:
124
+ self.data_id_item._resolve_odxlinks(odxlinks)
125
+ for option_item in self.option_items:
126
+ option_item._resolve_odxlinks(odxlinks)
127
+ for sdg in self.sdgs:
128
+ sdg._resolve_odxlinks(odxlinks)
129
+
130
+ def _resolve_snrefs(self, context: SnRefContext) -> None:
131
+ if self.config_id_item is not None:
132
+ self.config_id_item._resolve_snrefs(context)
133
+ for diag_comm_data_connector in self.diag_comm_data_connectors:
134
+ diag_comm_data_connector._resolve_snrefs(context)
135
+ for data_record in self.data_records:
136
+ data_record._resolve_snrefs(context)
137
+ if self.audience is not None:
138
+ self.audience._resolve_snrefs(context)
139
+ for system_item in self.system_items:
140
+ system_item._resolve_snrefs(context)
141
+ if self.data_id_item is not None:
142
+ self.data_id_item._resolve_snrefs(context)
143
+ for option_item in self.option_items:
144
+ option_item._resolve_snrefs(context)
145
+ for sdg in self.sdgs:
146
+ sdg._resolve_snrefs(context)
odxtools/database.py CHANGED
@@ -18,6 +18,7 @@ from .diaglayers.ecushareddata import EcuSharedData
18
18
  from .diaglayers.ecuvariant import EcuVariant
19
19
  from .diaglayers.functionalgroup import FunctionalGroup
20
20
  from .diaglayers.protocol import Protocol
21
+ from .ecuconfig import EcuConfig
21
22
  from .exceptions import odxraise, odxrequire
22
23
  from .flash import Flash
23
24
  from .nameditemlist import NamedItemList
@@ -40,6 +41,7 @@ class Database:
40
41
  self._diag_layer_containers = NamedItemList[DiagLayerContainer]()
41
42
  self._comparam_subsets = NamedItemList[ComparamSubset]()
42
43
  self._comparam_specs = NamedItemList[ComparamSpec]()
44
+ self._ecu_configs = NamedItemList[EcuConfig]()
43
45
  self._flashs = NamedItemList[Flash]()
44
46
  self._short_name = "odx_database"
45
47
 
@@ -114,6 +116,10 @@ class Database:
114
116
  self._comparam_subsets.append(ComparamSubset.from_et(category_et, context))
115
117
  else:
116
118
  self._comparam_specs.append(ComparamSpec.from_et(category_et, context))
119
+ elif category_tag == "ECU-CONFIG":
120
+ context = OdxDocContext(model_version,
121
+ (OdxDocFragment(category_sn, DocType.ECU_CONFIG),))
122
+ self._ecu_configs.append(EcuConfig.from_et(category_et, context))
117
123
  elif category_tag == "FLASH":
118
124
  context = OdxDocContext(model_version, (OdxDocFragment(category_sn, DocType.FLASH),))
119
125
  self._flashs.append(Flash.from_et(category_et, context))
@@ -148,6 +154,9 @@ class Database:
148
154
  for dlc in self.diag_layer_containers:
149
155
  dlc._resolve_odxlinks(self._odxlinks)
150
156
 
157
+ for ecu_config in self.ecu_configs:
158
+ ecu_config._resolve_odxlinks(self._odxlinks)
159
+
151
160
  for flash in self.flashs:
152
161
  flash._resolve_odxlinks(self._odxlinks)
153
162
 
@@ -163,6 +172,8 @@ class Database:
163
172
  spec._finalize_init(self, self._odxlinks)
164
173
  for dlc in self.diag_layer_containers:
165
174
  dlc._finalize_init(self, self._odxlinks)
175
+ for ecu_config in self.ecu_configs:
176
+ ecu_config._finalize_init(self, self._odxlinks)
166
177
  for flash in self.flashs:
167
178
  flash._finalize_init(self, self._odxlinks)
168
179
 
@@ -172,6 +183,8 @@ class Database:
172
183
  spec._resolve_snrefs(context)
173
184
  for dlc in self.diag_layer_containers:
174
185
  dlc._resolve_snrefs(context)
186
+ for ecu_config in self.ecu_configs:
187
+ ecu_config._resolve_snrefs(context)
175
188
  for flash in self.flashs:
176
189
  flash._resolve_snrefs(context)
177
190
 
@@ -187,6 +200,9 @@ class Database:
187
200
  for dlc in self.diag_layer_containers:
188
201
  result.update(dlc._build_odxlinks())
189
202
 
203
+ for ecu_config in self.ecu_configs:
204
+ result.update(ecu_config._build_odxlinks())
205
+
190
206
  for flash in self.flashs:
191
207
  result.update(flash._build_odxlinks())
192
208
 
@@ -263,6 +279,10 @@ class Database:
263
279
  def comparam_specs(self) -> NamedItemList[ComparamSpec]:
264
280
  return self._comparam_specs
265
281
 
282
+ @property
283
+ def ecu_configs(self) -> NamedItemList[EcuConfig]:
284
+ return self._ecu_configs
285
+
266
286
  @property
267
287
  def flashs(self) -> NamedItemList[Flash]:
268
288
  return self._flashs
@@ -274,4 +294,5 @@ class Database:
274
294
  f"diag_layer_containers={repr(self.diag_layer_containers)}, " \
275
295
  f"comparam_subsets={repr(self.comparam_subsets)}, " \
276
296
  f"comparam_specs={repr(self.comparam_specs)}, " \
297
+ f"ecu_configs={repr(self.ecu_configs)}, " \
277
298
  f"flashs={repr(self.flashs)})"
odxtools/dataiditem.py ADDED
@@ -0,0 +1,18 @@
1
+ # SPDX-License-Identifier: MIT
2
+ from dataclasses import dataclass
3
+ from xml.etree import ElementTree
4
+
5
+ from .configitem import ConfigItem
6
+ from .odxdoccontext import OdxDocContext
7
+ from .utils import dataclass_fields_asdict
8
+
9
+
10
+ @dataclass(kw_only=True)
11
+ class DataIdItem(ConfigItem):
12
+ """This class represents a DATA-ID-ITEM."""
13
+
14
+ @staticmethod
15
+ def from_et(et_element: ElementTree.Element, context: OdxDocContext) -> "DataIdItem":
16
+ kwargs = dataclass_fields_asdict(ConfigItem.from_et(et_element, context))
17
+
18
+ return DataIdItem(**kwargs)
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
  #: