pyxcp 0.25.2__cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.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.
- pyxcp/__init__.py +20 -0
- pyxcp/aml/EtasCANMonitoring.a2l +82 -0
- pyxcp/aml/EtasCANMonitoring.aml +67 -0
- pyxcp/aml/XCP_Common.aml +408 -0
- pyxcp/aml/XCPonCAN.aml +78 -0
- pyxcp/aml/XCPonEth.aml +33 -0
- pyxcp/aml/XCPonFlx.aml +113 -0
- pyxcp/aml/XCPonSxI.aml +66 -0
- pyxcp/aml/XCPonUSB.aml +106 -0
- pyxcp/aml/ifdata_CAN.a2l +20 -0
- pyxcp/aml/ifdata_Eth.a2l +11 -0
- pyxcp/aml/ifdata_Flx.a2l +94 -0
- pyxcp/aml/ifdata_SxI.a2l +13 -0
- pyxcp/aml/ifdata_USB.a2l +81 -0
- pyxcp/asam/__init__.py +0 -0
- pyxcp/asam/types.py +131 -0
- pyxcp/asamkeydll +0 -0
- pyxcp/asamkeydll.c +116 -0
- pyxcp/asamkeydll.sh +2 -0
- pyxcp/checksum.py +732 -0
- pyxcp/cmdline.py +83 -0
- pyxcp/config/__init__.py +1257 -0
- pyxcp/config/legacy.py +120 -0
- pyxcp/constants.py +47 -0
- pyxcp/cpp_ext/__init__.py +0 -0
- pyxcp/cpp_ext/aligned_buffer.hpp +168 -0
- pyxcp/cpp_ext/bin.hpp +105 -0
- pyxcp/cpp_ext/blockmem.hpp +58 -0
- pyxcp/cpp_ext/cpp_ext.cpython-310-x86_64-linux-gnu.so +0 -0
- pyxcp/cpp_ext/cpp_ext.cpython-311-x86_64-linux-gnu.so +0 -0
- pyxcp/cpp_ext/cpp_ext.cpython-312-x86_64-linux-gnu.so +0 -0
- pyxcp/cpp_ext/cpp_ext.cpython-313-x86_64-linux-gnu.so +0 -0
- pyxcp/cpp_ext/daqlist.hpp +374 -0
- pyxcp/cpp_ext/event.hpp +67 -0
- pyxcp/cpp_ext/extension_wrapper.cpp +131 -0
- pyxcp/cpp_ext/framing.hpp +360 -0
- pyxcp/cpp_ext/helper.hpp +280 -0
- pyxcp/cpp_ext/mcobject.hpp +248 -0
- pyxcp/cpp_ext/sxi_framing.hpp +332 -0
- pyxcp/cpp_ext/tsqueue.hpp +46 -0
- pyxcp/daq_stim/__init__.py +306 -0
- pyxcp/daq_stim/optimize/__init__.py +67 -0
- pyxcp/daq_stim/optimize/binpacking.py +41 -0
- pyxcp/daq_stim/scheduler.cpp +62 -0
- pyxcp/daq_stim/scheduler.hpp +75 -0
- pyxcp/daq_stim/stim.cpp +13 -0
- pyxcp/daq_stim/stim.cpython-310-x86_64-linux-gnu.so +0 -0
- pyxcp/daq_stim/stim.cpython-311-x86_64-linux-gnu.so +0 -0
- pyxcp/daq_stim/stim.cpython-312-x86_64-linux-gnu.so +0 -0
- pyxcp/daq_stim/stim.cpython-313-x86_64-linux-gnu.so +0 -0
- pyxcp/daq_stim/stim.hpp +604 -0
- pyxcp/daq_stim/stim_wrapper.cpp +50 -0
- pyxcp/dllif.py +100 -0
- pyxcp/errormatrix.py +878 -0
- pyxcp/examples/conf_can.toml +19 -0
- pyxcp/examples/conf_can_user.toml +16 -0
- pyxcp/examples/conf_can_vector.json +11 -0
- pyxcp/examples/conf_can_vector.toml +11 -0
- pyxcp/examples/conf_eth.toml +9 -0
- pyxcp/examples/conf_nixnet.json +20 -0
- pyxcp/examples/conf_socket_can.toml +12 -0
- pyxcp/examples/run_daq.py +165 -0
- pyxcp/examples/xcp_policy.py +60 -0
- pyxcp/examples/xcp_read_benchmark.py +38 -0
- pyxcp/examples/xcp_skel.py +48 -0
- pyxcp/examples/xcp_unlock.py +38 -0
- pyxcp/examples/xcp_user_supplied_driver.py +43 -0
- pyxcp/examples/xcphello.py +79 -0
- pyxcp/examples/xcphello_recorder.py +107 -0
- pyxcp/master/__init__.py +10 -0
- pyxcp/master/errorhandler.py +677 -0
- pyxcp/master/master.py +2645 -0
- pyxcp/py.typed +0 -0
- pyxcp/recorder/.idea/.gitignore +8 -0
- pyxcp/recorder/.idea/misc.xml +4 -0
- pyxcp/recorder/.idea/modules.xml +8 -0
- pyxcp/recorder/.idea/recorder.iml +6 -0
- pyxcp/recorder/.idea/sonarlint/issuestore/3/8/3808afc69ac1edb9d760000a2f137335b1b99728 +7 -0
- pyxcp/recorder/.idea/sonarlint/issuestore/9/a/9a2aa4db38d3115ed60da621e012c0efc0172aae +0 -0
- pyxcp/recorder/.idea/sonarlint/issuestore/b/4/b49006702b459496a8e8c94ebe60947108361b91 +0 -0
- pyxcp/recorder/.idea/sonarlint/issuestore/index.pb +7 -0
- pyxcp/recorder/.idea/sonarlint/securityhotspotstore/3/8/3808afc69ac1edb9d760000a2f137335b1b99728 +0 -0
- pyxcp/recorder/.idea/sonarlint/securityhotspotstore/9/a/9a2aa4db38d3115ed60da621e012c0efc0172aae +0 -0
- pyxcp/recorder/.idea/sonarlint/securityhotspotstore/b/4/b49006702b459496a8e8c94ebe60947108361b91 +0 -0
- pyxcp/recorder/.idea/sonarlint/securityhotspotstore/index.pb +7 -0
- pyxcp/recorder/.idea/vcs.xml +10 -0
- pyxcp/recorder/__init__.py +96 -0
- pyxcp/recorder/build_clang.cmd +1 -0
- pyxcp/recorder/build_clang.sh +2 -0
- pyxcp/recorder/build_gcc.cmd +1 -0
- pyxcp/recorder/build_gcc.sh +2 -0
- pyxcp/recorder/build_gcc_arm.sh +2 -0
- pyxcp/recorder/converter/__init__.py +445 -0
- pyxcp/recorder/lz4.c +2829 -0
- pyxcp/recorder/lz4.h +879 -0
- pyxcp/recorder/lz4hc.c +2041 -0
- pyxcp/recorder/lz4hc.h +413 -0
- pyxcp/recorder/mio.hpp +1714 -0
- pyxcp/recorder/reader.hpp +138 -0
- pyxcp/recorder/reco.py +278 -0
- pyxcp/recorder/recorder.rst +0 -0
- pyxcp/recorder/rekorder.cpp +59 -0
- pyxcp/recorder/rekorder.cpython-310-x86_64-linux-gnu.so +0 -0
- pyxcp/recorder/rekorder.cpython-311-x86_64-linux-gnu.so +0 -0
- pyxcp/recorder/rekorder.cpython-312-x86_64-linux-gnu.so +0 -0
- pyxcp/recorder/rekorder.cpython-313-x86_64-linux-gnu.so +0 -0
- pyxcp/recorder/rekorder.hpp +274 -0
- pyxcp/recorder/setup.py +41 -0
- pyxcp/recorder/test_reko.py +34 -0
- pyxcp/recorder/unfolder.hpp +1354 -0
- pyxcp/recorder/wrap.cpp +184 -0
- pyxcp/recorder/writer.hpp +302 -0
- pyxcp/scripts/__init__.py +0 -0
- pyxcp/scripts/pyxcp_probe_can_drivers.py +20 -0
- pyxcp/scripts/xcp_examples.py +64 -0
- pyxcp/scripts/xcp_fetch_a2l.py +40 -0
- pyxcp/scripts/xcp_id_scanner.py +18 -0
- pyxcp/scripts/xcp_info.py +144 -0
- pyxcp/scripts/xcp_profile.py +26 -0
- pyxcp/scripts/xmraw_converter.py +31 -0
- pyxcp/stim/__init__.py +0 -0
- pyxcp/tests/test_asam_types.py +24 -0
- pyxcp/tests/test_binpacking.py +186 -0
- pyxcp/tests/test_can.py +1324 -0
- pyxcp/tests/test_checksum.py +95 -0
- pyxcp/tests/test_daq.py +193 -0
- pyxcp/tests/test_daq_opt.py +426 -0
- pyxcp/tests/test_frame_padding.py +156 -0
- pyxcp/tests/test_framing.py +262 -0
- pyxcp/tests/test_master.py +2116 -0
- pyxcp/tests/test_transport.py +177 -0
- pyxcp/tests/test_utils.py +30 -0
- pyxcp/timing.py +60 -0
- pyxcp/transport/__init__.py +13 -0
- pyxcp/transport/base.py +484 -0
- pyxcp/transport/base_transport.hpp +0 -0
- pyxcp/transport/can.py +660 -0
- pyxcp/transport/eth.py +254 -0
- pyxcp/transport/sxi.py +209 -0
- pyxcp/transport/transport_ext.hpp +214 -0
- pyxcp/transport/transport_wrapper.cpp +249 -0
- pyxcp/transport/usb_transport.py +229 -0
- pyxcp/types.py +987 -0
- pyxcp/utils.py +127 -0
- pyxcp/vector/__init__.py +0 -0
- pyxcp/vector/map.py +82 -0
- pyxcp-0.25.2.dist-info/METADATA +341 -0
- pyxcp-0.25.2.dist-info/RECORD +151 -0
- pyxcp-0.25.2.dist-info/WHEEL +6 -0
- pyxcp-0.25.2.dist-info/entry_points.txt +9 -0
- pyxcp-0.25.2.dist-info/licenses/LICENSE +165 -0
pyxcp/master/__init__.py
ADDED
|
@@ -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
|