odxtools 7.2.0__py3-none-any.whl → 7.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 (49) hide show
  1. odxtools/basicstructure.py +10 -7
  2. odxtools/cli/_print_utils.py +3 -3
  3. odxtools/cli/browse.py +4 -2
  4. odxtools/cli/list.py +3 -3
  5. odxtools/commrelation.py +122 -0
  6. odxtools/comparaminstance.py +1 -1
  7. odxtools/comparamspec.py +1 -2
  8. odxtools/compumethods/linearsegment.py +0 -2
  9. odxtools/database.py +17 -11
  10. odxtools/decodestate.py +8 -2
  11. odxtools/diaglayer.py +23 -17
  12. odxtools/diaglayerraw.py +116 -23
  13. odxtools/diagnostictroublecode.py +2 -2
  14. odxtools/diagservice.py +33 -20
  15. odxtools/diagvariable.py +104 -0
  16. odxtools/dtcdop.py +39 -14
  17. odxtools/dyndefinedspec.py +179 -0
  18. odxtools/encodestate.py +14 -2
  19. odxtools/environmentdatadescription.py +137 -16
  20. odxtools/exceptions.py +10 -1
  21. odxtools/multiplexer.py +92 -56
  22. odxtools/multiplexercase.py +6 -5
  23. odxtools/multiplexerdefaultcase.py +7 -6
  24. odxtools/odxlink.py +19 -47
  25. odxtools/odxtypes.py +1 -1
  26. odxtools/parameterinfo.py +2 -2
  27. odxtools/parameters/nrcconstparameter.py +28 -37
  28. odxtools/parameters/systemparameter.py +1 -1
  29. odxtools/parameters/tablekeyparameter.py +11 -4
  30. odxtools/servicebinner.py +1 -1
  31. odxtools/specialdatagroup.py +1 -1
  32. odxtools/swvariable.py +21 -0
  33. odxtools/templates/macros/printComparam.xml.jinja2 +4 -2
  34. odxtools/templates/macros/printCompuMethod.xml.jinja2 +1 -8
  35. odxtools/templates/macros/printDiagVariable.xml.jinja2 +66 -0
  36. odxtools/templates/macros/printDynDefinedSpec.xml.jinja2 +48 -0
  37. odxtools/templates/macros/printEcuVariantPattern.xml.jinja2 +1 -1
  38. odxtools/templates/macros/printParam.xml.jinja2 +7 -8
  39. odxtools/templates/macros/printService.xml.jinja2 +3 -2
  40. odxtools/templates/macros/printSingleEcuJob.xml.jinja2 +2 -2
  41. odxtools/templates/macros/printVariant.xml.jinja2 +30 -13
  42. odxtools/variablegroup.py +22 -0
  43. odxtools/version.py +2 -2
  44. {odxtools-7.2.0.dist-info → odxtools-7.4.0.dist-info}/METADATA +18 -18
  45. {odxtools-7.2.0.dist-info → odxtools-7.4.0.dist-info}/RECORD +49 -42
  46. {odxtools-7.2.0.dist-info → odxtools-7.4.0.dist-info}/WHEEL +1 -1
  47. {odxtools-7.2.0.dist-info → odxtools-7.4.0.dist-info}/LICENSE +0 -0
  48. {odxtools-7.2.0.dist-info → odxtools-7.4.0.dist-info}/entry_points.txt +0 -0
  49. {odxtools-7.2.0.dist-info → odxtools-7.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,179 @@
1
+ # SPDX-License-Identifier: MIT
2
+ from dataclasses import dataclass
3
+ from typing import Any, Dict, List, Optional, Union
4
+ from xml.etree import ElementTree
5
+
6
+ from .diagcomm import DiagClassType, DiagComm
7
+ from .exceptions import odxraise, odxrequire
8
+ from .nameditemlist import NamedItemList
9
+ from .odxlink import OdxDocFragment, OdxLinkDatabase, OdxLinkId, OdxLinkRef, resolve_snref
10
+ from .snrefcontext import SnRefContext
11
+ from .table import Table
12
+
13
+
14
+ @dataclass
15
+ class DynIdDefModeInfo:
16
+ def_mode: str
17
+
18
+ clear_dyn_def_message_ref: Optional[OdxLinkRef]
19
+ clear_dyn_def_message_snref: Optional[str]
20
+
21
+ read_dyn_def_message_ref: Optional[OdxLinkRef]
22
+ read_dyn_def_message_snref: Optional[str]
23
+
24
+ dyn_def_message_ref: Optional[OdxLinkRef]
25
+ dyn_def_message_snref: Optional[str]
26
+
27
+ supported_dyn_ids: List[bytes]
28
+ selection_table_refs: List[Union[OdxLinkRef, str]]
29
+
30
+ @property
31
+ def clear_dyn_def_message(self) -> DiagComm:
32
+ return self._clear_dyn_def_message
33
+
34
+ @property
35
+ def read_dyn_def_message(self) -> DiagComm:
36
+ return self._read_dyn_def_message
37
+
38
+ @property
39
+ def dyn_def_message(self) -> DiagComm:
40
+ return self._dyn_def_message
41
+
42
+ @property
43
+ def selection_tables(self) -> NamedItemList[Table]:
44
+ return self._selection_tables
45
+
46
+ @staticmethod
47
+ def from_et(et_element: ElementTree.Element,
48
+ doc_frags: List[OdxDocFragment]) -> "DynIdDefModeInfo":
49
+ def_mode = odxrequire(et_element.findtext("DEF-MODE"))
50
+
51
+ clear_dyn_def_message_ref = OdxLinkRef.from_et(
52
+ et_element.find("CLEAR-DYN-DEF-MESSAGE-REF"), doc_frags)
53
+ if (snref_elem := et_element.find("CLEAR-DYN-DEF-MESSAGE-SNREF")) is not None:
54
+ clear_dyn_def_message_snref = snref_elem.attrib["SHORT-NAME"]
55
+
56
+ read_dyn_def_message_ref = OdxLinkRef.from_et(
57
+ et_element.find("READ-DYN-DEF-MESSAGE-REF"), doc_frags)
58
+ if (snref_elem := et_element.find("READ-DYN-DEF-MESSAGE-SNREF")) is not None:
59
+ read_dyn_def_message_snref = snref_elem.attrib["SHORT-NAME"]
60
+
61
+ dyn_def_message_ref = OdxLinkRef.from_et(et_element.find("DYN-DEF-MESSAGE-REF"), doc_frags)
62
+ if (snref_elem := et_element.find("DYN-DEF-MESSAGE-SNREF")) is not None:
63
+ dyn_def_message_snref = snref_elem.attrib["SHORT-NAME"]
64
+
65
+ supported_dyn_ids = [
66
+ bytes.fromhex(odxrequire(x.text))
67
+ for x in et_element.iterfind("SUPPORTED-DYN-IDS/SUPPORTED-DYN-ID")
68
+ ]
69
+
70
+ selection_table_refs: List[Union[OdxLinkRef, str]] = []
71
+ if (st_elems := et_element.find("SELECTION-TABLE-REFS")) is not None:
72
+ for st_elem in st_elems:
73
+ if st_elem.tag == "SELECTION-TABLE-REF":
74
+ selection_table_refs.append(OdxLinkRef.from_et(st_elem, doc_frags))
75
+ elif st_elem.tag == "SELECTION-TABLE-SNREF":
76
+ selection_table_refs.append(odxrequire(st_elem.get("SHORT-NAME")))
77
+ else:
78
+ odxraise()
79
+
80
+ return DynIdDefModeInfo(
81
+ def_mode=def_mode,
82
+ clear_dyn_def_message_ref=clear_dyn_def_message_ref,
83
+ clear_dyn_def_message_snref=clear_dyn_def_message_snref,
84
+ read_dyn_def_message_ref=read_dyn_def_message_ref,
85
+ read_dyn_def_message_snref=read_dyn_def_message_snref,
86
+ dyn_def_message_ref=dyn_def_message_ref,
87
+ dyn_def_message_snref=dyn_def_message_snref,
88
+ supported_dyn_ids=supported_dyn_ids,
89
+ selection_table_refs=selection_table_refs,
90
+ )
91
+
92
+ def _build_odxlinks(self) -> Dict[OdxLinkId, Any]:
93
+ result: Dict[OdxLinkId, Any] = {}
94
+
95
+ return result
96
+
97
+ def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
98
+ self._selection_tables = NamedItemList[Table]()
99
+
100
+ if self.clear_dyn_def_message_ref is not None:
101
+ self._clear_dyn_def_message = odxlinks.resolve(self.clear_dyn_def_message_ref, DiagComm)
102
+
103
+ if self.read_dyn_def_message_ref is not None:
104
+ self._read_dyn_def_message = odxlinks.resolve(self.read_dyn_def_message_ref, DiagComm)
105
+
106
+ if self.dyn_def_message_ref is not None:
107
+ self._dyn_def_message = odxlinks.resolve(self.dyn_def_message_ref, DiagComm)
108
+
109
+ # resolve the selection tables referenced using ODXLINK
110
+ for x in self.selection_table_refs:
111
+ if isinstance(x, OdxLinkRef):
112
+ self._selection_tables.append(odxlinks.resolve(x, Table))
113
+
114
+ def _resolve_snrefs(self, context: SnRefContext) -> None:
115
+ diag_layer = odxrequire(context.diag_layer)
116
+
117
+ if self.clear_dyn_def_message_snref is not None:
118
+ self._clear_dyn_def_message = resolve_snref(self.clear_dyn_def_message_snref,
119
+ diag_layer.diag_comms, DiagComm)
120
+
121
+ if self.read_dyn_def_message_snref is not None:
122
+ self._read_dyn_def_message = resolve_snref(self.read_dyn_def_message_snref,
123
+ diag_layer.diag_comms, DiagComm)
124
+
125
+ if self.dyn_def_message_snref is not None:
126
+ self._dyn_def_message = resolve_snref(self.dyn_def_message_snref, diag_layer.diag_comms,
127
+ DiagComm)
128
+
129
+ if self._clear_dyn_def_message.diagnostic_class != DiagClassType.CLEAR_DYN_DEF_MESSAGE:
130
+ odxraise(
131
+ f"Diagnostic communication object of wrong type referenced: "
132
+ f"({odxrequire(self._clear_dyn_def_message.diagnostic_class).value} instead of "
133
+ f"CLEAR-DYN-DEF-MESSAGE)")
134
+ if self._read_dyn_def_message.diagnostic_class != DiagClassType.READ_DYN_DEFINED_MESSAGE:
135
+ odxraise(f"Diagnostic communication object of wrong type referenced: "
136
+ f"({odxrequire(self._read_dyn_def_message.diagnostic_class).value} instead of "
137
+ f"READ-DYN-DEFINED-MESSAGE)")
138
+ if self._dyn_def_message.diagnostic_class != DiagClassType.DYN_DEF_MESSAGE:
139
+ odxraise(f"Diagnostic communication object of wrong type referenced: "
140
+ f"({odxrequire(self._dyn_def_message.diagnostic_class).value} instead of "
141
+ f"DYN-DEF-MESSAGE)")
142
+
143
+ # resolve the remaining selection tables that are referenced via SNREF
144
+ for i, x in enumerate(self.selection_table_refs):
145
+ if isinstance(x, str):
146
+ ddd_spec = odxrequire(diag_layer.diag_data_dictionary_spec)
147
+ self._selection_tables.insert(i, resolve_snref(x, ddd_spec.tables, Table))
148
+
149
+
150
+ @dataclass
151
+ class DynDefinedSpec:
152
+ dyn_id_def_mode_infos: List[DynIdDefModeInfo]
153
+
154
+ @staticmethod
155
+ def from_et(et_element: ElementTree.Element,
156
+ doc_frags: List[OdxDocFragment]) -> "DynDefinedSpec":
157
+ dyn_id_def_mode_infos = [
158
+ DynIdDefModeInfo.from_et(x, doc_frags)
159
+ for x in et_element.iterfind("DYN-ID-DEF-MODE-INFOS/DYN-ID-DEF-MODE-INFO")
160
+ ]
161
+ return DynDefinedSpec(dyn_id_def_mode_infos=dyn_id_def_mode_infos)
162
+
163
+ def _build_odxlinks(self) -> Dict[OdxLinkId, Any]:
164
+ result: Dict[OdxLinkId, Any] = {}
165
+
166
+ result.update(self._build_odxlinks())
167
+
168
+ for didmi in self.dyn_id_def_mode_infos:
169
+ result.update(didmi._build_odxlinks())
170
+
171
+ return result
172
+
173
+ def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
174
+ for didmi in self.dyn_id_def_mode_infos:
175
+ didmi._resolve_odxlinks(odxlinks)
176
+
177
+ def _resolve_snrefs(self, context: SnRefContext) -> None:
178
+ for didmi in self.dyn_id_def_mode_infos:
179
+ didmi._resolve_snrefs(context)
odxtools/encodestate.py CHANGED
@@ -1,16 +1,19 @@
1
1
  # SPDX-License-Identifier: MIT
2
2
  import warnings
3
3
  from dataclasses import dataclass, field
4
- from typing import Dict, Optional, SupportsBytes
4
+ from typing import TYPE_CHECKING, Dict, List, Optional, SupportsBytes, Tuple
5
5
 
6
6
  from .exceptions import EncodeError, OdxWarning, odxassert, odxraise
7
- from .odxtypes import AtomicOdxType, DataType
7
+ from .odxtypes import AtomicOdxType, DataType, ParameterValue
8
8
 
9
9
  try:
10
10
  import bitstruct.c as bitstruct
11
11
  except ImportError:
12
12
  import bitstruct
13
13
 
14
+ if TYPE_CHECKING:
15
+ from .parameters.parameter import Parameter
16
+
14
17
 
15
18
  @dataclass
16
19
  class EncodeState:
@@ -56,6 +59,15 @@ class EncodeState:
56
59
  #: (needed for MinMaxLengthType, EndOfPduField, etc.)
57
60
  is_end_of_pdu: bool = True
58
61
 
62
+ #: list of parameters that have been encoded so far. The journal
63
+ #: is used by some types of parameters which depend on the values of
64
+ #: other parameters; e.g., environment data description parameters
65
+ journal: List[Tuple["Parameter", Optional[ParameterValue]]] = field(default_factory=list)
66
+
67
+ #: If this is True, specifying unknown parameters for encoding
68
+ #: will raise an OdxError exception in strict mode.
69
+ allow_unknown_parameters = False
70
+
59
71
  def __post_init__(self) -> None:
60
72
  # if a coded message has been specified, but no used_mask, we
61
73
  # assume that all of the bits of the coded message are
@@ -1,17 +1,19 @@
1
1
  # SPDX-License-Identifier: MIT
2
2
  from dataclasses import dataclass
3
- from typing import Any, Dict, List, Optional
3
+ from typing import Any, Dict, List, Optional, cast
4
4
  from xml.etree import ElementTree
5
5
 
6
6
  from typing_extensions import override
7
7
 
8
8
  from .complexdop import ComplexDop
9
9
  from .decodestate import DecodeState
10
+ from .dtcdop import DtcDop
10
11
  from .encodestate import EncodeState
11
12
  from .environmentdata import EnvironmentData
12
13
  from .exceptions import odxraise, odxrequire
13
14
  from .odxlink import OdxDocFragment, OdxLinkDatabase, OdxLinkId, OdxLinkRef
14
- from .odxtypes import ParameterValue
15
+ from .odxtypes import ParameterValue, ParameterValueDict
16
+ from .parameters.parameter import Parameter
15
17
  from .snrefcontext import SnRefContext
16
18
  from .utils import dataclass_fields_asdict
17
19
 
@@ -27,16 +29,26 @@ class EnvironmentDataDescription(ComplexDop):
27
29
 
28
30
  """
29
31
 
32
+ param_snref: Optional[str]
33
+ param_snpathref: Optional[str]
34
+
30
35
  # in ODX 2.0.0, ENV-DATAS seems to be a mandatory
31
36
  # sub-element of ENV-DATA-DESC, in ODX 2.2 it is not
32
37
  # present
33
38
  env_datas: List[EnvironmentData]
34
39
  env_data_refs: List[OdxLinkRef]
35
- param_snref: Optional[str]
36
- param_snpathref: Optional[str]
37
40
 
38
- def __post_init__(self) -> None:
39
- self.bit_length = None
41
+ @property
42
+ def param(self) -> Parameter:
43
+ # the parameter referenced via SNREF cannot be resolved here
44
+ # because the relevant list of parameters depends on the
45
+ # concrete codec object processed, whilst an environment data
46
+ # description object can be featured in an arbitrary number of
47
+ # responses. Instead, lookup of the appropriate parameter is
48
+ # done within the encode and decode methods.
49
+ odxraise("The parameter of ENV-DATA-DESC objects cannot be resolved "
50
+ "because it depends on the context")
51
+ return cast(None, Parameter)
40
52
 
41
53
  @staticmethod
42
54
  def from_et(et_element: ElementTree.Element,
@@ -71,8 +83,9 @@ class EnvironmentDataDescription(ComplexDop):
71
83
  def _build_odxlinks(self) -> Dict[OdxLinkId, Any]:
72
84
  odxlinks = {self.odx_id: self}
73
85
 
74
- for ed in self.env_datas:
75
- odxlinks.update(ed._build_odxlinks())
86
+ if not self.env_data_refs:
87
+ for ed in self.env_datas:
88
+ odxlinks.update(ed._build_odxlinks())
76
89
 
77
90
  return odxlinks
78
91
 
@@ -96,17 +109,125 @@ class EnvironmentDataDescription(ComplexDop):
96
109
  def encode_into_pdu(self, physical_value: Optional[ParameterValue],
97
110
  encode_state: EncodeState) -> None:
98
111
  """Convert a physical value into bytes and emplace them into a PDU.
99
-
100
- Since environmental data is supposed to never appear on the
101
- wire, this method just raises an EncodeError exception.
102
112
  """
103
- odxraise("EnvironmentDataDescription DOPs cannot be encoded or decoded")
113
+
114
+ # retrieve the relevant DTC parameter which must be located in
115
+ # front of the environment data description.
116
+ if self.param_snref is None:
117
+ odxraise("Specifying the DTC parameter for environment data "
118
+ "descriptions via SNPATHREF is not supported yet")
119
+ return None
120
+
121
+ dtc_param: Optional[Parameter] = None
122
+ dtc_dop: Optional[DtcDop] = None
123
+ dtc_param_value: Optional[ParameterValue] = None
124
+ for prev_param, prev_param_value in reversed(encode_state.journal):
125
+ if prev_param.short_name == self.param_snref:
126
+ dtc_param = prev_param
127
+ prev_dop = getattr(prev_param, "dop", None)
128
+ if not isinstance(prev_dop, DtcDop):
129
+ odxraise(f"The DOP of the parameter referenced by environment data "
130
+ f"descriptions must be a DTC-DOP (is '{type(prev_dop).__name__}')")
131
+ return
132
+ dtc_dop = prev_dop
133
+ dtc_param_value = prev_param_value
134
+ break
135
+
136
+ if dtc_param is None:
137
+ odxraise("Environment data description parameters are only allowed following "
138
+ "the referenced value parameter.")
139
+ return
140
+
141
+ if dtc_param_value is None or dtc_dop is None:
142
+ # this should never happen
143
+ odxraise()
144
+ return
145
+
146
+ numerical_dtc = dtc_dop.convert_to_numerical_trouble_code(dtc_param_value)
147
+
148
+ # deal with the "all value" environment data. This holds
149
+ # parameters that are common to all DTCs. Be aware that the
150
+ # specification mandates that there is at most one such
151
+ # environment data object
152
+ for env_data in self.env_datas:
153
+ if env_data.all_value:
154
+ tmp = encode_state.allow_unknown_parameters
155
+ encode_state.allow_unknown_parameters = True
156
+ env_data.encode_into_pdu(physical_value, encode_state)
157
+ encode_state.allow_unknown_parameters = tmp
158
+ break
159
+
160
+ # find the environment data corresponding to the given trouble
161
+ # code
162
+ for env_data in self.env_datas:
163
+ if numerical_dtc in env_data.dtc_values:
164
+ tmp = encode_state.allow_unknown_parameters
165
+ encode_state.allow_unknown_parameters = True
166
+ env_data.encode_into_pdu(physical_value, encode_state)
167
+ encode_state.allow_unknown_parameters = tmp
168
+ break
104
169
 
105
170
  @override
106
171
  def decode_from_pdu(self, decode_state: DecodeState) -> ParameterValue:
107
172
  """Extract the bytes from a PDU and convert them to a physical value.
108
-
109
- Since environmental data is supposed to never appear on the
110
- wire, this method just raises an DecodeError exception.
111
173
  """
112
- odxraise("EnvironmentDataDescription DOPs cannot be encoded or decoded")
174
+
175
+ # retrieve the relevant DTC parameter which must be located in
176
+ # front of the environment data description.
177
+ if self.param_snref is None:
178
+ odxraise("Specifying the DTC parameter for environment data "
179
+ "descriptions via SNPATHREF is not supported yet")
180
+ return None
181
+
182
+ dtc_param: Optional[Parameter] = None
183
+ dtc_dop: Optional[DtcDop] = None
184
+ dtc_param_value: Optional[ParameterValue] = None
185
+ for prev_param, prev_param_value in reversed(decode_state.journal):
186
+ if prev_param.short_name == self.param_snref:
187
+ dtc_param = prev_param
188
+ prev_dop = getattr(prev_param, "dop", None)
189
+ if not isinstance(prev_dop, DtcDop):
190
+ odxraise(f"The DOP of the parameter referenced by environment data "
191
+ f"descriptions must be a DTC-DOP (is '{type(prev_dop).__name__}')")
192
+ return
193
+ dtc_dop = prev_dop
194
+ dtc_param_value = prev_param_value
195
+ break
196
+
197
+ if dtc_param is None:
198
+ odxraise("Environment data description parameters are only allowed following "
199
+ "the referenced value parameter.")
200
+ return
201
+
202
+ if dtc_param_value is None or dtc_dop is None:
203
+ # this should never happen
204
+ odxraise()
205
+ return
206
+
207
+ numerical_dtc = dtc_dop.convert_to_numerical_trouble_code(dtc_param_value)
208
+
209
+ result: ParameterValueDict = {}
210
+
211
+ # deal with the "all value" environment data. This holds
212
+ # parameters that are common to all DTCs. Be aware that the
213
+ # specification mandates that there is at most one such
214
+ # environment data object
215
+ for env_data in self.env_datas:
216
+ if env_data.all_value:
217
+ tmp = env_data.decode_from_pdu(decode_state)
218
+ if not isinstance(tmp, dict):
219
+ odxraise()
220
+ result.update(tmp)
221
+ break
222
+
223
+ # find the environment data corresponding to the given trouble
224
+ # code
225
+ for env_data in self.env_datas:
226
+ if numerical_dtc in env_data.dtc_values:
227
+ tmp = env_data.decode_from_pdu(decode_state)
228
+ if not isinstance(tmp, dict):
229
+ odxraise()
230
+ result.update(tmp)
231
+ break
232
+
233
+ return result
odxtools/exceptions.py CHANGED
@@ -9,13 +9,22 @@ class OdxError(Exception):
9
9
 
10
10
 
11
11
  class EncodeError(Warning, OdxError):
12
- """Encoding of a message to raw data failed."""
12
+ """Encoding of a message to raw data failed"""
13
13
 
14
14
 
15
15
  class DecodeError(Warning, OdxError):
16
16
  """Decoding raw data failed."""
17
17
 
18
18
 
19
+ class DecodeMismatch(DecodeError):
20
+ """Decoding failed because some parameters exhibit an incorrect value
21
+
22
+ This is can happen if NRC-CONST or environment data descriptions
23
+ are present.
24
+
25
+ """
26
+
27
+
19
28
  class OdxWarning(Warning):
20
29
  """Any warning that happens during interacting with diagnostic objects."""
21
30
 
odxtools/multiplexer.py CHANGED
@@ -1,6 +1,6 @@
1
1
  # SPDX-License-Identifier: MIT
2
2
  from dataclasses import dataclass
3
- from typing import Any, Dict, List, Optional, Tuple
3
+ from typing import Any, Dict, List, Optional, Tuple, Union, cast
4
4
  from xml.etree import ElementTree
5
5
 
6
6
  from typing_extensions import override
@@ -8,7 +8,7 @@ from typing_extensions import override
8
8
  from .complexdop import ComplexDop
9
9
  from .decodestate import DecodeState
10
10
  from .encodestate import EncodeState
11
- from .exceptions import DecodeError, EncodeError, odxraise, odxrequire
11
+ from .exceptions import DecodeError, EncodeError, odxassert, odxraise, odxrequire
12
12
  from .multiplexercase import MultiplexerCase
13
13
  from .multiplexerdefaultcase import MultiplexerDefaultCase
14
14
  from .multiplexerswitchkey import MultiplexerSwitchKey
@@ -79,87 +79,123 @@ class Multiplexer(ComplexDop):
79
79
  def encode_into_pdu(self, physical_value: ParameterValue, encode_state: EncodeState) -> None:
80
80
 
81
81
  if encode_state.cursor_bit_position != 0:
82
- raise EncodeError(f"Multiplexer must be aligned, i.e. bit_position=0, but "
82
+ raise EncodeError(f"Multiplexer parameters must be aligned, i.e. bit_position=0, but "
83
83
  f"{self.short_name} was passed the bit position "
84
84
  f"{encode_state.cursor_bit_position}")
85
85
 
86
- if not isinstance(physical_value, dict) or len(physical_value) != 1:
87
- raise EncodeError("""Multiplexer should be defined as a dict
88
- with only one key equal to the desired case""")
89
-
90
86
  orig_origin = encode_state.origin_byte_position
91
-
92
87
  encode_state.origin_byte_position = encode_state.cursor_byte_position
93
88
 
94
- case_name, case_value = next(iter(physical_value.items()))
95
-
96
- for mux_case in self.cases or []:
97
- if mux_case.short_name == case_name:
98
- key_value, _ = self._get_case_limits(mux_case)
99
-
100
- if self.switch_key.byte_position is not None:
101
- encode_state.cursor_byte_position = encode_state.origin_byte_position + self.switch_key.byte_position
102
- encode_state.cursor_bit_position = self.switch_key.bit_position or 0
103
-
104
- self.switch_key.dop.encode_into_pdu(
105
- physical_value=key_value, encode_state=encode_state)
106
-
107
- if self.byte_position is not None:
108
- encode_state.cursor_byte_position = encode_state.origin_byte_position + self.byte_position
109
- encode_state.cursor_bit_position = 0
110
-
111
- if mux_case._structure is None:
112
- odxraise(f"Multiplexer case '{mux_case.short_name}' does not "
113
- f"reference a structure.")
114
- return
115
-
116
- mux_case.structure.encode_into_pdu(
117
- physical_value=key_value, encode_state=encode_state)
118
-
119
- encode_state.origin_byte_position = orig_origin
120
- return
121
-
122
- raise EncodeError(f"The case {case_name} is not found in Multiplexer {self.short_name}")
89
+ if isinstance(physical_value, (list, tuple)) and len(physical_value) == 2:
90
+ case_spec, case_value = physical_value
91
+ elif isinstance(physical_value, dict) and len(physical_value) == 1:
92
+ case_spec, case_value = next(iter(physical_value.items()))
93
+ else:
94
+ raise EncodeError(
95
+ f"Values of multiplexer parameters must be defined as a "
96
+ f"(case_name, content_value) tuple instead of as '{physical_value!r}'")
97
+
98
+ mux_case: Union[MultiplexerCase, MultiplexerDefaultCase]
99
+ if isinstance(case_spec, str):
100
+ applicable_cases = [x for x in self.cases if x.short_name == case_spec]
101
+ if len(applicable_cases) == 0:
102
+ raise EncodeError(
103
+ f"Multiplexer {self.short_name} does not know any case called {case_spec}")
104
+
105
+ odxassert(len(applicable_cases) == 1)
106
+ mux_case = applicable_cases[0]
107
+ key_value, _ = self._get_case_limits(mux_case)
108
+ elif isinstance(case_spec, int):
109
+ applicable_cases = []
110
+ for x in self.cases:
111
+ lower, upper = cast(Tuple[int, int], self._get_case_limits(x))
112
+ if lower <= case_spec and case_spec <= upper:
113
+ applicable_cases.append(x)
114
+
115
+ if len(applicable_cases) == 0:
116
+ if self.default_case is None:
117
+ raise EncodeError(
118
+ f"Multiplexer {self.short_name} does not know any case called {case_spec}")
119
+ mux_case = self.default_case
120
+ key_value = case_spec
121
+ else:
122
+ mux_case = applicable_cases[0]
123
+ key_value = case_spec
124
+ elif isinstance(case_spec, MultiplexerCase):
125
+ mux_case = case_spec
126
+ key_value, _ = self._get_case_limits(mux_case)
127
+ elif case_spec is None:
128
+ if self.default_case is None:
129
+ raise EncodeError(f"Multiplexer {self.short_name} does not define a default case")
130
+ key_value = 0
131
+ else:
132
+ raise EncodeError(f"Illegal case specification '{case_spec}' for "
133
+ f"multiplexer {self.short_name}")
134
+
135
+ # the byte position of the switch key is relative to
136
+ # the multiplexer's position
137
+ encode_state.cursor_byte_position = encode_state.origin_byte_position + self.switch_key.byte_position
138
+ encode_state.cursor_bit_position = self.switch_key.bit_position or 0
139
+ self.switch_key.dop.encode_into_pdu(physical_value=key_value, encode_state=encode_state)
140
+ encode_state.cursor_bit_position = 0
141
+
142
+ if mux_case.structure is not None:
143
+ # the byte position of the content is specified by the
144
+ # BYTE-POSITION attribute of the multiplexer
145
+ encode_state.cursor_byte_position = encode_state.origin_byte_position + self.byte_position
146
+ mux_case.structure.encode_into_pdu(physical_value=case_value, encode_state=encode_state)
147
+
148
+ encode_state.origin_byte_position = orig_origin
123
149
 
124
150
  @override
125
151
  def decode_from_pdu(self, decode_state: DecodeState) -> ParameterValue:
126
-
127
- # multiplexers are structures and thus the origin position
128
- # must be moved to the start of the multiplexer
129
152
  orig_origin = decode_state.origin_byte_position
130
- if self.byte_position is not None:
131
- decode_state.cursor_byte_position = decode_state.origin_byte_position + self.byte_position
132
153
  decode_state.origin_byte_position = decode_state.cursor_byte_position
133
154
 
155
+ # Decode the switch key. Its BYTE-POSITION is relative to the
156
+ # that of the multiplexer.
157
+ if self.switch_key.byte_position is not None:
158
+ decode_state.cursor_byte_position = decode_state.origin_byte_position + self.switch_key.byte_position
159
+ decode_state.cursor_bit_position = self.switch_key.bit_position or 0
134
160
  key_value = self.switch_key.dop.decode_from_pdu(decode_state)
161
+ decode_state.cursor_bit_position = 0
135
162
 
136
163
  if not isinstance(key_value, int):
137
164
  odxraise(f"Multiplexer keys must be integers (is '{type(key_value).__name__}'"
138
165
  f" for multiplexer '{self.short_name}')")
139
166
 
140
- case_value: Optional[ParameterValue] = None
141
- mux_case = None
142
- for mux_case in self.cases or []:
167
+ # "If a matching CASE is found, the referenced STRUCTURE is
168
+ # analyzed at the BYTE-POSITION (child element of MUX)
169
+ # relatively to the byte position of the MUX."
170
+ decode_state.cursor_byte_position = decode_state.origin_byte_position + self.byte_position
171
+
172
+ applicable_case: Optional[Union[MultiplexerCase, MultiplexerDefaultCase]] = None
173
+ for mux_case in self.cases:
143
174
  lower, upper = self._get_case_limits(mux_case)
144
175
  if lower <= key_value and key_value <= upper: # type: ignore[operator]
145
- if mux_case._structure:
146
- case_value = mux_case._structure.decode_from_pdu(decode_state)
176
+ applicable_case = mux_case
147
177
  break
148
178
 
149
- if case_value is None and self.default_case is not None:
150
- if self.default_case._structure:
151
- case_value = self.default_case._structure.decode_from_pdu(decode_state)
179
+ if applicable_case is None:
180
+ applicable_case = self.default_case
181
+
182
+ if applicable_case is None:
183
+ odxraise(
184
+ f"Cannot find an applicable case for value {key_value} in "
185
+ f"multiplexer {self.short_name}", DecodeError)
186
+ decode_state.origin_byte_position = orig_origin
187
+ return (None, None)
152
188
 
153
- if mux_case is None or case_value is None:
154
- odxraise(f"Failed to find a matching case in {self.short_name} for value {key_value!r}",
155
- DecodeError)
189
+ if applicable_case.structure is not None:
190
+ case_value = applicable_case.structure.decode_from_pdu(decode_state)
191
+ else:
192
+ case_value = {}
156
193
 
157
- mux_value = (mux_case.short_name, case_value)
194
+ result = (applicable_case.short_name, case_value)
158
195
 
159
- # go back to the original origin
160
196
  decode_state.origin_byte_position = orig_origin
161
197
 
162
- return mux_value
198
+ return result
163
199
 
164
200
  @override
165
201
  def _build_odxlinks(self) -> Dict[OdxLinkId, Any]: