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.
- 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.c +116 -0
- pyxcp/asamkeydll.exe +0 -0
- pyxcp/asamkeydll.sh +2 -0
- pyxcp/checksum.py +732 -0
- pyxcp/cmdline.py +71 -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.cp310-win_amd64.pyd +0 -0
- pyxcp/cpp_ext/cpp_ext.cp311-win_amd64.pyd +0 -0
- pyxcp/cpp_ext/cpp_ext.cp312-win_amd64.pyd +0 -0
- pyxcp/cpp_ext/cpp_ext.cp313-win_amd64.pyd +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 +291 -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.cp310-win_amd64.pyd +0 -0
- pyxcp/daq_stim/stim.cp311-win_amd64.pyd +0 -0
- pyxcp/daq_stim/stim.cp312-win_amd64.pyd +0 -0
- pyxcp/daq_stim/stim.cp313-win_amd64.pyd +0 -0
- pyxcp/daq_stim/stim.cpp +13 -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 +36 -0
- pyxcp/examples/xcp_user_supplied_driver.py +43 -0
- pyxcp/examples/xcphello.py +65 -0
- pyxcp/examples/xcphello_recorder.py +107 -0
- pyxcp/master/__init__.py +10 -0
- pyxcp/master/errorhandler.py +677 -0
- pyxcp/master/master.py +2641 -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 +444 -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.cp310-win_amd64.pyd +0 -0
- pyxcp/recorder/rekorder.cp311-win_amd64.pyd +0 -0
- pyxcp/recorder/rekorder.cp312-win_amd64.pyd +0 -0
- pyxcp/recorder/rekorder.cp313-win_amd64.pyd +0 -0
- pyxcp/recorder/rekorder.cpp +59 -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 +159 -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/hdf5_policy.py +129 -0
- pyxcp/transport/sxi.py +209 -0
- pyxcp/transport/transport_ext.cp310-win_amd64.pyd +0 -0
- pyxcp/transport/transport_ext.cp311-win_amd64.pyd +0 -0
- pyxcp/transport/transport_ext.cp312-win_amd64.pyd +0 -0
- pyxcp/transport/transport_ext.cp313-win_amd64.pyd +0 -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/__init__.py +127 -0
- pyxcp/utils/cli.py +78 -0
- pyxcp/vector/__init__.py +0 -0
- pyxcp/vector/map.py +82 -0
- pyxcp-0.25.4.dist-info/METADATA +341 -0
- pyxcp-0.25.4.dist-info/RECORD +157 -0
- pyxcp-0.25.4.dist-info/WHEEL +4 -0
- pyxcp-0.25.4.dist-info/entry_points.txt +9 -0
- 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__}"
|