odxtools 6.7.1__py3-none-any.whl → 7.1.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 (60) hide show
  1. odxtools/basicstructure.py +102 -112
  2. odxtools/dataobjectproperty.py +3 -7
  3. odxtools/decodestate.py +1 -13
  4. odxtools/diagcodedtype.py +9 -96
  5. odxtools/diagcomm.py +11 -3
  6. odxtools/diagdatadictionaryspec.py +14 -14
  7. odxtools/diaglayer.py +52 -1
  8. odxtools/diaglayerraw.py +13 -7
  9. odxtools/diagservice.py +12 -16
  10. odxtools/dopbase.py +5 -8
  11. odxtools/dtcdop.py +21 -19
  12. odxtools/dynamicendmarkerfield.py +119 -0
  13. odxtools/dynamiclengthfield.py +39 -29
  14. odxtools/dynenddopref.py +38 -0
  15. odxtools/encodestate.py +188 -23
  16. odxtools/endofpdufield.py +33 -18
  17. odxtools/environmentdata.py +8 -1
  18. odxtools/environmentdatadescription.py +21 -15
  19. odxtools/field.py +4 -3
  20. odxtools/leadinglengthinfotype.py +25 -12
  21. odxtools/matchingparameter.py +2 -2
  22. odxtools/minmaxlengthtype.py +36 -26
  23. odxtools/multiplexer.py +42 -23
  24. odxtools/multiplexercase.py +3 -3
  25. odxtools/multiplexerdefaultcase.py +7 -3
  26. odxtools/nameditemlist.py +14 -0
  27. odxtools/odxlink.py +38 -4
  28. odxtools/odxtypes.py +20 -2
  29. odxtools/parameterinfo.py +126 -40
  30. odxtools/parameters/codedconstparameter.py +17 -13
  31. odxtools/parameters/dynamicparameter.py +5 -4
  32. odxtools/parameters/lengthkeyparameter.py +66 -17
  33. odxtools/parameters/matchingrequestparameter.py +23 -11
  34. odxtools/parameters/nrcconstparameter.py +42 -22
  35. odxtools/parameters/parameter.py +35 -42
  36. odxtools/parameters/parameterwithdop.py +15 -22
  37. odxtools/parameters/physicalconstantparameter.py +16 -16
  38. odxtools/parameters/reservedparameter.py +5 -2
  39. odxtools/parameters/systemparameter.py +3 -2
  40. odxtools/parameters/tableentryparameter.py +3 -2
  41. odxtools/parameters/tablekeyparameter.py +88 -39
  42. odxtools/parameters/tablestructparameter.py +45 -44
  43. odxtools/parameters/valueparameter.py +16 -17
  44. odxtools/paramlengthinfotype.py +30 -22
  45. odxtools/request.py +9 -0
  46. odxtools/response.py +5 -13
  47. odxtools/standardlengthtype.py +51 -13
  48. odxtools/statechart.py +5 -9
  49. odxtools/statetransition.py +3 -8
  50. odxtools/staticfield.py +30 -20
  51. odxtools/tablerow.py +5 -3
  52. odxtools/templates/macros/printDynamicEndmarkerField.xml.jinja2 +16 -0
  53. odxtools/templates/macros/printVariant.xml.jinja2 +8 -0
  54. odxtools/version.py +2 -2
  55. {odxtools-6.7.1.dist-info → odxtools-7.1.0.dist-info}/METADATA +1 -1
  56. {odxtools-6.7.1.dist-info → odxtools-7.1.0.dist-info}/RECORD +60 -57
  57. {odxtools-6.7.1.dist-info → odxtools-7.1.0.dist-info}/LICENSE +0 -0
  58. {odxtools-6.7.1.dist-info → odxtools-7.1.0.dist-info}/WHEEL +0 -0
  59. {odxtools-6.7.1.dist-info → odxtools-7.1.0.dist-info}/entry_points.txt +0 -0
  60. {odxtools-6.7.1.dist-info → odxtools-7.1.0.dist-info}/top_level.txt +0 -0
odxtools/diaglayer.py CHANGED
@@ -1,6 +1,7 @@
1
1
  # SPDX-License-Identifier: MIT
2
2
  import re
3
3
  import warnings
4
+ from copy import copy
4
5
  from dataclasses import dataclass
5
6
  from functools import cached_property
6
7
  from itertools import chain
@@ -14,6 +15,7 @@ from .admindata import AdminData
14
15
  from .companydata import CompanyData
15
16
  from .comparaminstance import ComparamInstance
16
17
  from .comparamspec import ComparamSpec
18
+ from .comparamsubset import ComparamSubset
17
19
  from .diagcomm import DiagComm
18
20
  from .diagdatadictionaryspec import DiagDataDictionarySpec
19
21
  from .diaglayerraw import DiagLayerRaw
@@ -76,6 +78,50 @@ class DiagLayer:
76
78
  def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
77
79
  """Recursively resolve all references."""
78
80
 
81
+ # deal with the import references: these basically extend the
82
+ # pool of objects that are referenceable without having to
83
+ # explicitly specify the DOCREF attribute in the
84
+ # reference. This mechanism can thus be seen as a kind of
85
+ # "poor man's inheritance".
86
+ if self.import_refs:
87
+ imported_links: Dict[OdxLinkId, Any] = {}
88
+ for import_ref in self.import_refs:
89
+ imported_dl = odxlinks.resolve(import_ref, DiagLayer)
90
+
91
+ odxassert(
92
+ imported_dl.variant_type == DiagLayerType.ECU_SHARED_DATA,
93
+ f"Tried to import references from diagnostic layer "
94
+ f"'{imported_dl.short_name}' of type {imported_dl.variant_type.value}. "
95
+ f"Only ECU-SHARED-DATA layers may be referenced using the "
96
+ f"IMPORT-REF mechanism")
97
+
98
+ # TODO: ensure that the imported diagnostic layer has
99
+ # not been referenced in any PARENT-REF of the current
100
+ # layer or any of its parents.
101
+
102
+ # TODO: detect and complain about cyclic IMPORT-REFs
103
+
104
+ # TODO (?): detect conflicts with locally-defined
105
+ # objects
106
+
107
+ imported_dl_links = imported_dl._build_odxlinks()
108
+ for link_id, obj in imported_dl_links.items():
109
+ # the imported objects shall behave as if they
110
+ # were defined by the importing layer. IOW, they
111
+ # must be visible in the same document fragments.
112
+ link_id = OdxLinkId(link_id.local_id, self.odx_id.doc_fragments)
113
+ imported_links[link_id] = obj
114
+
115
+ # We need to copy the odxlink database here since this
116
+ # function must not modify its argument because the
117
+ # imported references only apply within this specific
118
+ # diagnostic layer
119
+ extended_odxlinks = copy(odxlinks)
120
+ extended_odxlinks.update(imported_links)
121
+
122
+ self.diag_layer_raw._resolve_odxlinks(extended_odxlinks)
123
+ return
124
+
79
125
  self.diag_layer_raw._resolve_odxlinks(odxlinks)
80
126
 
81
127
  def _finalize_init(self, odxlinks: OdxLinkDatabase) -> None:
@@ -171,6 +217,10 @@ class DiagLayer:
171
217
  lambda ddd_spec: ddd_spec.end_of_pdu_fields,
172
218
  lambda parent_ref: parent_ref.not_inherited_dops,
173
219
  )
220
+ dynamic_endmarker_fields = self._compute_available_ddd_spec_items(
221
+ lambda ddd_spec: ddd_spec.dynamic_endmarker_fields,
222
+ lambda parent_ref: parent_ref.not_inherited_dops,
223
+ )
174
224
  dynamic_length_fields = self._compute_available_ddd_spec_items(
175
225
  lambda ddd_spec: ddd_spec.dynamic_length_fields,
176
226
  lambda parent_ref: parent_ref.not_inherited_dops,
@@ -201,6 +251,7 @@ class DiagLayer:
201
251
  structures=structures,
202
252
  static_fields=NamedItemList(),
203
253
  end_of_pdu_fields=end_of_pdu_fields,
254
+ dynamic_endmarker_fields=dynamic_endmarker_fields,
204
255
  dynamic_length_fields=dynamic_length_fields,
205
256
  tables=tables,
206
257
  env_data_descs=env_data_descs,
@@ -306,7 +357,7 @@ class DiagLayer:
306
357
  return self.diag_layer_raw.prot_stack_snref
307
358
 
308
359
  @property
309
- def comparam_spec(self) -> Optional[ComparamSpec]:
360
+ def comparam_spec(self) -> Optional[Union[ComparamSpec, ComparamSubset]]:
310
361
  return self.diag_layer_raw.comparam_spec
311
362
 
312
363
  @property
odxtools/diaglayerraw.py CHANGED
@@ -9,6 +9,7 @@ from .admindata import AdminData
9
9
  from .companydata import CompanyData
10
10
  from .comparaminstance import ComparamInstance
11
11
  from .comparamspec import ComparamSpec
12
+ from .comparamsubset import ComparamSubset
12
13
  from .createsdgs import create_sdgs_from_et
13
14
  from .diagcomm import DiagComm
14
15
  from .diagdatadictionaryspec import DiagDataDictionarySpec
@@ -19,7 +20,7 @@ from .element import IdentifiableElement
19
20
  from .exceptions import odxassert, odxraise, odxrequire
20
21
  from .functionalclass import FunctionalClass
21
22
  from .nameditemlist import NamedItemList
22
- from .odxlink import OdxDocFragment, OdxLinkDatabase, OdxLinkId, OdxLinkRef
23
+ from .odxlink import OdxDocFragment, OdxLinkDatabase, OdxLinkId, OdxLinkRef, resolve_snref
23
24
  from .parentref import ParentRef
24
25
  from .protstack import ProtStack
25
26
  from .request import Request
@@ -243,7 +244,10 @@ class DiagLayerRaw(IdentifiableElement):
243
244
  """Recursively resolve all references."""
244
245
 
245
246
  if self.comparam_spec_ref is not None:
246
- self._comparam_spec = odxlinks.resolve(self.comparam_spec_ref, ComparamSpec)
247
+ spec = odxlinks.resolve(self.comparam_spec_ref)
248
+ if not isinstance(spec, (ComparamSubset, ComparamSpec)):
249
+ odxraise(f"Type {type(spec).__name__} is not allowed for comparam specs")
250
+ self._comparam_spec = spec
247
251
 
248
252
  # do ODXLINK reference resolution
249
253
  if self.admin_data is not None:
@@ -281,8 +285,10 @@ class DiagLayerRaw(IdentifiableElement):
281
285
  def _resolve_snrefs(self, diag_layer: "DiagLayer") -> None:
282
286
  self._prot_stack: Optional[ProtStack] = None
283
287
  if self.prot_stack_snref is not None:
284
- self._prot_stack = odxrequire(
285
- odxrequire(self.comparam_spec).prot_stacks.get(self.prot_stack_snref))
288
+ cp_spec = self.comparam_spec
289
+ if isinstance(cp_spec, ComparamSpec):
290
+ self._prot_stack = resolve_snref(self.prot_stack_snref, cp_spec.prot_stacks,
291
+ ProtStack)
286
292
 
287
293
  # do short-name reference resolution
288
294
  if self.admin_data is not None:
@@ -292,8 +298,8 @@ class DiagLayerRaw(IdentifiableElement):
292
298
 
293
299
  for company_data in self.company_datas:
294
300
  company_data._resolve_snrefs(diag_layer)
295
- for functional_classe in self.functional_classes:
296
- functional_classe._resolve_snrefs(diag_layer)
301
+ for functional_class in self.functional_classes:
302
+ functional_class._resolve_snrefs(diag_layer)
297
303
  for diag_comm in self.diag_comms:
298
304
  if isinstance(diag_comm, OdxLinkRef):
299
305
  continue
@@ -318,7 +324,7 @@ class DiagLayerRaw(IdentifiableElement):
318
324
  comparam._resolve_snrefs(diag_layer)
319
325
 
320
326
  @property
321
- def comparam_spec(self) -> Optional[ComparamSpec]:
327
+ def comparam_spec(self) -> Optional[Union[ComparamSpec, ComparamSubset]]:
322
328
  return self._comparam_spec
323
329
 
324
330
  @property
odxtools/diagservice.py CHANGED
@@ -215,13 +215,9 @@ class DiagService(DiagComm):
215
215
  coding_object=coding_object,
216
216
  param_dict=param_dict)
217
217
 
218
- def encode_request(self, **params: ParameterValue) -> bytes:
218
+ def encode_request(self, **kwargs: ParameterValue) -> bytes:
219
219
  """
220
- Composes an UDS request as list of bytes for this service.
221
- Parameters:
222
- ----------
223
- params: dict
224
- Parameters of the RPC as mapping from SHORT-NAME of the parameter to the physical value
220
+ Composes an UDS request an array of bytes for this service.
225
221
  """
226
222
  # make sure that all parameters which are required for
227
223
  # encoding are specified (parameters which have a default are
@@ -230,31 +226,31 @@ class DiagService(DiagComm):
230
226
  return b''
231
227
 
232
228
  missing_params = {x.short_name
233
- for x in self.request.required_parameters}.difference(params.keys())
229
+ for x in self.request.required_parameters}.difference(kwargs.keys())
234
230
  odxassert(
235
231
  len(missing_params) == 0, f"The parameters {missing_params} are required but missing!")
236
232
 
237
233
  # make sure that no unknown parameters are specified
238
234
  rq_all_param_names = {x.short_name for x in self.request.parameters}
239
235
  odxassert(
240
- set(params.keys()).issubset(rq_all_param_names),
241
- f"Unknown parameters specified for encoding: {params.keys()}, "
236
+ set(kwargs.keys()).issubset(rq_all_param_names),
237
+ f"Unknown parameters specified for encoding: {kwargs.keys()}, "
242
238
  f"known parameters are: {rq_all_param_names}")
243
- return self.request.encode(coded_request=None, **params)
239
+ return self.request.encode(**kwargs)
244
240
 
245
241
  def encode_positive_response(self,
246
242
  coded_request: bytes,
247
243
  response_index: int = 0,
248
- **params: ParameterValue) -> bytes:
244
+ **kwargs: ParameterValue) -> bytes:
249
245
  # TODO: Should the user decide the positive response or what are the differences?
250
- return self.positive_responses[response_index].encode(coded_request, **params)
246
+ return self.positive_responses[response_index].encode(coded_request, **kwargs)
251
247
 
252
248
  def encode_negative_response(self,
253
249
  coded_request: bytes,
254
250
  response_index: int = 0,
255
- **params: ParameterValue) -> bytes:
256
- return self.negative_responses[response_index].encode(coded_request, **params)
251
+ **kwargs: ParameterValue) -> bytes:
252
+ return self.negative_responses[response_index].encode(coded_request, **kwargs)
257
253
 
258
- def __call__(self, **params: ParameterValue) -> bytes:
254
+ def __call__(self, **kwargs: ParameterValue) -> bytes:
259
255
  """Encode a request."""
260
- return self.encode_request(**params)
256
+ return self.encode_request(**kwargs)
odxtools/dopbase.py CHANGED
@@ -21,7 +21,8 @@ if TYPE_CHECKING:
21
21
  class DopBase(IdentifiableElement):
22
22
  """Base class for all DOPs.
23
23
 
24
- Any class that a parameter can reference via a DOP-REF should inherit from this class.
24
+ Any class that a parameter can reference via a DOP-REF should
25
+ inherit from this class.
25
26
  """
26
27
 
27
28
  admin_data: Optional[AdminData]
@@ -60,15 +61,11 @@ class DopBase(IdentifiableElement):
60
61
  return None
61
62
 
62
63
  def is_valid_physical_value(self, physical_value: ParameterValue) -> bool:
63
- """Determine if a phyical value can be handled by the DOP
64
- """
64
+ """Determine if a phyical value can be handled by the DOP"""
65
65
  raise NotImplementedError
66
66
 
67
- def convert_physical_to_bytes(self,
68
- physical_value: ParameterValue,
69
- encode_state: EncodeState,
70
- bit_position: int = 0) -> bytes:
71
- """Convert the physical value into bytes."""
67
+ def encode_into_pdu(self, physical_value: ParameterValue, encode_state: EncodeState) -> None:
68
+ """Convert the physical value to bytes and emplace them into a PDU."""
72
69
  raise NotImplementedError
73
70
 
74
71
  def decode_from_pdu(self, decode_state: DecodeState) -> ParameterValue:
odxtools/dtcdop.py CHANGED
@@ -4,6 +4,8 @@ from dataclasses import dataclass
4
4
  from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, cast
5
5
  from xml.etree import ElementTree
6
6
 
7
+ from typing_extensions import override
8
+
7
9
  from .compumethods.compumethod import CompuMethod
8
10
  from .compumethods.createanycompumethod import create_any_compu_method_from_et
9
11
  from .createanydiagcodedtype import create_any_diag_coded_type_from_et
@@ -12,7 +14,7 @@ from .diagcodedtype import DiagCodedType
12
14
  from .diagnostictroublecode import DiagnosticTroubleCode
13
15
  from .dopbase import DopBase
14
16
  from .encodestate import EncodeState
15
- from .exceptions import DecodeError, EncodeError, odxassert, odxrequire
17
+ from .exceptions import DecodeError, EncodeError, odxassert, odxraise, odxrequire
16
18
  from .nameditemlist import NamedItemList
17
19
  from .odxlink import OdxDocFragment, OdxLinkDatabase, OdxLinkId, OdxLinkRef
18
20
  from .odxtypes import ParameterValue, odxstr_to_bool
@@ -87,17 +89,20 @@ class DtcDop(DopBase):
87
89
  def linked_dtc_dops(self) -> NamedItemList["DtcDop"]:
88
90
  return self._linked_dtc_dops
89
91
 
92
+ @override
90
93
  def decode_from_pdu(self, decode_state: DecodeState) -> ParameterValue:
91
94
 
92
95
  int_trouble_code = self.diag_coded_type.decode_from_pdu(decode_state)
93
96
 
94
- if self.compu_method.is_valid_internal_value(int_trouble_code):
95
- trouble_code = self.compu_method.convert_internal_to_physical(int_trouble_code)
96
- else:
97
+ if not self.compu_method.is_valid_internal_value(int_trouble_code):
97
98
  # TODO: How to prevent this?
98
- raise DecodeError(
99
+ odxraise(
99
100
  f"DTC-DOP {self.short_name} could not convert the coded value "
100
- f" {repr(int_trouble_code)} to physical type {self.physical_type.base_data_type}.")
101
+ f" {repr(int_trouble_code)} to physical type {self.physical_type.base_data_type}.",
102
+ DecodeError)
103
+ return
104
+
105
+ trouble_code = self.compu_method.convert_internal_to_physical(int_trouble_code)
101
106
 
102
107
  assert isinstance(trouble_code, int)
103
108
 
@@ -110,10 +115,12 @@ class DtcDop(DopBase):
110
115
  return dtcs[0]
111
116
 
112
117
  # the DTC was not specified. This probably means that the
113
- # diagnostic description file is incomplete. We do not bail
114
- # out but we cannot provide an interpretation for it out of the
115
- # box...
116
- dtc = DiagnosticTroubleCode(
118
+ # diagnostic description file is incomplete.
119
+ odxraise(
120
+ f"Encountered DTC 0x{trouble_code:06x} which has not been defined "
121
+ f"by the database", DecodeError)
122
+
123
+ return DiagnosticTroubleCode(
117
124
  trouble_code=trouble_code,
118
125
  odx_id=cast(OdxLinkId, None),
119
126
  short_name=f'DTC_{trouble_code:06x}',
@@ -126,12 +133,9 @@ class DtcDop(DopBase):
126
133
  sdgs=[],
127
134
  )
128
135
 
129
- return dtc
130
-
131
- def convert_physical_to_bytes(self,
132
- physical_value: ParameterValue,
133
- encode_state: EncodeState,
134
- bit_position: int = 0) -> bytes:
136
+ @override
137
+ def encode_into_pdu(self, physical_value: Optional[ParameterValue],
138
+ encode_state: EncodeState) -> None:
135
139
  if isinstance(physical_value, DiagnosticTroubleCode):
136
140
  trouble_code = physical_value.trouble_code
137
141
  elif isinstance(physical_value, int):
@@ -147,9 +151,7 @@ class DtcDop(DopBase):
147
151
  f" DiagnosticTroubleCode but got {physical_value!r}.")
148
152
 
149
153
  internal_trouble_code = self.compu_method.convert_physical_to_internal(trouble_code)
150
-
151
- return self.diag_coded_type.convert_internal_to_bytes(
152
- internal_trouble_code, encode_state=encode_state, bit_position=bit_position)
154
+ self.diag_coded_type.encode_into_pdu(internal_trouble_code, encode_state)
153
155
 
154
156
  def _build_odxlinks(self) -> Dict[OdxLinkId, Any]:
155
157
  odxlinks = super()._build_odxlinks()
@@ -0,0 +1,119 @@
1
+ # SPDX-License-Identifier: MIT
2
+ from dataclasses import dataclass
3
+ from typing import TYPE_CHECKING, Any, Dict, List, Sequence
4
+ from xml.etree import ElementTree
5
+
6
+ from typing_extensions import override
7
+
8
+ from .dataobjectproperty import DataObjectProperty
9
+ from .decodestate import DecodeState
10
+ from .dynenddopref import DynEndDopRef
11
+ from .encodestate import EncodeState
12
+ from .exceptions import DecodeError, EncodeError, odxassert, odxraise, odxrequire
13
+ from .field import Field
14
+ from .odxlink import OdxDocFragment, OdxLinkDatabase, OdxLinkId
15
+ from .odxtypes import AtomicOdxType, ParameterValue
16
+ from .utils import dataclass_fields_asdict
17
+
18
+ if TYPE_CHECKING:
19
+ from .diaglayer import DiagLayer
20
+
21
+
22
+ @dataclass
23
+ class DynamicEndmarkerField(Field):
24
+ """Array of a structure with variable length determined by a termination sequence"""
25
+
26
+ dyn_end_dop_ref: DynEndDopRef
27
+
28
+ @staticmethod
29
+ def from_et(et_element: ElementTree.Element,
30
+ doc_frags: List[OdxDocFragment]) -> "DynamicEndmarkerField":
31
+ kwargs = dataclass_fields_asdict(Field.from_et(et_element, doc_frags))
32
+
33
+ dyn_end_dop_ref = DynEndDopRef.from_et(
34
+ odxrequire(et_element.find("DYN-END-DOP-REF")), doc_frags)
35
+
36
+ return DynamicEndmarkerField(dyn_end_dop_ref=dyn_end_dop_ref, **kwargs)
37
+
38
+ def _build_odxlinks(self) -> Dict[OdxLinkId, Any]:
39
+ odxlinks = super()._build_odxlinks()
40
+ return odxlinks
41
+
42
+ def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
43
+ super()._resolve_odxlinks(odxlinks)
44
+
45
+ self._dyn_end_dop = odxlinks.resolve(self.dyn_end_dop_ref, DataObjectProperty)
46
+
47
+ tv_string = self.dyn_end_dop_ref.termination_value_raw
48
+ tv_physical = self._dyn_end_dop.diag_coded_type.base_data_type.from_string(tv_string)
49
+
50
+ self._termination_value = tv_physical
51
+
52
+ def _resolve_snrefs(self, diag_layer: "DiagLayer") -> None:
53
+ super()._resolve_snrefs(diag_layer)
54
+
55
+ @property
56
+ def dyn_end_dop(self) -> DataObjectProperty:
57
+ return self._dyn_end_dop
58
+
59
+ @property
60
+ def termination_value(self) -> AtomicOdxType:
61
+ return self._termination_value
62
+
63
+ @override
64
+ def encode_into_pdu(self, physical_value: ParameterValue, encode_state: EncodeState) -> None:
65
+
66
+ odxassert(encode_state.cursor_bit_position == 0,
67
+ "No bit position can be specified for dynamic endmarker fields!")
68
+ if not isinstance(physical_value, Sequence):
69
+ odxraise(
70
+ f"Expected a sequence of values for dynamic endmarker field {self.short_name}, "
71
+ f"got {type(physical_value).__name__}", EncodeError)
72
+ return
73
+
74
+ orig_is_end_of_pdu = encode_state.is_end_of_pdu
75
+ encode_state.is_end_of_pdu = False
76
+ for i, item in enumerate(physical_value):
77
+ if i == len(physical_value) - 1:
78
+ encode_state.is_end_of_pdu = orig_is_end_of_pdu
79
+
80
+ self.structure.encode_into_pdu(item, encode_state)
81
+ encode_state.is_end_of_pdu = orig_is_end_of_pdu
82
+
83
+ if not encode_state.is_end_of_pdu:
84
+ # only add an endmarker if we are not at the end of the PDU
85
+ self.dyn_end_dop.encode_into_pdu(self.termination_value, encode_state)
86
+
87
+ @override
88
+ def decode_from_pdu(self, decode_state: DecodeState) -> ParameterValue:
89
+
90
+ odxassert(decode_state.cursor_bit_position == 0,
91
+ "No bit position can be specified for dynamic endmarker fields!")
92
+
93
+ orig_origin = decode_state.origin_byte_position
94
+ orig_cursor = decode_state.cursor_byte_position
95
+ decode_state.origin_byte_position = decode_state.cursor_byte_position
96
+
97
+ result: List[ParameterValue] = []
98
+ while True:
99
+ # check if we're at the end of the PDU
100
+ if decode_state.cursor_byte_position == len(decode_state.coded_message):
101
+ break
102
+
103
+ # check if the cursor currently points to a termination
104
+ # value
105
+ tmp_cursor = decode_state.cursor_byte_position
106
+ try:
107
+ tv_candidate = self.dyn_end_dop.decode_from_pdu(decode_state)
108
+ if tv_candidate == self.termination_value:
109
+ break
110
+ except DecodeError:
111
+ pass
112
+ decode_state.cursor_byte_position = tmp_cursor
113
+
114
+ result.append(self.structure.decode_from_pdu(decode_state))
115
+
116
+ decode_state.origin_byte_position = orig_origin
117
+ decode_state.cursor_byte_position = max(orig_cursor, decode_state.cursor_byte_position)
118
+
119
+ return result
@@ -1,8 +1,10 @@
1
1
  # SPDX-License-Identifier: MIT
2
2
  from dataclasses import dataclass
3
- from typing import TYPE_CHECKING, Any, Dict, List
3
+ from typing import TYPE_CHECKING, Any, Dict, List, Sequence
4
4
  from xml.etree import ElementTree
5
5
 
6
+ from typing_extensions import override
7
+
6
8
  from .decodestate import DecodeState
7
9
  from .determinenumberofitems import DetermineNumberOfItems
8
10
  from .encodestate import EncodeState
@@ -47,44 +49,52 @@ class DynamicLengthField(Field):
47
49
  super()._resolve_snrefs(diag_layer)
48
50
  self.determine_number_of_items._resolve_snrefs(diag_layer)
49
51
 
50
- def convert_physical_to_bytes(
51
- self,
52
- physical_value: ParameterValue,
53
- encode_state: EncodeState,
54
- bit_position: int = 0,
55
- ) -> bytes:
52
+ @override
53
+ def encode_into_pdu(self, physical_value: ParameterValue, encode_state: EncodeState) -> None:
54
+
55
+ odxassert(encode_state.cursor_bit_position == 0,
56
+ "No bit position can be specified for dynamic length fields!")
56
57
 
57
- odxassert(bit_position == 0, "No bit position can be specified for dynamic length fields!")
58
- if not isinstance(physical_value, list):
58
+ if not isinstance(physical_value, Sequence):
59
59
  odxraise(
60
60
  f"Expected a list of values for dynamic length field {self.short_name}, "
61
61
  f"got {type(physical_value)}", EncodeError)
62
62
 
63
+ # move the origin to the cursor position
64
+ orig_cursor = encode_state.cursor_byte_position
65
+ orig_origin = encode_state.origin_byte_position
66
+ encode_state.origin_byte_position = encode_state.cursor_byte_position
67
+
63
68
  det_num_items = self.determine_number_of_items
64
- field_len = det_num_items.dop.convert_physical_to_bytes(
65
- len(physical_value), encode_state, det_num_items.bit_position or 0)
66
-
67
- # hack to emplace the length specifier at the correct location
68
- tmp = encode_state.coded_message
69
- encode_state.coded_message = bytearray()
70
- encode_state.emplace_atomic_value(field_len, self.short_name + ".num_items",
71
- det_num_items.byte_position)
72
- result = encode_state.coded_message
73
- encode_state.coded_message = tmp
74
-
75
- # if required, add padding between the length specifier and
76
- # the first item
77
- if len(result) < self.offset:
78
- result.extend([0] * (self.offset - len(result)))
79
- elif len(result) > self.offset:
69
+ encode_state.cursor_bit_position = self.determine_number_of_items.bit_position or 0
70
+ encode_state.cursor_byte_position = encode_state.origin_byte_position + det_num_items.byte_position
71
+ det_num_items.dop.encode_into_pdu(len(physical_value), encode_state)
72
+
73
+ if encode_state.cursor_byte_position - encode_state.origin_byte_position > self.offset:
80
74
  odxraise(f"The length specifier of field {self.short_name} overlaps "
81
- f"with the first item!")
75
+ f"with its first item!")
82
76
 
83
- for value in physical_value:
84
- result += self.structure.convert_physical_to_bytes(value, encode_state)
77
+ encode_state.cursor_byte_position = encode_state.origin_byte_position + self.offset
78
+ encode_state.cursor_bit_position = 0
85
79
 
86
- return result
80
+ orig_is_end_of_pdu = encode_state.is_end_of_pdu
81
+ encode_state.is_end_of_pdu = False
82
+ for i, value in enumerate(physical_value):
83
+ if i == len(physical_value) - 1:
84
+ encode_state.is_end_of_pdu = orig_is_end_of_pdu
85
+
86
+ self.structure.encode_into_pdu(value, encode_state)
87
+ encode_state.is_end_of_pdu = orig_is_end_of_pdu
88
+
89
+ # ensure the correct message size if the field is empty
90
+ if len(physical_value) == 0:
91
+ encode_state.emplace_bytes(b"")
92
+
93
+ # move cursor and origin positions
94
+ encode_state.origin_byte_position = orig_origin
95
+ encode_state.cursor_byte_position = max(orig_cursor, encode_state.cursor_byte_position)
87
96
 
97
+ @override
88
98
  def decode_from_pdu(self, decode_state: DecodeState) -> ParameterValue:
89
99
 
90
100
  odxassert(decode_state.cursor_bit_position == 0,
@@ -0,0 +1,38 @@
1
+ # SPDX-License-Identifier: MIT
2
+ from dataclasses import dataclass
3
+ from typing import List, Optional, overload
4
+ from xml.etree import ElementTree
5
+
6
+ from .exceptions import odxraise, odxrequire
7
+ from .odxlink import OdxDocFragment, OdxLinkRef
8
+ from .utils import dataclass_fields_asdict
9
+
10
+
11
+ @dataclass
12
+ class DynEndDopRef(OdxLinkRef):
13
+ termination_value_raw: str
14
+
15
+ @staticmethod
16
+ @overload
17
+ def from_et(et_element: None, source_doc_frags: List[OdxDocFragment]) -> None:
18
+ ...
19
+
20
+ @staticmethod
21
+ @overload
22
+ def from_et(et_element: ElementTree.Element,
23
+ source_doc_frags: List[OdxDocFragment]) -> "DynEndDopRef":
24
+ ...
25
+
26
+ @staticmethod
27
+ def from_et(et_element: Optional[ElementTree.Element],
28
+ source_doc_frags: List[OdxDocFragment]) -> Optional["DynEndDopRef"]:
29
+
30
+ if et_element is None:
31
+ odxraise("Mandatory DYN-END-DOP-REF tag is missing")
32
+ return None
33
+
34
+ kwargs = dataclass_fields_asdict(OdxLinkRef.from_et(et_element, source_doc_frags))
35
+
36
+ termination_value_raw = odxrequire(et_element.findtext("TERMINATION-VALUE"))
37
+
38
+ return DynEndDopRef(termination_value_raw=termination_value_raw, **kwargs)