pyxcp 0.25.4__cp313-cp313-win_amd64.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 (157) hide show
  1. pyxcp/__init__.py +20 -0
  2. pyxcp/aml/EtasCANMonitoring.a2l +82 -0
  3. pyxcp/aml/EtasCANMonitoring.aml +67 -0
  4. pyxcp/aml/XCP_Common.aml +408 -0
  5. pyxcp/aml/XCPonCAN.aml +78 -0
  6. pyxcp/aml/XCPonEth.aml +33 -0
  7. pyxcp/aml/XCPonFlx.aml +113 -0
  8. pyxcp/aml/XCPonSxI.aml +66 -0
  9. pyxcp/aml/XCPonUSB.aml +106 -0
  10. pyxcp/aml/ifdata_CAN.a2l +20 -0
  11. pyxcp/aml/ifdata_Eth.a2l +11 -0
  12. pyxcp/aml/ifdata_Flx.a2l +94 -0
  13. pyxcp/aml/ifdata_SxI.a2l +13 -0
  14. pyxcp/aml/ifdata_USB.a2l +81 -0
  15. pyxcp/asam/__init__.py +0 -0
  16. pyxcp/asam/types.py +131 -0
  17. pyxcp/asamkeydll.c +116 -0
  18. pyxcp/asamkeydll.exe +0 -0
  19. pyxcp/asamkeydll.sh +2 -0
  20. pyxcp/checksum.py +732 -0
  21. pyxcp/cmdline.py +71 -0
  22. pyxcp/config/__init__.py +1257 -0
  23. pyxcp/config/legacy.py +120 -0
  24. pyxcp/constants.py +47 -0
  25. pyxcp/cpp_ext/__init__.py +0 -0
  26. pyxcp/cpp_ext/aligned_buffer.hpp +168 -0
  27. pyxcp/cpp_ext/bin.hpp +105 -0
  28. pyxcp/cpp_ext/blockmem.hpp +58 -0
  29. pyxcp/cpp_ext/cpp_ext.cp310-win_amd64.pyd +0 -0
  30. pyxcp/cpp_ext/cpp_ext.cp311-win_amd64.pyd +0 -0
  31. pyxcp/cpp_ext/cpp_ext.cp312-win_amd64.pyd +0 -0
  32. pyxcp/cpp_ext/cpp_ext.cp313-win_amd64.pyd +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 +291 -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.cp310-win_amd64.pyd +0 -0
  47. pyxcp/daq_stim/stim.cp311-win_amd64.pyd +0 -0
  48. pyxcp/daq_stim/stim.cp312-win_amd64.pyd +0 -0
  49. pyxcp/daq_stim/stim.cp313-win_amd64.pyd +0 -0
  50. pyxcp/daq_stim/stim.cpp +13 -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 +36 -0
  67. pyxcp/examples/xcp_user_supplied_driver.py +43 -0
  68. pyxcp/examples/xcphello.py +65 -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 +2641 -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 +444 -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.cp310-win_amd64.pyd +0 -0
  103. pyxcp/recorder/rekorder.cp311-win_amd64.pyd +0 -0
  104. pyxcp/recorder/rekorder.cp312-win_amd64.pyd +0 -0
  105. pyxcp/recorder/rekorder.cp313-win_amd64.pyd +0 -0
  106. pyxcp/recorder/rekorder.cpp +59 -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 +159 -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/hdf5_policy.py +129 -0
  140. pyxcp/transport/sxi.py +209 -0
  141. pyxcp/transport/transport_ext.cp310-win_amd64.pyd +0 -0
  142. pyxcp/transport/transport_ext.cp311-win_amd64.pyd +0 -0
  143. pyxcp/transport/transport_ext.cp312-win_amd64.pyd +0 -0
  144. pyxcp/transport/transport_ext.cp313-win_amd64.pyd +0 -0
  145. pyxcp/transport/transport_ext.hpp +214 -0
  146. pyxcp/transport/transport_wrapper.cpp +249 -0
  147. pyxcp/transport/usb_transport.py +229 -0
  148. pyxcp/types.py +987 -0
  149. pyxcp/utils/__init__.py +127 -0
  150. pyxcp/utils/cli.py +78 -0
  151. pyxcp/vector/__init__.py +0 -0
  152. pyxcp/vector/map.py +82 -0
  153. pyxcp-0.25.4.dist-info/METADATA +341 -0
  154. pyxcp-0.25.4.dist-info/RECORD +157 -0
  155. pyxcp-0.25.4.dist-info/WHEEL +4 -0
  156. pyxcp-0.25.4.dist-info/entry_points.txt +9 -0
  157. pyxcp-0.25.4.dist-info/licenses/LICENSE +165 -0
pyxcp/transport/can.py ADDED
@@ -0,0 +1,660 @@
1
+ #!/usr/bin/env python
2
+ """ """
3
+
4
+ import functools
5
+ import operator
6
+ from abc import ABC, abstractmethod
7
+ from bisect import bisect_left
8
+ from enum import IntEnum
9
+ from typing import Any, Dict, List, Optional, Union
10
+
11
+ from can import (
12
+ BusState,
13
+ CanError,
14
+ CanInitializationError,
15
+ Message,
16
+ detect_available_configs,
17
+ )
18
+ from can.bus import BusABC
19
+ from can.interface import _get_class_for_interface
20
+ from rich.console import Console
21
+
22
+ from pyxcp.config import CAN_INTERFACE_MAP
23
+ from pyxcp.transport.base import (
24
+ BaseTransport,
25
+ ChecksumType,
26
+ XcpFramingConfig,
27
+ XcpTransportLayerType,
28
+ )
29
+
30
+ from ..utils import seconds_to_nanoseconds, short_sleep
31
+
32
+
33
+ console = Console()
34
+
35
+ CAN_EXTENDED_ID = 0x80000000
36
+ MAX_11_BIT_IDENTIFIER = (1 << 11) - 1
37
+ MAX_29_BIT_IDENTIFIER = (1 << 29) - 1
38
+ MAX_DLC_CLASSIC = 8
39
+ CAN_FD_DLCS = (12, 16, 20, 24, 32, 48, 64) # Discrete CAN-FD DLCs in case DLC > 8.
40
+
41
+
42
+ class FilterState(IntEnum):
43
+ REJECT_ALL = 0
44
+ ACCEPT_ALL = 1
45
+ FILTERING = 2
46
+
47
+
48
+ class SoftwareFilter:
49
+ """Additional CAN filters in software."""
50
+
51
+ def __init__(self) -> None:
52
+ self.filters = None
53
+ self.reject_all()
54
+
55
+ def set_filters(self, filters: List[Dict]) -> None:
56
+ self.filters = filters
57
+ self.filtering()
58
+
59
+ def reject_all(self) -> None:
60
+ self.filter_state = FilterState.REJECT_ALL
61
+
62
+ def accept_all(self) -> None:
63
+ self.filter_state = FilterState.ACCEPT_ALL
64
+
65
+ def filtering(self) -> None:
66
+ self.filter_state = FilterState.FILTERING
67
+
68
+ @property
69
+ def state(self) -> FilterState:
70
+ return self.filter_state
71
+
72
+ def accept(self, msg: Message) -> bool:
73
+ """
74
+ Based on: https://github.com/hardbyte/python-can/blob/bc248e8aaf96280a574c06e8e7d2778a67f091e3/can/bus.py#L430
75
+ """
76
+ if self.filter_state == FilterState.REJECT_ALL:
77
+ return False
78
+ elif self.filter_state == FilterState.ACCEPT_ALL or self.filters is None:
79
+ return True
80
+ for filter in self.filters:
81
+ if "extended" in filter:
82
+ if filter["extended"] != msg.is_extended_id:
83
+ continue
84
+ can_id = filter["can_id"]
85
+ can_mask = filter["can_mask"]
86
+ if (can_id ^ msg.arbitration_id) & can_mask == 0:
87
+ return True
88
+ return False
89
+
90
+
91
+ class IdentifierOutOfRangeError(Exception):
92
+ """Signals an identifier greater then :obj:`MAX_11_BIT_IDENTIFIER` or :obj:`MAX_29_BIT_IDENTIFIER`."""
93
+
94
+ pass
95
+
96
+
97
+ def is_extended_identifier(identifier: int) -> bool:
98
+ """Check for extendend CAN identifier.
99
+
100
+ Parameters
101
+ ----------
102
+ identifier: int
103
+
104
+ Returns
105
+ -------
106
+ bool
107
+ """
108
+ return (identifier & CAN_EXTENDED_ID) == CAN_EXTENDED_ID
109
+
110
+
111
+ def stripIdentifier(identifier: int) -> int:
112
+ """Get raw CAN identifier (remove :obj:`CAN_EXTENDED_ID` bit if present).
113
+
114
+ Parameters
115
+ ----------
116
+ identifier: int
117
+
118
+ Returns
119
+ -------
120
+ int
121
+ """
122
+ return identifier & (~0xE0000000)
123
+
124
+
125
+ def samplePointToTsegs(tqs: int, samplePoint: float) -> tuple:
126
+ """Calculate TSEG1 and TSEG2 from time-quantas and sample-point.
127
+
128
+ Parameters
129
+ ----------
130
+ tqs: int
131
+ Number of time-quantas
132
+ samplePoint: float or int
133
+ Sample-point as a percentage value.
134
+
135
+ Returns
136
+ -------
137
+ tuple (TSEG1, TSEG2)
138
+ """
139
+ factor = samplePoint / 100.0
140
+ tseg1 = int(tqs * factor)
141
+ tseg2 = tqs - tseg1
142
+ return (tseg1, tseg2)
143
+
144
+
145
+ def pad_frame(frame: bytes, pad_frame: bool, padding_value: int) -> bytes:
146
+ """Pad frame to next discrete DLC value (CAN-FD) or on request (CAN-Classic).
147
+
148
+ References:
149
+ -----------
150
+ ISO/DIS 15765 - 4; 8.2 Data length Code (DLC)
151
+ AUTOSAR CP Release 4.3.0, Specification of CAN Transport Layer; 7.3.8 N-PDU padding
152
+ AUTOSAR CP Release 4.3.0, Specification of CAN Driver; [SWS_CAN_00502], [ECUC_Can_00485]
153
+ AUTOSAR CP Release 4.3.0, Requirements on CAN; [SRS_Can_01073], [SRS_Can_01086], [SRS_Can_01160]
154
+ """
155
+ frame_len = len(frame)
156
+ if frame_len <= MAX_DLC_CLASSIC:
157
+ actual_len = MAX_DLC_CLASSIC if pad_frame else frame_len
158
+ else:
159
+ actual_len = CAN_FD_DLCS[bisect_left(CAN_FD_DLCS, frame_len)]
160
+ # append fill bytes up to MAX_DLC resp. next discrete FD DLC.
161
+ if len(frame) < actual_len:
162
+ frame += bytes([padding_value]) * (actual_len - len(frame))
163
+ return frame
164
+
165
+
166
+ class Identifier:
167
+ """Convenience class for XCP formatted CAN identifiers.
168
+
169
+ Parameters:
170
+ -----------
171
+ raw_id: int
172
+ Bit 32 set (i.e. 0x80000000) signals an extended (29-bit) identifier.
173
+
174
+ Raises
175
+ ------
176
+ :class:`IdentifierOutOfRangeError`
177
+ """
178
+
179
+ def __init__(self, raw_id: int):
180
+ self._raw_id = raw_id
181
+ self._id = stripIdentifier(raw_id)
182
+ self._is_extended = is_extended_identifier(raw_id)
183
+ if self._is_extended:
184
+ if self._id > MAX_29_BIT_IDENTIFIER:
185
+ raise IdentifierOutOfRangeError(f"29-bit identifier {self._id!r} is out of range")
186
+ else:
187
+ if self._id > MAX_11_BIT_IDENTIFIER:
188
+ raise IdentifierOutOfRangeError(f"11-bit identifier {self._id!r} is out of range")
189
+
190
+ @property
191
+ def id(self) -> int:
192
+ """
193
+ Returns
194
+ -------
195
+ int
196
+ Identifier as seen on bus.
197
+ """
198
+ return self._id
199
+
200
+ @property
201
+ def raw_id(self) -> int:
202
+ """
203
+ Returns
204
+ -------
205
+ int
206
+ Raw XCP formatted identifier.
207
+ """
208
+ return self._raw_id
209
+
210
+ @property
211
+ def is_extended(self) -> bool:
212
+ """
213
+ Returns
214
+ -------
215
+ bool
216
+ - True - 29-bit identifier.
217
+ - False - 11-bit identifier.
218
+ """
219
+ return self._is_extended
220
+
221
+ @property
222
+ def type_str(self) -> str:
223
+ """
224
+
225
+ Returns
226
+ -------
227
+ str
228
+ - "S" - 11-bit identifier.
229
+ - "E" - 29-bit identifier.
230
+ """
231
+ return "E" if self.is_extended else "S"
232
+
233
+ @staticmethod
234
+ def make_identifier(identifier: int, extended: bool) -> "Identifier":
235
+ """Factory method.
236
+
237
+ Parameters
238
+ ----------
239
+ identifier: int
240
+ Identifier as seen on bus.
241
+
242
+ extended: bool
243
+ bool
244
+ - True - 29-bit identifier.
245
+ - False - 11-bit identifier.
246
+ Returns
247
+ -------
248
+ :class:`Identifier`
249
+
250
+ Raises
251
+ ------
252
+ :class:`IdentifierOutOfRangeError`
253
+ """
254
+ return Identifier(identifier if not extended else (identifier | CAN_EXTENDED_ID))
255
+
256
+ def create_filter_from_id(self) -> Dict:
257
+ """Create a single CAN filter entry.
258
+ s. https://python-can.readthedocs.io/en/stable/bus.html#filtering
259
+ """
260
+ return {
261
+ "can_id": self.id,
262
+ "can_mask": MAX_29_BIT_IDENTIFIER if self.is_extended else MAX_11_BIT_IDENTIFIER,
263
+ "extended": self.is_extended,
264
+ }
265
+
266
+ def __eq__(self, other) -> bool:
267
+ return (self.id == other.id) and (self.is_extended == other.is_extended)
268
+
269
+ def __str__(self) -> str:
270
+ return f"Identifier(id = 0x{self.id:08x}, is_extended = {self.is_extended})"
271
+
272
+ def __repr__(self) -> str:
273
+ return f"Identifier(0x{self.raw_id:08x})"
274
+
275
+
276
+ class Frame:
277
+ """"""
278
+
279
+ def __init__(self, id_: Identifier, dlc: int, data: bytes, timestamp: int) -> None:
280
+ self.id: Identifier = id_
281
+ self.dlc: int = dlc
282
+ self.data: bytes = data
283
+ self.timestamp: int = timestamp
284
+
285
+ def __repr__(self) -> str:
286
+ return f"Frame(id = 0x{self.id:08x}, dlc = {self.dlc}, data = {self.data}, timestamp = {self.timestamp})"
287
+
288
+ __str__ = __repr__
289
+
290
+
291
+ class PythonCanWrapper:
292
+ """Wrapper around python-can - github.com/hardbyte/python-can"""
293
+
294
+ def __init__(self, parent, interface_name: str, timeout: int, **parameters) -> None:
295
+ self.parent = parent
296
+ self.interface_name: str = interface_name
297
+ self.timeout: int = timeout
298
+ self.parameters = parameters
299
+ if not self.parent.has_user_supplied_interface:
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
307
+ else:
308
+ self.can_interface_class = None
309
+ self.can_interface: BusABC
310
+ self.connected: bool = False
311
+ self.software_filter = SoftwareFilter()
312
+ self.saved_filters = []
313
+
314
+ def connect(self) -> None:
315
+ if self.connected:
316
+ return
317
+ can_filters = []
318
+ can_filters.append(self.parent.can_id_slave.create_filter_from_id()) # Primary CAN filter.
319
+ if self.parent.daq_identifier:
320
+ # Add filters for DAQ identifiers.
321
+ for daq_id in self.parent.daq_identifier:
322
+ can_filters.append(daq_id.create_filter_from_id())
323
+ if self.parent.has_user_supplied_interface:
324
+ self.saved_filters = self.parent.transport_layer_interface.filters
325
+ if self.saved_filters:
326
+ merged_filters = can_filters[::]
327
+ for fltr in self.saved_filters:
328
+ if fltr not in merged_filters:
329
+ merged_filters.append(fltr)
330
+ self.can_interface = self.parent.transport_layer_interface
331
+ self.can_interface.set_filters(merged_filters)
332
+ self.software_filter.set_filters(can_filters) # Filter unwanted traffic.
333
+ else:
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
343
+ self.software_filter.accept_all()
344
+ self.parent.logger.info(f"XCPonCAN - Using Interface: '{self.can_interface!s}'")
345
+ self.parent.logger.info(f"XCPonCAN - Filters used: {self.can_interface.filters}")
346
+ self.parent.logger.info(f"XCPonCAN - State: {self.can_interface.state!s}")
347
+ self.connected = True
348
+
349
+ def close(self) -> None:
350
+ if self.connected and not self.parent.has_user_supplied_interface:
351
+ self.can_interface.shutdown()
352
+ if self.saved_filters:
353
+ self.can_interface.set_filters(self.saved_filters)
354
+ self.connected = False
355
+
356
+ def transmit(self, payload: bytes) -> None:
357
+ frame = Message(
358
+ arbitration_id=self.parent.can_id_master.id,
359
+ is_extended_id=True if self.parent.can_id_master.is_extended else False,
360
+ is_fd=self.parent.fd,
361
+ data=payload,
362
+ )
363
+ self.can_interface.send(frame)
364
+
365
+ def read(self) -> Optional[Frame]:
366
+ if not self.connected:
367
+ return None
368
+ try:
369
+ frame = self.can_interface.recv(self.timeout)
370
+ except CanError:
371
+ return None
372
+ else:
373
+ if frame is None or not len(frame.data):
374
+ return None # Timeout condition.
375
+ if not self.software_filter.accept(frame):
376
+ return None # Filter out unwanted traffic.
377
+ extended = frame.is_extended_id
378
+ identifier = Identifier.make_identifier(frame.arbitration_id, extended)
379
+ return Frame(
380
+ id_=identifier,
381
+ dlc=frame.dlc,
382
+ data=frame.data,
383
+ timestamp=seconds_to_nanoseconds(frame.timestamp),
384
+ )
385
+
386
+ def get_timestamp_resolution(self) -> int:
387
+ return 10 * 1000
388
+
389
+
390
+ class EmptyHeader:
391
+ """There is no header for XCP on CAN"""
392
+
393
+ def pack(self, *args, **kwargs):
394
+ return b""
395
+
396
+
397
+ class Can(BaseTransport):
398
+ """"""
399
+
400
+ MAX_DATAGRAM_SIZE = 7
401
+ HEADER = EmptyHeader()
402
+ HEADER_SIZE = 0
403
+
404
+ def __init__(self, config, policy=None, transport_layer_interface: Optional[BusABC] = None):
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)
414
+ self.load_config(config)
415
+ self.useDefaultListener = self.config.use_default_listener
416
+ self.can_id_master = Identifier(self.config.can_id_master)
417
+ self.can_id_slave = Identifier(self.config.can_id_slave)
418
+
419
+ # Regarding CAN-FD s. AUTOSAR CP Release 4.3.0, Requirements on CAN; [SRS_Can_01160] Padding of bytes due to discrete CAN FD DLC]:
420
+ # "... If a PDU does not exactly match these configurable sizes the unused bytes shall be padded."
421
+ #
422
+ self.fd = self.config.fd
423
+ self.daq_identifier = []
424
+ if self.config.daq_identifier:
425
+ for daq_id in self.config.daq_identifier:
426
+ self.daq_identifier.append(Identifier(daq_id))
427
+ self.max_dlc_required = self.config.max_dlc_required
428
+ self.padding_value = self.config.padding_value
429
+ if transport_layer_interface is None:
430
+ self.interface_name = self.config.interface
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 = []
442
+ parameters = self.get_interface_parameters()
443
+ else:
444
+ self.interface_name = "custom"
445
+ # print("TRY GET PARAMs", self.get_interface_parameters())
446
+ parameters = {}
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
458
+ self.logger.info(f"XCPonCAN - Interface-Type: {self.interface_name!r} Parameters: {list(parameters.items())}")
459
+ self.logger.info(
460
+ f"XCPonCAN - Master-ID (Tx): 0x{self.can_id_master.id:08X}{self.can_id_master.type_str} -- "
461
+ f"Slave-ID (Rx): 0x{self.can_id_slave.id:08X}{self.can_id_slave.type_str}"
462
+ )
463
+
464
+ def get_interface_parameters(self) -> Dict[str, Any]:
465
+ result = dict(channel=self.config.channel)
466
+
467
+ can_interface_config_class = CAN_INTERFACE_MAP[self.interface_name]
468
+
469
+ # Optional base class parameters.
470
+ optional_parameters = [(p, p.removeprefix("has_")) for p in can_interface_config_class.OPTIONAL_BASE_PARAMS]
471
+ for o, n in optional_parameters:
472
+ opt = getattr(can_interface_config_class, o)
473
+ value = getattr(self.config, n)
474
+ if opt:
475
+ if value is not None:
476
+ result[n] = value
477
+ elif value is not None:
478
+ self.logger.warning(f"XCPonCAN - {self.interface_name!r} has no support for parameter {n!r}.")
479
+ # Parameter names that need to be mapped.
480
+ for base_name, name in can_interface_config_class.CAN_PARAM_MAP.items():
481
+ value = getattr(self.config, base_name)
482
+ if name is not None and value is not None:
483
+ result[name] = value
484
+ # Interface specific parameters.
485
+ cxx = getattr(self.config, self.interface_name)
486
+ for name in can_interface_config_class.class_own_traits().keys():
487
+ value = getattr(cxx, name)
488
+ if value is not None:
489
+ result[name] = value
490
+ return result
491
+
492
+ def data_received(self, payload: bytes, recv_timestamp: int):
493
+ self.process_response(
494
+ payload,
495
+ len(payload),
496
+ counter=(self.counter_received + 1) & 0xFFFF,
497
+ recv_timestamp=recv_timestamp,
498
+ )
499
+
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
+
519
+ while True:
520
+ # Check if we should exit the loop
521
+ if close_event_set():
522
+ return
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
545
+
546
+ def connect(self):
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.
549
+ try:
550
+ self.can_interface.connect()
551
+ except CanInitializationError:
552
+ # Ensure any previously-started listener is stopped to prevent hangs.
553
+ self.finish_listener()
554
+ console.print("[red]\nThere may be a problem with the configuration of your CAN-interface.\n")
555
+ console.print(f"[grey]Current configuration of interface {self.interface_name!r}:")
556
+ console.print(self.interface_configuration)
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()
575
+ self.status = 1 # connected
576
+
577
+ def send(self, frame: bytes) -> None:
578
+ # send the request
579
+ self.pre_send_timestamp = self.timestamp.value
580
+ self.can_interface.transmit(payload=pad_frame(frame, self.max_dlc_required, self.padding_value))
581
+ self.post_send_timestamp = self.timestamp.value
582
+
583
+ def close_connection(self):
584
+ if hasattr(self, "can_interface"):
585
+ self.can_interface.close()
586
+
587
+ def close(self):
588
+ self.finish_listener()
589
+ self.close_connection()
590
+
591
+
592
+ def set_DLC(length: int):
593
+ """Return DLC value according to CAN-FD.
594
+
595
+ :param length: Length value to be mapped to a valid CAN-FD DLC.
596
+ ( 0 <= length <= 64)
597
+ """
598
+
599
+ if length < 0:
600
+ raise ValueError("Non-negative length value required.")
601
+ elif length <= MAX_DLC_CLASSIC:
602
+ return length
603
+ elif length <= 64:
604
+ for dlc in CAN_FD_DLCS:
605
+ if length <= dlc:
606
+ return dlc
607
+ else:
608
+ raise ValueError("DLC could be at most 64.")
609
+
610
+
611
+ def calculate_filter(ids: list):
612
+ """
613
+ :param ids: An iterable (usually list or tuple) containing CAN identifiers.
614
+
615
+ :return: Calculated filter and mask.
616
+ :rtype: tuple (int, int)
617
+ """
618
+ any_extended_ids = any(is_extended_identifier(i) for i in ids)
619
+ raw_ids = [stripIdentifier(i) for i in ids]
620
+ cfilter = functools.reduce(operator.and_, raw_ids)
621
+ cmask = functools.reduce(operator.or_, raw_ids) ^ cfilter
622
+ cmask ^= 0x1FFFFFFF if any_extended_ids else 0x7FF
623
+ return (cfilter, cmask)
624
+
625
+
626
+ class CanInterfaceBase(ABC):
627
+ """
628
+ Base class for custom CAN interfaces.
629
+ This is basically a subset of python-CANs `BusABC`.
630
+ """
631
+
632
+ @abstractmethod
633
+ def set_filters(self, filters: Optional[List[Dict[str, Union[int, bool]]]] = None) -> None:
634
+ """Apply filtering to all messages received by this Bus.
635
+
636
+ filters:
637
+ A list of dictionaries, each containing a 'can_id', 'can_mask', and 'extended' field, e.g.:
638
+ [{"can_id": 0x11, "can_mask": 0x21, "extended": False}]
639
+ """
640
+
641
+ @abstractmethod
642
+ def recv(self, timeout: Optional[float] = None) -> Optional[Message]:
643
+ """Block waiting for a message from the Bus."""
644
+
645
+ @abstractmethod
646
+ def send(self, msg: Message) -> None:
647
+ """Transmit a message to the CAN bus."""
648
+
649
+ @property
650
+ @abstractmethod
651
+ def filters(self) -> Optional[List[Dict[str, Union[int, bool]]]]:
652
+ """Modify the filters of this bus."""
653
+
654
+ @property
655
+ @abstractmethod
656
+ def state(self) -> BusState:
657
+ """Return the current state of the hardware."""
658
+
659
+ def __repr__(self):
660
+ return f"{self.__class__.__name__}"