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/transport/base.py
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
import abc
|
|
3
|
+
import logging
|
|
4
|
+
import threading
|
|
5
|
+
from collections import deque
|
|
6
|
+
from typing import Any, Dict, Optional, Type
|
|
7
|
+
from pyxcp.timing import Timing
|
|
8
|
+
import pyxcp.types as types
|
|
9
|
+
|
|
10
|
+
from pyxcp.cpp_ext.cpp_ext import Timestamp, TimestampType
|
|
11
|
+
from pyxcp.transport.transport_ext import (
|
|
12
|
+
FrameCategory,
|
|
13
|
+
FrameAcquisitionPolicy,
|
|
14
|
+
LegacyFrameAcquisitionPolicy,
|
|
15
|
+
XcpFraming,
|
|
16
|
+
XcpFramingConfig,
|
|
17
|
+
XcpTransportLayerType, # noqa: F401
|
|
18
|
+
ChecksumType, # noqa: F401
|
|
19
|
+
)
|
|
20
|
+
from pyxcp.utils import (
|
|
21
|
+
CurrentDatetime,
|
|
22
|
+
hexDump,
|
|
23
|
+
seconds_to_nanoseconds,
|
|
24
|
+
short_sleep,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class EmptyFrameError(Exception):
|
|
29
|
+
"""Raised when an empty frame is received."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def parse_header_format(header_format: str) -> tuple:
|
|
33
|
+
"""SxI and USB framing is configurable."""
|
|
34
|
+
if header_format == "HEADER_LEN_BYTE":
|
|
35
|
+
return 1, 0, 0
|
|
36
|
+
elif header_format == "HEADER_LEN_CTR_BYTE":
|
|
37
|
+
return 1, 1, 0
|
|
38
|
+
elif header_format == "HEADER_LEN_FILL_BYTE":
|
|
39
|
+
return 1, 0, 1
|
|
40
|
+
elif header_format == "HEADER_LEN_WORD":
|
|
41
|
+
return 2, 0, 0
|
|
42
|
+
elif header_format == "HEADER_LEN_CTR_WORD":
|
|
43
|
+
return 2, 2, 0
|
|
44
|
+
elif header_format == "HEADER_LEN_FILL_WORD":
|
|
45
|
+
return 2, 0, 2
|
|
46
|
+
else:
|
|
47
|
+
raise ValueError(f"Invalid header format: {header_format}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class BaseTransport(metaclass=abc.ABCMeta):
|
|
51
|
+
"""Base class for transport-layers (Can, Eth, Sxi).
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
config: dict-like
|
|
56
|
+
Parameters like bitrate.
|
|
57
|
+
loglevel: ["INFO", "WARN", "DEBUG", "ERROR", "CRITICAL"]
|
|
58
|
+
Controls the verbosity of log messages.
|
|
59
|
+
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
config,
|
|
65
|
+
framing_config: XcpFramingConfig,
|
|
66
|
+
policy: Optional[FrameAcquisitionPolicy] = None,
|
|
67
|
+
transport_layer_interface: Optional[Any] = None,
|
|
68
|
+
):
|
|
69
|
+
self.has_user_supplied_interface: bool = transport_layer_interface is not None
|
|
70
|
+
self.transport_layer_interface: Optional[Any] = transport_layer_interface
|
|
71
|
+
self.parent = None
|
|
72
|
+
self.framing = XcpFraming(framing_config)
|
|
73
|
+
self.policy: FrameAcquisitionPolicy = policy or LegacyFrameAcquisitionPolicy()
|
|
74
|
+
self.closeEvent: threading.Event = threading.Event()
|
|
75
|
+
|
|
76
|
+
self.command_lock: threading.Lock = threading.Lock()
|
|
77
|
+
self.policy_lock: threading.Lock = threading.Lock()
|
|
78
|
+
|
|
79
|
+
self.logger = logging.getLogger("PyXCP")
|
|
80
|
+
self._debug: bool = self.logger.level == 10
|
|
81
|
+
if transport_layer_interface:
|
|
82
|
+
self.logger.info(f"Transport - User Supplied Transport-Layer Interface: '{transport_layer_interface!s}'")
|
|
83
|
+
self.counter_received: int = -1
|
|
84
|
+
self.create_daq_timestamps: bool = config.create_daq_timestamps
|
|
85
|
+
self.timestamp = Timestamp(TimestampType.ABSOLUTE_TS)
|
|
86
|
+
self._start_datetime: CurrentDatetime = CurrentDatetime(self.timestamp.initial_value)
|
|
87
|
+
self.alignment: int = config.alignment
|
|
88
|
+
self.timeout: int = seconds_to_nanoseconds(config.timeout)
|
|
89
|
+
self.timer_restart_event: threading.Event = threading.Event()
|
|
90
|
+
self.timing: Timing = Timing()
|
|
91
|
+
self.resQueue: deque = deque()
|
|
92
|
+
self.listener: threading.Thread = threading.Thread(
|
|
93
|
+
target=self.listen,
|
|
94
|
+
args=(),
|
|
95
|
+
kwargs={},
|
|
96
|
+
daemon=True,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
self.first_daq_timestamp: Optional[int] = None
|
|
100
|
+
# self.timestamp_origin = self.timestamp.value
|
|
101
|
+
# self.datetime_origin = datetime.fromtimestamp(self.timestamp_origin)
|
|
102
|
+
self.pre_send_timestamp: int = self.timestamp.value
|
|
103
|
+
self.post_send_timestamp: int = self.timestamp.value
|
|
104
|
+
self.recv_timestamp: int = self.timestamp.value
|
|
105
|
+
# Ring buffer for last PDUs to aid diagnostics on failures
|
|
106
|
+
try:
|
|
107
|
+
from collections import deque as _dq
|
|
108
|
+
|
|
109
|
+
self._last_pdus = _dq(maxlen=200)
|
|
110
|
+
except Exception:
|
|
111
|
+
self._last_pdus = []
|
|
112
|
+
|
|
113
|
+
def __del__(self) -> None:
|
|
114
|
+
self.finish_listener()
|
|
115
|
+
self.close_connection()
|
|
116
|
+
|
|
117
|
+
def load_config(self, config) -> None:
|
|
118
|
+
"""Load configuration data."""
|
|
119
|
+
class_name: str = self.__class__.__name__.lower()
|
|
120
|
+
self.config: Any = getattr(config, class_name)
|
|
121
|
+
|
|
122
|
+
def close(self) -> None:
|
|
123
|
+
"""Close the transport-layer connection and event-loop."""
|
|
124
|
+
self.finish_listener()
|
|
125
|
+
# Avoid indefinite blocking on buggy threads
|
|
126
|
+
try:
|
|
127
|
+
if self.listener.is_alive():
|
|
128
|
+
self.listener.join(timeout=2.0)
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
self.close_connection()
|
|
132
|
+
|
|
133
|
+
@abc.abstractmethod
|
|
134
|
+
def connect(self) -> None:
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
def get(self):
|
|
138
|
+
"""Get an item from a deque considering a timeout condition."""
|
|
139
|
+
start: int = self.timestamp.value
|
|
140
|
+
while not self.resQueue:
|
|
141
|
+
if self.timer_restart_event.is_set():
|
|
142
|
+
start: int = self.timestamp.value
|
|
143
|
+
self.timer_restart_event.clear()
|
|
144
|
+
if self.timestamp.value - start > self.timeout:
|
|
145
|
+
raise EmptyFrameError
|
|
146
|
+
short_sleep()
|
|
147
|
+
item = self.resQueue.popleft()
|
|
148
|
+
return item
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def start_datetime(self) -> int:
|
|
152
|
+
"""datetime of program start.
|
|
153
|
+
|
|
154
|
+
Returns
|
|
155
|
+
-------
|
|
156
|
+
int
|
|
157
|
+
"""
|
|
158
|
+
return self._start_datetime
|
|
159
|
+
|
|
160
|
+
def start_listener(self):
|
|
161
|
+
if self.listener.is_alive():
|
|
162
|
+
self.finish_listener()
|
|
163
|
+
# Avoid indefinite blocking on buggy threads
|
|
164
|
+
self.listener.join(timeout=2.0)
|
|
165
|
+
|
|
166
|
+
# Ensure the close event is cleared before starting a new listener thread.
|
|
167
|
+
if hasattr(self, "closeEvent"):
|
|
168
|
+
self.closeEvent.clear()
|
|
169
|
+
|
|
170
|
+
self.listener = threading.Thread(target=self.listen, daemon=True)
|
|
171
|
+
self.listener.start()
|
|
172
|
+
|
|
173
|
+
def finish_listener(self):
|
|
174
|
+
if hasattr(self, "closeEvent"):
|
|
175
|
+
self.closeEvent.set()
|
|
176
|
+
|
|
177
|
+
def _request_internal(self, cmd, ignore_timeout=False, *data):
|
|
178
|
+
with self.command_lock:
|
|
179
|
+
frame = self._prepare_request(cmd, *data)
|
|
180
|
+
self.timing.start()
|
|
181
|
+
with self.policy_lock:
|
|
182
|
+
self.policy.feed(FrameCategory.CMD, self.framing.counter_send, self.timestamp.value, frame)
|
|
183
|
+
self.send(frame)
|
|
184
|
+
try:
|
|
185
|
+
xcpPDU = self.get()
|
|
186
|
+
except EmptyFrameError:
|
|
187
|
+
if not ignore_timeout:
|
|
188
|
+
MSG = f"Response timed out (timeout={self.timeout / 1_000_000_000}s)"
|
|
189
|
+
with self.policy_lock:
|
|
190
|
+
self.policy.feed(
|
|
191
|
+
FrameCategory.METADATA, self.framing.counter_send, self.timestamp.value, bytes(MSG, "ascii")
|
|
192
|
+
) if self._diagnostics_enabled() else ""
|
|
193
|
+
self.logger.debug("XCP request timeout", extra={"event": "timeout", "command": cmd.name})
|
|
194
|
+
raise types.XcpTimeoutError(MSG) from None
|
|
195
|
+
else:
|
|
196
|
+
self.timing.stop()
|
|
197
|
+
return
|
|
198
|
+
self.timing.stop()
|
|
199
|
+
pid = types.Response.parse(xcpPDU).type
|
|
200
|
+
if pid == "ERR" and cmd.name != "SYNCH":
|
|
201
|
+
with self.policy_lock:
|
|
202
|
+
self.policy.feed(FrameCategory.ERROR, self.counter_received, self.timestamp.value, xcpPDU[1:])
|
|
203
|
+
err = types.XcpError.parse(xcpPDU[1:])
|
|
204
|
+
raise types.XcpResponseError(err)
|
|
205
|
+
return xcpPDU[1:]
|
|
206
|
+
|
|
207
|
+
def request(self, cmd, *data):
|
|
208
|
+
return self._request_internal(cmd, False, *data)
|
|
209
|
+
|
|
210
|
+
def request_optional_response(self, cmd, *data):
|
|
211
|
+
return self._request_internal(cmd, True, *data)
|
|
212
|
+
|
|
213
|
+
def block_request(self, cmd, *data):
|
|
214
|
+
"""
|
|
215
|
+
Implements packet transmission for block communication model (e.g. DOWNLOAD block mode)
|
|
216
|
+
All parameters are the same as in request(), but it does not receive response.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
# check response queue before each block request, so that if the slave device
|
|
220
|
+
# has responded with a negative response (e.g. ACCESS_DENIED or SEQUENCE_ERROR), we can
|
|
221
|
+
# process it.
|
|
222
|
+
if self.resQueue:
|
|
223
|
+
xcpPDU = self.resQueue.popleft()
|
|
224
|
+
pid = types.Response.parse(xcpPDU).type
|
|
225
|
+
if pid == "ERR" and cmd.name != "SYNCH":
|
|
226
|
+
err = types.XcpError.parse(xcpPDU[1:])
|
|
227
|
+
raise types.XcpResponseError(err)
|
|
228
|
+
with self.command_lock:
|
|
229
|
+
if isinstance(data, list):
|
|
230
|
+
data = data[0] # C++ interfacing.
|
|
231
|
+
frame = self._prepare_request(cmd, *data)
|
|
232
|
+
with self.policy_lock:
|
|
233
|
+
self.policy.feed(
|
|
234
|
+
FrameCategory.CMD if int(cmd) >= 0xC0 else FrameCategory.STIM,
|
|
235
|
+
self.framing.counter_send,
|
|
236
|
+
self.timestamp.value,
|
|
237
|
+
frame,
|
|
238
|
+
)
|
|
239
|
+
self.send(frame)
|
|
240
|
+
|
|
241
|
+
def _prepare_request(self, cmd, *data):
|
|
242
|
+
"""
|
|
243
|
+
Prepares a request to be sent
|
|
244
|
+
"""
|
|
245
|
+
if self._debug:
|
|
246
|
+
self.logger.debug(cmd.name)
|
|
247
|
+
self.parent._setService(cmd)
|
|
248
|
+
frame = self.framing.prepare_request(cmd, *data)
|
|
249
|
+
if self._debug:
|
|
250
|
+
self.logger.debug(f"-> {hexDump(frame)}")
|
|
251
|
+
return frame
|
|
252
|
+
|
|
253
|
+
def block_receive(self, length_required: int) -> bytes:
|
|
254
|
+
"""
|
|
255
|
+
Implements packet reception for block communication model
|
|
256
|
+
(e.g. for XCP on CAN)
|
|
257
|
+
|
|
258
|
+
Parameters
|
|
259
|
+
----------
|
|
260
|
+
length_required: int
|
|
261
|
+
number of bytes to be expected in block response packets
|
|
262
|
+
|
|
263
|
+
Returns
|
|
264
|
+
-------
|
|
265
|
+
bytes
|
|
266
|
+
all payload bytes received in block response packets
|
|
267
|
+
|
|
268
|
+
Raises
|
|
269
|
+
------
|
|
270
|
+
:class:`pyxcp.types.XcpTimeoutError`
|
|
271
|
+
"""
|
|
272
|
+
block_response = b""
|
|
273
|
+
start = self.timestamp.value
|
|
274
|
+
while len(block_response) < length_required:
|
|
275
|
+
if len(self.resQueue):
|
|
276
|
+
partial_response = self.resQueue.popleft()
|
|
277
|
+
block_response += partial_response[1:]
|
|
278
|
+
else:
|
|
279
|
+
if self.timestamp.value - start > self.timeout:
|
|
280
|
+
waited = (self.timestamp.value - start) / 1e9 if hasattr(self.timestamp, "value") else None
|
|
281
|
+
msg = f"Response timed out [block_receive]: received {len(block_response)} of {length_required} bytes"
|
|
282
|
+
if waited is not None:
|
|
283
|
+
msg += f" after {waited:.3f}s"
|
|
284
|
+
# Attach diagnostics
|
|
285
|
+
# diag = self._build_diagnostics_dump() if self._diagnostics_enabled() else ""
|
|
286
|
+
self.logger.debug("XCP block_receive timeout", extra={"event": "timeout"})
|
|
287
|
+
raise types.XcpTimeoutError(msg) from None
|
|
288
|
+
short_sleep()
|
|
289
|
+
return block_response
|
|
290
|
+
|
|
291
|
+
@abc.abstractmethod
|
|
292
|
+
def send(self, frame):
|
|
293
|
+
pass
|
|
294
|
+
|
|
295
|
+
@abc.abstractmethod
|
|
296
|
+
def close_connection(self):
|
|
297
|
+
"""Does the actual connection shutdown.
|
|
298
|
+
Needs to be implemented by any sub-class.
|
|
299
|
+
"""
|
|
300
|
+
pass
|
|
301
|
+
|
|
302
|
+
@abc.abstractmethod
|
|
303
|
+
def listen(self):
|
|
304
|
+
pass
|
|
305
|
+
|
|
306
|
+
def process_event_packet(self, packet):
|
|
307
|
+
packet = packet[1:]
|
|
308
|
+
ev_type = packet[0]
|
|
309
|
+
self.logger.debug(f"EVENT-PACKET: {hexDump(packet)}")
|
|
310
|
+
if ev_type == types.Event.EV_CMD_PENDING:
|
|
311
|
+
self.timer_restart_event.set()
|
|
312
|
+
|
|
313
|
+
def process_response(self, response: bytes, length: int, counter: int, recv_timestamp: int) -> None:
|
|
314
|
+
# Important: determine PID first so duplicate counter handling can be applied selectively.
|
|
315
|
+
pid = response[0]
|
|
316
|
+
|
|
317
|
+
if pid >= 0xFC:
|
|
318
|
+
# Do not drop RESPONSE/EVENT/SERV frames even if the transport counter repeats.
|
|
319
|
+
# Some slaves may reuse the counter while DAQ traffic is active, and we must not lose
|
|
320
|
+
# command responses; otherwise request() can stall until timeout.
|
|
321
|
+
if self._debug:
|
|
322
|
+
self.logger.debug(f"<- L{length} C{counter} {hexDump(response)}")
|
|
323
|
+
self.counter_received = counter
|
|
324
|
+
# Record incoming non-DAQ frames for diagnostics
|
|
325
|
+
self._record_pdu(
|
|
326
|
+
"in",
|
|
327
|
+
(FrameCategory.RESPONSE if pid >= 0xFE else FrameCategory.SERV if pid == 0xFC else FrameCategory.EVENT),
|
|
328
|
+
counter,
|
|
329
|
+
recv_timestamp,
|
|
330
|
+
response,
|
|
331
|
+
length,
|
|
332
|
+
)
|
|
333
|
+
if pid >= 0xFE:
|
|
334
|
+
self.resQueue.append(response)
|
|
335
|
+
with self.policy_lock:
|
|
336
|
+
self.policy.feed(FrameCategory.RESPONSE, self.counter_received, self.timestamp.value, response)
|
|
337
|
+
self.recv_timestamp = recv_timestamp
|
|
338
|
+
elif pid == 0xFD:
|
|
339
|
+
self.process_event_packet(response)
|
|
340
|
+
with self.policy_lock:
|
|
341
|
+
self.policy.feed(FrameCategory.EVENT, self.counter_received, self.timestamp.value, response)
|
|
342
|
+
elif pid == 0xFC:
|
|
343
|
+
with self.policy_lock:
|
|
344
|
+
self.policy.feed(FrameCategory.SERV, self.counter_received, self.timestamp.value, response)
|
|
345
|
+
else:
|
|
346
|
+
# DAQ traffic: Some transports reuse or do not advance the counter for DAQ frames.
|
|
347
|
+
# Do not drop DAQ frames on duplicate counters to avoid losing measurements.
|
|
348
|
+
if counter == self.counter_received:
|
|
349
|
+
self.logger.debug(f"Duplicate message counter {counter} received (DAQ) - not dropping")
|
|
350
|
+
# DAQ still flowing – reset request timeout window to avoid false timeouts while
|
|
351
|
+
# the slave is busy but has not yet responded to a command.
|
|
352
|
+
self.timer_restart_event.set()
|
|
353
|
+
# Fall through and process the frame as usual.
|
|
354
|
+
self.counter_received = counter
|
|
355
|
+
if self._debug:
|
|
356
|
+
self.logger.debug(f"<- L{length} C{counter} ODT_Data[0:8] {hexDump(response[:8])}")
|
|
357
|
+
if self.first_daq_timestamp is None:
|
|
358
|
+
self.first_daq_timestamp = recv_timestamp
|
|
359
|
+
if self.create_daq_timestamps:
|
|
360
|
+
timestamp = recv_timestamp
|
|
361
|
+
else:
|
|
362
|
+
timestamp = 0
|
|
363
|
+
# Record DAQ frame (only keep small prefix in payload string later)
|
|
364
|
+
self._record_pdu("in", FrameCategory.DAQ, counter, timestamp, response, length)
|
|
365
|
+
# DAQ activity indicates the slave is alive/busy; keep extending the wait window for any
|
|
366
|
+
# outstanding request, similar to EV_CMD_PENDING behavior on stacks that don't emit it.
|
|
367
|
+
self.timer_restart_event.set()
|
|
368
|
+
with self.policy_lock:
|
|
369
|
+
self.policy.feed(FrameCategory.DAQ, self.counter_received, timestamp, response)
|
|
370
|
+
|
|
371
|
+
def _record_pdu(
|
|
372
|
+
self,
|
|
373
|
+
direction: str,
|
|
374
|
+
category: FrameCategory,
|
|
375
|
+
counter: int,
|
|
376
|
+
timestamp: int,
|
|
377
|
+
payload: bytes,
|
|
378
|
+
length: Optional[int] = None,
|
|
379
|
+
) -> None:
|
|
380
|
+
try:
|
|
381
|
+
entry = {
|
|
382
|
+
"dir": direction,
|
|
383
|
+
"cat": category.name,
|
|
384
|
+
"ctr": int(counter),
|
|
385
|
+
"ts": int(timestamp),
|
|
386
|
+
"len": int(length if length is not None else len(payload)),
|
|
387
|
+
"data": hexDump(payload if category != FrameCategory.DAQ else payload[:8])[:512],
|
|
388
|
+
}
|
|
389
|
+
self._last_pdus.append(entry)
|
|
390
|
+
except Exception:
|
|
391
|
+
pass # nosec
|
|
392
|
+
|
|
393
|
+
def _build_diagnostics_dump(self) -> str:
|
|
394
|
+
import json as _json
|
|
395
|
+
|
|
396
|
+
# transport params
|
|
397
|
+
tp = {"transport": self.__class__.__name__}
|
|
398
|
+
cfg = getattr(self, "config", None)
|
|
399
|
+
# Extract common Eth/Can fields when available
|
|
400
|
+
for key in (
|
|
401
|
+
"host",
|
|
402
|
+
"port",
|
|
403
|
+
"protocol",
|
|
404
|
+
"ipv6",
|
|
405
|
+
"bind_to_address",
|
|
406
|
+
"bind_to_port",
|
|
407
|
+
"fd",
|
|
408
|
+
"bitrate",
|
|
409
|
+
"data_bitrate",
|
|
410
|
+
"can_id_master",
|
|
411
|
+
"can_id_slave",
|
|
412
|
+
):
|
|
413
|
+
if cfg is not None and hasattr(cfg, key):
|
|
414
|
+
try:
|
|
415
|
+
tp[key] = getattr(cfg, key)
|
|
416
|
+
except Exception:
|
|
417
|
+
pass # nosec
|
|
418
|
+
last_n = 20
|
|
419
|
+
try:
|
|
420
|
+
app = getattr(self.config, "parent", None)
|
|
421
|
+
app = getattr(app, "parent", None)
|
|
422
|
+
if app is not None and hasattr(app, "general") and hasattr(app.general, "diagnostics_last_pdus"):
|
|
423
|
+
last_n = int(app.general.diagnostics_last_pdus or last_n)
|
|
424
|
+
except Exception:
|
|
425
|
+
pass # nosec
|
|
426
|
+
pdus = list(self._last_pdus)[-last_n:]
|
|
427
|
+
payload = {
|
|
428
|
+
"transport_params": tp,
|
|
429
|
+
"last_pdus": pdus,
|
|
430
|
+
}
|
|
431
|
+
try:
|
|
432
|
+
body = _json.dumps(payload, ensure_ascii=False, default=str, indent=2)
|
|
433
|
+
except Exception:
|
|
434
|
+
body = str(payload)
|
|
435
|
+
# Add a small header to explain what follows
|
|
436
|
+
header = "--- Diagnostics (for troubleshooting) ---"
|
|
437
|
+
return f"{header}\n{body}"
|
|
438
|
+
|
|
439
|
+
def _diagnostics_enabled(self) -> bool:
|
|
440
|
+
try:
|
|
441
|
+
app = getattr(self.config, "parent", None)
|
|
442
|
+
app = getattr(app, "parent", None)
|
|
443
|
+
if app is not None and hasattr(app, "general"):
|
|
444
|
+
return bool(getattr(app.general, "diagnostics_on_failure", True))
|
|
445
|
+
except Exception:
|
|
446
|
+
return True
|
|
447
|
+
return True
|
|
448
|
+
|
|
449
|
+
# @abc.abstractproperty
|
|
450
|
+
# @property
|
|
451
|
+
# def transport_layer_interface(self) -> Any:
|
|
452
|
+
# pass
|
|
453
|
+
|
|
454
|
+
# @transport_layer_interface.setter
|
|
455
|
+
# def transport_layer_interface(self, value: Any) -> None:
|
|
456
|
+
# self._transport_layer_interface = value
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def create_transport(name: str, *args, **kws) -> BaseTransport:
|
|
460
|
+
"""Factory function for transports.
|
|
461
|
+
|
|
462
|
+
Returns
|
|
463
|
+
-------
|
|
464
|
+
:class:`BaseTransport` derived instance.
|
|
465
|
+
"""
|
|
466
|
+
name = name.lower()
|
|
467
|
+
transports = available_transports()
|
|
468
|
+
if name in transports:
|
|
469
|
+
transport_class: Type[BaseTransport] = transports[name]
|
|
470
|
+
else:
|
|
471
|
+
raise ValueError(f"{name!r} is an invalid transport -- please choose one of [{' | '.join(transports.keys())}].")
|
|
472
|
+
return transport_class(*args, **kws)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def available_transports() -> Dict[str, Type[BaseTransport]]:
|
|
476
|
+
"""List all subclasses of :class:`BaseTransport`.
|
|
477
|
+
|
|
478
|
+
Returns
|
|
479
|
+
-------
|
|
480
|
+
dict
|
|
481
|
+
name: class
|
|
482
|
+
"""
|
|
483
|
+
transports = BaseTransport.__subclasses__()
|
|
484
|
+
return {t.__name__.lower(): t for t in transports}
|
|
File without changes
|