odxtools 7.4.0__py3-none-any.whl → 8.0.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 (50) hide show
  1. odxtools/cli/_print_utils.py +4 -3
  2. odxtools/cli/browse.py +13 -11
  3. odxtools/cli/compare.py +1 -1
  4. odxtools/cli/list.py +20 -10
  5. odxtools/cli/snoop.py +28 -5
  6. odxtools/database.py +46 -10
  7. odxtools/diagcomm.py +4 -4
  8. odxtools/diaglayercontainer.py +23 -11
  9. odxtools/diaglayers/basevariant.py +128 -0
  10. odxtools/diaglayers/basevariantraw.py +117 -0
  11. odxtools/diaglayers/diaglayer.py +422 -0
  12. odxtools/{diaglayerraw.py → diaglayers/diaglayerraw.py} +18 -156
  13. odxtools/diaglayers/ecushareddata.py +96 -0
  14. odxtools/diaglayers/ecushareddataraw.py +81 -0
  15. odxtools/diaglayers/ecuvariant.py +124 -0
  16. odxtools/diaglayers/ecuvariantraw.py +123 -0
  17. odxtools/diaglayers/functionalgroup.py +110 -0
  18. odxtools/diaglayers/functionalgroupraw.py +100 -0
  19. odxtools/{diaglayer.py → diaglayers/hierarchyelement.py} +183 -498
  20. odxtools/diaglayers/hierarchyelementraw.py +52 -0
  21. odxtools/diaglayers/protocol.py +64 -0
  22. odxtools/diaglayers/protocolraw.py +85 -0
  23. odxtools/diagvariable.py +10 -1
  24. odxtools/ecuvariantmatcher.py +6 -7
  25. odxtools/field.py +3 -4
  26. odxtools/matchingparameter.py +1 -1
  27. odxtools/nameditemlist.py +4 -3
  28. odxtools/parameterinfo.py +10 -1
  29. odxtools/parameters/parameterwithdop.py +2 -3
  30. odxtools/parentref.py +1 -1
  31. odxtools/snrefcontext.py +1 -1
  32. odxtools/templates/diag_layer_container.odx-d.xml.jinja2 +11 -6
  33. odxtools/templates/macros/printBaseVariant.xml.jinja2 +53 -0
  34. odxtools/templates/macros/printCompuMethod.xml.jinja2 +1 -1
  35. odxtools/templates/macros/printDiagLayer.xml.jinja2 +206 -0
  36. odxtools/templates/macros/printEcuSharedData.xml.jinja2 +30 -0
  37. odxtools/templates/macros/printEcuVariant.xml.jinja2 +53 -0
  38. odxtools/templates/macros/printFunctionalGroup.xml.jinja2 +40 -0
  39. odxtools/templates/macros/printHierarchyElement.xml.jinja2 +24 -0
  40. odxtools/templates/macros/printProtocol.xml.jinja2 +30 -0
  41. odxtools/variablegroup.py +11 -1
  42. odxtools/version.py +2 -2
  43. {odxtools-7.4.0.dist-info → odxtools-8.0.0.dist-info}/METADATA +1 -1
  44. {odxtools-7.4.0.dist-info → odxtools-8.0.0.dist-info}/RECORD +49 -31
  45. odxtools/templates/macros/printVariant.xml.jinja2 +0 -241
  46. /odxtools/{diaglayertype.py → diaglayers/diaglayertype.py} +0 -0
  47. {odxtools-7.4.0.dist-info → odxtools-8.0.0.dist-info}/LICENSE +0 -0
  48. {odxtools-7.4.0.dist-info → odxtools-8.0.0.dist-info}/WHEEL +0 -0
  49. {odxtools-7.4.0.dist-info → odxtools-8.0.0.dist-info}/entry_points.txt +0 -0
  50. {odxtools-7.4.0.dist-info → odxtools-8.0.0.dist-info}/top_level.txt +0 -0
@@ -1,149 +1,96 @@
1
1
  # SPDX-License-Identifier: MIT
2
2
  import re
3
3
  import warnings
4
- from copy import copy, deepcopy
4
+ from copy import deepcopy
5
5
  from dataclasses import dataclass
6
6
  from functools import cached_property
7
- from itertools import chain
8
7
  from typing import (TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, Tuple, TypeVar,
9
8
  Union, cast)
10
9
  from xml.etree import ElementTree
11
10
 
12
11
  from deprecation import deprecated
13
12
 
14
- from .additionalaudience import AdditionalAudience
15
- from .admindata import AdminData
16
- from .companydata import CompanyData
17
- from .comparaminstance import ComparamInstance
18
- from .comparamspec import ComparamSpec
19
- from .comparamsubset import ComparamSubset
20
- from .description import Description
21
- from .diagcomm import DiagComm
22
- from .diagdatadictionaryspec import DiagDataDictionarySpec
23
- from .diaglayerraw import DiagLayerRaw
24
- from .diaglayertype import DiagLayerType
25
- from .diagservice import DiagService
26
- from .ecuvariantpattern import EcuVariantPattern
27
- from .exceptions import DecodeError, OdxWarning, odxassert, odxraise
28
- from .functionalclass import FunctionalClass
29
- from .message import Message
30
- from .nameditemlist import NamedItemList, OdxNamed
31
- from .odxlink import OdxDocFragment, OdxLinkDatabase, OdxLinkId, OdxLinkRef
32
- from .parentref import ParentRef
33
- from .protstack import ProtStack
34
- from .request import Request
35
- from .response import Response
36
- from .servicebinner import ServiceBinner
37
- from .singleecujob import SingleEcuJob
38
- from .snrefcontext import SnRefContext
39
- from .specialdatagroup import SpecialDataGroup
40
- from .statechart import StateChart
41
- from .table import Table
42
- from .unitgroup import UnitGroup
43
- from .unitspec import UnitSpec
13
+ from ..additionalaudience import AdditionalAudience
14
+ from ..admindata import AdminData
15
+ from ..comparaminstance import ComparamInstance
16
+ from ..diagcomm import DiagComm
17
+ from ..diagdatadictionaryspec import DiagDataDictionarySpec
18
+ from ..diagservice import DiagService
19
+ from ..exceptions import OdxWarning, odxassert, odxraise
20
+ from ..functionalclass import FunctionalClass
21
+ from ..nameditemlist import NamedItemList, OdxNamed
22
+ from ..odxlink import OdxDocFragment, OdxLinkDatabase, OdxLinkId
23
+ from ..parentref import ParentRef
24
+ from ..response import Response
25
+ from ..singleecujob import SingleEcuJob
26
+ from ..snrefcontext import SnRefContext
27
+ from ..specialdatagroup import SpecialDataGroup
28
+ from ..statechart import StateChart
29
+ from ..unitgroup import UnitGroup
30
+ from ..unitspec import UnitSpec
31
+ from .diaglayer import DiagLayer
32
+ from .hierarchyelementraw import HierarchyElementRaw
44
33
 
45
34
  if TYPE_CHECKING:
46
35
  from .database import Database
36
+ from .protocol import Protocol
47
37
 
48
38
  TNamed = TypeVar("TNamed", bound=OdxNamed)
49
39
 
50
- PrefixTree = Dict[int, Union[List[DiagService], "PrefixTree"]]
51
-
52
40
 
53
41
  @dataclass
54
- class DiagLayer:
55
- """This class represents a "logical view" upon a diagnostic layer
56
- according to the ODX standard.
57
-
58
- i.e. it handles the value inheritance, communication parameters,
59
- encoding/decoding of data, etc.
42
+ class HierarchyElement(DiagLayer):
43
+ """This is the base class for diagnostic layers that may be involved in value inheritance
60
44
  """
61
45
 
62
- diag_layer_raw: DiagLayerRaw
46
+ @property
47
+ def hierarchy_element_raw(self) -> HierarchyElementRaw:
48
+ return cast(HierarchyElementRaw, self.diag_layer_raw)
49
+
50
+ @staticmethod
51
+ def from_et(et_element: ElementTree.Element,
52
+ doc_frags: List[OdxDocFragment]) -> "HierarchyElement":
53
+ hierarchy_element_raw = HierarchyElementRaw.from_et(et_element, doc_frags)
54
+
55
+ return HierarchyElement(diag_layer_raw=hierarchy_element_raw)
63
56
 
64
57
  def __post_init__(self) -> None:
65
- self._global_negative_responses: NamedItemList[Response]
58
+ super().__post_init__()
66
59
 
67
- @staticmethod
68
- def from_et(et_element: ElementTree.Element, doc_frags: List[OdxDocFragment]) -> "DiagLayer":
69
- diag_layer_raw = DiagLayerRaw.from_et(et_element, doc_frags)
60
+ self._global_negative_responses: NamedItemList[Response]
70
61
 
71
- # Create DiagLayer
72
- return DiagLayer(diag_layer_raw=diag_layer_raw)
62
+ odxassert(
63
+ isinstance(self.diag_layer_raw, HierarchyElementRaw),
64
+ "The raw diagnostic layer passed to HierarchyElement "
65
+ "must be a HierarchyElementRaw")
73
66
 
74
67
  def _build_odxlinks(self) -> Dict[OdxLinkId, Any]:
75
- """Construct a mapping from IDs to all objects that are contained in this diagnostic layer."""
76
- result = self.diag_layer_raw._build_odxlinks()
77
-
78
- # we want to get the full diag layer, not just the raw layer
79
- # when referencing...
80
- result[self.odx_id] = self
68
+ result = super()._build_odxlinks()
81
69
 
82
70
  return result
83
71
 
84
72
  def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
85
- """Recursively resolve all references."""
86
-
87
- # deal with the import references: these basically extend the
88
- # pool of objects that are referenceable without having to
89
- # explicitly specify the DOCREF attribute in the
90
- # reference. This mechanism can thus be seen as a kind of
91
- # "poor man's inheritance".
92
- if self.import_refs:
93
- imported_links: Dict[OdxLinkId, Any] = {}
94
- for import_ref in self.import_refs:
95
- imported_dl = odxlinks.resolve(import_ref, DiagLayer)
96
-
97
- odxassert(
98
- imported_dl.variant_type == DiagLayerType.ECU_SHARED_DATA,
99
- f"Tried to import references from diagnostic layer "
100
- f"'{imported_dl.short_name}' of type {imported_dl.variant_type.value}. "
101
- f"Only ECU-SHARED-DATA layers may be referenced using the "
102
- f"IMPORT-REF mechanism")
103
-
104
- # TODO: ensure that the imported diagnostic layer has
105
- # not been referenced in any PARENT-REF of the current
106
- # layer or any of its parents.
107
-
108
- # TODO: detect and complain about cyclic IMPORT-REFs
109
-
110
- # TODO (?): detect conflicts with locally-defined
111
- # objects
112
-
113
- imported_dl_links = imported_dl._build_odxlinks()
114
- for link_id, obj in imported_dl_links.items():
115
- # the imported objects shall behave as if they
116
- # were defined by the importing layer. IOW, they
117
- # must be visible in the same document fragments.
118
- link_id = OdxLinkId(link_id.local_id, self.odx_id.doc_fragments)
119
- imported_links[link_id] = obj
120
-
121
- # We need to copy the odxlink database here since this
122
- # function must not modify its argument because the
123
- # imported references only apply within this specific
124
- # diagnostic layer
125
- extended_odxlinks = copy(odxlinks)
126
- extended_odxlinks.update(imported_links)
127
-
128
- self.diag_layer_raw._resolve_odxlinks(extended_odxlinks)
129
- return
130
-
131
- self.diag_layer_raw._resolve_odxlinks(odxlinks)
73
+ super()._resolve_odxlinks(odxlinks)
74
+
75
+ def _resolve_snrefs(self, context: SnRefContext) -> None:
76
+ super()._resolve_snrefs(context)
132
77
 
133
78
  def __deepcopy__(self, memo: Dict[int, Any]) -> Any:
134
- """Create a deep copy of the diagnostic layer
79
+ """Create a deep copy of the hierarchy element
135
80
 
136
81
  Note that the copied diagnostic layer is not fully
137
82
  initialized, so `_finalize_init()` should to be called on it
138
83
  before it can be used normally.
139
84
  """
140
- cls = self.__class__
141
- result = cls.__new__(cls)
142
- memo[id(self)] = result
143
85
 
144
- result.diag_layer_raw = deepcopy(self.diag_layer_raw, memo)
86
+ new_he = super().__deepcopy__(memo)
145
87
 
146
- return result
88
+ # note that the self.hierarchy_element_raw object is *not*
89
+ # copied at this place because the attribute points to the
90
+ # same object as self.diag_layer_raw.
91
+ new_he.hierarchy_element_raw = deepcopy(self.hierarchy_element_raw)
92
+
93
+ return new_he
147
94
 
148
95
  def _finalize_init(self, database: "Database", odxlinks: OdxLinkDatabase) -> None:
149
96
  """This method deals with everything inheritance related and
@@ -165,27 +112,7 @@ class DiagLayer:
165
112
  # fill in all applicable objects that use value inheritance
166
113
  #####
167
114
 
168
- # diagnostic communication objects with the ODXLINKs resolved
169
- diag_comms = self._compute_available_diag_comms(odxlinks)
170
- self._diag_comms = NamedItemList(diag_comms)
171
-
172
- # filter the diag comms for services and single-ECU jobs
173
- diag_services = [dc for dc in diag_comms if isinstance(dc, DiagService)]
174
- single_ecu_jobs = [dc for dc in diag_comms if isinstance(dc, SingleEcuJob)]
175
- self._diag_services = NamedItemList(diag_services)
176
- self._single_ecu_jobs = NamedItemList(single_ecu_jobs)
177
-
178
- global_negative_responses = self._compute_available_global_neg_responses(odxlinks)
179
- self._global_negative_responses = NamedItemList(global_negative_responses)
180
-
181
- functional_classes = self._compute_available_functional_classes()
182
- self._functional_classes = NamedItemList(functional_classes)
183
-
184
- additional_audiences = self._compute_available_additional_audiences()
185
- self._additional_audiences = NamedItemList(additional_audiences)
186
-
187
- state_charts = self._compute_available_state_charts()
188
- self._state_charts = NamedItemList(state_charts)
115
+ self._compute_value_inheritance(odxlinks)
189
116
 
190
117
  ############
191
118
  # create a new unit_spec object. This is necessary because
@@ -234,6 +161,10 @@ class DiagLayer:
234
161
  lambda ddd_spec: ddd_spec.dtc_dops,
235
162
  lambda parent_ref: parent_ref.not_inherited_dops,
236
163
  )
164
+ static_fields = self._compute_available_ddd_spec_items(
165
+ lambda ddd_spec: ddd_spec.static_fields,
166
+ lambda parent_ref: parent_ref.not_inherited_dops,
167
+ )
237
168
  end_of_pdu_fields = self._compute_available_ddd_spec_items(
238
169
  lambda ddd_spec: ddd_spec.end_of_pdu_fields,
239
170
  lambda parent_ref: parent_ref.not_inherited_dops,
@@ -256,21 +187,22 @@ class DiagLayer:
256
187
  lambda ddd_spec: ddd_spec.muxs, lambda parent_ref: parent_ref.not_inherited_dops)
257
188
  tables = self._compute_available_ddd_spec_items(
258
189
  lambda ddd_spec: ddd_spec.tables, lambda parent_ref: parent_ref.not_inherited_tables)
259
- ddds_sdgs: List[SpecialDataGroup]
190
+
191
+ ddds_admin_data: Optional[AdminData] = None
192
+ ddds_sdgs: List[SpecialDataGroup] = []
260
193
  if self.diag_layer_raw.diag_data_dictionary_spec:
194
+ ddds_admin_data = self.diag_layer_raw.diag_data_dictionary_spec.admin_data
261
195
  ddds_sdgs = self.diag_layer_raw.diag_data_dictionary_spec.sdgs
262
- else:
263
- ddds_sdgs = []
264
196
 
265
197
  # create a DiagDataDictionarySpec which includes all the
266
198
  # inherited objects. To me, this seems rather inelegant, but
267
199
  # hey, it's described like this in the standard.
268
200
  self._diag_data_dictionary_spec = DiagDataDictionarySpec(
269
- admin_data=None,
201
+ admin_data=ddds_admin_data,
270
202
  data_object_props=dops,
271
203
  dtc_dops=dtc_dops,
272
204
  structures=structures,
273
- static_fields=NamedItemList(),
205
+ static_fields=static_fields,
274
206
  end_of_pdu_fields=end_of_pdu_fields,
275
207
  dynamic_endmarker_fields=dynamic_endmarker_fields,
276
208
  dynamic_length_fields=dynamic_length_fields,
@@ -299,175 +231,38 @@ class DiagLayer:
299
231
  #####
300
232
  context = SnRefContext(database=database)
301
233
  context.diag_layer = self
302
- self.diag_layer_raw._resolve_snrefs(context)
234
+ self._resolve_snrefs(context)
303
235
  context.diag_layer = None
304
236
 
305
237
  #####
306
- # <convenience functionality>
307
- #####
308
- @cached_property
309
- def service_groups(self) -> ServiceBinner:
310
- return ServiceBinner(self.services)
311
-
312
- #####
313
- # </convenience functionality>
314
- #####
315
-
316
- #####
317
- # <properties forwarded to the "raw" diag layer>
318
- #####
319
- @property
320
- def variant_type(self) -> DiagLayerType:
321
- return self.diag_layer_raw.variant_type
322
-
323
- @property
324
- def odx_id(self) -> OdxLinkId:
325
- return self.diag_layer_raw.odx_id
326
-
327
- @property
328
- def short_name(self) -> str:
329
- return self.diag_layer_raw.short_name
330
-
331
- @property
332
- def long_name(self) -> Optional[str]:
333
- return self.diag_layer_raw.long_name
334
-
335
- @property
336
- def description(self) -> Optional[Description]:
337
- return self.diag_layer_raw.description
338
-
339
- @property
340
- def admin_data(self) -> Optional[AdminData]:
341
- return self.diag_layer_raw.admin_data
342
-
343
- @property
344
- def company_datas(self) -> NamedItemList[CompanyData]:
345
- return self.diag_layer_raw.company_datas
346
-
347
- @property
348
- def requests(self) -> NamedItemList[Request]:
349
- return self.diag_layer_raw.requests
350
-
351
- @property
352
- def positive_responses(self) -> NamedItemList[Response]:
353
- return self.diag_layer_raw.positive_responses
354
-
355
- @property
356
- def negative_responses(self) -> NamedItemList[Response]:
357
- return self.diag_layer_raw.negative_responses
358
-
359
- @property
360
- def import_refs(self) -> List[OdxLinkRef]:
361
- return self.diag_layer_raw.import_refs
362
-
363
- @property
364
- def sdgs(self) -> List[SpecialDataGroup]:
365
- return self.diag_layer_raw.sdgs
366
-
367
- @property
368
- def parent_refs(self) -> List[ParentRef]:
369
- return self.diag_layer_raw.parent_refs
370
-
371
- @property
372
- def ecu_variant_patterns(self) -> List[EcuVariantPattern]:
373
- return self.diag_layer_raw.ecu_variant_patterns
374
-
375
- @property
376
- def comparam_spec_ref(self) -> Optional[OdxLinkRef]:
377
- return self.diag_layer_raw.comparam_spec_ref
378
-
379
- @property
380
- def prot_stack_snref(self) -> Optional[str]:
381
- return self.diag_layer_raw.prot_stack_snref
382
-
383
- @property
384
- def comparam_spec(self) -> Optional[Union[ComparamSpec, ComparamSubset]]:
385
- return self.diag_layer_raw.comparam_spec
386
-
387
- @property
388
- def prot_stack(self) -> Optional[ProtStack]:
389
- return self.diag_layer_raw.prot_stack
390
-
391
- #####
392
- # </properties forwarded to the "raw" diag layer>
238
+ # <value inheritance mechanism helpers>
393
239
  #####
240
+ def _compute_value_inheritance(self, odxlinks: OdxLinkDatabase) -> None:
241
+ # diagnostic communication objects with the ODXLINKs resolved
242
+ diag_comms = self._compute_available_diag_comms(odxlinks)
243
+ self._diag_comms = NamedItemList[DiagComm](diag_comms)
394
244
 
395
- #######
396
- # <stuff subject to value inheritance>
397
- #######
398
- @property
399
- def diag_comms(self) -> NamedItemList[DiagComm]:
400
- """All diagnostic communication primitives applicable to this DiagLayer
401
-
402
- Diagnostic communication primitives are diagnostic services as
403
- well as single-ECU jobs. This list has all references
404
- resolved.
405
- """
406
- return self._diag_comms
407
-
408
- @property
409
- def services(self) -> NamedItemList[DiagService]:
410
- """This property is an alias for `.diag_services`"""
411
- return self._diag_services
412
-
413
- @property
414
- def diag_services(self) -> NamedItemList[DiagService]:
415
- """All diagnostic services applicable to this DiagLayer
416
-
417
- This is a subset of all diagnostic communication
418
- primitives. All references are resolved in the list returned.
419
- """
420
- return self._diag_services
421
-
422
- @property
423
- def single_ecu_jobs(self) -> NamedItemList[SingleEcuJob]:
424
- """All single-ECU jobs applicable to this DiagLayer
425
-
426
- This is a subset of all diagnostic communication
427
- primitives. All references are resolved in the list returned.
428
- """
429
- return self._single_ecu_jobs
430
-
431
- @property
432
- def global_negative_responses(self) -> NamedItemList[Response]:
433
- """All global negative responses applicable to this DiagLayer"""
434
- return self._global_negative_responses
435
-
436
- @property
437
- @deprecated(details="use diag_data_dictionary_spec.tables") # type: ignore[misc]
438
- def tables(self) -> NamedItemList[Table]:
439
- return self.diag_data_dictionary_spec.tables
440
-
441
- @property
442
- def functional_classes(self) -> NamedItemList[FunctionalClass]:
443
- """All functional classes applicable to this DiagLayer"""
444
- return self._functional_classes
245
+ # filter the diag comms for services and single-ECU jobs
246
+ diag_services = [dc for dc in diag_comms if isinstance(dc, DiagService)]
247
+ single_ecu_jobs = [dc for dc in diag_comms if isinstance(dc, SingleEcuJob)]
248
+ self._diag_services = NamedItemList(diag_services)
249
+ self._single_ecu_jobs = NamedItemList(single_ecu_jobs)
445
250
 
446
- @property
447
- def state_charts(self) -> NamedItemList[StateChart]:
448
- """All state charts applicable to this DiagLayer"""
449
- return self._state_charts
251
+ global_negative_responses = self._compute_available_global_neg_responses(odxlinks)
252
+ self._global_negative_responses = NamedItemList(global_negative_responses)
450
253
 
451
- @property
452
- def additional_audiences(self) -> NamedItemList[AdditionalAudience]:
453
- """All audiences applicable to this DiagLayer"""
454
- return self._additional_audiences
254
+ functional_classes = self._compute_available_functional_classes()
255
+ self._functional_classes = NamedItemList(functional_classes)
455
256
 
456
- @property
457
- def diag_data_dictionary_spec(self) -> DiagDataDictionarySpec:
458
- """The DiagDataDictionarySpec applicable to this DiagLayer"""
459
- return self._diag_data_dictionary_spec
257
+ additional_audiences = self._compute_available_additional_audiences()
258
+ self._additional_audiences = NamedItemList(additional_audiences)
460
259
 
461
- #######
462
- # </stuff subject to value inheritance>
463
- #######
260
+ state_charts = self._compute_available_state_charts()
261
+ self._state_charts = NamedItemList(state_charts)
464
262
 
465
- #####
466
- # <value inheritance mechanism helpers>
467
- #####
468
263
  def _get_parent_refs_sorted_by_priority(self, reverse: bool = False) -> Iterable[ParentRef]:
469
264
  return sorted(
470
- self.diag_layer_raw.parent_refs,
265
+ getattr(self.diag_layer_raw, "parent_refs", []),
471
266
  key=lambda pr: pr.layer.variant_type.inheritance_priority,
472
267
  reverse=reverse)
473
268
 
@@ -561,41 +356,6 @@ class DiagLayer:
561
356
 
562
357
  return [x[0] for x in result_dict.values()]
563
358
 
564
- def _get_local_diag_comms(self, odxlinks: OdxLinkDatabase) -> Iterable[DiagComm]:
565
- """Return the list of locally defined diagnostic communications.
566
-
567
- This is not completely trivial as it requires to resolving the
568
- references specified in the <DIAG-COMMS> XML tag.
569
- """
570
- result_dict: Dict[str, DiagComm] = {}
571
-
572
- # TODO (?): add objects from the import-refs
573
-
574
- for dc_proxy in self.diag_layer_raw.diag_comms:
575
- if isinstance(dc_proxy, OdxLinkRef):
576
- dc = odxlinks.resolve(dc_proxy)
577
- else:
578
- dc = dc_proxy
579
-
580
- odxassert(isinstance(dc, DiagComm))
581
- odxassert(
582
- dc.short_name not in result_dict,
583
- f"Multiple definitions of DIAG-COMM '{dc.short_name}' in "
584
- f"layer '{self.short_name}'")
585
- result_dict[dc.short_name] = dc
586
-
587
- return result_dict.values()
588
-
589
- def _get_local_unit_groups(self) -> Iterable[UnitGroup]:
590
- if self.diag_layer_raw.diag_data_dictionary_spec is None:
591
- return []
592
-
593
- unit_spec = self.diag_layer_raw.diag_data_dictionary_spec.unit_spec
594
- if unit_spec is None:
595
- return []
596
-
597
- return unit_spec.unit_groups
598
-
599
359
  def _compute_available_diag_comms(self, odxlinks: OdxLinkDatabase) -> Iterable[DiagComm]:
600
360
 
601
361
  def get_local_objects_fn(dl: DiagLayer) -> Iterable[DiagComm]:
@@ -675,6 +435,70 @@ class DiagLayer:
675
435
  # </value inheritance mechanism helpers>
676
436
  #####
677
437
 
438
+ #######
439
+ # <properties subject to value inheritance>
440
+ #######
441
+ @property
442
+ def diag_data_dictionary_spec(self) -> DiagDataDictionarySpec:
443
+ return self._diag_data_dictionary_spec
444
+
445
+ @property
446
+ def diag_comms(self) -> NamedItemList[DiagComm]:
447
+ """All diagnostic communication primitives applicable to this DiagLayer
448
+
449
+ Diagnostic communication primitives are diagnostic services as
450
+ well as single-ECU jobs. This list has all references
451
+ resolved.
452
+ """
453
+ return self._diag_comms
454
+
455
+ @property
456
+ def services(self) -> NamedItemList[DiagService]:
457
+ """This property is an alias for `.diag_services`"""
458
+ return self._diag_services
459
+
460
+ @property
461
+ def diag_services(self) -> NamedItemList[DiagService]:
462
+ """All diagnostic services applicable to this DiagLayer
463
+
464
+ This is a subset of all diagnostic communication
465
+ primitives. All references are resolved in the list returned.
466
+ """
467
+ return self._diag_services
468
+
469
+ @property
470
+ def single_ecu_jobs(self) -> NamedItemList[SingleEcuJob]:
471
+ """All single-ECU jobs applicable to this DiagLayer
472
+
473
+ This is a subset of all diagnostic communication
474
+ primitives. All references are resolved in the list returned.
475
+ """
476
+ return self._single_ecu_jobs
477
+
478
+ @property
479
+ def global_negative_responses(self) -> NamedItemList[Response]:
480
+ """All global negative responses applicable to this DiagLayer"""
481
+ return self._global_negative_responses
482
+
483
+ @property
484
+ def functional_classes(self) -> NamedItemList[FunctionalClass]:
485
+ """All functional classes applicable to this DiagLayer"""
486
+ return self._functional_classes
487
+
488
+ @property
489
+ def state_charts(self) -> NamedItemList[StateChart]:
490
+ """All state charts applicable to this DiagLayer"""
491
+ return self._state_charts
492
+
493
+ @property
494
+ def additional_audiences(self) -> NamedItemList[AdditionalAudience]:
495
+ """All audiences applicable to this DiagLayer"""
496
+ return self._additional_audiences
497
+
498
+ #######
499
+ # </properties subject to value inheritance>
500
+ #######
501
+
678
502
  #####
679
503
  # <communication parameter handling>
680
504
  #####
@@ -708,11 +532,14 @@ class DiagLayer:
708
532
  # parameters. First fetch the communication parameters from
709
533
  # low priority parents, then update with increasing priority.
710
534
  for parent_ref in self._get_parent_refs_sorted_by_priority():
711
- for cp in parent_ref.layer._compute_available_commmunication_parameters():
535
+ parent_layer = parent_ref.layer
536
+ if not isinstance(parent_layer, HierarchyElement):
537
+ continue
538
+ for cp in parent_layer._compute_available_commmunication_parameters():
712
539
  com_params_dict[(cp.spec_ref.ref_id, cp.protocol_snref)] = cp
713
540
 
714
541
  # finally, handle the locally defined communication parameters
715
- for cp in self.diag_layer_raw.comparam_refs:
542
+ for cp in getattr(self.hierarchy_element_raw, "comparam_refs", []):
716
543
  com_params_dict[(cp.spec_ref.ref_id, cp.protocol_snref)] = cp
717
544
 
718
545
  return list(com_params_dict.values())
@@ -728,19 +555,22 @@ class DiagLayer:
728
555
  return self._comparam_refs
729
556
 
730
557
  @cached_property
731
- def protocols(self) -> NamedItemList["DiagLayer"]:
558
+ def protocols(self) -> NamedItemList["Protocol"]:
732
559
  """Return the set of all protocols which are applicable to the diagnostic layer
733
560
 
734
- Note that protocols are *not* explicitly inherited objects,
735
- but the parent diagnostic layers of variant type "PROTOCOL".
561
+ Note that protocols are *not* explicitly defined by the XML,
562
+ but they are the parent layers of variant type "PROTOCOL".
563
+
736
564
  """
737
- result_dict: Dict[str, DiagLayer] = {}
565
+ from .protocol import Protocol
738
566
 
739
- for parent_ref in self._get_parent_refs_sorted_by_priority():
740
- for prot in parent_ref.layer.protocols:
567
+ result_dict: Dict[str, Protocol] = {}
568
+
569
+ for parent_ref in getattr(self, "parent_refs", []):
570
+ for prot in getattr(parent_ref.layer, "protocols", []):
741
571
  result_dict[prot.short_name] = prot
742
572
 
743
- if self.diag_layer_raw.variant_type == DiagLayerType.PROTOCOL:
573
+ if isinstance(self, Protocol):
744
574
  result_dict[self.diag_layer_raw.short_name] = self
745
575
 
746
576
  return NamedItemList(result_dict.values())
@@ -749,14 +579,16 @@ class DiagLayer:
749
579
  self,
750
580
  cp_short_name: str,
751
581
  *,
752
- protocol: Optional[Union[str, "DiagLayer"]] = None,
582
+ protocol: Optional[Union[str, "Protocol"]] = None,
753
583
  ) -> Optional[ComparamInstance]:
754
584
  """Find a specific communication parameter according to some criteria.
755
585
 
756
586
  Setting a given parameter to `None` means "don't care"."""
757
587
 
588
+ from .protocol import Protocol
589
+
758
590
  protocol_name: Optional[str]
759
- if isinstance(protocol, DiagLayer):
591
+ if isinstance(protocol, Protocol):
760
592
  protocol_name = protocol.short_name
761
593
  else:
762
594
  protocol_name = protocol
@@ -780,7 +612,7 @@ class DiagLayer:
780
612
 
781
613
  def get_max_can_payload_size(self,
782
614
  protocol: Optional[Union[str,
783
- "DiagLayer"]] = None) -> Optional[int]:
615
+ "Protocol"]] = None) -> Optional[int]:
784
616
  """Return the maximum size of a CAN frame payload that can be
785
617
  transmitted in bytes.
786
618
 
@@ -810,13 +642,13 @@ class DiagLayer:
810
642
  # unexpected format of parameter value
811
643
  return 8
812
644
 
813
- def uses_can(self, protocol: Optional[Union[str, "DiagLayer"]] = None) -> bool:
645
+ def uses_can(self, protocol: Optional[Union[str, "Protocol"]] = None) -> bool:
814
646
  """
815
647
  Check if CAN ought to be used as the link layer protocol.
816
648
  """
817
649
  return self.get_can_receive_id(protocol=protocol) is not None
818
650
 
819
- def uses_can_fd(self, protocol: Optional[Union[str, "DiagLayer"]] = None) -> bool:
651
+ def uses_can_fd(self, protocol: Optional[Union[str, "Protocol"]] = None) -> bool:
820
652
  """Check if CAN-FD ought to be used.
821
653
 
822
654
  If the ECU is not using CAN-FD for the specified protocol, `False`
@@ -835,7 +667,7 @@ class DiagLayer:
835
667
 
836
668
  return "CANFD" in com_param.value
837
669
 
838
- def get_can_baudrate(self, protocol: Optional[Union[str, "DiagLayer"]] = None) -> Optional[int]:
670
+ def get_can_baudrate(self, protocol: Optional[Union[str, "Protocol"]] = None) -> Optional[int]:
839
671
  """Baudrate of the CAN bus which is used by the ECU [bits/s]
840
672
 
841
673
  If the ECU is not using CAN for the specified protocol, None
@@ -853,7 +685,7 @@ class DiagLayer:
853
685
  return int(val)
854
686
 
855
687
  def get_can_fd_baudrate(self,
856
- protocol: Optional[Union[str, "DiagLayer"]] = None) -> Optional[int]:
688
+ protocol: Optional[Union[str, "Protocol"]] = None) -> Optional[int]:
857
689
  """Data baudrate of the CAN bus which is used by the ECU [bits/s]
858
690
 
859
691
  If the ECU is not using CAN-FD for the specified protocol,
@@ -873,7 +705,7 @@ class DiagLayer:
873
705
  return int(val)
874
706
 
875
707
  def get_can_receive_id(self,
876
- protocol: Optional[Union[str, "DiagLayer"]] = None) -> Optional[int]:
708
+ protocol: Optional[Union[str, "Protocol"]] = None) -> Optional[int]:
877
709
  """CAN ID to which the ECU listens for diagnostic messages"""
878
710
  com_param = self.get_comparam("CP_UniqueRespIdTable", protocol=protocol)
879
711
  if com_param is None:
@@ -896,7 +728,7 @@ class DiagLayer:
896
728
  def get_receive_id(self) -> Optional[int]:
897
729
  return self.get_can_receive_id()
898
730
 
899
- def get_can_send_id(self, protocol: Optional[Union[str, "DiagLayer"]] = None) -> Optional[int]:
731
+ def get_can_send_id(self, protocol: Optional[Union[str, "Protocol"]] = None) -> Optional[int]:
900
732
  """CAN ID to which the ECU sends replies to diagnostic messages"""
901
733
 
902
734
  # this hopefully resolves to the 'CP_UniqueRespIdTable'
@@ -926,7 +758,7 @@ class DiagLayer:
926
758
  return self.get_can_send_id()
927
759
 
928
760
  def get_can_func_req_id(self,
929
- protocol: Optional[Union[str, "DiagLayer"]] = None) -> Optional[int]:
761
+ protocol: Optional[Union[str, "Protocol"]] = None) -> Optional[int]:
930
762
  """CAN Functional Request Id."""
931
763
  com_param = self.get_comparam("CP_CanFuncReqId", protocol=protocol)
932
764
  if com_param is None:
@@ -940,8 +772,8 @@ class DiagLayer:
940
772
  return int(result)
941
773
 
942
774
  def get_doip_logical_ecu_address(self,
943
- protocol: Optional[Union[str, "DiagLayer"]] = None
944
- ) -> Optional[int]:
775
+ protocol: Optional[Union[str,
776
+ "Protocol"]] = None) -> Optional[int]:
945
777
  """Return the address of the ECU when using functional addressing.
946
778
 
947
779
  The parameter protocol is used to distinguish between
@@ -972,7 +804,7 @@ class DiagLayer:
972
804
  return int(ecu_addr)
973
805
 
974
806
  def get_doip_logical_gateway_address(self,
975
- protocol: Optional[Union[str, "DiagLayer"]] = None
807
+ protocol: Optional[Union[str, "Protocol"]] = None
976
808
  ) -> Optional[int]:
977
809
  """The logical gateway address for the diagnosis over IP transport protocol"""
978
810
 
@@ -990,7 +822,7 @@ class DiagLayer:
990
822
  return int(result)
991
823
 
992
824
  def get_doip_logical_tester_address(self,
993
- protocol: Optional[Union[str, "DiagLayer"]] = None
825
+ protocol: Optional[Union[str, "Protocol"]] = None
994
826
  ) -> Optional[int]:
995
827
  """DoIp logical gateway address"""
996
828
 
@@ -1008,7 +840,7 @@ class DiagLayer:
1008
840
  return int(result)
1009
841
 
1010
842
  def get_doip_logical_functional_address(self,
1011
- protocol: Optional[Union[str, "DiagLayer"]] = None
843
+ protocol: Optional[Union[str, "Protocol"]] = None
1012
844
  ) -> Optional[int]:
1013
845
  """The logical functional DoIP address of the ECU."""
1014
846
 
@@ -1029,7 +861,7 @@ class DiagLayer:
1029
861
  return int(result)
1030
862
 
1031
863
  def get_doip_routing_activation_timeout(self,
1032
- protocol: Optional[Union[str, "DiagLayer"]] = None
864
+ protocol: Optional[Union[str, "Protocol"]] = None
1033
865
  ) -> Optional[float]:
1034
866
  """The timout for the DoIP routing activation request in seconds"""
1035
867
 
@@ -1047,7 +879,7 @@ class DiagLayer:
1047
879
  return float(result) / 1e6
1048
880
 
1049
881
  def get_doip_routing_activation_type(self,
1050
- protocol: Optional[Union[str, "DiagLayer"]] = None
882
+ protocol: Optional[Union[str, "Protocol"]] = None
1051
883
  ) -> Optional[int]:
1052
884
  """The DoIP routing activation type
1053
885
 
@@ -1074,7 +906,7 @@ class DiagLayer:
1074
906
 
1075
907
  def get_tester_present_time(self,
1076
908
  protocol: Optional[Union[str,
1077
- "DiagLayer"]] = None) -> Optional[float]:
909
+ "Protocol"]] = None) -> Optional[float]:
1078
910
  """Timeout on inactivity in seconds.
1079
911
 
1080
912
  This is defined by the communication parameter "CP_TesterPresentTime".
@@ -1101,150 +933,3 @@ class DiagLayer:
1101
933
  #####
1102
934
  # </communication parameter handling>
1103
935
  #####
1104
-
1105
- #####
1106
- # <PDU decoding>
1107
- #####
1108
-
1109
- @cached_property
1110
- def _prefix_tree(self) -> PrefixTree:
1111
- """Constructs the coded prefix tree of the services.
1112
-
1113
- Each leaf node is a list of `DiagService`s. (This is because
1114
- navigating from a service to the request/ responses is easier
1115
- than finding the service for a given request/response object.)
1116
-
1117
- Example:
1118
- Let there be four services with corresponding requests:
1119
- * Request 1 has the coded constant prefix `12 34`.
1120
- * Request 2 has the coded constant prefix `12 34`.
1121
- * Request 3 has the coded constant prefix `12 56`.
1122
- * Request 4 has the coded constant prefix `12 56 00`.
1123
-
1124
- Then, the constructed prefix tree is the dict
1125
- ```
1126
- {0x12: {0x34: {-1: [<Service 1>, <Service 2>]},
1127
- 0x56: {-1: [<Service 3>],
1128
- 0x0: {-1: [<Service 4>]}
1129
- }}}
1130
- ```
1131
- Note, that the inner `-1` are constant to distinguish them
1132
- from possible service IDs.
1133
-
1134
- Also note, that it is actually allowed that
1135
- (a) SIDs for different services are the same like for service 1 and 2 (thus each leaf node is a list) and
1136
- (b) one SID is the prefix of another SID like for service 3 and 4 (thus the constant `-1` key).
1137
-
1138
- """
1139
- prefix_tree: PrefixTree = {}
1140
- for s in self.services:
1141
- # Compute prefixes for the service's request and all
1142
- # possible responses. We need to consider the global
1143
- # negative responses here, because they might contain
1144
- # MATCHING-REQUEST parameters. If these global responses
1145
- # do not contain such parameters, this will potentially
1146
- # result in an enormous amount of decoded messages for
1147
- # global negative responses. (I.e., one for each
1148
- # service. This can be avoided by specifying the
1149
- # corresponding request for `decode_response()`.)
1150
- request_prefix = b''
1151
- if s.request is not None:
1152
- request_prefix = s.request.coded_const_prefix()
1153
- prefixes = [request_prefix]
1154
- prefixes += [
1155
- x.coded_const_prefix(request_prefix=request_prefix) for x in chain(
1156
- s.positive_responses, s.negative_responses, self.global_negative_responses)
1157
- ]
1158
- for coded_prefix in prefixes:
1159
- self._extend_prefix_tree(prefix_tree, coded_prefix, s)
1160
-
1161
- return prefix_tree
1162
-
1163
- @staticmethod
1164
- def _extend_prefix_tree(prefix_tree: PrefixTree, coded_prefix: bytes,
1165
- service: DiagService) -> None:
1166
-
1167
- # make sure that tree has an entry for the given prefix
1168
- sub_tree = prefix_tree
1169
- for b in coded_prefix:
1170
- if b not in sub_tree:
1171
- sub_tree[b] = {}
1172
- sub_tree = cast(PrefixTree, sub_tree[b])
1173
-
1174
- # Store the object as in the prefix tree. This is done by
1175
- # assigning the list of possible objects to the key -1 of the
1176
- # dictionary (this is quite hacky...)
1177
- if sub_tree.get(-1) is None:
1178
- sub_tree[-1] = [service]
1179
- else:
1180
- cast(List[DiagService], sub_tree[-1]).append(service)
1181
-
1182
- def _find_services_for_uds(self, message: bytes) -> List[DiagService]:
1183
- prefix_tree = self._prefix_tree
1184
-
1185
- # Find matching service(s) in prefix tree
1186
- possible_services: List[DiagService] = []
1187
- for b in message:
1188
- if b in prefix_tree:
1189
- odxassert(isinstance(prefix_tree[b], dict))
1190
- prefix_tree = cast(PrefixTree, prefix_tree[b])
1191
- else:
1192
- break
1193
- if -1 in prefix_tree:
1194
- possible_services += cast(List[DiagService], prefix_tree[-1])
1195
- return possible_services
1196
-
1197
- def _decode(self, message: bytes, candidate_services: Iterable[DiagService]) -> List[Message]:
1198
- decoded_messages: List[Message] = []
1199
-
1200
- for service in candidate_services:
1201
- try:
1202
- decoded_messages.append(service.decode_message(message))
1203
- except DecodeError as e:
1204
- # check if the message can be decoded as a global
1205
- # negative response for the service
1206
- gnr_found = False
1207
- for gnr in self.global_negative_responses:
1208
- try:
1209
- decoded_gnr = gnr.decode(message)
1210
- gnr_found = True
1211
- if not isinstance(decoded_gnr, dict):
1212
- odxraise(
1213
- f"Expected the decoded value of a global "
1214
- f"negative response to be a dictionary, "
1215
- f"got {type(decoded_gnr)} for {self.short_name}", DecodeError)
1216
-
1217
- decoded_messages.append(
1218
- Message(
1219
- coded_message=message,
1220
- service=service,
1221
- coding_object=gnr,
1222
- param_dict=decoded_gnr))
1223
- except DecodeError:
1224
- pass
1225
-
1226
- if not gnr_found:
1227
- raise e
1228
-
1229
- if len(decoded_messages) == 0:
1230
- raise DecodeError(
1231
- f"None of the services {[x.short_name for x in candidate_services]} could parse {message.hex()}."
1232
- )
1233
-
1234
- return decoded_messages
1235
-
1236
- def decode(self, message: bytes) -> List[Message]:
1237
- candidate_services = self._find_services_for_uds(message)
1238
-
1239
- return self._decode(message, candidate_services)
1240
-
1241
- def decode_response(self, response: bytes, request: bytes) -> List[Message]:
1242
- candidate_services = self._find_services_for_uds(request)
1243
- if candidate_services is None:
1244
- raise DecodeError(f"Couldn't find corresponding service for request {request.hex()}.")
1245
-
1246
- return self._decode(response, candidate_services)
1247
-
1248
- #####
1249
- # </PDU decoding>
1250
- #####