pyxcp 0.22.33__cp314-cp314-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.
Potentially problematic release.
This version of pyxcp might be problematic. Click here for more details.
- 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.sh +2 -0
- pyxcp/checksum.py +732 -0
- pyxcp/cmdline.py +52 -0
- pyxcp/config/__init__.py +1102 -0
- pyxcp/config/legacy.py +120 -0
- pyxcp/constants.py +47 -0
- pyxcp/cpp_ext/__init__.py +0 -0
- pyxcp/cpp_ext/bin.hpp +104 -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/cpp_ext.cp314-win_amd64.pyd +0 -0
- pyxcp/cpp_ext/daqlist.hpp +206 -0
- pyxcp/cpp_ext/event.hpp +67 -0
- pyxcp/cpp_ext/extension_wrapper.cpp +100 -0
- pyxcp/cpp_ext/helper.hpp +280 -0
- pyxcp/cpp_ext/mcobject.hpp +246 -0
- pyxcp/cpp_ext/tsqueue.hpp +46 -0
- pyxcp/daq_stim/__init__.py +232 -0
- pyxcp/daq_stim/optimize/__init__.py +67 -0
- pyxcp/daq_stim/optimize/binpacking.py +41 -0
- pyxcp/daq_stim/scheduler.cpp +28 -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.cp314-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 +95 -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/conf_sxi.json +9 -0
- pyxcp/examples/conf_sxi.toml +7 -0
- pyxcp/examples/run_daq.py +163 -0
- pyxcp/examples/xcp_policy.py +60 -0
- pyxcp/examples/xcp_read_benchmark.py +38 -0
- pyxcp/examples/xcp_skel.py +49 -0
- pyxcp/examples/xcp_unlock.py +38 -0
- pyxcp/examples/xcp_user_supplied_driver.py +54 -0
- pyxcp/examples/xcphello.py +79 -0
- pyxcp/examples/xcphello_recorder.py +107 -0
- pyxcp/master/__init__.py +9 -0
- pyxcp/master/errorhandler.py +442 -0
- pyxcp/master/master.py +2046 -0
- pyxcp/py.typed +0 -0
- pyxcp/recorder/__init__.py +101 -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 +451 -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 +139 -0
- pyxcp/recorder/reco.py +277 -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.cp314-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 +1332 -0
- pyxcp/recorder/wrap.cpp +189 -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 +19 -0
- pyxcp/scripts/xcp_info.py +146 -0
- pyxcp/scripts/xcp_profile.py +27 -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_master.py +2006 -0
- pyxcp/tests/test_transport.py +81 -0
- pyxcp/tests/test_utils.py +30 -0
- pyxcp/timing.py +60 -0
- pyxcp/transport/__init__.py +10 -0
- pyxcp/transport/base.py +440 -0
- pyxcp/transport/base_transport.hpp +0 -0
- pyxcp/transport/can.py +441 -0
- pyxcp/transport/eth.py +219 -0
- pyxcp/transport/sxi.py +135 -0
- pyxcp/transport/transport_wrapper.cpp +0 -0
- pyxcp/transport/usb_transport.py +213 -0
- pyxcp/types.py +1000 -0
- pyxcp/utils.py +127 -0
- pyxcp/vector/__init__.py +0 -0
- pyxcp/vector/map.py +82 -0
- pyxcp-0.22.33.dist-info/LICENSE +165 -0
- pyxcp-0.22.33.dist-info/METADATA +107 -0
- pyxcp-0.22.33.dist-info/RECORD +137 -0
- pyxcp-0.22.33.dist-info/WHEEL +4 -0
- pyxcp-0.22.33.dist-info/entry_points.txt +9 -0
pyxcp/transport/can.py
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
import operator
|
|
7
|
+
from bisect import bisect_left
|
|
8
|
+
from typing import Any, Dict, Optional, Type
|
|
9
|
+
|
|
10
|
+
from can import CanError, CanInitializationError, Message, detect_available_configs
|
|
11
|
+
from can.bus import BusABC
|
|
12
|
+
from can.interface import _get_class_for_interface
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
|
|
15
|
+
from pyxcp.config import CAN_INTERFACE_MAP
|
|
16
|
+
from pyxcp.transport.base import BaseTransport
|
|
17
|
+
|
|
18
|
+
from ..utils import seconds_to_nanoseconds
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
CAN_EXTENDED_ID = 0x80000000
|
|
24
|
+
MAX_11_BIT_IDENTIFIER = (1 << 11) - 1
|
|
25
|
+
MAX_29_BIT_IDENTIFIER = (1 << 29) - 1
|
|
26
|
+
MAX_DLC_CLASSIC = 8
|
|
27
|
+
CAN_FD_DLCS = (12, 16, 20, 24, 32, 48, 64) # Discrete CAN-FD DLCs in case DLC > 8.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class IdentifierOutOfRangeError(Exception):
|
|
31
|
+
"""Signals an identifier greater then :obj:`MAX_11_BIT_IDENTIFIER` or :obj:`MAX_29_BIT_IDENTIFIER`."""
|
|
32
|
+
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_extended_identifier(identifier: int) -> bool:
|
|
37
|
+
"""Check for extendend CAN identifier.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
identifier: int
|
|
42
|
+
|
|
43
|
+
Returns
|
|
44
|
+
-------
|
|
45
|
+
bool
|
|
46
|
+
"""
|
|
47
|
+
return (identifier & CAN_EXTENDED_ID) == CAN_EXTENDED_ID
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def stripIdentifier(identifier: int) -> int:
|
|
51
|
+
"""Get raw CAN identifier (remove :obj:`CAN_EXTENDED_ID` bit if present).
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
identifier: int
|
|
56
|
+
|
|
57
|
+
Returns
|
|
58
|
+
-------
|
|
59
|
+
int
|
|
60
|
+
"""
|
|
61
|
+
return identifier & (~0xE0000000)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def samplePointToTsegs(tqs: int, samplePoint: float) -> tuple:
|
|
65
|
+
"""Calculate TSEG1 and TSEG2 from time-quantas and sample-point.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
tqs: int
|
|
70
|
+
Number of time-quantas
|
|
71
|
+
samplePoint: float or int
|
|
72
|
+
Sample-point as a percentage value.
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
tuple (TSEG1, TSEG2)
|
|
77
|
+
"""
|
|
78
|
+
factor = samplePoint / 100.0
|
|
79
|
+
tseg1 = int(tqs * factor)
|
|
80
|
+
tseg2 = tqs - tseg1
|
|
81
|
+
return (tseg1, tseg2)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def pad_frame(frame: bytes, pad_frame: bool, padding_value: int) -> bytes:
|
|
85
|
+
"""Pad frame to next discrete DLC value (CAN-FD) or on request (CAN-Classic).
|
|
86
|
+
|
|
87
|
+
References:
|
|
88
|
+
-----------
|
|
89
|
+
ISO/DIS 15765 - 4; 8.2 Data length Code (DLC)
|
|
90
|
+
AUTOSAR CP Release 4.3.0, Specification of CAN Transport Layer; 7.3.8 N-PDU padding
|
|
91
|
+
AUTOSAR CP Release 4.3.0, Specification of CAN Driver; [SWS_CAN_00502], [ECUC_Can_00485]
|
|
92
|
+
AUTOSAR CP Release 4.3.0, Requirements on CAN; [SRS_Can_01073], [SRS_Can_01086], [SRS_Can_01160]
|
|
93
|
+
"""
|
|
94
|
+
frame_len = len(frame)
|
|
95
|
+
if frame_len <= MAX_DLC_CLASSIC:
|
|
96
|
+
actual_len = MAX_DLC_CLASSIC if pad_frame else frame_len
|
|
97
|
+
else:
|
|
98
|
+
actual_len = CAN_FD_DLCS[bisect_left(CAN_FD_DLCS, frame_len)]
|
|
99
|
+
# append fill bytes up to MAX_DLC resp. next discrete FD DLC.
|
|
100
|
+
if len(frame) < actual_len:
|
|
101
|
+
frame += bytes([padding_value]) * (actual_len - len(frame))
|
|
102
|
+
return frame
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class Identifier:
|
|
106
|
+
"""Convenience class for XCP formatted CAN identifiers.
|
|
107
|
+
|
|
108
|
+
Parameters:
|
|
109
|
+
-----------
|
|
110
|
+
raw_id: int
|
|
111
|
+
Bit 32 set (i.e. 0x80000000) signals an extended (29-bit) identifier.
|
|
112
|
+
|
|
113
|
+
Raises
|
|
114
|
+
------
|
|
115
|
+
:class:`IdentifierOutOfRangeError`
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def __init__(self, raw_id: int):
|
|
119
|
+
self._raw_id = raw_id
|
|
120
|
+
self._id = stripIdentifier(raw_id)
|
|
121
|
+
self._is_extended = is_extended_identifier(raw_id)
|
|
122
|
+
if self._is_extended:
|
|
123
|
+
if self._id > MAX_29_BIT_IDENTIFIER:
|
|
124
|
+
raise IdentifierOutOfRangeError(f"29-bit identifier {self._id!r} is out of range")
|
|
125
|
+
else:
|
|
126
|
+
if self._id > MAX_11_BIT_IDENTIFIER:
|
|
127
|
+
raise IdentifierOutOfRangeError(f"11-bit identifier {self._id!r} is out of range")
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def id(self) -> int:
|
|
131
|
+
"""
|
|
132
|
+
Returns
|
|
133
|
+
-------
|
|
134
|
+
int
|
|
135
|
+
Identifier as seen on bus.
|
|
136
|
+
"""
|
|
137
|
+
return self._id
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def raw_id(self) -> int:
|
|
141
|
+
"""
|
|
142
|
+
Returns
|
|
143
|
+
-------
|
|
144
|
+
int
|
|
145
|
+
Raw XCP formatted identifier.
|
|
146
|
+
"""
|
|
147
|
+
return self._raw_id
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def is_extended(self) -> bool:
|
|
151
|
+
"""
|
|
152
|
+
Returns
|
|
153
|
+
-------
|
|
154
|
+
bool
|
|
155
|
+
- True - 29-bit identifier.
|
|
156
|
+
- False - 11-bit identifier.
|
|
157
|
+
"""
|
|
158
|
+
return self._is_extended
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def type_str(self) -> str:
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
Returns
|
|
165
|
+
-------
|
|
166
|
+
str
|
|
167
|
+
- "S" - 11-bit identifier.
|
|
168
|
+
- "E" - 29-bit identifier.
|
|
169
|
+
"""
|
|
170
|
+
return "E" if self.is_extended else "S"
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def make_identifier(identifier: int, extended: bool) -> "Identifier":
|
|
174
|
+
"""Factory method.
|
|
175
|
+
|
|
176
|
+
Parameters
|
|
177
|
+
----------
|
|
178
|
+
identifier: int
|
|
179
|
+
Identifier as seen on bus.
|
|
180
|
+
|
|
181
|
+
extended: bool
|
|
182
|
+
bool
|
|
183
|
+
- True - 29-bit identifier.
|
|
184
|
+
- False - 11-bit identifier.
|
|
185
|
+
Returns
|
|
186
|
+
-------
|
|
187
|
+
:class:`Identifier`
|
|
188
|
+
|
|
189
|
+
Raises
|
|
190
|
+
------
|
|
191
|
+
:class:`IdentifierOutOfRangeError`
|
|
192
|
+
"""
|
|
193
|
+
return Identifier(identifier if not extended else (identifier | CAN_EXTENDED_ID))
|
|
194
|
+
|
|
195
|
+
def create_filter_from_id(self) -> Dict:
|
|
196
|
+
"""Create a single CAN filter entry.
|
|
197
|
+
s. https://python-can.readthedocs.io/en/stable/bus.html#filtering
|
|
198
|
+
"""
|
|
199
|
+
return {
|
|
200
|
+
"can_id": self.id,
|
|
201
|
+
"can_mask": MAX_29_BIT_IDENTIFIER if self.is_extended else MAX_11_BIT_IDENTIFIER,
|
|
202
|
+
"extended": self.is_extended,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
def __eq__(self, other) -> bool:
|
|
206
|
+
return (self.id == other.id) and (self.is_extended == other.is_extended)
|
|
207
|
+
|
|
208
|
+
def __str__(self) -> str:
|
|
209
|
+
return f"Identifier(id = 0x{self.id:08x}, is_extended = {self.is_extended})"
|
|
210
|
+
|
|
211
|
+
def __repr__(self) -> str:
|
|
212
|
+
return f"Identifier(0x{self.raw_id:08x})"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class Frame:
|
|
216
|
+
""""""
|
|
217
|
+
|
|
218
|
+
def __init__(self, id_: Identifier, dlc: int, data: bytes, timestamp: int) -> None:
|
|
219
|
+
self.id: Identifier = id_
|
|
220
|
+
self.dlc: int = dlc
|
|
221
|
+
self.data: bytes = data
|
|
222
|
+
self.timestamp: int = timestamp
|
|
223
|
+
|
|
224
|
+
def __repr__(self) -> str:
|
|
225
|
+
return f"Frame(id = 0x{self.id:08x}, dlc = {self.dlc}, data = {self.data}, timestamp = {self.timestamp})"
|
|
226
|
+
|
|
227
|
+
__str__ = __repr__
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class PythonCanWrapper:
|
|
231
|
+
"""Wrapper around python-can - github.com/hardbyte/python-can"""
|
|
232
|
+
|
|
233
|
+
def __init__(self, parent, interface_name: str, timeout: int, **parameters) -> None:
|
|
234
|
+
self.parent = parent
|
|
235
|
+
self.interface_name: str = interface_name
|
|
236
|
+
self.timeout: int = timeout
|
|
237
|
+
self.parameters = parameters
|
|
238
|
+
self.can_interface_class: Type[BusABC] = _get_class_for_interface(self.interface_name)
|
|
239
|
+
self.can_interface: BusABC
|
|
240
|
+
self.connected: bool = False
|
|
241
|
+
|
|
242
|
+
def connect(self):
|
|
243
|
+
if self.connected:
|
|
244
|
+
return
|
|
245
|
+
can_filters = []
|
|
246
|
+
can_filters.append(self.parent.can_id_slave.create_filter_from_id()) # Primary CAN filter.
|
|
247
|
+
if self.parent.has_user_supplied_interface:
|
|
248
|
+
self.can_interface = self.parent.transport_layer_interface
|
|
249
|
+
else:
|
|
250
|
+
self.can_interface = self.can_interface_class(interface=self.interface_name, **self.parameters)
|
|
251
|
+
if self.parent.daq_identifier:
|
|
252
|
+
# Add filters for DAQ identifiers.
|
|
253
|
+
for daq_id in self.parent.daq_identifier:
|
|
254
|
+
can_filters.append(daq_id.create_filter_from_id())
|
|
255
|
+
self.can_interface.set_filters(can_filters)
|
|
256
|
+
self.parent.logger.info(f"XCPonCAN - Using Interface: '{self.can_interface!s}'")
|
|
257
|
+
self.parent.logger.info(f"XCPonCAN - Filters used: {self.can_interface.filters}")
|
|
258
|
+
self.parent.logger.info(f"XCPonCAN - State: {self.can_interface.state!s}")
|
|
259
|
+
self.connected = True
|
|
260
|
+
|
|
261
|
+
def close(self):
|
|
262
|
+
if self.connected and not self.parent.has_user_supplied_interface:
|
|
263
|
+
self.can_interface.shutdown()
|
|
264
|
+
self.connected = False
|
|
265
|
+
|
|
266
|
+
def transmit(self, payload: bytes) -> None:
|
|
267
|
+
frame = Message(
|
|
268
|
+
arbitration_id=self.parent.can_id_master.id,
|
|
269
|
+
is_extended_id=True if self.parent.can_id_master.is_extended else False,
|
|
270
|
+
is_fd=self.parent.fd,
|
|
271
|
+
data=payload,
|
|
272
|
+
)
|
|
273
|
+
self.can_interface.send(frame)
|
|
274
|
+
|
|
275
|
+
def read(self) -> Optional[Frame]:
|
|
276
|
+
if not self.connected:
|
|
277
|
+
return None
|
|
278
|
+
try:
|
|
279
|
+
frame = self.can_interface.recv(self.timeout)
|
|
280
|
+
except CanError:
|
|
281
|
+
return None
|
|
282
|
+
else:
|
|
283
|
+
if frame is None or not len(frame.data):
|
|
284
|
+
return None # Timeout condition.
|
|
285
|
+
extended = frame.is_extended_id
|
|
286
|
+
identifier = Identifier.make_identifier(frame.arbitration_id, extended)
|
|
287
|
+
return Frame(
|
|
288
|
+
id_=identifier,
|
|
289
|
+
dlc=frame.dlc,
|
|
290
|
+
data=frame.data,
|
|
291
|
+
timestamp=seconds_to_nanoseconds(frame.timestamp),
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def get_timestamp_resolution(self) -> int:
|
|
295
|
+
return 10 * 1000
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class EmptyHeader:
|
|
299
|
+
"""There is no header for XCP on CAN"""
|
|
300
|
+
|
|
301
|
+
def pack(self, *args, **kwargs):
|
|
302
|
+
return b""
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class Can(BaseTransport):
|
|
306
|
+
""""""
|
|
307
|
+
|
|
308
|
+
MAX_DATAGRAM_SIZE = 7
|
|
309
|
+
HEADER = EmptyHeader()
|
|
310
|
+
HEADER_SIZE = 0
|
|
311
|
+
|
|
312
|
+
def __init__(self, config, policy=None, transport_layer_interface: Optional[BusABC] = None):
|
|
313
|
+
super().__init__(config, policy, transport_layer_interface)
|
|
314
|
+
self.load_config(config)
|
|
315
|
+
self.useDefaultListener = self.config.use_default_listener
|
|
316
|
+
self.can_id_master = Identifier(self.config.can_id_master)
|
|
317
|
+
self.can_id_slave = Identifier(self.config.can_id_slave)
|
|
318
|
+
|
|
319
|
+
# 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]:
|
|
320
|
+
# "... If a PDU does not exactly match these configurable sizes the unused bytes shall be padded."
|
|
321
|
+
#
|
|
322
|
+
self.fd = self.config.fd
|
|
323
|
+
self.daq_identifier = []
|
|
324
|
+
if self.config.daq_identifier:
|
|
325
|
+
for daq_id in self.config.daq_identifier:
|
|
326
|
+
self.daq_identifier.append(Identifier(daq_id))
|
|
327
|
+
self.max_dlc_required = self.config.max_dlc_required
|
|
328
|
+
self.padding_value = self.config.padding_value
|
|
329
|
+
self.interface_name = self.config.interface
|
|
330
|
+
self.interface_configuration = detect_available_configs(interfaces=[self.interface_name])
|
|
331
|
+
parameters = self.get_interface_parameters()
|
|
332
|
+
self.can_interface = PythonCanWrapper(self, self.interface_name, config.timeout, **parameters)
|
|
333
|
+
self.logger.info(f"XCPonCAN - Interface-Type: {self.interface_name!r} Parameters: {list(parameters.items())}")
|
|
334
|
+
self.logger.info(
|
|
335
|
+
f"XCPonCAN - Master-ID (Tx): 0x{self.can_id_master.id:08X}{self.can_id_master.type_str} -- "
|
|
336
|
+
f"Slave-ID (Rx): 0x{self.can_id_slave.id:08X}{self.can_id_slave.type_str}"
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
def get_interface_parameters(self) -> Dict[str, Any]:
|
|
340
|
+
result = dict(channel=self.config.channel)
|
|
341
|
+
|
|
342
|
+
can_interface_config_class = CAN_INTERFACE_MAP[self.interface_name]
|
|
343
|
+
|
|
344
|
+
# Optional base class parameters.
|
|
345
|
+
optional_parameters = [(p, p.removeprefix("has_")) for p in can_interface_config_class.OPTIONAL_BASE_PARAMS]
|
|
346
|
+
for o, n in optional_parameters:
|
|
347
|
+
opt = getattr(can_interface_config_class, o)
|
|
348
|
+
value = getattr(self.config, n)
|
|
349
|
+
if opt:
|
|
350
|
+
if value is not None:
|
|
351
|
+
result[n] = value
|
|
352
|
+
elif value is not None:
|
|
353
|
+
self.logger.warning(f"XCPonCAN - {self.interface_name!r} has no support for parameter {n!r}.")
|
|
354
|
+
# Parameter names that need to be mapped.
|
|
355
|
+
for base_name, name in can_interface_config_class.CAN_PARAM_MAP.items():
|
|
356
|
+
value = getattr(self.config, base_name)
|
|
357
|
+
if name is not None and value is not None:
|
|
358
|
+
result[name] = value
|
|
359
|
+
# Interface specific parameters.
|
|
360
|
+
cxx = getattr(self.config, self.interface_name)
|
|
361
|
+
for name in can_interface_config_class.class_own_traits().keys():
|
|
362
|
+
value = getattr(cxx, name)
|
|
363
|
+
if value is not None:
|
|
364
|
+
result[name] = value
|
|
365
|
+
return result
|
|
366
|
+
|
|
367
|
+
def data_received(self, payload: bytes, recv_timestamp: int):
|
|
368
|
+
self.process_response(
|
|
369
|
+
payload,
|
|
370
|
+
len(payload),
|
|
371
|
+
counter=(self.counter_received + 1) & 0xFFFF,
|
|
372
|
+
recv_timestamp=recv_timestamp,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
def listen(self):
|
|
376
|
+
while True:
|
|
377
|
+
if self.closeEvent.is_set():
|
|
378
|
+
return
|
|
379
|
+
frame = self.can_interface.read()
|
|
380
|
+
if frame:
|
|
381
|
+
self.data_received(frame.data, frame.timestamp)
|
|
382
|
+
|
|
383
|
+
def connect(self):
|
|
384
|
+
if self.useDefaultListener:
|
|
385
|
+
self.start_listener()
|
|
386
|
+
try:
|
|
387
|
+
self.can_interface.connect()
|
|
388
|
+
except CanInitializationError:
|
|
389
|
+
console.print("[red]\nThere may be a problem with the configuration of your CAN-interface.\n")
|
|
390
|
+
console.print(f"[grey]Current configuration of interface {self.interface_name!r}:")
|
|
391
|
+
console.print(self.interface_configuration)
|
|
392
|
+
raise
|
|
393
|
+
self.status = 1 # connected
|
|
394
|
+
|
|
395
|
+
def send(self, frame: bytes) -> None:
|
|
396
|
+
# send the request
|
|
397
|
+
self.pre_send_timestamp = self.timestamp.value
|
|
398
|
+
self.can_interface.transmit(payload=pad_frame(frame, self.max_dlc_required, self.padding_value))
|
|
399
|
+
self.post_send_timestamp = self.timestamp.value
|
|
400
|
+
|
|
401
|
+
def close_connection(self):
|
|
402
|
+
if hasattr(self, "can_interface"):
|
|
403
|
+
self.can_interface.close()
|
|
404
|
+
|
|
405
|
+
def close(self):
|
|
406
|
+
self.finish_listener()
|
|
407
|
+
self.close_connection()
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def set_DLC(length: int):
|
|
411
|
+
"""Return DLC value according to CAN-FD.
|
|
412
|
+
|
|
413
|
+
:param length: Length value to be mapped to a valid CAN-FD DLC.
|
|
414
|
+
( 0 <= length <= 64)
|
|
415
|
+
"""
|
|
416
|
+
|
|
417
|
+
if length < 0:
|
|
418
|
+
raise ValueError("Non-negative length value required.")
|
|
419
|
+
elif length <= MAX_DLC_CLASSIC:
|
|
420
|
+
return length
|
|
421
|
+
elif length <= 64:
|
|
422
|
+
for dlc in CAN_FD_DLCS:
|
|
423
|
+
if length <= dlc:
|
|
424
|
+
return dlc
|
|
425
|
+
else:
|
|
426
|
+
raise ValueError("DLC could be at most 64.")
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def calculate_filter(ids: list):
|
|
430
|
+
"""
|
|
431
|
+
:param ids: An iterable (usually list or tuple) containing CAN identifiers.
|
|
432
|
+
|
|
433
|
+
:return: Calculated filter and mask.
|
|
434
|
+
:rtype: tuple (int, int)
|
|
435
|
+
"""
|
|
436
|
+
any_extended_ids = any(is_extended_identifier(i) for i in ids)
|
|
437
|
+
raw_ids = [stripIdentifier(i) for i in ids]
|
|
438
|
+
cfilter = functools.reduce(operator.and_, raw_ids)
|
|
439
|
+
cmask = functools.reduce(operator.or_, raw_ids) ^ cfilter
|
|
440
|
+
cmask ^= 0x1FFFFFFF if any_extended_ids else 0x7FF
|
|
441
|
+
return (cfilter, cmask)
|
pyxcp/transport/eth.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
import selectors
|
|
3
|
+
import socket
|
|
4
|
+
import struct
|
|
5
|
+
import threading
|
|
6
|
+
from collections import deque
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from pyxcp.transport.base import BaseTransport
|
|
10
|
+
from pyxcp.utils import short_sleep
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
DEFAULT_XCP_PORT = 5555
|
|
14
|
+
RECV_SIZE = 8196
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def socket_to_str(sock: socket.socket) -> str:
|
|
18
|
+
peer = sock.getpeername()
|
|
19
|
+
local = sock.getsockname()
|
|
20
|
+
AF = {
|
|
21
|
+
socket.AF_INET: "AF_INET",
|
|
22
|
+
socket.AF_INET6: "AF_INET6",
|
|
23
|
+
}
|
|
24
|
+
TYPE = {
|
|
25
|
+
socket.SOCK_DGRAM: "SOCK_DGRAM",
|
|
26
|
+
socket.SOCK_STREAM: "SOCK_STREAM",
|
|
27
|
+
}
|
|
28
|
+
family = AF.get(sock.family, "OTHER")
|
|
29
|
+
typ = TYPE.get(sock.type, "UNKNOWN")
|
|
30
|
+
res = f"XCPonEth - Connected to: {peer[0]}:{peer[1]} local address: {local[0]}:{local[1]} [{family}][{typ}]"
|
|
31
|
+
return res
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Eth(BaseTransport):
|
|
35
|
+
""""""
|
|
36
|
+
|
|
37
|
+
MAX_DATAGRAM_SIZE = 512
|
|
38
|
+
HEADER = struct.Struct("<HH")
|
|
39
|
+
HEADER_SIZE = HEADER.size
|
|
40
|
+
|
|
41
|
+
def __init__(self, config=None, policy=None, transport_layer_interface: Optional[socket.socket] = None) -> None:
|
|
42
|
+
super().__init__(config, policy, transport_layer_interface)
|
|
43
|
+
self.load_config(config)
|
|
44
|
+
self.host: str = self.config.host
|
|
45
|
+
self.port: int = self.config.port
|
|
46
|
+
self.protocol: int = self.config.protocol
|
|
47
|
+
self.ipv6: bool = self.config.ipv6
|
|
48
|
+
self.use_tcp_no_delay: bool = self.config.tcp_nodelay
|
|
49
|
+
address_to_bind: str = self.config.bind_to_address
|
|
50
|
+
bind_to_port: int = self.config.bind_to_port
|
|
51
|
+
self._local_address = (address_to_bind, bind_to_port) if address_to_bind else None
|
|
52
|
+
if self.ipv6 and not socket.has_ipv6:
|
|
53
|
+
msg = "XCPonEth - IPv6 not supported by your platform."
|
|
54
|
+
self.logger.critical(msg)
|
|
55
|
+
raise RuntimeError(msg)
|
|
56
|
+
else:
|
|
57
|
+
address_family = socket.AF_INET6 if self.ipv6 else socket.AF_INET
|
|
58
|
+
proto = socket.SOCK_STREAM if self.protocol == "TCP" else socket.SOCK_DGRAM
|
|
59
|
+
if self.host.lower() == "localhost":
|
|
60
|
+
self.host = "::1" if self.ipv6 else "localhost"
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
addrinfo = socket.getaddrinfo(self.host, self.port, address_family, proto)
|
|
64
|
+
(
|
|
65
|
+
self.address_family,
|
|
66
|
+
self.socktype,
|
|
67
|
+
self.proto,
|
|
68
|
+
self.canonname,
|
|
69
|
+
self.sockaddr,
|
|
70
|
+
) = addrinfo[0]
|
|
71
|
+
except BaseException as ex: # noqa: B036
|
|
72
|
+
msg = f"XCPonEth - Failed to resolve address {self.host}:{self.port}"
|
|
73
|
+
self.logger.critical(msg)
|
|
74
|
+
raise Exception(msg) from ex
|
|
75
|
+
self.status: int = 0
|
|
76
|
+
self.sock = socket.socket(self.address_family, self.socktype, self.proto)
|
|
77
|
+
self.selector = selectors.DefaultSelector()
|
|
78
|
+
self.selector.register(self.sock, selectors.EVENT_READ)
|
|
79
|
+
self.use_tcp = self.protocol == "TCP"
|
|
80
|
+
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
81
|
+
if self.use_tcp and self.use_tcp_no_delay:
|
|
82
|
+
self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|
83
|
+
if hasattr(socket, "SO_REUSEPORT"):
|
|
84
|
+
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
|
85
|
+
self.sock.settimeout(0.5)
|
|
86
|
+
if self._local_address:
|
|
87
|
+
try:
|
|
88
|
+
self.sock.bind(self._local_address)
|
|
89
|
+
except BaseException as ex: # noqa: B036
|
|
90
|
+
msg = f"XCPonEth - Failed to bind socket to given address {self._local_address}"
|
|
91
|
+
self.logger.critical(msg)
|
|
92
|
+
raise Exception(msg) from ex
|
|
93
|
+
self._packet_listener = threading.Thread(
|
|
94
|
+
target=self._packet_listen,
|
|
95
|
+
args=(),
|
|
96
|
+
kwargs={},
|
|
97
|
+
)
|
|
98
|
+
self._packets = deque()
|
|
99
|
+
|
|
100
|
+
def connect(self) -> None:
|
|
101
|
+
if self.status == 0:
|
|
102
|
+
self.sock.connect(self.sockaddr)
|
|
103
|
+
self.logger.info(socket_to_str(self.sock))
|
|
104
|
+
self.start_listener()
|
|
105
|
+
self.status = 1 # connected
|
|
106
|
+
|
|
107
|
+
def start_listener(self) -> None:
|
|
108
|
+
super().start_listener()
|
|
109
|
+
if self._packet_listener.is_alive():
|
|
110
|
+
self._packet_listener.join()
|
|
111
|
+
self._packet_listener = threading.Thread(target=self._packet_listen)
|
|
112
|
+
self._packet_listener.start()
|
|
113
|
+
|
|
114
|
+
def close(self) -> None:
|
|
115
|
+
"""Close the transport-layer connection and event-loop."""
|
|
116
|
+
self.finish_listener()
|
|
117
|
+
if self.listener.is_alive():
|
|
118
|
+
self.listener.join()
|
|
119
|
+
if self._packet_listener.is_alive():
|
|
120
|
+
self._packet_listener.join()
|
|
121
|
+
self.close_connection()
|
|
122
|
+
|
|
123
|
+
def _packet_listen(self) -> None:
|
|
124
|
+
use_tcp: bool = self.use_tcp
|
|
125
|
+
EVENT_READ = selectors.EVENT_READ
|
|
126
|
+
close_event_set = self.closeEvent.is_set
|
|
127
|
+
socket_fileno = self.sock.fileno
|
|
128
|
+
select = self.selector.select
|
|
129
|
+
_packets = self._packets
|
|
130
|
+
if use_tcp:
|
|
131
|
+
sock_recv = self.sock.recv
|
|
132
|
+
else:
|
|
133
|
+
sock_recv = self.sock.recvfrom
|
|
134
|
+
while True:
|
|
135
|
+
try:
|
|
136
|
+
if close_event_set() or socket_fileno() == -1:
|
|
137
|
+
return
|
|
138
|
+
sel = select(0.02)
|
|
139
|
+
for _, events in sel:
|
|
140
|
+
if events & EVENT_READ:
|
|
141
|
+
recv_timestamp = self.timestamp.value
|
|
142
|
+
if use_tcp:
|
|
143
|
+
response = sock_recv(RECV_SIZE)
|
|
144
|
+
if not response:
|
|
145
|
+
self.sock.close()
|
|
146
|
+
self.status = 0
|
|
147
|
+
break
|
|
148
|
+
else:
|
|
149
|
+
_packets.append((response, recv_timestamp))
|
|
150
|
+
else:
|
|
151
|
+
response, _ = sock_recv(Eth.MAX_DATAGRAM_SIZE)
|
|
152
|
+
if not response:
|
|
153
|
+
self.sock.close()
|
|
154
|
+
self.status = 0
|
|
155
|
+
break
|
|
156
|
+
else:
|
|
157
|
+
_packets.append((response, recv_timestamp))
|
|
158
|
+
except BaseException: # noqa: B036
|
|
159
|
+
self.status = 0 # disconnected
|
|
160
|
+
break
|
|
161
|
+
|
|
162
|
+
def listen(self) -> None:
|
|
163
|
+
HEADER_UNPACK_FROM = self.HEADER.unpack_from
|
|
164
|
+
HEADER_SIZE = self.HEADER_SIZE
|
|
165
|
+
process_response = self.process_response
|
|
166
|
+
popleft = self._packets.popleft
|
|
167
|
+
close_event_set = self.closeEvent.is_set
|
|
168
|
+
socket_fileno = self.sock.fileno
|
|
169
|
+
_packets = self._packets
|
|
170
|
+
length: Optional[int] = None
|
|
171
|
+
counter: int = 0
|
|
172
|
+
data: bytearray = bytearray(b"")
|
|
173
|
+
while True:
|
|
174
|
+
if close_event_set() or socket_fileno() == -1:
|
|
175
|
+
return
|
|
176
|
+
count: int = len(_packets)
|
|
177
|
+
if not count:
|
|
178
|
+
short_sleep()
|
|
179
|
+
continue
|
|
180
|
+
for _ in range(count):
|
|
181
|
+
bts, timestamp = popleft()
|
|
182
|
+
data += bts
|
|
183
|
+
current_size: int = len(data)
|
|
184
|
+
current_position: int = 0
|
|
185
|
+
while True:
|
|
186
|
+
if length is None:
|
|
187
|
+
if current_size >= HEADER_SIZE:
|
|
188
|
+
length, counter = HEADER_UNPACK_FROM(data, current_position)
|
|
189
|
+
current_position += HEADER_SIZE
|
|
190
|
+
current_size -= HEADER_SIZE
|
|
191
|
+
else:
|
|
192
|
+
data = data[current_position:]
|
|
193
|
+
break
|
|
194
|
+
else:
|
|
195
|
+
if current_size >= length:
|
|
196
|
+
response = data[current_position : current_position + length]
|
|
197
|
+
process_response(response, length, counter, timestamp)
|
|
198
|
+
current_size -= length
|
|
199
|
+
current_position += length
|
|
200
|
+
length = None
|
|
201
|
+
else:
|
|
202
|
+
data = data[current_position:]
|
|
203
|
+
break
|
|
204
|
+
|
|
205
|
+
def send(self, frame) -> None:
|
|
206
|
+
self.pre_send_timestamp = self.timestamp.value
|
|
207
|
+
self.sock.send(frame)
|
|
208
|
+
self.post_send_timestamp = self.timestamp.value
|
|
209
|
+
|
|
210
|
+
def close_connection(self) -> None:
|
|
211
|
+
if not self.invalidSocket:
|
|
212
|
+
# Seems to be problematic /w IPv6
|
|
213
|
+
# if self.status == 1:
|
|
214
|
+
# self.sock.shutdown(socket.SHUT_RDWR)
|
|
215
|
+
self.sock.close()
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def invalidSocket(self) -> bool:
|
|
219
|
+
return not hasattr(self, "sock") or self.sock.fileno() == -1
|