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.
Files changed (151) hide show
  1. pyxcp/__init__.py +20 -0
  2. pyxcp/aml/EtasCANMonitoring.a2l +82 -0
  3. pyxcp/aml/EtasCANMonitoring.aml +67 -0
  4. pyxcp/aml/XCP_Common.aml +408 -0
  5. pyxcp/aml/XCPonCAN.aml +78 -0
  6. pyxcp/aml/XCPonEth.aml +33 -0
  7. pyxcp/aml/XCPonFlx.aml +113 -0
  8. pyxcp/aml/XCPonSxI.aml +66 -0
  9. pyxcp/aml/XCPonUSB.aml +106 -0
  10. pyxcp/aml/ifdata_CAN.a2l +20 -0
  11. pyxcp/aml/ifdata_Eth.a2l +11 -0
  12. pyxcp/aml/ifdata_Flx.a2l +94 -0
  13. pyxcp/aml/ifdata_SxI.a2l +13 -0
  14. pyxcp/aml/ifdata_USB.a2l +81 -0
  15. pyxcp/asam/__init__.py +0 -0
  16. pyxcp/asam/types.py +131 -0
  17. pyxcp/asamkeydll +0 -0
  18. pyxcp/asamkeydll.c +116 -0
  19. pyxcp/asamkeydll.sh +2 -0
  20. pyxcp/checksum.py +732 -0
  21. pyxcp/cmdline.py +83 -0
  22. pyxcp/config/__init__.py +1257 -0
  23. pyxcp/config/legacy.py +120 -0
  24. pyxcp/constants.py +47 -0
  25. pyxcp/cpp_ext/__init__.py +0 -0
  26. pyxcp/cpp_ext/aligned_buffer.hpp +168 -0
  27. pyxcp/cpp_ext/bin.hpp +105 -0
  28. pyxcp/cpp_ext/blockmem.hpp +58 -0
  29. pyxcp/cpp_ext/cpp_ext.cpython-310-x86_64-linux-gnu.so +0 -0
  30. pyxcp/cpp_ext/cpp_ext.cpython-311-x86_64-linux-gnu.so +0 -0
  31. pyxcp/cpp_ext/cpp_ext.cpython-312-x86_64-linux-gnu.so +0 -0
  32. pyxcp/cpp_ext/cpp_ext.cpython-313-x86_64-linux-gnu.so +0 -0
  33. pyxcp/cpp_ext/daqlist.hpp +374 -0
  34. pyxcp/cpp_ext/event.hpp +67 -0
  35. pyxcp/cpp_ext/extension_wrapper.cpp +131 -0
  36. pyxcp/cpp_ext/framing.hpp +360 -0
  37. pyxcp/cpp_ext/helper.hpp +280 -0
  38. pyxcp/cpp_ext/mcobject.hpp +248 -0
  39. pyxcp/cpp_ext/sxi_framing.hpp +332 -0
  40. pyxcp/cpp_ext/tsqueue.hpp +46 -0
  41. pyxcp/daq_stim/__init__.py +306 -0
  42. pyxcp/daq_stim/optimize/__init__.py +67 -0
  43. pyxcp/daq_stim/optimize/binpacking.py +41 -0
  44. pyxcp/daq_stim/scheduler.cpp +62 -0
  45. pyxcp/daq_stim/scheduler.hpp +75 -0
  46. pyxcp/daq_stim/stim.cpp +13 -0
  47. pyxcp/daq_stim/stim.cpython-310-x86_64-linux-gnu.so +0 -0
  48. pyxcp/daq_stim/stim.cpython-311-x86_64-linux-gnu.so +0 -0
  49. pyxcp/daq_stim/stim.cpython-312-x86_64-linux-gnu.so +0 -0
  50. pyxcp/daq_stim/stim.cpython-313-x86_64-linux-gnu.so +0 -0
  51. pyxcp/daq_stim/stim.hpp +604 -0
  52. pyxcp/daq_stim/stim_wrapper.cpp +50 -0
  53. pyxcp/dllif.py +100 -0
  54. pyxcp/errormatrix.py +878 -0
  55. pyxcp/examples/conf_can.toml +19 -0
  56. pyxcp/examples/conf_can_user.toml +16 -0
  57. pyxcp/examples/conf_can_vector.json +11 -0
  58. pyxcp/examples/conf_can_vector.toml +11 -0
  59. pyxcp/examples/conf_eth.toml +9 -0
  60. pyxcp/examples/conf_nixnet.json +20 -0
  61. pyxcp/examples/conf_socket_can.toml +12 -0
  62. pyxcp/examples/run_daq.py +165 -0
  63. pyxcp/examples/xcp_policy.py +60 -0
  64. pyxcp/examples/xcp_read_benchmark.py +38 -0
  65. pyxcp/examples/xcp_skel.py +48 -0
  66. pyxcp/examples/xcp_unlock.py +38 -0
  67. pyxcp/examples/xcp_user_supplied_driver.py +43 -0
  68. pyxcp/examples/xcphello.py +79 -0
  69. pyxcp/examples/xcphello_recorder.py +107 -0
  70. pyxcp/master/__init__.py +10 -0
  71. pyxcp/master/errorhandler.py +677 -0
  72. pyxcp/master/master.py +2645 -0
  73. pyxcp/py.typed +0 -0
  74. pyxcp/recorder/.idea/.gitignore +8 -0
  75. pyxcp/recorder/.idea/misc.xml +4 -0
  76. pyxcp/recorder/.idea/modules.xml +8 -0
  77. pyxcp/recorder/.idea/recorder.iml +6 -0
  78. pyxcp/recorder/.idea/sonarlint/issuestore/3/8/3808afc69ac1edb9d760000a2f137335b1b99728 +7 -0
  79. pyxcp/recorder/.idea/sonarlint/issuestore/9/a/9a2aa4db38d3115ed60da621e012c0efc0172aae +0 -0
  80. pyxcp/recorder/.idea/sonarlint/issuestore/b/4/b49006702b459496a8e8c94ebe60947108361b91 +0 -0
  81. pyxcp/recorder/.idea/sonarlint/issuestore/index.pb +7 -0
  82. pyxcp/recorder/.idea/sonarlint/securityhotspotstore/3/8/3808afc69ac1edb9d760000a2f137335b1b99728 +0 -0
  83. pyxcp/recorder/.idea/sonarlint/securityhotspotstore/9/a/9a2aa4db38d3115ed60da621e012c0efc0172aae +0 -0
  84. pyxcp/recorder/.idea/sonarlint/securityhotspotstore/b/4/b49006702b459496a8e8c94ebe60947108361b91 +0 -0
  85. pyxcp/recorder/.idea/sonarlint/securityhotspotstore/index.pb +7 -0
  86. pyxcp/recorder/.idea/vcs.xml +10 -0
  87. pyxcp/recorder/__init__.py +96 -0
  88. pyxcp/recorder/build_clang.cmd +1 -0
  89. pyxcp/recorder/build_clang.sh +2 -0
  90. pyxcp/recorder/build_gcc.cmd +1 -0
  91. pyxcp/recorder/build_gcc.sh +2 -0
  92. pyxcp/recorder/build_gcc_arm.sh +2 -0
  93. pyxcp/recorder/converter/__init__.py +445 -0
  94. pyxcp/recorder/lz4.c +2829 -0
  95. pyxcp/recorder/lz4.h +879 -0
  96. pyxcp/recorder/lz4hc.c +2041 -0
  97. pyxcp/recorder/lz4hc.h +413 -0
  98. pyxcp/recorder/mio.hpp +1714 -0
  99. pyxcp/recorder/reader.hpp +138 -0
  100. pyxcp/recorder/reco.py +278 -0
  101. pyxcp/recorder/recorder.rst +0 -0
  102. pyxcp/recorder/rekorder.cpp +59 -0
  103. pyxcp/recorder/rekorder.cpython-310-x86_64-linux-gnu.so +0 -0
  104. pyxcp/recorder/rekorder.cpython-311-x86_64-linux-gnu.so +0 -0
  105. pyxcp/recorder/rekorder.cpython-312-x86_64-linux-gnu.so +0 -0
  106. pyxcp/recorder/rekorder.cpython-313-x86_64-linux-gnu.so +0 -0
  107. pyxcp/recorder/rekorder.hpp +274 -0
  108. pyxcp/recorder/setup.py +41 -0
  109. pyxcp/recorder/test_reko.py +34 -0
  110. pyxcp/recorder/unfolder.hpp +1354 -0
  111. pyxcp/recorder/wrap.cpp +184 -0
  112. pyxcp/recorder/writer.hpp +302 -0
  113. pyxcp/scripts/__init__.py +0 -0
  114. pyxcp/scripts/pyxcp_probe_can_drivers.py +20 -0
  115. pyxcp/scripts/xcp_examples.py +64 -0
  116. pyxcp/scripts/xcp_fetch_a2l.py +40 -0
  117. pyxcp/scripts/xcp_id_scanner.py +18 -0
  118. pyxcp/scripts/xcp_info.py +144 -0
  119. pyxcp/scripts/xcp_profile.py +26 -0
  120. pyxcp/scripts/xmraw_converter.py +31 -0
  121. pyxcp/stim/__init__.py +0 -0
  122. pyxcp/tests/test_asam_types.py +24 -0
  123. pyxcp/tests/test_binpacking.py +186 -0
  124. pyxcp/tests/test_can.py +1324 -0
  125. pyxcp/tests/test_checksum.py +95 -0
  126. pyxcp/tests/test_daq.py +193 -0
  127. pyxcp/tests/test_daq_opt.py +426 -0
  128. pyxcp/tests/test_frame_padding.py +156 -0
  129. pyxcp/tests/test_framing.py +262 -0
  130. pyxcp/tests/test_master.py +2116 -0
  131. pyxcp/tests/test_transport.py +177 -0
  132. pyxcp/tests/test_utils.py +30 -0
  133. pyxcp/timing.py +60 -0
  134. pyxcp/transport/__init__.py +13 -0
  135. pyxcp/transport/base.py +484 -0
  136. pyxcp/transport/base_transport.hpp +0 -0
  137. pyxcp/transport/can.py +660 -0
  138. pyxcp/transport/eth.py +254 -0
  139. pyxcp/transport/sxi.py +209 -0
  140. pyxcp/transport/transport_ext.hpp +214 -0
  141. pyxcp/transport/transport_wrapper.cpp +249 -0
  142. pyxcp/transport/usb_transport.py +229 -0
  143. pyxcp/types.py +987 -0
  144. pyxcp/utils.py +127 -0
  145. pyxcp/vector/__init__.py +0 -0
  146. pyxcp/vector/map.py +82 -0
  147. pyxcp-0.25.2.dist-info/METADATA +341 -0
  148. pyxcp-0.25.2.dist-info/RECORD +151 -0
  149. pyxcp-0.25.2.dist-info/WHEEL +6 -0
  150. pyxcp-0.25.2.dist-info/entry_points.txt +9 -0
  151. pyxcp-0.25.2.dist-info/licenses/LICENSE +165 -0
@@ -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