pyxcp 0.25.2__cp314-cp314-win_arm64.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 (154) hide show
  1. pyxcp/__init__.py +20 -0
  2. pyxcp/aml/EtasCANMonitoring.a2l +82 -0
  3. pyxcp/aml/EtasCANMonitoring.aml +67 -0
  4. pyxcp/aml/XCP_Common.aml +408 -0
  5. pyxcp/aml/XCPonCAN.aml +78 -0
  6. pyxcp/aml/XCPonEth.aml +33 -0
  7. pyxcp/aml/XCPonFlx.aml +113 -0
  8. pyxcp/aml/XCPonSxI.aml +66 -0
  9. pyxcp/aml/XCPonUSB.aml +106 -0
  10. pyxcp/aml/ifdata_CAN.a2l +20 -0
  11. pyxcp/aml/ifdata_Eth.a2l +11 -0
  12. pyxcp/aml/ifdata_Flx.a2l +94 -0
  13. pyxcp/aml/ifdata_SxI.a2l +13 -0
  14. pyxcp/aml/ifdata_USB.a2l +81 -0
  15. pyxcp/asam/__init__.py +0 -0
  16. pyxcp/asam/types.py +131 -0
  17. pyxcp/asamkeydll.c +116 -0
  18. pyxcp/asamkeydll.exe +0 -0
  19. pyxcp/asamkeydll.sh +2 -0
  20. pyxcp/checksum.py +732 -0
  21. pyxcp/cmdline.py +83 -0
  22. pyxcp/config/__init__.py +1257 -0
  23. pyxcp/config/legacy.py +120 -0
  24. pyxcp/constants.py +47 -0
  25. pyxcp/cpp_ext/__init__.py +0 -0
  26. pyxcp/cpp_ext/aligned_buffer.hpp +168 -0
  27. pyxcp/cpp_ext/bin.hpp +105 -0
  28. pyxcp/cpp_ext/blockmem.hpp +58 -0
  29. pyxcp/cpp_ext/cpp_ext.cp310-win_arm64.pyd +0 -0
  30. pyxcp/cpp_ext/cpp_ext.cp311-win_arm64.pyd +0 -0
  31. pyxcp/cpp_ext/cpp_ext.cp312-win_arm64.pyd +0 -0
  32. pyxcp/cpp_ext/cpp_ext.cp313-win_arm64.pyd +0 -0
  33. pyxcp/cpp_ext/cpp_ext.cp314-win_arm64.pyd +0 -0
  34. pyxcp/cpp_ext/daqlist.hpp +374 -0
  35. pyxcp/cpp_ext/event.hpp +67 -0
  36. pyxcp/cpp_ext/extension_wrapper.cpp +131 -0
  37. pyxcp/cpp_ext/framing.hpp +360 -0
  38. pyxcp/cpp_ext/helper.hpp +280 -0
  39. pyxcp/cpp_ext/mcobject.hpp +248 -0
  40. pyxcp/cpp_ext/sxi_framing.hpp +332 -0
  41. pyxcp/cpp_ext/tsqueue.hpp +46 -0
  42. pyxcp/daq_stim/__init__.py +306 -0
  43. pyxcp/daq_stim/optimize/__init__.py +67 -0
  44. pyxcp/daq_stim/optimize/binpacking.py +41 -0
  45. pyxcp/daq_stim/scheduler.cpp +62 -0
  46. pyxcp/daq_stim/scheduler.hpp +75 -0
  47. pyxcp/daq_stim/stim.cp310-win_arm64.pyd +0 -0
  48. pyxcp/daq_stim/stim.cp311-win_arm64.pyd +0 -0
  49. pyxcp/daq_stim/stim.cp312-win_arm64.pyd +0 -0
  50. pyxcp/daq_stim/stim.cp313-win_arm64.pyd +0 -0
  51. pyxcp/daq_stim/stim.cp314-win_arm64.pyd +0 -0
  52. pyxcp/daq_stim/stim.cpp +13 -0
  53. pyxcp/daq_stim/stim.hpp +604 -0
  54. pyxcp/daq_stim/stim_wrapper.cpp +50 -0
  55. pyxcp/dllif.py +100 -0
  56. pyxcp/errormatrix.py +878 -0
  57. pyxcp/examples/conf_can.toml +19 -0
  58. pyxcp/examples/conf_can_user.toml +16 -0
  59. pyxcp/examples/conf_can_vector.json +11 -0
  60. pyxcp/examples/conf_can_vector.toml +11 -0
  61. pyxcp/examples/conf_eth.toml +9 -0
  62. pyxcp/examples/conf_nixnet.json +20 -0
  63. pyxcp/examples/conf_socket_can.toml +12 -0
  64. pyxcp/examples/run_daq.py +165 -0
  65. pyxcp/examples/xcp_policy.py +60 -0
  66. pyxcp/examples/xcp_read_benchmark.py +38 -0
  67. pyxcp/examples/xcp_skel.py +48 -0
  68. pyxcp/examples/xcp_unlock.py +38 -0
  69. pyxcp/examples/xcp_user_supplied_driver.py +43 -0
  70. pyxcp/examples/xcphello.py +79 -0
  71. pyxcp/examples/xcphello_recorder.py +107 -0
  72. pyxcp/master/__init__.py +10 -0
  73. pyxcp/master/errorhandler.py +677 -0
  74. pyxcp/master/master.py +2645 -0
  75. pyxcp/py.typed +0 -0
  76. pyxcp/recorder/.idea/.gitignore +8 -0
  77. pyxcp/recorder/.idea/misc.xml +4 -0
  78. pyxcp/recorder/.idea/modules.xml +8 -0
  79. pyxcp/recorder/.idea/recorder.iml +6 -0
  80. pyxcp/recorder/.idea/sonarlint/issuestore/3/8/3808afc69ac1edb9d760000a2f137335b1b99728 +7 -0
  81. pyxcp/recorder/.idea/sonarlint/issuestore/9/a/9a2aa4db38d3115ed60da621e012c0efc0172aae +0 -0
  82. pyxcp/recorder/.idea/sonarlint/issuestore/b/4/b49006702b459496a8e8c94ebe60947108361b91 +0 -0
  83. pyxcp/recorder/.idea/sonarlint/issuestore/index.pb +7 -0
  84. pyxcp/recorder/.idea/sonarlint/securityhotspotstore/3/8/3808afc69ac1edb9d760000a2f137335b1b99728 +0 -0
  85. pyxcp/recorder/.idea/sonarlint/securityhotspotstore/9/a/9a2aa4db38d3115ed60da621e012c0efc0172aae +0 -0
  86. pyxcp/recorder/.idea/sonarlint/securityhotspotstore/b/4/b49006702b459496a8e8c94ebe60947108361b91 +0 -0
  87. pyxcp/recorder/.idea/sonarlint/securityhotspotstore/index.pb +7 -0
  88. pyxcp/recorder/.idea/vcs.xml +10 -0
  89. pyxcp/recorder/__init__.py +96 -0
  90. pyxcp/recorder/build_clang.cmd +1 -0
  91. pyxcp/recorder/build_clang.sh +2 -0
  92. pyxcp/recorder/build_gcc.cmd +1 -0
  93. pyxcp/recorder/build_gcc.sh +2 -0
  94. pyxcp/recorder/build_gcc_arm.sh +2 -0
  95. pyxcp/recorder/converter/__init__.py +445 -0
  96. pyxcp/recorder/lz4.c +2829 -0
  97. pyxcp/recorder/lz4.h +879 -0
  98. pyxcp/recorder/lz4hc.c +2041 -0
  99. pyxcp/recorder/lz4hc.h +413 -0
  100. pyxcp/recorder/mio.hpp +1714 -0
  101. pyxcp/recorder/reader.hpp +138 -0
  102. pyxcp/recorder/reco.py +278 -0
  103. pyxcp/recorder/recorder.rst +0 -0
  104. pyxcp/recorder/rekorder.cp310-win_arm64.pyd +0 -0
  105. pyxcp/recorder/rekorder.cp311-win_arm64.pyd +0 -0
  106. pyxcp/recorder/rekorder.cp312-win_arm64.pyd +0 -0
  107. pyxcp/recorder/rekorder.cp313-win_arm64.pyd +0 -0
  108. pyxcp/recorder/rekorder.cp314-win_arm64.pyd +0 -0
  109. pyxcp/recorder/rekorder.cpp +59 -0
  110. pyxcp/recorder/rekorder.hpp +274 -0
  111. pyxcp/recorder/setup.py +41 -0
  112. pyxcp/recorder/test_reko.py +34 -0
  113. pyxcp/recorder/unfolder.hpp +1354 -0
  114. pyxcp/recorder/wrap.cpp +184 -0
  115. pyxcp/recorder/writer.hpp +302 -0
  116. pyxcp/scripts/__init__.py +0 -0
  117. pyxcp/scripts/pyxcp_probe_can_drivers.py +20 -0
  118. pyxcp/scripts/xcp_examples.py +64 -0
  119. pyxcp/scripts/xcp_fetch_a2l.py +40 -0
  120. pyxcp/scripts/xcp_id_scanner.py +18 -0
  121. pyxcp/scripts/xcp_info.py +144 -0
  122. pyxcp/scripts/xcp_profile.py +26 -0
  123. pyxcp/scripts/xmraw_converter.py +31 -0
  124. pyxcp/stim/__init__.py +0 -0
  125. pyxcp/tests/test_asam_types.py +24 -0
  126. pyxcp/tests/test_binpacking.py +186 -0
  127. pyxcp/tests/test_can.py +1324 -0
  128. pyxcp/tests/test_checksum.py +95 -0
  129. pyxcp/tests/test_daq.py +193 -0
  130. pyxcp/tests/test_daq_opt.py +426 -0
  131. pyxcp/tests/test_frame_padding.py +156 -0
  132. pyxcp/tests/test_framing.py +262 -0
  133. pyxcp/tests/test_master.py +2116 -0
  134. pyxcp/tests/test_transport.py +177 -0
  135. pyxcp/tests/test_utils.py +30 -0
  136. pyxcp/timing.py +60 -0
  137. pyxcp/transport/__init__.py +13 -0
  138. pyxcp/transport/base.py +484 -0
  139. pyxcp/transport/base_transport.hpp +0 -0
  140. pyxcp/transport/can.py +660 -0
  141. pyxcp/transport/eth.py +254 -0
  142. pyxcp/transport/sxi.py +209 -0
  143. pyxcp/transport/transport_ext.hpp +214 -0
  144. pyxcp/transport/transport_wrapper.cpp +249 -0
  145. pyxcp/transport/usb_transport.py +229 -0
  146. pyxcp/types.py +987 -0
  147. pyxcp/utils.py +127 -0
  148. pyxcp/vector/__init__.py +0 -0
  149. pyxcp/vector/map.py +82 -0
  150. pyxcp-0.25.2.dist-info/METADATA +341 -0
  151. pyxcp-0.25.2.dist-info/RECORD +154 -0
  152. pyxcp-0.25.2.dist-info/WHEEL +4 -0
  153. pyxcp-0.25.2.dist-info/entry_points.txt +9 -0
  154. pyxcp-0.25.2.dist-info/licenses/LICENSE +165 -0
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env python
2
+ """Lowlevel API reflecting available XCP services
3
+
4
+ .. note:: For technical reasons the API is split into two parts;
5
+ common methods and a Python version specific part.
6
+
7
+ .. [1] XCP Specification, Part 2 - Protocol Layer Specification
8
+ """
9
+
10
+ from .master import Master # noqa: F401
@@ -0,0 +1,677 @@
1
+ #!/usr/bin/env python
2
+ """Implements error-handling according to XCP spec."""
3
+
4
+ import functools
5
+ import logging
6
+ import threading
7
+ import time
8
+ import types
9
+ from collections import namedtuple
10
+ from typing import Generic, List, Optional, TypeVar
11
+
12
+ import can
13
+
14
+ from pyxcp.errormatrix import ERROR_MATRIX, Action, PreAction
15
+ from pyxcp.types import COMMAND_CATEGORIES, XcpError, XcpResponseError, XcpTimeoutError
16
+
17
+
18
+ handle_errors = True # enable/disable XCP error-handling.
19
+
20
+ # Thread-local flag to suppress logging for expected XCP negative responses
21
+
22
+
23
+ _thread_flags = threading.local()
24
+
25
+
26
+ def set_suppress_xcp_error_log(value: bool) -> None:
27
+ try:
28
+ _thread_flags.suppress_xcp_error_log = bool(value)
29
+ except Exception:
30
+ pass
31
+
32
+
33
+ def is_suppress_xcp_error_log() -> bool:
34
+ try:
35
+ return bool(getattr(_thread_flags, "suppress_xcp_error_log", False))
36
+ except Exception:
37
+ return False
38
+
39
+
40
+ class SingletonBase:
41
+ _lock = threading.Lock()
42
+
43
+ def __new__(cls, *args, **kws):
44
+ # Double-Checked Locking
45
+ if not hasattr(cls, "_instance"):
46
+ try:
47
+ cls._lock.acquire()
48
+ if not hasattr(cls, "_instance"):
49
+ cls._instance = super().__new__(cls)
50
+ finally:
51
+ cls._lock.release()
52
+ return cls._instance
53
+
54
+
55
+ Function = namedtuple("Function", "fun arguments") # store: var | load: var
56
+
57
+
58
+ class InternalError(Exception):
59
+ """Indicates an internal error, like invalid service."""
60
+
61
+
62
+ class SystemExit(Exception):
63
+ """"""
64
+
65
+ def __init__(self, msg: str, error_code: int = None, *args, **kws):
66
+ super().__init__(*args, **kws)
67
+ self.error_code = error_code
68
+ self.msg = msg
69
+
70
+ def __str__(self):
71
+ return f"SystemExit(error_code={self.error_code}, message={self.msg!r})"
72
+
73
+ __repr__ = __str__
74
+
75
+
76
+ class UnrecoverableError(Exception):
77
+ """"""
78
+
79
+
80
+ def func_name(func):
81
+ return func.__qualname__ if func is not None else None
82
+
83
+
84
+ def getErrorHandler(service):
85
+ """"""
86
+ return ERROR_MATRIX.get(service)
87
+
88
+
89
+ def getTimeoutHandler(service):
90
+ """"""
91
+ handler = getErrorHandler(service)
92
+ if handler is None:
93
+ raise InternalError("Invalid Service")
94
+ return handler.get(XcpError.ERR_TIMEOUT)
95
+
96
+
97
+ def getActions(service, error_code):
98
+ """"""
99
+ error_str = str(error_code)
100
+ if error_code == XcpError.ERR_TIMEOUT:
101
+ preActions, actions = getTimeoutHandler(service)
102
+ else:
103
+ eh = getErrorHandler(service)
104
+ if eh is None:
105
+ raise InternalError(f"Invalid Service 0x{service:02x}")
106
+ # print(f"Try to handle error -- Service: {service.name} Error-Code: {error_code}")
107
+ handler = eh.get(error_str)
108
+ if handler is None:
109
+ raise SystemExit(f"Service {service.name!r} has no handler for {error_code}.", error_code=error_code)
110
+ preActions, actions = handler
111
+ return preActions, actions
112
+
113
+
114
+ def actionIter(actions):
115
+ """Iterate over action from :file:`errormatrix.py`"""
116
+ if isinstance(actions, (tuple, list)):
117
+ yield from actions
118
+ else:
119
+ yield actions
120
+
121
+
122
+ class Arguments:
123
+ """Container for positional and keyword arguments.
124
+
125
+ Parameters
126
+ ----------
127
+ args: tuple
128
+ Positional arguments
129
+ kwargs: dict
130
+ Keyword arguments.
131
+ """
132
+
133
+ def __init__(self, args=None, kwargs=None):
134
+ if args is None:
135
+ self.args = ()
136
+ else:
137
+ if not hasattr(args, "__iter__"):
138
+ self.args = (args,)
139
+ else:
140
+ self.args = tuple(args)
141
+ self.kwargs = kwargs or {}
142
+
143
+ def __str__(self) -> str:
144
+ res = f"{self.__class__.__name__}(ARGS = {self.args}, KWS = {self.kwargs})"
145
+ return res
146
+
147
+ def __eq__(self, other) -> bool:
148
+ return (self.args == other.args if other is not None else False) and (
149
+ self.kwargs == other.kwargs if other is not None else False
150
+ )
151
+
152
+ __repr__ = __str__
153
+
154
+
155
+ class Repeater:
156
+ """A required action of some XCP errorhandler is repetition.
157
+
158
+ Parameters
159
+ ----------
160
+ initial_value: int
161
+ The actual values are predetermined by XCP:
162
+ - REPEAT (one time)
163
+ - REPEAT_2_TIMES (two times)
164
+ - REPEAT_INF_TIMES ("forever")
165
+ """
166
+
167
+ REPEAT = 1
168
+ REPEAT_2_TIMES = 2
169
+ INFINITE = -1
170
+
171
+ def __init__(self, initial_value: int):
172
+ self._counter = initial_value
173
+ # print("\tREPEATER ctor", hex(id(self)))
174
+
175
+ def repeat(self):
176
+ """Check if repetition is required.
177
+
178
+ Returns
179
+ -------
180
+ bool
181
+ """
182
+ # print("\t\tCOUNTER:", hex(id(self)), self._counter)
183
+ if self._counter == Repeater.INFINITE:
184
+ return True
185
+ elif self._counter > 0:
186
+ self._counter -= 1
187
+ return True
188
+ else:
189
+ return False
190
+
191
+
192
+ def display_error():
193
+ """Display error information.
194
+
195
+ TODO: callback.
196
+
197
+ """
198
+
199
+
200
+ class Handler:
201
+ """"""
202
+
203
+ def __init__(self, instance, func, arguments, error_code=None):
204
+ self.instance = instance
205
+ if hasattr(func, "__closure__") and func.__closure__:
206
+ self.func = func.__closure__[0].cell_contents # Use original, undecorated function to prevent
207
+ # nasty recursion problems.
208
+ else:
209
+ self.func = func
210
+ self.arguments = arguments
211
+ self.service = self.instance.service
212
+ self._error_code: int = 0
213
+ if error_code is not None:
214
+ self._error_code = error_code
215
+ self._repeater = None
216
+ self.logger = logging.getLogger("PyXCP")
217
+
218
+ def _diagnostics_enabled(self) -> bool:
219
+ try:
220
+ app = getattr(self.instance, "config", None)
221
+ if app is None:
222
+ return True
223
+ general = getattr(app, "general", None)
224
+ if general is None:
225
+ return True
226
+ return bool(getattr(general, "diagnostics_on_failure", True))
227
+ except Exception:
228
+ return True
229
+
230
+ def _build_transport_diagnostics(self) -> str:
231
+ try:
232
+ transport = getattr(self.instance, "transport", None)
233
+ if transport is None:
234
+ return ""
235
+ if hasattr(transport, "_build_diagnostics_dump"):
236
+ return transport._build_diagnostics_dump() # type: ignore[attr-defined]
237
+ except Exception:
238
+ pass
239
+ return ""
240
+
241
+ def _append_diag(self, msg: str) -> str:
242
+ # Suppress diagnostics entirely when XCP error logging is suppressed (e.g., try_command probing)
243
+ if is_suppress_xcp_error_log():
244
+ return msg
245
+ if not self._diagnostics_enabled():
246
+ return msg
247
+ diag = self._build_transport_diagnostics()
248
+ if not diag:
249
+ return msg
250
+ # Prefer a Rich-formatted table for compact, readable diagnostics.
251
+ try:
252
+ header = "--- Diagnostics (for troubleshooting) ---"
253
+ body = diag
254
+ if "\n" in diag and diag.startswith("--- Diagnostics"):
255
+ header, body = diag.split("\n", 1)
256
+
257
+ # Try to parse the structured JSON body produced by transports
258
+ import json as _json # Local import to avoid hard dependency at module import time
259
+
260
+ payload = _json.loads(body)
261
+ transport_params = payload.get("transport_params") or {}
262
+ last_pdus = payload.get("last_pdus") or []
263
+
264
+ # Try to use rich if available
265
+ try:
266
+ from rich.console import Console
267
+ from rich.table import Table
268
+ from rich.panel import Panel
269
+ from textwrap import shorten
270
+
271
+ console = Console(file=None, force_terminal=False, width=120, record=True, markup=False)
272
+
273
+ # Transport parameters table
274
+ tp_table = Table(title="Transport Parameters", title_style="bold", show_header=True, header_style="bold magenta")
275
+ tp_table.add_column("Key", style="cyan", no_wrap=True)
276
+ tp_table.add_column("Value", style="white")
277
+ from rich.markup import escape as _escape
278
+
279
+ for k, v in (transport_params or {}).items():
280
+ # Convert complex values to compact repr
281
+ sv = repr(v)
282
+ sv = shorten(sv, width=80, placeholder="…")
283
+ tp_table.add_row(_escape(str(k)), _escape(sv))
284
+
285
+ # Last PDUs table (most recent last)
286
+ pdu_table = Table(
287
+ title="Last PDUs (most recent last)", title_style="bold", show_header=True, header_style="bold magenta"
288
+ )
289
+ for col in ("dir", "cat", "ctr", "ts", "len", "data"):
290
+ pdu_table.add_column(col, no_wrap=(col in {"dir", "cat", "ctr", "len"}), style="white")
291
+ for pdu in last_pdus:
292
+ try:
293
+ dir_ = str(pdu.get("dir", ""))
294
+ cat = str(pdu.get("cat", ""))
295
+ ctr = str(pdu.get("ctr", ""))
296
+ # Format timestamp: convert ns -> s with 5 decimals if numeric
297
+ ts_val = pdu.get("ts", "")
298
+ try:
299
+ ts_num = int(ts_val)
300
+ ts = f"{ts_num / 1_000_000_000:.5f}"
301
+ except Exception:
302
+ ts = str(ts_val)
303
+ ln = str(pdu.get("len", ""))
304
+ # Prefer showing actual data content; avoid repr quotes
305
+ data_val = pdu.get("data", "")
306
+ try:
307
+ if isinstance(data_val, (bytes, bytearray, list, tuple)):
308
+ # Lazily import to avoid hard dependency
309
+ from pyxcp.utils import hexDump as _hexDump
310
+
311
+ data_str = _hexDump(data_val)
312
+ else:
313
+ data_str = str(data_val)
314
+ except Exception:
315
+ data_str = str(data_val)
316
+ # Shorten potentially huge values to keep table compact
317
+ from textwrap import shorten as _shorten
318
+
319
+ ts = _shorten(ts, width=20, placeholder="…")
320
+ data = _shorten(data_str, width=40, placeholder="…")
321
+ # Escape strings to avoid Rich markup interpretation (e.g., '[' ']' in hex dumps)
322
+ dir_e = _escape(dir_)
323
+ cat_e = _escape(cat)
324
+ ctr_e = _escape(ctr)
325
+ ts_e = _escape(ts)
326
+ ln_e = _escape(ln)
327
+ data_e = _escape(data)
328
+ pdu_table.add_row(dir_e, cat_e, ctr_e, ts_e, ln_e, data_e)
329
+ except Exception:
330
+ # If anything odd in structure, add a single-cell row with repr
331
+ from textwrap import shorten as _shorten
332
+
333
+ pdu_table.add_row(_shorten(repr(pdu), width=80, placeholder="…"), "", "", "", "", "")
334
+
335
+ # Combine into a single panel and capture as text
336
+ console.print(Panel.fit(tp_table, title=header))
337
+ if last_pdus:
338
+ console.print(pdu_table)
339
+ rendered = console.export_text(clear=False)
340
+
341
+ except Exception:
342
+ # Rich not available or rendering failed; fallback to compact logger lines
343
+ self.logger.error(header)
344
+ if transport_params:
345
+ self.logger.error("transport_params: %s", transport_params)
346
+ if last_pdus:
347
+ self.logger.error("last_pdus (most recent last):")
348
+ for pdu in last_pdus:
349
+ try:
350
+ ts_val = pdu.get("ts", "")
351
+ try:
352
+ ts_num = int(ts_val)
353
+ ts_fmt = f"{ts_num / 1_000_000_000:.5f}"
354
+ except Exception:
355
+ ts_fmt = str(ts_val)
356
+ data_val = pdu.get("data", "")
357
+ if isinstance(data_val, (bytes, bytearray, list, tuple)):
358
+ from pyxcp.utils import hexDump as _hexDump
359
+
360
+ data_str = _hexDump(data_val)
361
+ else:
362
+ data_str = str(data_val)
363
+ pdu_copy = dict(pdu)
364
+ pdu_copy["ts"] = ts_fmt
365
+ pdu_copy["data"] = data_str
366
+ self.logger.error("%s", pdu_copy)
367
+ except Exception:
368
+ self.logger.error("%s", pdu)
369
+ except Exception:
370
+ # As a last resort, emit the whole diagnostics blob verbatim
371
+ try:
372
+ for line in diag.splitlines():
373
+ self.logger.error(line)
374
+ except Exception:
375
+ pass
376
+ return msg
377
+
378
+ def __str__(self):
379
+ return f"Handler(func = {func_name(self.func)} -- {self.arguments} service = {self.service} error_code = {self.error_code})"
380
+
381
+ def __eq__(self, other):
382
+ if other is None:
383
+ return False
384
+ return (self.instance == other.instance) and (self.func == other.func) and (self.arguments == other.arguments)
385
+
386
+ @property
387
+ def error_code(self) -> int:
388
+ return self._error_code
389
+
390
+ @error_code.setter
391
+ def error_code(self, value: int) -> None:
392
+ self._error_code = value
393
+
394
+ @property
395
+ def repeater(self):
396
+ # print("\tGet repeater", hex(id(self._repeater)), self._repeater is None)
397
+ return self._repeater
398
+
399
+ @repeater.setter
400
+ def repeater(self, value):
401
+ # print("\tSet repeater", hex(id(value)))
402
+ self._repeater = value
403
+
404
+ def execute(self):
405
+ self.logger.debug(f"Execute({func_name(self.func)} -- {self.arguments})")
406
+ if isinstance(self.func, types.MethodType):
407
+ return self.func(*self.arguments.args, **self.arguments.kwargs)
408
+ else:
409
+ return self.func(self.instance, *self.arguments.args, **self.arguments.kwargs)
410
+
411
+ def actions(self, preActions, actions):
412
+ """Preprocess errorhandling pre-actions and actions."""
413
+ result_pre_actions = []
414
+ result_actions = []
415
+ repetitionCount = 0
416
+ for item in actionIter(preActions):
417
+ if item == PreAction.NONE:
418
+ pass
419
+ elif item == PreAction.WAIT_T7:
420
+ time.sleep(0.02) # Completely arbitrary for now.
421
+ elif item == PreAction.SYNCH:
422
+ fn = Function(self.instance.synch, Arguments())
423
+ result_pre_actions.append(fn)
424
+ elif item == PreAction.GET_SEED_UNLOCK:
425
+ raise NotImplementedError("Pre-action GET_SEED_UNLOCK")
426
+ elif item == PreAction.SET_MTA:
427
+ fn = Function(self.instance.setMta, Arguments(self.instance.mta))
428
+ result_pre_actions.append(fn)
429
+ elif item == PreAction.SET_DAQ_PTR:
430
+ fn = Function(self.instance.setDaqPtr, Arguments(self.instance.currentDaqPtr))
431
+ elif item == PreAction.START_STOP_X:
432
+ raise NotImplementedError("Pre-action START_STOP_X")
433
+ elif item == PreAction.REINIT_DAQ:
434
+ raise NotImplementedError("Pre-action REINIT_DAQ")
435
+ elif item == PreAction.DISPLAY_ERROR:
436
+ pass
437
+ elif item == PreAction.DOWNLOAD:
438
+ raise NotImplementedError("Pre-action DOWNLOAD")
439
+ elif item == PreAction.PROGRAM:
440
+ raise NotImplementedError("Pre-action PROGRAM")
441
+ elif item == PreAction.UPLOAD:
442
+ raise NotImplementedError("Pre-action UPLOAD")
443
+ elif item == PreAction.UNLOCK_SLAVE:
444
+ resource = COMMAND_CATEGORIES.get(self.instance.service) # noqa: F841
445
+ raise NotImplementedError("Pre-action UNLOCK_SLAVE")
446
+ for item in actionIter(actions):
447
+ if item == Action.NONE:
448
+ pass
449
+ elif item == Action.DISPLAY_ERROR:
450
+ raise SystemExit(self._append_diag("Could not proceed due to unhandled error (DISPLAY_ERROR)."), self.error_code)
451
+ elif item == Action.RETRY_SYNTAX:
452
+ raise SystemExit(self._append_diag("Could not proceed due to unhandled error (RETRY_SYNTAX)."), self.error_code)
453
+ elif item == Action.RETRY_PARAM:
454
+ raise SystemExit(self._append_diag("Could not proceed due to unhandled error (RETRY_PARAM)."), self.error_code)
455
+ elif item == Action.USE_A2L:
456
+ raise SystemExit(self._append_diag("Could not proceed due to unhandled error (USE_A2L)."), self.error_code)
457
+ elif item == Action.USE_ALTERATIVE:
458
+ raise SystemExit(
459
+ self._append_diag("Could not proceed due to unhandled error (USE_ALTERATIVE)."), self.error_code
460
+ ) # TODO: check alternatives.
461
+ elif item == Action.REPEAT:
462
+ repetitionCount = Repeater.REPEAT
463
+ elif item == Action.REPEAT_2_TIMES:
464
+ repetitionCount = Repeater.REPEAT_2_TIMES
465
+ elif item == Action.REPEAT_INF_TIMES:
466
+ repetitionCount = Repeater.INFINITE
467
+ elif item == Action.RESTART_SESSION:
468
+ raise SystemExit(self._append_diag("Could not proceed due to unhandled error (RESTART_SESSION)."), self.error_code)
469
+ elif item == Action.TERMINATE_SESSION:
470
+ raise SystemExit(
471
+ self._append_diag("Could not proceed due to unhandled error (TERMINATE_SESSION)."), self.error_code
472
+ )
473
+ elif item == Action.SKIP:
474
+ pass
475
+ elif item == Action.NEW_FLASH_WARE:
476
+ raise SystemExit(self._append_diag("Could not proceed due to unhandled error (NEW_FLASH_WARE)"), self.error_code)
477
+ return result_pre_actions, result_actions, Repeater(repetitionCount)
478
+
479
+
480
+ T = TypeVar("T")
481
+
482
+
483
+ class HandlerStack(Generic[T]):
484
+ """"""
485
+
486
+ def __init__(self) -> None:
487
+ self._stack: List[T] = []
488
+
489
+ def push(self, value: T):
490
+ if value != self.tos():
491
+ self._stack.append(value)
492
+
493
+ def pop(self) -> None:
494
+ if len(self) > 0:
495
+ self._stack.pop()
496
+
497
+ def tos(self) -> Optional[T]:
498
+ if len(self) > 0:
499
+ return self._stack[-1]
500
+ else:
501
+ return None
502
+ # raise ValueError("empty stack.")
503
+
504
+ def empty(self) -> bool:
505
+ return self._stack == []
506
+
507
+ def __len__(self) -> int:
508
+ return len(self._stack)
509
+
510
+ def __repr__(self) -> str:
511
+ result = []
512
+ for idx in range(len(self)):
513
+ result.append(str(self[idx]))
514
+ return "\n".join(result)
515
+
516
+ def __getitem__(self, ndx: int) -> T:
517
+ return self._stack[ndx]
518
+
519
+ __str__ = __repr__
520
+
521
+
522
+ class Executor(SingletonBase):
523
+ """"""
524
+
525
+ def __init__(self):
526
+ self.handlerStack = HandlerStack()
527
+ self.repeater = None
528
+ self.logger = logging.getLogger("PyXCP")
529
+ self.previous_error_code = None
530
+ self.error_code = None
531
+ self.func = None
532
+ self.arguments = None
533
+
534
+ def __call__(self, inst, func, arguments):
535
+ self.inst = inst
536
+ self.func = func
537
+ self.arguments = arguments
538
+ handler = Handler(inst, func, arguments)
539
+ self.handlerStack.push(handler)
540
+ connect_retries = inst.config.connect_retries
541
+ try:
542
+ while True:
543
+ try:
544
+ handler = self.handlerStack.tos()
545
+ res = handler.execute()
546
+ except XcpResponseError as e:
547
+ # self.logger.critical(f"XcpResponseError [{e.get_error_code()}]")
548
+ self.error_code = e.get_error_code()
549
+ handler.error_code = self.error_code
550
+ try:
551
+ svc = getattr(inst.service, "name", None)
552
+ # Derive a human-friendly error name if available
553
+ try:
554
+ err_name = (
555
+ getattr(XcpError, int(self.error_code)).name
556
+ if hasattr(XcpError, "__members__")
557
+ else str(self.error_code)
558
+ )
559
+ except Exception:
560
+ # Fallbacks: try enum-style .name or string conversion
561
+ err_name = getattr(self.error_code, "name", None) or str(self.error_code)
562
+ try:
563
+ err_code_int = int(self.error_code)
564
+ except Exception:
565
+ err_code_int = self.error_code # best effort
566
+ msg = f"XCP negative response: {err_name} (0x{err_code_int:02X})"
567
+ if svc:
568
+ msg += f" on service {svc}"
569
+ # Suppress noisy ERROR log if requested by caller context
570
+ if is_suppress_xcp_error_log():
571
+ self.logger.debug(
572
+ msg,
573
+ extra={
574
+ "event": "xcp_error_suppressed",
575
+ "service": svc,
576
+ "error_code": err_code_int,
577
+ "error_name": err_name,
578
+ },
579
+ )
580
+ else:
581
+ self.logger.error(
582
+ msg,
583
+ extra={
584
+ "event": "xcp_error",
585
+ "service": svc,
586
+ "error_code": err_code_int,
587
+ "error_name": err_name,
588
+ },
589
+ )
590
+ except Exception:
591
+ pass
592
+ except XcpTimeoutError:
593
+ is_connect = func.__name__ == "connect"
594
+ self.logger.warning(f"XcpTimeoutError -- Service: {func.__name__!r}")
595
+ self.error_code = XcpError.ERR_TIMEOUT
596
+ handler.error_code = self.error_code
597
+ if is_connect and connect_retries is not None:
598
+ if connect_retries == 0:
599
+ raise XcpTimeoutError("Maximum CONNECT retries reached.")
600
+ connect_retries -= 1
601
+ except TimeoutError:
602
+ raise
603
+ except can.CanError:
604
+ # self.logger.critical(f"Exception raised by Python CAN [{str(e)}]")
605
+ raise
606
+ except Exception:
607
+ # self.logger.critical(f"Exception [{str(e)}]")
608
+ raise
609
+ else:
610
+ self.error_code = None
611
+ self.handlerStack.pop()
612
+ if self.handlerStack.empty():
613
+ return res
614
+
615
+ if self.error_code == XcpError.ERR_CMD_SYNCH:
616
+ # Don't care about SYNCH for now...
617
+ self.inst.logger.info("SYNCH received.")
618
+ continue
619
+
620
+ if self.error_code is not None:
621
+ preActions, actions, repeater = handler.actions(*getActions(inst.service, self.error_code))
622
+ if handler.repeater is None:
623
+ handler.repeater = repeater
624
+ for f, a in reversed(preActions):
625
+ self.handlerStack.push(Handler(inst, f, a, self.error_code))
626
+ self.previous_error_code = self.error_code
627
+ if handler.repeater:
628
+ if handler.repeater.repeat():
629
+ continue
630
+ else:
631
+ msg = f"Max. repetition count reached while trying to execute service {handler.func.__name__!r}."
632
+ # Try to append diagnostics from the transport
633
+ try:
634
+ if hasattr(handler, "_append_diag"):
635
+ msg = handler._append_diag(msg)
636
+ except Exception:
637
+ pass
638
+ try:
639
+ self.logger.error(
640
+ "XCP unrecoverable",
641
+ extra={"event": "xcp_unrecoverable", "service": getattr(inst.service, "name", None)},
642
+ )
643
+ except Exception:
644
+ pass
645
+ raise UnrecoverableError(msg)
646
+ finally:
647
+ # cleanup of class variables
648
+ self.previous_error_code = None
649
+ while not self.handlerStack.empty():
650
+ self.handlerStack.pop()
651
+ self.error_code = None
652
+ self.func = None
653
+ self.arguments = None
654
+
655
+
656
+ def disable_error_handling(value: bool):
657
+ """Disable XCP error-handling (mainly for performance reasons)."""
658
+
659
+ global handle_errors
660
+ handle_errors = not bool(value)
661
+
662
+
663
+ def wrapped(func):
664
+ """This decorator is XCP error-handling enabled."""
665
+
666
+ @functools.wraps(func)
667
+ def inner(*args, **kwargs):
668
+ if handle_errors:
669
+ inst = args[0] # First parameter is 'self'.
670
+ arguments = Arguments(args[1:], kwargs)
671
+ executor = Executor()
672
+ res = executor(inst, func, arguments)
673
+ else:
674
+ res = func(*args, **kwargs)
675
+ return res
676
+
677
+ return inner