pyxcp 0.23.3__cp312-cp312-win_arm64.whl → 0.25.6__cp312-cp312-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.
- pyxcp/__init__.py +1 -1
- pyxcp/asamkeydll.exe +0 -0
- pyxcp/cmdline.py +15 -30
- pyxcp/config/__init__.py +73 -20
- pyxcp/cpp_ext/aligned_buffer.hpp +168 -0
- pyxcp/cpp_ext/bin.hpp +7 -6
- pyxcp/cpp_ext/cpp_ext.cp310-win_arm64.pyd +0 -0
- pyxcp/cpp_ext/cpp_ext.cp311-win_arm64.pyd +0 -0
- pyxcp/cpp_ext/cpp_ext.cp312-win_arm64.pyd +0 -0
- pyxcp/cpp_ext/daqlist.hpp +241 -73
- pyxcp/cpp_ext/extension_wrapper.cpp +123 -15
- pyxcp/cpp_ext/framing.hpp +360 -0
- pyxcp/cpp_ext/mcobject.hpp +5 -3
- pyxcp/cpp_ext/sxi_framing.hpp +332 -0
- pyxcp/daq_stim/__init__.py +182 -45
- pyxcp/daq_stim/optimize/binpacking.py +2 -2
- pyxcp/daq_stim/scheduler.cpp +8 -8
- pyxcp/daq_stim/stim.cp310-win_arm64.pyd +0 -0
- pyxcp/daq_stim/stim.cp311-win_arm64.pyd +0 -0
- pyxcp/daq_stim/stim.cp312-win_arm64.pyd +0 -0
- pyxcp/errormatrix.py +2 -2
- pyxcp/examples/run_daq.py +5 -3
- pyxcp/examples/xcp_policy.py +6 -6
- pyxcp/examples/xcp_read_benchmark.py +2 -2
- pyxcp/examples/xcp_skel.py +1 -2
- pyxcp/examples/xcp_unlock.py +10 -12
- pyxcp/examples/xcp_user_supplied_driver.py +1 -2
- pyxcp/examples/xcphello.py +2 -15
- pyxcp/examples/xcphello_recorder.py +2 -2
- pyxcp/master/__init__.py +1 -0
- pyxcp/master/errorhandler.py +248 -13
- pyxcp/master/master.py +838 -250
- 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 +5 -10
- pyxcp/recorder/converter/__init__.py +4 -10
- pyxcp/recorder/reader.hpp +0 -1
- pyxcp/recorder/reco.py +1 -0
- pyxcp/recorder/rekorder.cp310-win_arm64.pyd +0 -0
- pyxcp/recorder/rekorder.cp311-win_arm64.pyd +0 -0
- pyxcp/recorder/rekorder.cp312-win_arm64.pyd +0 -0
- pyxcp/recorder/unfolder.hpp +129 -107
- pyxcp/recorder/wrap.cpp +3 -8
- pyxcp/scripts/xcp_fetch_a2l.py +2 -2
- pyxcp/scripts/xcp_id_scanner.py +1 -2
- pyxcp/scripts/xcp_info.py +66 -51
- pyxcp/scripts/xcp_profile.py +1 -2
- pyxcp/tests/test_daq.py +1 -1
- pyxcp/tests/test_framing.py +262 -0
- pyxcp/tests/test_master.py +210 -100
- pyxcp/tests/test_transport.py +138 -42
- pyxcp/timing.py +1 -1
- pyxcp/transport/__init__.py +8 -5
- pyxcp/transport/base.py +187 -143
- pyxcp/transport/can.py +117 -13
- pyxcp/transport/eth.py +55 -20
- pyxcp/transport/hdf5_policy.py +167 -0
- pyxcp/transport/sxi.py +126 -52
- pyxcp/transport/transport_ext.cp310-win_arm64.pyd +0 -0
- pyxcp/transport/transport_ext.cp311-win_arm64.pyd +0 -0
- pyxcp/transport/transport_ext.cp312-win_arm64.pyd +0 -0
- pyxcp/transport/transport_ext.hpp +214 -0
- pyxcp/transport/transport_wrapper.cpp +249 -0
- pyxcp/transport/usb_transport.py +47 -31
- pyxcp/types.py +0 -13
- pyxcp/{utils.py → utils/__init__.py} +3 -4
- pyxcp/utils/cli.py +78 -0
- pyxcp-0.25.6.dist-info/METADATA +341 -0
- pyxcp-0.25.6.dist-info/RECORD +153 -0
- {pyxcp-0.23.3.dist-info → pyxcp-0.25.6.dist-info}/WHEEL +1 -1
- pyxcp/examples/conf_sxi.json +0 -9
- pyxcp/examples/conf_sxi.toml +0 -7
- pyxcp-0.23.3.dist-info/METADATA +0 -219
- pyxcp-0.23.3.dist-info/RECORD +0 -131
- {pyxcp-0.23.3.dist-info → pyxcp-0.25.6.dist-info}/entry_points.txt +0 -0
- {pyxcp-0.23.3.dist-info → pyxcp-0.25.6.dist-info/licenses}/LICENSE +0 -0
pyxcp/transport/base.py
CHANGED
|
@@ -3,127 +3,50 @@ import abc
|
|
|
3
3
|
import logging
|
|
4
4
|
import threading
|
|
5
5
|
from collections import deque
|
|
6
|
-
from typing import Any, Dict, Optional,
|
|
7
|
-
|
|
6
|
+
from typing import Any, Dict, Optional, Type
|
|
7
|
+
from pyxcp.timing import Timing
|
|
8
8
|
import pyxcp.types as types
|
|
9
|
+
|
|
9
10
|
from pyxcp.cpp_ext.cpp_ext import Timestamp, TimestampType
|
|
10
|
-
from pyxcp.
|
|
11
|
-
|
|
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
|
+
)
|
|
12
20
|
from pyxcp.utils import (
|
|
13
21
|
CurrentDatetime,
|
|
14
|
-
flatten,
|
|
15
22
|
hexDump,
|
|
16
23
|
seconds_to_nanoseconds,
|
|
17
24
|
short_sleep,
|
|
18
25
|
)
|
|
19
26
|
|
|
20
27
|
|
|
21
|
-
class FrameAcquisitionPolicy:
|
|
22
|
-
"""
|
|
23
|
-
Base class for all frame acquisition policies.
|
|
24
|
-
|
|
25
|
-
Parameters
|
|
26
|
-
---------
|
|
27
|
-
filter_out: set or None
|
|
28
|
-
A set of frame types to filter out.
|
|
29
|
-
If None, all frame types are accepted for further processing.
|
|
30
|
-
|
|
31
|
-
Example: (FrameType.REQUEST, FrameType.RESPONSE, FrameType.EVENT, FrameType.SERV)
|
|
32
|
-
==> care only about DAQ frames.
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
|
-
def __init__(self, filter_out: Optional[Set[types.FrameCategory]] = None):
|
|
36
|
-
self._frame_types_to_filter_out = filter_out or set()
|
|
37
|
-
|
|
38
|
-
@property
|
|
39
|
-
def filtered_out(self) -> Set[types.FrameCategory]:
|
|
40
|
-
return self._frame_types_to_filter_out
|
|
41
|
-
|
|
42
|
-
def feed(self, frame_type: types.FrameCategory, counter: int, timestamp: int, payload: bytes) -> None: ... # noqa: E704
|
|
43
|
-
|
|
44
|
-
def finalize(self) -> None:
|
|
45
|
-
"""
|
|
46
|
-
Finalize the frame acquisition policy (if required).
|
|
47
|
-
"""
|
|
48
|
-
...
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
class NoOpPolicy(FrameAcquisitionPolicy):
|
|
52
|
-
"""
|
|
53
|
-
No operation / do nothing policy.
|
|
54
|
-
"""
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
class LegacyFrameAcquisitionPolicy(FrameAcquisitionPolicy):
|
|
58
|
-
"""Dequeue based frame acquisition policy.
|
|
59
|
-
|
|
60
|
-
Deprecated: Use only for compatibility reasons.
|
|
61
|
-
"""
|
|
62
|
-
|
|
63
|
-
def __init__(self, filter_out: Optional[Set[types.FrameCategory]] = None) -> None:
|
|
64
|
-
super().__init__(filter_out)
|
|
65
|
-
self.reqQueue = deque()
|
|
66
|
-
self.resQueue = deque()
|
|
67
|
-
self.daqQueue = deque()
|
|
68
|
-
self.evQueue = deque()
|
|
69
|
-
self.servQueue = deque()
|
|
70
|
-
self.metaQueue = deque()
|
|
71
|
-
self.errorQueue = deque()
|
|
72
|
-
self.stimQueue = deque()
|
|
73
|
-
self.QUEUE_MAP = {
|
|
74
|
-
types.FrameCategory.CMD: self.reqQueue,
|
|
75
|
-
types.FrameCategory.RESPONSE: self.resQueue,
|
|
76
|
-
types.FrameCategory.EVENT: self.evQueue,
|
|
77
|
-
types.FrameCategory.SERV: self.servQueue,
|
|
78
|
-
types.FrameCategory.DAQ: self.daqQueue,
|
|
79
|
-
types.FrameCategory.METADATA: self.metaQueue,
|
|
80
|
-
types.FrameCategory.ERROR: self.errorQueue,
|
|
81
|
-
types.FrameCategory.STIM: self.stimQueue,
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
def feed(self, frame_type: types.FrameCategory, counter: int, timestamp: int, payload: bytes) -> None:
|
|
85
|
-
if frame_type not in self.filtered_out:
|
|
86
|
-
queue = self.QUEUE_MAP.get(frame_type)
|
|
87
|
-
if queue is not None:
|
|
88
|
-
queue.append((counter, timestamp, payload))
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
class FrameRecorderPolicy(FrameAcquisitionPolicy):
|
|
92
|
-
"""Frame acquisition policy that records frames."""
|
|
93
|
-
|
|
94
|
-
def __init__(
|
|
95
|
-
self,
|
|
96
|
-
file_name: str,
|
|
97
|
-
filter_out: Optional[Set[types.FrameCategory]] = None,
|
|
98
|
-
prealloc: int = 10,
|
|
99
|
-
chunk_size: int = 1,
|
|
100
|
-
) -> None:
|
|
101
|
-
super().__init__(filter_out)
|
|
102
|
-
self.recorder = XcpLogFileWriter(file_name, prealloc=prealloc, chunk_size=chunk_size)
|
|
103
|
-
|
|
104
|
-
def feed(self, frame_type: types.FrameCategory, counter: int, timestamp: int, payload: bytes) -> None:
|
|
105
|
-
if frame_type not in self.filtered_out:
|
|
106
|
-
self.recorder.add_frame(frame_type, counter, timestamp, payload)
|
|
107
|
-
|
|
108
|
-
def finalize(self) -> None:
|
|
109
|
-
self.recorder.finalize()
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
class StdoutPolicy(FrameAcquisitionPolicy):
|
|
113
|
-
"""Frame acquisition policy that prints frames to stdout."""
|
|
114
|
-
|
|
115
|
-
def __init__(self, filter_out: Optional[Set[types.FrameCategory]] = None) -> None:
|
|
116
|
-
super().__init__(filter_out)
|
|
117
|
-
|
|
118
|
-
def feed(self, frame_type: types.FrameCategory, counter: int, timestamp: int, payload: bytes) -> None:
|
|
119
|
-
if frame_type not in self.filtered_out:
|
|
120
|
-
print(f"{frame_type.name:8} {counter:6} {timestamp:8d} {hexDump(payload)}")
|
|
121
|
-
|
|
122
|
-
|
|
123
28
|
class EmptyFrameError(Exception):
|
|
124
29
|
"""Raised when an empty frame is received."""
|
|
125
30
|
|
|
126
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
|
+
|
|
127
50
|
class BaseTransport(metaclass=abc.ABCMeta):
|
|
128
51
|
"""Base class for transport-layers (Can, Eth, Sxi).
|
|
129
52
|
|
|
@@ -136,10 +59,17 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
136
59
|
|
|
137
60
|
"""
|
|
138
61
|
|
|
139
|
-
def __init__(
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
config,
|
|
65
|
+
framing_config: XcpFramingConfig,
|
|
66
|
+
policy: Optional[FrameAcquisitionPolicy] = None,
|
|
67
|
+
transport_layer_interface: Optional[Any] = None,
|
|
68
|
+
):
|
|
140
69
|
self.has_user_supplied_interface: bool = transport_layer_interface is not None
|
|
141
70
|
self.transport_layer_interface: Optional[Any] = transport_layer_interface
|
|
142
71
|
self.parent = None
|
|
72
|
+
self.framing = XcpFraming(framing_config)
|
|
143
73
|
self.policy: FrameAcquisitionPolicy = policy or LegacyFrameAcquisitionPolicy()
|
|
144
74
|
self.closeEvent: threading.Event = threading.Event()
|
|
145
75
|
|
|
@@ -150,7 +80,6 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
150
80
|
self._debug: bool = self.logger.level == 10
|
|
151
81
|
if transport_layer_interface:
|
|
152
82
|
self.logger.info(f"Transport - User Supplied Transport-Layer Interface: '{transport_layer_interface!s}'")
|
|
153
|
-
self.counter_send: int = 0
|
|
154
83
|
self.counter_received: int = -1
|
|
155
84
|
self.create_daq_timestamps: bool = config.create_daq_timestamps
|
|
156
85
|
self.timestamp = Timestamp(TimestampType.ABSOLUTE_TS)
|
|
@@ -164,6 +93,7 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
164
93
|
target=self.listen,
|
|
165
94
|
args=(),
|
|
166
95
|
kwargs={},
|
|
96
|
+
daemon=True,
|
|
167
97
|
)
|
|
168
98
|
|
|
169
99
|
self.first_daq_timestamp: Optional[int] = None
|
|
@@ -172,6 +102,13 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
172
102
|
self.pre_send_timestamp: int = self.timestamp.value
|
|
173
103
|
self.post_send_timestamp: int = self.timestamp.value
|
|
174
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 = []
|
|
175
112
|
|
|
176
113
|
def __del__(self) -> None:
|
|
177
114
|
self.finish_listener()
|
|
@@ -185,8 +122,12 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
185
122
|
def close(self) -> None:
|
|
186
123
|
"""Close the transport-layer connection and event-loop."""
|
|
187
124
|
self.finish_listener()
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
190
131
|
self.close_connection()
|
|
191
132
|
|
|
192
133
|
@abc.abstractmethod
|
|
@@ -204,7 +145,6 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
204
145
|
raise EmptyFrameError
|
|
205
146
|
short_sleep()
|
|
206
147
|
item = self.resQueue.popleft()
|
|
207
|
-
# print("Q", item)
|
|
208
148
|
return item
|
|
209
149
|
|
|
210
150
|
@property
|
|
@@ -220,9 +160,14 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
220
160
|
def start_listener(self):
|
|
221
161
|
if self.listener.is_alive():
|
|
222
162
|
self.finish_listener()
|
|
223
|
-
|
|
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()
|
|
224
169
|
|
|
225
|
-
self.listener = threading.Thread(target=self.listen)
|
|
170
|
+
self.listener = threading.Thread(target=self.listen, daemon=True)
|
|
226
171
|
self.listener.start()
|
|
227
172
|
|
|
228
173
|
def finish_listener(self):
|
|
@@ -234,7 +179,7 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
234
179
|
frame = self._prepare_request(cmd, *data)
|
|
235
180
|
self.timing.start()
|
|
236
181
|
with self.policy_lock:
|
|
237
|
-
self.policy.feed(
|
|
182
|
+
self.policy.feed(FrameCategory.CMD, self.framing.counter_send, self.timestamp.value, frame)
|
|
238
183
|
self.send(frame)
|
|
239
184
|
try:
|
|
240
185
|
xcpPDU = self.get()
|
|
@@ -242,7 +187,10 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
242
187
|
if not ignore_timeout:
|
|
243
188
|
MSG = f"Response timed out (timeout={self.timeout / 1_000_000_000}s)"
|
|
244
189
|
with self.policy_lock:
|
|
245
|
-
self.policy.feed(
|
|
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})
|
|
246
194
|
raise types.XcpTimeoutError(MSG) from None
|
|
247
195
|
else:
|
|
248
196
|
self.timing.stop()
|
|
@@ -251,7 +199,7 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
251
199
|
pid = types.Response.parse(xcpPDU).type
|
|
252
200
|
if pid == "ERR" and cmd.name != "SYNCH":
|
|
253
201
|
with self.policy_lock:
|
|
254
|
-
self.policy.feed(
|
|
202
|
+
self.policy.feed(FrameCategory.ERROR, self.counter_received, self.timestamp.value, xcpPDU[1:])
|
|
255
203
|
err = types.XcpError.parse(xcpPDU[1:])
|
|
256
204
|
raise types.XcpResponseError(err)
|
|
257
205
|
return xcpPDU[1:]
|
|
@@ -283,8 +231,8 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
283
231
|
frame = self._prepare_request(cmd, *data)
|
|
284
232
|
with self.policy_lock:
|
|
285
233
|
self.policy.feed(
|
|
286
|
-
|
|
287
|
-
self.counter_send,
|
|
234
|
+
FrameCategory.CMD if int(cmd) >= 0xC0 else FrameCategory.STIM,
|
|
235
|
+
self.framing.counter_send,
|
|
288
236
|
self.timestamp.value,
|
|
289
237
|
frame,
|
|
290
238
|
)
|
|
@@ -297,19 +245,7 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
297
245
|
if self._debug:
|
|
298
246
|
self.logger.debug(cmd.name)
|
|
299
247
|
self.parent._setService(cmd)
|
|
300
|
-
|
|
301
|
-
cmd_len = cmd.bit_length() // 8 # calculate bytes needed for cmd
|
|
302
|
-
packet = bytes(flatten(cmd.to_bytes(cmd_len, "big"), data))
|
|
303
|
-
|
|
304
|
-
header = self.HEADER.pack(len(packet), self.counter_send)
|
|
305
|
-
self.counter_send = (self.counter_send + 1) & 0xFFFF
|
|
306
|
-
|
|
307
|
-
frame = header + packet
|
|
308
|
-
|
|
309
|
-
remainder = len(frame) % self.alignment
|
|
310
|
-
if remainder:
|
|
311
|
-
frame += b"\0" * (self.alignment - remainder)
|
|
312
|
-
|
|
248
|
+
frame = self.framing.prepare_request(cmd, *data)
|
|
313
249
|
if self._debug:
|
|
314
250
|
self.logger.debug(f"-> {hexDump(frame)}")
|
|
315
251
|
return frame
|
|
@@ -341,7 +277,14 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
341
277
|
block_response += partial_response[1:]
|
|
342
278
|
else:
|
|
343
279
|
if self.timestamp.value - start > self.timeout:
|
|
344
|
-
|
|
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
|
|
345
288
|
short_sleep()
|
|
346
289
|
return block_response
|
|
347
290
|
|
|
@@ -368,29 +311,47 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
368
311
|
self.timer_restart_event.set()
|
|
369
312
|
|
|
370
313
|
def process_response(self, response: bytes, length: int, counter: int, recv_timestamp: int) -> None:
|
|
371
|
-
|
|
372
|
-
self.logger.warning(f"Duplicate message counter {counter} received from the XCP slave")
|
|
373
|
-
if self._debug:
|
|
374
|
-
self.logger.debug(f"<- L{length} C{counter} {hexDump(response[:512])}")
|
|
375
|
-
return
|
|
376
|
-
self.counter_received = counter
|
|
314
|
+
# Important: determine PID first so duplicate counter handling can be applied selectively.
|
|
377
315
|
pid = response[0]
|
|
316
|
+
|
|
378
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.
|
|
379
321
|
if self._debug:
|
|
380
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
|
+
)
|
|
381
333
|
if pid >= 0xFE:
|
|
382
334
|
self.resQueue.append(response)
|
|
383
335
|
with self.policy_lock:
|
|
384
|
-
self.policy.feed(
|
|
336
|
+
self.policy.feed(FrameCategory.RESPONSE, self.counter_received, self.timestamp.value, response)
|
|
385
337
|
self.recv_timestamp = recv_timestamp
|
|
386
338
|
elif pid == 0xFD:
|
|
387
339
|
self.process_event_packet(response)
|
|
388
340
|
with self.policy_lock:
|
|
389
|
-
self.policy.feed(
|
|
341
|
+
self.policy.feed(FrameCategory.EVENT, self.counter_received, self.timestamp.value, response)
|
|
390
342
|
elif pid == 0xFC:
|
|
391
343
|
with self.policy_lock:
|
|
392
|
-
self.policy.feed(
|
|
344
|
+
self.policy.feed(FrameCategory.SERV, self.counter_received, self.timestamp.value, response)
|
|
393
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
|
|
394
355
|
if self._debug:
|
|
395
356
|
self.logger.debug(f"<- L{length} C{counter} ODT_Data[0:8] {hexDump(response[:8])}")
|
|
396
357
|
if self.first_daq_timestamp is None:
|
|
@@ -399,8 +360,91 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
399
360
|
timestamp = recv_timestamp
|
|
400
361
|
else:
|
|
401
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()
|
|
402
368
|
with self.policy_lock:
|
|
403
|
-
self.policy.feed(
|
|
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
|
|
404
448
|
|
|
405
449
|
# @abc.abstractproperty
|
|
406
450
|
# @property
|
pyxcp/transport/can.py
CHANGED
|
@@ -20,9 +20,14 @@ from can.interface import _get_class_for_interface
|
|
|
20
20
|
from rich.console import Console
|
|
21
21
|
|
|
22
22
|
from pyxcp.config import CAN_INTERFACE_MAP
|
|
23
|
-
from pyxcp.transport.base import
|
|
23
|
+
from pyxcp.transport.base import (
|
|
24
|
+
BaseTransport,
|
|
25
|
+
ChecksumType,
|
|
26
|
+
XcpFramingConfig,
|
|
27
|
+
XcpTransportLayerType,
|
|
28
|
+
)
|
|
24
29
|
|
|
25
|
-
from ..utils import seconds_to_nanoseconds
|
|
30
|
+
from ..utils import seconds_to_nanoseconds, short_sleep
|
|
26
31
|
|
|
27
32
|
|
|
28
33
|
console = Console()
|
|
@@ -292,7 +297,13 @@ class PythonCanWrapper:
|
|
|
292
297
|
self.timeout: int = timeout
|
|
293
298
|
self.parameters = parameters
|
|
294
299
|
if not self.parent.has_user_supplied_interface:
|
|
295
|
-
|
|
300
|
+
try:
|
|
301
|
+
self.can_interface_class = _get_class_for_interface(self.interface_name)
|
|
302
|
+
except Exception as ex:
|
|
303
|
+
# Provide clearer message if interface not supported by python-can on this platform
|
|
304
|
+
raise CanInitializationError(
|
|
305
|
+
f"Unsupported or unavailable CAN interface {self.interface_name!r}: {ex.__class__.__name__}: {ex}"
|
|
306
|
+
) from ex
|
|
296
307
|
else:
|
|
297
308
|
self.can_interface_class = None
|
|
298
309
|
self.can_interface: BusABC
|
|
@@ -320,7 +331,15 @@ class PythonCanWrapper:
|
|
|
320
331
|
self.can_interface.set_filters(merged_filters)
|
|
321
332
|
self.software_filter.set_filters(can_filters) # Filter unwanted traffic.
|
|
322
333
|
else:
|
|
323
|
-
|
|
334
|
+
try:
|
|
335
|
+
self.can_interface = self.can_interface_class(
|
|
336
|
+
interface=self.interface_name, can_filters=can_filters, **self.parameters
|
|
337
|
+
)
|
|
338
|
+
except OSError as ex:
|
|
339
|
+
# Typical when selecting socketcan on unsupported OS (e.g., Windows)
|
|
340
|
+
raise CanInitializationError(
|
|
341
|
+
f"OS error while creating CAN interface {self.interface_name!r}: {ex.__class__.__name__}: {ex}"
|
|
342
|
+
) from ex
|
|
324
343
|
self.software_filter.accept_all()
|
|
325
344
|
self.parent.logger.info(f"XCPonCAN - Using Interface: '{self.can_interface!s}'")
|
|
326
345
|
self.parent.logger.info(f"XCPonCAN - Filters used: {self.can_interface.filters}")
|
|
@@ -383,7 +402,15 @@ class Can(BaseTransport):
|
|
|
383
402
|
HEADER_SIZE = 0
|
|
384
403
|
|
|
385
404
|
def __init__(self, config, policy=None, transport_layer_interface: Optional[BusABC] = None):
|
|
386
|
-
|
|
405
|
+
framing_config = XcpFramingConfig(
|
|
406
|
+
transport_layer_type=XcpTransportLayerType.CAN,
|
|
407
|
+
header_len=0,
|
|
408
|
+
header_ctr=0,
|
|
409
|
+
header_fill=0,
|
|
410
|
+
tail_fill=False,
|
|
411
|
+
tail_cs=ChecksumType.NO_CHECKSUM,
|
|
412
|
+
)
|
|
413
|
+
super().__init__(config, framing_config, policy, transport_layer_interface)
|
|
387
414
|
self.load_config(config)
|
|
388
415
|
self.useDefaultListener = self.config.use_default_listener
|
|
389
416
|
self.can_id_master = Identifier(self.config.can_id_master)
|
|
@@ -401,13 +428,33 @@ class Can(BaseTransport):
|
|
|
401
428
|
self.padding_value = self.config.padding_value
|
|
402
429
|
if transport_layer_interface is None:
|
|
403
430
|
self.interface_name = self.config.interface
|
|
404
|
-
|
|
431
|
+
# On platforms that do not support certain backends (e.g., SocketCAN on Windows),
|
|
432
|
+
# python-can may raise OSError deep inside interface initialization. We want to
|
|
433
|
+
# fail fast with a clearer hint and avoid unhandled low-level errors.
|
|
434
|
+
try:
|
|
435
|
+
self.interface_configuration = detect_available_configs(interfaces=[self.interface_name])
|
|
436
|
+
except Exception as ex:
|
|
437
|
+
# Best-effort graceful message; keep original exception context
|
|
438
|
+
self.logger.critical(
|
|
439
|
+
f"XCPonCAN - Failed to query available configs for interface {self.interface_name!r}: {ex.__class__.__name__}: {ex}"
|
|
440
|
+
)
|
|
441
|
+
self.interface_configuration = []
|
|
405
442
|
parameters = self.get_interface_parameters()
|
|
406
443
|
else:
|
|
407
444
|
self.interface_name = "custom"
|
|
408
445
|
# print("TRY GET PARAMs", self.get_interface_parameters())
|
|
409
446
|
parameters = {}
|
|
410
|
-
|
|
447
|
+
try:
|
|
448
|
+
self.can_interface = PythonCanWrapper(self, self.interface_name, config.timeout, **parameters)
|
|
449
|
+
except OSError as ex:
|
|
450
|
+
# Catch platform-specific socket errors early (e.g., SocketCAN on Windows)
|
|
451
|
+
msg = (
|
|
452
|
+
f"XCPonCAN - Failed to initialize CAN interface {self.interface_name!r}: "
|
|
453
|
+
f"{ex.__class__.__name__}: {ex}.\n"
|
|
454
|
+
f"Hint: Interface may be unsupported on this OS or missing drivers."
|
|
455
|
+
)
|
|
456
|
+
self.logger.critical(msg)
|
|
457
|
+
raise CanInitializationError(msg) from ex
|
|
411
458
|
self.logger.info(f"XCPonCAN - Interface-Type: {self.interface_name!r} Parameters: {list(parameters.items())}")
|
|
412
459
|
self.logger.info(
|
|
413
460
|
f"XCPonCAN - Master-ID (Tx): 0x{self.can_id_master.id:08X}{self.can_id_master.type_str} -- "
|
|
@@ -451,23 +498,80 @@ class Can(BaseTransport):
|
|
|
451
498
|
)
|
|
452
499
|
|
|
453
500
|
def listen(self):
|
|
501
|
+
"""Process CAN frames received from the interface.
|
|
502
|
+
|
|
503
|
+
This method runs in a separate thread and continuously polls the CAN interface
|
|
504
|
+
for new frames. When a frame is received, it extracts the data and timestamp
|
|
505
|
+
and passes them to the data_received method for further processing.
|
|
506
|
+
|
|
507
|
+
The method includes periodic sleep to prevent CPU hogging and error handling
|
|
508
|
+
to ensure the listener thread doesn't crash on exceptions.
|
|
509
|
+
"""
|
|
510
|
+
# Cache frequently used methods and attributes for better performance
|
|
511
|
+
close_event_set = self.closeEvent.is_set
|
|
512
|
+
can_interface_read = self.can_interface.read
|
|
513
|
+
data_received = self.data_received
|
|
514
|
+
|
|
515
|
+
# State variables for processing
|
|
516
|
+
last_sleep = self.timestamp.value
|
|
517
|
+
FIVE_MS = 5_000_000 # Five milliseconds in nanoseconds
|
|
518
|
+
|
|
454
519
|
while True:
|
|
455
|
-
if
|
|
520
|
+
# Check if we should exit the loop
|
|
521
|
+
if close_event_set():
|
|
456
522
|
return
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
523
|
+
|
|
524
|
+
# Periodically sleep to prevent CPU hogging
|
|
525
|
+
if self.timestamp.value - last_sleep >= FIVE_MS:
|
|
526
|
+
short_sleep()
|
|
527
|
+
last_sleep = self.timestamp.value
|
|
528
|
+
|
|
529
|
+
try:
|
|
530
|
+
# Try to read a frame from the CAN interface
|
|
531
|
+
frame = can_interface_read()
|
|
532
|
+
if frame:
|
|
533
|
+
# Process the frame if one was received
|
|
534
|
+
data_received(frame.data, frame.timestamp)
|
|
535
|
+
else:
|
|
536
|
+
# No frame available, sleep briefly to avoid busy waiting
|
|
537
|
+
short_sleep()
|
|
538
|
+
last_sleep = self.timestamp.value
|
|
539
|
+
except Exception as e:
|
|
540
|
+
# Log any exceptions but continue processing
|
|
541
|
+
self.logger.error(f"Error in CAN listen thread: {e}")
|
|
542
|
+
# Sleep briefly to avoid tight error loops
|
|
543
|
+
short_sleep()
|
|
544
|
+
last_sleep = self.timestamp.value
|
|
460
545
|
|
|
461
546
|
def connect(self):
|
|
462
|
-
|
|
463
|
-
|
|
547
|
+
# Start listener lazily after a successful interface connection to avoid a dangling
|
|
548
|
+
# thread waiting on a not-yet-connected interface if initialization fails.
|
|
464
549
|
try:
|
|
465
550
|
self.can_interface.connect()
|
|
466
551
|
except CanInitializationError:
|
|
552
|
+
# Ensure any previously-started listener is stopped to prevent hangs.
|
|
553
|
+
self.finish_listener()
|
|
467
554
|
console.print("[red]\nThere may be a problem with the configuration of your CAN-interface.\n")
|
|
468
555
|
console.print(f"[grey]Current configuration of interface {self.interface_name!r}:")
|
|
469
556
|
console.print(self.interface_configuration)
|
|
470
557
|
raise
|
|
558
|
+
except OSError as ex:
|
|
559
|
+
# Ensure any previously-started listener is stopped to prevent hangs.
|
|
560
|
+
self.finish_listener()
|
|
561
|
+
# E.g., attempting to instantiate SocketCAN on Windows raises an OSError from socket layer.
|
|
562
|
+
# Provide a clearer, actionable message and keep the original exception.
|
|
563
|
+
msg = (
|
|
564
|
+
f"XCPonCAN - OS error while initializing interface {self.interface_name!r}: "
|
|
565
|
+
f"{ex.__class__.__name__}: {ex}.\n"
|
|
566
|
+
f"Hint: This interface may not be supported on your platform. "
|
|
567
|
+
f"On Windows, use e.g. 'vector', 'kvaser', 'pcan', or other vendor backends instead of 'socketcan'."
|
|
568
|
+
)
|
|
569
|
+
self.logger.critical(msg)
|
|
570
|
+
raise CanInitializationError(msg) from ex
|
|
571
|
+
else:
|
|
572
|
+
# Only now start the default listener if requested.
|
|
573
|
+
if self.useDefaultListener:
|
|
574
|
+
self.start_listener()
|
|
471
575
|
self.status = 1 # connected
|
|
472
576
|
|
|
473
577
|
def send(self, frame: bytes) -> None:
|