odxtools 6.6.1__py3-none-any.whl → 6.7.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 (81) hide show
  1. odxtools/__init__.py +5 -5
  2. odxtools/basicstructure.py +7 -8
  3. odxtools/cli/_parser_utils.py +15 -0
  4. odxtools/cli/_print_utils.py +4 -3
  5. odxtools/cli/browse.py +19 -14
  6. odxtools/cli/compare.py +24 -16
  7. odxtools/cli/decode.py +2 -1
  8. odxtools/cli/dummy_sub_parser.py +3 -1
  9. odxtools/cli/find.py +2 -1
  10. odxtools/cli/list.py +2 -1
  11. odxtools/cli/main.py +1 -0
  12. odxtools/cli/snoop.py +4 -1
  13. odxtools/comparaminstance.py +7 -5
  14. odxtools/compumethods/compumethod.py +2 -4
  15. odxtools/compumethods/compuscale.py +45 -5
  16. odxtools/compumethods/createanycompumethod.py +27 -35
  17. odxtools/compumethods/limit.py +70 -36
  18. odxtools/compumethods/linearcompumethod.py +68 -59
  19. odxtools/compumethods/tabintpcompumethod.py +19 -8
  20. odxtools/compumethods/texttablecompumethod.py +32 -36
  21. odxtools/dataobjectproperty.py +13 -10
  22. odxtools/decodestate.py +6 -3
  23. odxtools/determinenumberofitems.py +1 -1
  24. odxtools/diagcodedtype.py +5 -4
  25. odxtools/diagdatadictionaryspec.py +108 -83
  26. odxtools/diaglayer.py +75 -35
  27. odxtools/diaglayertype.py +17 -5
  28. odxtools/diagservice.py +1 -1
  29. odxtools/dopbase.py +4 -2
  30. odxtools/dtcdop.py +7 -5
  31. odxtools/dynamiclengthfield.py +6 -5
  32. odxtools/endofpdufield.py +4 -4
  33. odxtools/environmentdatadescription.py +4 -2
  34. odxtools/inputparam.py +1 -1
  35. odxtools/internalconstr.py +14 -5
  36. odxtools/isotp_state_machine.py +14 -6
  37. odxtools/message.py +1 -1
  38. odxtools/multiplexer.py +18 -13
  39. odxtools/multiplexercase.py +27 -5
  40. odxtools/multiplexerswitchkey.py +1 -1
  41. odxtools/nameditemlist.py +7 -6
  42. odxtools/odxlink.py +2 -2
  43. odxtools/odxtypes.py +56 -3
  44. odxtools/outputparam.py +2 -2
  45. odxtools/parameterinfo.py +12 -5
  46. odxtools/parameters/codedconstparameter.py +33 -12
  47. odxtools/parameters/createanyparameter.py +19 -193
  48. odxtools/parameters/dynamicparameter.py +21 -1
  49. odxtools/parameters/lengthkeyparameter.py +28 -4
  50. odxtools/parameters/matchingrequestparameter.py +27 -9
  51. odxtools/parameters/nrcconstparameter.py +34 -11
  52. odxtools/parameters/parameter.py +58 -32
  53. odxtools/parameters/parameterwithdop.py +28 -15
  54. odxtools/parameters/physicalconstantparameter.py +28 -4
  55. odxtools/parameters/reservedparameter.py +32 -18
  56. odxtools/parameters/systemparameter.py +25 -2
  57. odxtools/parameters/tableentryparameter.py +45 -6
  58. odxtools/parameters/tablekeyparameter.py +43 -10
  59. odxtools/parameters/tablestructparameter.py +36 -14
  60. odxtools/parameters/valueparameter.py +24 -2
  61. odxtools/paramlengthinfotype.py +4 -1
  62. odxtools/parentref.py +4 -1
  63. odxtools/scaleconstr.py +11 -5
  64. odxtools/statetransition.py +1 -1
  65. odxtools/staticfield.py +101 -0
  66. odxtools/table.py +2 -1
  67. odxtools/tablerow.py +11 -4
  68. odxtools/templates/macros/printDOP.xml.jinja2 +30 -34
  69. odxtools/templates/macros/printMux.xml.jinja2 +3 -2
  70. odxtools/templates/macros/printParam.xml.jinja2 +9 -9
  71. odxtools/templates/macros/printStaticField.xml.jinja2 +15 -0
  72. odxtools/templates/macros/printVariant.xml.jinja2 +8 -0
  73. odxtools/uds.py +2 -2
  74. odxtools/version.py +2 -2
  75. odxtools/write_pdx_file.py +3 -3
  76. {odxtools-6.6.1.dist-info → odxtools-6.7.0.dist-info}/METADATA +28 -16
  77. {odxtools-6.6.1.dist-info → odxtools-6.7.0.dist-info}/RECORD +81 -79
  78. {odxtools-6.6.1.dist-info → odxtools-6.7.0.dist-info}/WHEEL +1 -1
  79. {odxtools-6.6.1.dist-info → odxtools-6.7.0.dist-info}/LICENSE +0 -0
  80. {odxtools-6.6.1.dist-info → odxtools-6.7.0.dist-info}/entry_points.txt +0 -0
  81. {odxtools-6.6.1.dist-info → odxtools-6.7.0.dist-info}/top_level.txt +0 -0
@@ -1,16 +1,14 @@
1
1
  # SPDX-License-Identifier: MIT
2
- import warnings
3
2
  from typing import Any, Dict, List, Optional
4
3
  from xml.etree import ElementTree
5
4
 
6
- from ..exceptions import OdxWarning, odxassert, odxraise, odxrequire
7
- from ..globals import logger
5
+ from ..exceptions import odxassert, odxraise, odxrequire
8
6
  from ..odxlink import OdxDocFragment
9
7
  from ..odxtypes import DataType
10
8
  from .compumethod import CompuMethod
11
9
  from .compuscale import CompuScale
12
10
  from .identicalcompumethod import IdenticalCompuMethod
13
- from .limit import IntervalType, Limit
11
+ from .limit import Limit
14
12
  from .linearcompumethod import LinearCompuMethod
15
13
  from .scalelinearcompumethod import ScaleLinearCompuMethod
16
14
  from .tabintpcompumethod import TabIntpCompuMethod
@@ -18,8 +16,9 @@ from .texttablecompumethod import TexttableCompuMethod
18
16
 
19
17
 
20
18
  def _parse_compu_scale_to_linear_compu_method(
19
+ et_element: ElementTree.Element,
20
+ doc_frags: List[OdxDocFragment],
21
21
  *,
22
- scale_element: ElementTree.Element,
23
22
  internal_type: DataType,
24
23
  physical_type: DataType,
25
24
  is_scale_linear: bool = False,
@@ -38,7 +37,7 @@ def _parse_compu_scale_to_linear_compu_method(
38
37
  DataType.A_UINT32,
39
38
  ])
40
39
 
41
- if physical_type.as_python_type() == float:
40
+ if physical_type.python_type == float:
42
41
  computation_python_type = physical_type.from_string
43
42
  else:
44
43
  computation_python_type = internal_type.from_string
@@ -47,7 +46,7 @@ def _parse_compu_scale_to_linear_compu_method(
47
46
  kwargs["internal_type"] = internal_type
48
47
  kwargs["physical_type"] = physical_type
49
48
 
50
- coeffs = odxrequire(scale_element.find("COMPU-RATIONAL-COEFFS"))
49
+ coeffs = odxrequire(et_element.find("COMPU-RATIONAL-COEFFS"))
51
50
  nums = coeffs.iterfind("COMPU-NUMERATOR/V")
52
51
 
53
52
  offset = computation_python_type(odxrequire(next(nums).text))
@@ -57,32 +56,24 @@ def _parse_compu_scale_to_linear_compu_method(
57
56
  if (string := coeffs.findtext("COMPU-DENOMINATOR/V")) is not None:
58
57
  denominator = float(string)
59
58
  if denominator == 0:
60
- warnings.warn(
61
- "CompuMethod: A denominator of zero will lead to divisions by zero.",
62
- OdxWarning,
63
- stacklevel=1)
59
+ odxraise("CompuMethod: A denominator of zero will lead to divisions by zero.")
60
+
64
61
  # Read lower limit
65
- internal_lower_limit = Limit.from_et(
66
- scale_element.find("LOWER-LIMIT"),
67
- internal_type=internal_type,
62
+ internal_lower_limit = Limit.limit_from_et(
63
+ et_element.find("LOWER-LIMIT"),
64
+ doc_frags,
65
+ value_type=internal_type,
68
66
  )
69
- if internal_lower_limit is None:
70
- internal_lower_limit = Limit(0, IntervalType.INFINITE)
67
+
71
68
  kwargs["internal_lower_limit"] = internal_lower_limit
72
69
 
73
70
  # Read upper limit
74
- internal_upper_limit = Limit.from_et(
75
- scale_element.find("UPPER-LIMIT"),
76
- internal_type=internal_type,
71
+ internal_upper_limit = Limit.limit_from_et(
72
+ et_element.find("UPPER-LIMIT"),
73
+ doc_frags,
74
+ value_type=internal_type,
77
75
  )
78
- if internal_upper_limit is None:
79
- if not is_scale_linear:
80
- internal_upper_limit = Limit(0, IntervalType.INFINITE)
81
- else:
82
- odxassert(internal_lower_limit is not None and
83
- internal_lower_limit.interval_type == IntervalType.CLOSED)
84
- logger.info("Scale linear without UPPER-LIMIT")
85
- internal_upper_limit = internal_lower_limit
76
+
86
77
  kwargs["internal_upper_limit"] = internal_upper_limit
87
78
  kwargs["denominator"] = denominator
88
79
  kwargs["factor"] = factor
@@ -92,19 +83,19 @@ def _parse_compu_scale_to_linear_compu_method(
92
83
 
93
84
 
94
85
  def create_compu_default_value(et_element: Optional[ElementTree.Element],
95
- doc_frags: List[OdxDocFragment], internal_type: DataType,
86
+ doc_frags: List[OdxDocFragment], internal_type: DataType, *,
96
87
  physical_type: DataType) -> Optional[CompuScale]:
97
88
  if et_element is None:
98
89
  return None
99
90
  compu_const = physical_type.create_from_et(et_element)
100
- scale = CompuScale.from_et(
91
+ scale = CompuScale.compuscale_from_et(
101
92
  et_element, doc_frags, internal_type=internal_type, physical_type=physical_type)
102
93
  scale.compu_const = compu_const
103
94
  return scale
104
95
 
105
96
 
106
97
  def create_any_compu_method_from_et(et_element: ElementTree.Element,
107
- doc_frags: List[OdxDocFragment], internal_type: DataType,
98
+ doc_frags: List[OdxDocFragment], *, internal_type: DataType,
108
99
  physical_type: DataType) -> CompuMethod:
109
100
  compu_category = et_element.findtext("CATEGORY")
110
101
  odxassert(compu_category in [
@@ -142,7 +133,7 @@ def create_any_compu_method_from_et(et_element: ElementTree.Element,
142
133
  internal_to_phys: List[CompuScale] = []
143
134
  for scale_elem in compu_internal_to_phys.iterfind("COMPU-SCALES/COMPU-SCALE"):
144
135
  internal_to_phys.append(
145
- CompuScale.from_et(
136
+ CompuScale.compuscale_from_et(
146
137
  scale_elem, doc_frags, internal_type=internal_type,
147
138
  physical_type=physical_type))
148
139
  compu_default_value = create_compu_default_value(
@@ -158,13 +149,13 @@ def create_any_compu_method_from_et(et_element: ElementTree.Element,
158
149
  # Compu method can be described by the function f(x) = (offset + factor * x) / denominator
159
150
 
160
151
  scale_elem = odxrequire(et_element.find("COMPU-INTERNAL-TO-PHYS/COMPU-SCALES/COMPU-SCALE"))
161
- return _parse_compu_scale_to_linear_compu_method(scale_element=scale_elem, **kwargs)
152
+ return _parse_compu_scale_to_linear_compu_method(scale_elem, doc_frags, **kwargs)
162
153
 
163
154
  elif compu_category == "SCALE-LINEAR":
164
155
 
165
156
  scale_elems = et_element.iterfind("COMPU-INTERNAL-TO-PHYS/COMPU-SCALES/COMPU-SCALE")
166
157
  linear_methods = [
167
- _parse_compu_scale_to_linear_compu_method(scale_element=scale_elem, **kwargs)
158
+ _parse_compu_scale_to_linear_compu_method(scale_elem, doc_frags, **kwargs)
168
159
  for scale_elem in scale_elems
169
160
  ]
170
161
  return ScaleLinearCompuMethod(linear_methods=linear_methods, **kwargs)
@@ -189,6 +180,7 @@ def create_any_compu_method_from_et(et_element: ElementTree.Element,
189
180
  return TabIntpCompuMethod(
190
181
  internal_points=internal_points, physical_points=physical_points, **kwargs)
191
182
 
192
- # TODO: Implement other categories (never instantiate CompuMethod)
193
- logger.warning(f"Warning: Computation category {compu_category} is not implemented!")
183
+ # TODO: Implement all categories (never instantiate the CompuMethod base class!)
184
+ odxraise(f"Warning: Computation category {compu_category} is not implemented!")
185
+
194
186
  return IdenticalCompuMethod(internal_type=DataType.A_UINT32, physical_type=DataType.A_UINT32)
@@ -1,11 +1,12 @@
1
1
  # SPDX-License-Identifier: MIT
2
2
  from dataclasses import dataclass
3
3
  from enum import Enum
4
- from typing import Optional
4
+ from typing import List, Optional, overload
5
5
  from xml.etree import ElementTree
6
6
 
7
- from ..exceptions import odxassert, odxraise, odxrequire
8
- from ..odxtypes import AtomicOdxType, DataType
7
+ from ..exceptions import odxraise
8
+ from ..odxlink import OdxDocFragment
9
+ from ..odxtypes import AtomicOdxType, DataType, compare_odx_values
9
10
 
10
11
 
11
12
  class IntervalType(Enum):
@@ -16,42 +17,54 @@ class IntervalType(Enum):
16
17
 
17
18
  @dataclass
18
19
  class Limit:
19
- value: AtomicOdxType
20
- interval_type: IntervalType = IntervalType.CLOSED
20
+ value_raw: Optional[str]
21
+ value_type: Optional[DataType]
22
+ interval_type: Optional[IntervalType]
21
23
 
22
24
  def __post_init__(self) -> None:
23
- if self.interval_type == IntervalType.INFINITE:
24
- self.value = 0
25
+ self._value: Optional[AtomicOdxType] = None
26
+
27
+ if self.value_type is not None:
28
+ self.set_value_type(self.value_type)
29
+
30
+ @staticmethod
31
+ @overload
32
+ def limit_from_et(et_element: ElementTree.Element, doc_frags: List[OdxDocFragment],
33
+ value_type: Optional[DataType]) -> "Limit":
34
+ ...
35
+
36
+ @staticmethod
37
+ @overload
38
+ def limit_from_et(et_element: None, doc_frags: List[OdxDocFragment],
39
+ value_type: Optional[DataType]) -> None:
40
+ ...
25
41
 
26
42
  @staticmethod
27
- def from_et(et_element: Optional[ElementTree.Element], *,
28
- internal_type: DataType) -> Optional["Limit"]:
43
+ def limit_from_et(et_element: Optional[ElementTree.Element], doc_frags: List[OdxDocFragment],
44
+ value_type: Optional[DataType]) -> Optional["Limit"]:
29
45
 
30
46
  if et_element is None:
31
47
  return None
32
48
 
49
+ interval_type = None
33
50
  if (interval_type_str := et_element.get("INTERVAL-TYPE")) is not None:
34
51
  try:
35
52
  interval_type = IntervalType(interval_type_str)
36
53
  except ValueError:
37
- interval_type = IntervalType.CLOSED
38
54
  odxraise(f"Encountered unknown interval type '{interval_type_str}'")
39
- else:
40
- interval_type = IntervalType.CLOSED
41
-
42
- if interval_type == IntervalType.INFINITE:
43
- if et_element.tag == "LOWER-LIMIT":
44
- return Limit(float("-inf"), interval_type)
45
- else:
46
- odxassert(et_element.tag == "UPPER-LIMIT")
47
- return Limit(float("inf"), interval_type)
48
- elif internal_type == DataType.A_BYTEFIELD:
49
- hex_text = odxrequire(et_element.text)
50
- if len(hex_text) % 2 == 1:
51
- hex_text = "0" + hex_text
52
- return Limit(bytes.fromhex(hex_text), interval_type)
53
- else:
54
- return Limit(internal_type.from_string(odxrequire(et_element.text)), interval_type)
55
+
56
+ value_raw = et_element.text
57
+
58
+ return Limit(value_raw=value_raw, interval_type=interval_type, value_type=value_type)
59
+
60
+ def set_value_type(self, value_type: DataType) -> None:
61
+ self.value_type = value_type
62
+ if self.value_raw is not None:
63
+ self._value = value_type.from_string(self.value_raw)
64
+
65
+ @property
66
+ def value(self) -> Optional[AtomicOdxType]:
67
+ return self._value
55
68
 
56
69
  def complies_to_upper(self, value: AtomicOdxType) -> bool:
57
70
  """Checks if the value is in the range w.r.t. the upper limit.
@@ -60,13 +73,23 @@ class Limit:
60
73
  * If the interval type is open, return `value < limit.value`.
61
74
  * If the interval type is infinite, return `True`.
62
75
  """
63
- if self.interval_type == IntervalType.CLOSED:
64
- return value <= self.value # type: ignore[operator]
65
- elif self.interval_type == IntervalType.OPEN:
66
- return value < self.value # type: ignore[operator]
67
- elif self.interval_type == IntervalType.INFINITE:
76
+ if self._value is None:
77
+ # if no value is specified, assume interval type INFINITE
78
+ # (what are we supposed to compare against?)
68
79
  return True
69
80
 
81
+ if self.interval_type is None or self.interval_type == IntervalType.CLOSED:
82
+ # assume interval type CLOSED if a value was specified,
83
+ # but no interval type
84
+ return compare_odx_values(value, self._value) <= 0
85
+ elif self.interval_type == IntervalType.OPEN:
86
+ return compare_odx_values(value, self._value) < 0
87
+
88
+ if self.interval_type != IntervalType.INFINITE:
89
+ odxraise("Unhandled interval type {self.interval_type}")
90
+
91
+ return True
92
+
70
93
  def complies_to_lower(self, value: AtomicOdxType) -> bool:
71
94
  """Checks if the value is in the range w.r.t. the lower limit.
72
95
 
@@ -74,9 +97,20 @@ class Limit:
74
97
  * If the interval type is open, return `limit.value < value`.
75
98
  * If the interval type is infinite, return `True`.
76
99
  """
77
- if self.interval_type == IntervalType.CLOSED:
78
- return self.value <= value # type: ignore[operator]
79
- elif self.interval_type == IntervalType.OPEN:
80
- return self.value < value # type: ignore[operator]
81
- elif self.interval_type == IntervalType.INFINITE:
100
+
101
+ if self._value is None:
102
+ # if no value is specified, assume interval type INFINITE
103
+ # (what are we supposed to compare against?)
82
104
  return True
105
+
106
+ if self.interval_type is None or self.interval_type == IntervalType.CLOSED:
107
+ # assume interval type CLOSED if a value was specified,
108
+ # but no interval type
109
+ return compare_odx_values(value, self._value) >= 0
110
+ elif self.interval_type == IntervalType.OPEN:
111
+ return compare_odx_values(value, self._value) > 0
112
+
113
+ if self.interval_type != IntervalType.INFINITE:
114
+ odxraise("Unhandled interval type {self.interval_type}")
115
+
116
+ return True
@@ -1,11 +1,11 @@
1
1
  # SPDX-License-Identifier: MIT
2
2
  from dataclasses import dataclass
3
- from typing import cast
3
+ from typing import Optional
4
4
 
5
- from ..exceptions import DecodeError, EncodeError, odxassert
5
+ from ..exceptions import DecodeError, EncodeError, odxassert, odxraise
6
6
  from ..odxtypes import AtomicOdxType, DataType
7
7
  from .compumethod import CompuMethod, CompuMethodCategory
8
- from .limit import IntervalType, Limit
8
+ from .limit import Limit
9
9
 
10
10
 
11
11
  @dataclass
@@ -60,8 +60,8 @@ class LinearCompuMethod(CompuMethod):
60
60
  offset: float
61
61
  factor: float
62
62
  denominator: float
63
- internal_lower_limit: Limit
64
- internal_upper_limit: Limit
63
+ internal_lower_limit: Optional[Limit]
64
+ internal_upper_limit: Optional[Limit]
65
65
 
66
66
  def __post_init__(self) -> None:
67
67
  odxassert(self.denominator > 0)
@@ -73,11 +73,11 @@ class LinearCompuMethod(CompuMethod):
73
73
  return "LINEAR"
74
74
 
75
75
  @property
76
- def physical_lower_limit(self) -> Limit:
76
+ def physical_lower_limit(self) -> Optional[Limit]:
77
77
  return self._physical_lower_limit
78
78
 
79
79
  @property
80
- def physical_upper_limit(self) -> Limit:
80
+ def physical_upper_limit(self) -> Optional[Limit]:
81
81
  return self._physical_upper_limit
82
82
 
83
83
  def __compute_physical_limits(self) -> None:
@@ -86,65 +86,61 @@ class LinearCompuMethod(CompuMethod):
86
86
  This method is only called during the initialization of a LinearCompuMethod.
87
87
  """
88
88
 
89
- def convert_to_limit_to_physical(limit: Limit, is_upper_limit: bool) -> Limit:
89
+ def convert_internal_to_physical_limit(internal_limit: Optional[Limit],
90
+ is_upper_limit: bool) -> Optional[Limit]:
90
91
  """Helper method
91
92
 
92
93
  Parameters:
93
94
 
94
- limit
95
+ internal_limit
95
96
  the internal limit to be converted to a physical limit
96
97
  is_upper_limit
97
98
  True iff limit is the internal upper limit
98
99
  """
99
- odxassert(isinstance(limit.value, (int, float)))
100
- if limit.interval_type == IntervalType.INFINITE:
101
- return limit
102
- elif (limit.interval_type == limit.interval_type.OPEN and
103
- self.internal_type.as_python_type() == int):
104
- closed_limit = limit.value - 1 if is_upper_limit else limit.value + 1
105
- return Limit(
106
- value=self._convert_internal_to_physical(closed_limit),
107
- interval_type=IntervalType.CLOSED,
108
- )
109
- else:
110
- return Limit(
111
- value=self._convert_internal_to_physical(limit.value),
112
- interval_type=limit.interval_type,
113
- )
100
+ if internal_limit is None or internal_limit.value_raw is None:
101
+ return None
102
+
103
+ internal_value = self.internal_type.from_string(internal_limit.value_raw)
104
+ physical_value = self._convert_internal_to_physical(internal_value)
105
+
106
+ result = Limit(
107
+ value_raw=str(physical_value),
108
+ value_type=self.physical_type,
109
+ interval_type=internal_limit.interval_type)
110
+
111
+ return result
112
+
113
+ self._physical_lower_limit = None
114
+ self._physical_upper_limit = None
114
115
 
115
116
  if self.factor >= 0:
116
- self._physical_lower_limit = convert_to_limit_to_physical(self.internal_lower_limit,
117
- False)
118
- self._physical_upper_limit = convert_to_limit_to_physical(self.internal_upper_limit,
119
- True)
117
+ self._physical_lower_limit = convert_internal_to_physical_limit(
118
+ self.internal_lower_limit, False)
119
+ self._physical_upper_limit = convert_internal_to_physical_limit(
120
+ self.internal_upper_limit, True)
120
121
  else:
121
122
  # If the factor is negative, the lower and upper limit are swapped
122
- self._physical_lower_limit = convert_to_limit_to_physical(self.internal_upper_limit,
123
- True)
124
- self._physical_upper_limit = convert_to_limit_to_physical(self.internal_lower_limit,
125
- False)
126
-
127
- if self.physical_type == DataType.A_UINT32:
128
- # If the data type is unsigned, the physical lower limit should be at least 0.
129
- if (self._physical_lower_limit.interval_type == IntervalType.INFINITE or
130
- cast(float, self._physical_lower_limit.value) < 0):
131
- self._physical_lower_limit = Limit(value=0, interval_type=IntervalType.CLOSED)
123
+ self._physical_lower_limit = convert_internal_to_physical_limit(
124
+ self.internal_upper_limit, True)
125
+ self._physical_upper_limit = convert_internal_to_physical_limit(
126
+ self.internal_lower_limit, False)
132
127
 
133
128
  def _convert_internal_to_physical(self, internal_value: AtomicOdxType) -> AtomicOdxType:
134
129
  if not isinstance(internal_value, (int, float)):
135
- raise DecodeError("The type of internal values of linear compumethods must "
136
- "either int or float")
130
+ raise DecodeError(f"The type of internal values of linear compumethods must "
131
+ f"either int or float (is: {type(internal_value).__name__})")
137
132
 
138
133
  if self.denominator is None:
139
134
  result = self.offset + self.factor * internal_value
140
135
  else:
141
136
  result = (self.offset + self.factor * internal_value) / self.denominator
142
137
 
143
- if self.internal_type == DataType.A_FLOAT64 and self.physical_type in [
138
+ if self.physical_type in [
144
139
  DataType.A_INT32,
145
140
  DataType.A_UINT32,
146
141
  ]:
147
142
  result = round(result)
143
+
148
144
  return self.physical_type.make_from(result)
149
145
 
150
146
  def convert_internal_to_physical(self, internal_value: AtomicOdxType) -> AtomicOdxType:
@@ -153,8 +149,10 @@ class LinearCompuMethod(CompuMethod):
153
149
 
154
150
  def convert_physical_to_internal(self, physical_value: AtomicOdxType) -> AtomicOdxType:
155
151
  if not isinstance(physical_value, (int, float)):
156
- raise EncodeError("The type of physical values of linear compumethods must "
157
- "either int or float")
152
+ odxraise(
153
+ "The type of physical values of linear compumethods must "
154
+ "either int or float", EncodeError)
155
+ return 0
158
156
 
159
157
  odxassert(
160
158
  self.is_valid_physical_value(physical_value),
@@ -166,7 +164,7 @@ class LinearCompuMethod(CompuMethod):
166
164
  else:
167
165
  result = ((physical_value * self.denominator) - self.offset) / self.factor
168
166
 
169
- if self.physical_type == DataType.A_FLOAT64 and self.internal_type in [
167
+ if self.internal_type in [
170
168
  DataType.A_INT32,
171
169
  DataType.A_UINT32,
172
170
  ]:
@@ -175,29 +173,40 @@ class LinearCompuMethod(CompuMethod):
175
173
 
176
174
  def is_valid_physical_value(self, physical_value: AtomicOdxType) -> bool:
177
175
  # Do type checks
178
- expected_type = self.physical_type.as_python_type()
179
- if expected_type == float and not isinstance(physical_value, (int, float)):
180
- return False
181
- elif expected_type != float and not isinstance(physical_value, expected_type):
182
- return False
176
+ expected_type = self.physical_type.python_type
177
+ if issubclass(expected_type, float):
178
+ if not isinstance(physical_value, (int, float)):
179
+ return False
180
+ else:
181
+ if not isinstance(physical_value, expected_type):
182
+ return False
183
183
 
184
- # Compare to the limits
185
- if not self.physical_lower_limit.complies_to_lower(physical_value):
184
+ # Check the limits
185
+ if self.physical_lower_limit is not None and not self.physical_lower_limit.complies_to_lower(
186
+ physical_value):
186
187
  return False
187
- if not self.physical_upper_limit.complies_to_upper(physical_value):
188
+ if self.physical_upper_limit is not None and not self.physical_upper_limit.complies_to_upper(
189
+ physical_value):
188
190
  return False
191
+
189
192
  return True
190
193
 
191
194
  def is_valid_internal_value(self, internal_value: AtomicOdxType) -> bool:
192
- expected_type = self.internal_type.as_python_type()
193
- if expected_type == float and not isinstance(internal_value, (int, float)):
194
- return False
195
- elif expected_type != float and not isinstance(internal_value, expected_type):
196
- return False
195
+ # Do type checks
196
+ expected_type = self.internal_type.python_type
197
+ if issubclass(expected_type, float):
198
+ if not isinstance(internal_value, (int, float)):
199
+ return False
200
+ else:
201
+ if not isinstance(internal_value, expected_type):
202
+ return False
197
203
 
198
- if not self.internal_lower_limit.complies_to_lower(internal_value):
204
+ # Check the limits
205
+ if self.internal_lower_limit is not None and not self.internal_lower_limit.complies_to_lower(
206
+ internal_value):
199
207
  return False
200
- if not self.internal_upper_limit.complies_to_upper(internal_value):
208
+ if self.internal_upper_limit is not None and not self.internal_upper_limit.complies_to_upper(
209
+ internal_value):
201
210
  return False
202
211
 
203
212
  return True
@@ -69,8 +69,23 @@ class TabIntpCompuMethod(CompuMethod):
69
69
  physical_points: List[Union[float, int]]
70
70
 
71
71
  def __post_init__(self) -> None:
72
- self._physical_lower_limit = Limit(min(self.physical_points), IntervalType.CLOSED)
73
- self._physical_upper_limit = Limit(max(self.physical_points), IntervalType.CLOSED)
72
+ self._physical_lower_limit = Limit(
73
+ value_raw=str(min(self.physical_points)),
74
+ value_type=self.physical_type,
75
+ interval_type=IntervalType.CLOSED)
76
+ self._physical_upper_limit = Limit(
77
+ value_raw=str(max(self.physical_points)),
78
+ value_type=self.physical_type,
79
+ interval_type=IntervalType.CLOSED)
80
+
81
+ self._internal_lower_limit = Limit(
82
+ value_raw=str(min(self.internal_points)),
83
+ value_type=self.internal_type,
84
+ interval_type=IntervalType.CLOSED)
85
+ self._internal_upper_limit = Limit(
86
+ value_raw=str(max(self.internal_points)),
87
+ value_type=self.internal_type,
88
+ interval_type=IntervalType.CLOSED)
74
89
 
75
90
  self._assert_validity()
76
91
 
@@ -124,9 +139,7 @@ class TabIntpCompuMethod(CompuMethod):
124
139
  odxassert(
125
140
  isinstance(physical_value, (int, float)),
126
141
  "Only integers and floats can be piecewise linearly interpolated")
127
- result = self._piecewise_linear_interpolate(
128
- physical_value, # type: ignore[arg-type]
129
- reference_points)
142
+ result = self._piecewise_linear_interpolate(physical_value, reference_points)
130
143
 
131
144
  if result is None:
132
145
  raise EncodeError(f"Internal value {physical_value!r} must be inside the range"
@@ -142,9 +155,7 @@ class TabIntpCompuMethod(CompuMethod):
142
155
  "either int or float")
143
156
 
144
157
  reference_points = list(zip(self.internal_points, self.physical_points))
145
- result = self._piecewise_linear_interpolate(
146
- internal_value, # type: ignore[arg-type]
147
- reference_points)
158
+ result = self._piecewise_linear_interpolate(internal_value, reference_points)
148
159
 
149
160
  if result is None:
150
161
  raise DecodeError(f"Internal value {internal_value!r} must be inside the range"
@@ -1,8 +1,8 @@
1
1
  # SPDX-License-Identifier: MIT
2
2
  from dataclasses import dataclass
3
- from typing import List, Optional
3
+ from typing import List, Optional, cast
4
4
 
5
- from ..exceptions import DecodeError, EncodeError, odxassert
5
+ from ..exceptions import DecodeError, EncodeError, odxassert, odxraise
6
6
  from ..odxtypes import AtomicOdxType, DataType
7
7
  from .compumethod import CompuMethod, CompuMethodCategory
8
8
  from .compuscale import CompuScale
@@ -28,53 +28,49 @@ class TexttableCompuMethod(CompuMethod):
28
28
  def category(self) -> CompuMethodCategory:
29
29
  return "TEXTTABLE"
30
30
 
31
- def get_scales(self) -> List[CompuScale]:
32
- scales = list(self.internal_to_phys)
33
- if self.compu_default_value:
34
- # Default is last, since it's a fallback
35
- scales.append(self.compu_default_value)
36
- return scales
37
-
38
31
  def convert_physical_to_internal(self, physical_value: AtomicOdxType) -> AtomicOdxType:
39
- matching_scales = [x for x in self.get_scales() if x.compu_const == physical_value]
32
+ matching_scales = [x for x in self.internal_to_phys if x.compu_const == physical_value]
40
33
  for scale in matching_scales:
41
34
  if scale.compu_inverse_value is not None:
42
35
  return scale.compu_inverse_value
43
- elif scale.lower_limit is not None:
44
- return scale.lower_limit.value
45
- elif scale.upper_limit is not None:
46
- return scale.upper_limit.value
36
+ elif scale.lower_limit is not None and scale.lower_limit._value is not None:
37
+ return scale.lower_limit._value
38
+ elif scale.upper_limit is not None and scale.upper_limit._value is not None:
39
+ return scale.upper_limit._value
40
+
41
+ if self.compu_default_value is not None and self.compu_default_value.compu_inverse_value is not None:
42
+ return self.compu_default_value.compu_inverse_value
47
43
 
48
44
  raise EncodeError(f"Texttable compu method could not encode '{physical_value!r}'.")
49
45
 
50
46
  def __is_internal_in_scale(self, internal_value: AtomicOdxType, scale: CompuScale) -> bool:
51
47
  if scale == self.compu_default_value:
52
48
  return True
53
- if scale.lower_limit is not None and not scale.lower_limit.complies_to_lower(
54
- internal_value):
55
- return False
56
- # If no UPPER-LIMIT is defined
57
- # the COMPU-SCALE will be applied only for the value defined in LOWER-LIMIT
58
- upper_limit = scale.upper_limit or scale.lower_limit
59
- if upper_limit is not None and not upper_limit.complies_to_upper(internal_value):
60
- return False
61
- # value complies to the defined limits
62
- return True
49
+
50
+ return scale.applies(internal_value)
63
51
 
64
52
  def convert_internal_to_physical(self, internal_value: AtomicOdxType) -> AtomicOdxType:
65
- scale = next(
66
- filter(
67
- lambda scale: self.__is_internal_in_scale(internal_value, scale),
68
- self.get_scales(),
69
- ), None)
70
- if scale is None or scale.compu_const is None:
71
- raise DecodeError(
72
- f"Texttable compu method could not decode {internal_value!r} to string.")
73
- return scale.compu_const
53
+ matching_scales = [x for x in self.internal_to_phys if x.applies(internal_value)]
54
+ if len(matching_scales) == 0:
55
+ if self.compu_default_value is None or self.compu_default_value.compu_const is None:
56
+ odxraise(f"Texttable could not decode {internal_value!r}.", DecodeError)
57
+ return cast(None, AtomicOdxType)
58
+
59
+ return self.compu_default_value.compu_const
60
+
61
+ if len(matching_scales) != 1 or matching_scales[0].compu_const is None:
62
+ odxraise(f"Texttable could not decode {internal_value!r}.", DecodeError)
63
+
64
+ return matching_scales[0].compu_const
74
65
 
75
66
  def is_valid_physical_value(self, physical_value: AtomicOdxType) -> bool:
76
- return any(x.compu_const == physical_value for x in self.get_scales())
67
+ if self.compu_default_value is not None:
68
+ return True
69
+
70
+ return any(x.compu_const == physical_value for x in self.internal_to_phys)
77
71
 
78
72
  def is_valid_internal_value(self, internal_value: AtomicOdxType) -> bool:
79
- return any(
80
- self.__is_internal_in_scale(internal_value, scale) for scale in self.get_scales())
73
+ if self.compu_default_value is not None:
74
+ return True
75
+
76
+ return any(scale.applies(internal_value) for scale in self.internal_to_phys)