pyxcp 0.22.33__cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.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.

Files changed (128) 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.sh +2 -0
  19. pyxcp/checksum.py +732 -0
  20. pyxcp/cmdline.py +52 -0
  21. pyxcp/config/__init__.py +1102 -0
  22. pyxcp/config/legacy.py +120 -0
  23. pyxcp/constants.py +47 -0
  24. pyxcp/cpp_ext/__init__.py +0 -0
  25. pyxcp/cpp_ext/bin.hpp +104 -0
  26. pyxcp/cpp_ext/blockmem.hpp +58 -0
  27. pyxcp/cpp_ext/cpp_ext.cpython-310-aarch64-linux-gnu.so +0 -0
  28. pyxcp/cpp_ext/cpp_ext.cpython-311-aarch64-linux-gnu.so +0 -0
  29. pyxcp/cpp_ext/daqlist.hpp +206 -0
  30. pyxcp/cpp_ext/event.hpp +67 -0
  31. pyxcp/cpp_ext/extension_wrapper.cpp +100 -0
  32. pyxcp/cpp_ext/helper.hpp +280 -0
  33. pyxcp/cpp_ext/mcobject.hpp +246 -0
  34. pyxcp/cpp_ext/tsqueue.hpp +46 -0
  35. pyxcp/daq_stim/__init__.py +232 -0
  36. pyxcp/daq_stim/optimize/__init__.py +67 -0
  37. pyxcp/daq_stim/optimize/binpacking.py +41 -0
  38. pyxcp/daq_stim/scheduler.cpp +28 -0
  39. pyxcp/daq_stim/scheduler.hpp +75 -0
  40. pyxcp/daq_stim/stim.cpp +13 -0
  41. pyxcp/daq_stim/stim.cpython-310-aarch64-linux-gnu.so +0 -0
  42. pyxcp/daq_stim/stim.cpython-311-aarch64-linux-gnu.so +0 -0
  43. pyxcp/daq_stim/stim.hpp +604 -0
  44. pyxcp/daq_stim/stim_wrapper.cpp +50 -0
  45. pyxcp/dllif.py +95 -0
  46. pyxcp/errormatrix.py +878 -0
  47. pyxcp/examples/conf_can.toml +19 -0
  48. pyxcp/examples/conf_can_user.toml +16 -0
  49. pyxcp/examples/conf_can_vector.json +11 -0
  50. pyxcp/examples/conf_can_vector.toml +11 -0
  51. pyxcp/examples/conf_eth.toml +9 -0
  52. pyxcp/examples/conf_nixnet.json +20 -0
  53. pyxcp/examples/conf_socket_can.toml +12 -0
  54. pyxcp/examples/conf_sxi.json +9 -0
  55. pyxcp/examples/conf_sxi.toml +7 -0
  56. pyxcp/examples/run_daq.py +163 -0
  57. pyxcp/examples/xcp_policy.py +60 -0
  58. pyxcp/examples/xcp_read_benchmark.py +38 -0
  59. pyxcp/examples/xcp_skel.py +49 -0
  60. pyxcp/examples/xcp_unlock.py +38 -0
  61. pyxcp/examples/xcp_user_supplied_driver.py +54 -0
  62. pyxcp/examples/xcphello.py +79 -0
  63. pyxcp/examples/xcphello_recorder.py +107 -0
  64. pyxcp/master/__init__.py +9 -0
  65. pyxcp/master/errorhandler.py +442 -0
  66. pyxcp/master/master.py +2046 -0
  67. pyxcp/py.typed +0 -0
  68. pyxcp/recorder/__init__.py +101 -0
  69. pyxcp/recorder/build_clang.cmd +1 -0
  70. pyxcp/recorder/build_clang.sh +2 -0
  71. pyxcp/recorder/build_gcc.cmd +1 -0
  72. pyxcp/recorder/build_gcc.sh +2 -0
  73. pyxcp/recorder/build_gcc_arm.sh +2 -0
  74. pyxcp/recorder/converter/__init__.py +451 -0
  75. pyxcp/recorder/lz4.c +2829 -0
  76. pyxcp/recorder/lz4.h +879 -0
  77. pyxcp/recorder/lz4hc.c +2041 -0
  78. pyxcp/recorder/lz4hc.h +413 -0
  79. pyxcp/recorder/mio.hpp +1714 -0
  80. pyxcp/recorder/reader.hpp +139 -0
  81. pyxcp/recorder/reco.py +277 -0
  82. pyxcp/recorder/recorder.rst +0 -0
  83. pyxcp/recorder/rekorder.cpp +59 -0
  84. pyxcp/recorder/rekorder.cpython-310-aarch64-linux-gnu.so +0 -0
  85. pyxcp/recorder/rekorder.cpython-311-aarch64-linux-gnu.so +0 -0
  86. pyxcp/recorder/rekorder.hpp +274 -0
  87. pyxcp/recorder/setup.py +41 -0
  88. pyxcp/recorder/test_reko.py +34 -0
  89. pyxcp/recorder/unfolder.hpp +1332 -0
  90. pyxcp/recorder/wrap.cpp +189 -0
  91. pyxcp/recorder/writer.hpp +302 -0
  92. pyxcp/scripts/__init__.py +0 -0
  93. pyxcp/scripts/pyxcp_probe_can_drivers.py +20 -0
  94. pyxcp/scripts/xcp_examples.py +64 -0
  95. pyxcp/scripts/xcp_fetch_a2l.py +40 -0
  96. pyxcp/scripts/xcp_id_scanner.py +19 -0
  97. pyxcp/scripts/xcp_info.py +146 -0
  98. pyxcp/scripts/xcp_profile.py +27 -0
  99. pyxcp/scripts/xmraw_converter.py +31 -0
  100. pyxcp/stim/__init__.py +0 -0
  101. pyxcp/tests/test_asam_types.py +24 -0
  102. pyxcp/tests/test_binpacking.py +186 -0
  103. pyxcp/tests/test_can.py +1324 -0
  104. pyxcp/tests/test_checksum.py +95 -0
  105. pyxcp/tests/test_daq.py +193 -0
  106. pyxcp/tests/test_daq_opt.py +426 -0
  107. pyxcp/tests/test_frame_padding.py +156 -0
  108. pyxcp/tests/test_master.py +2006 -0
  109. pyxcp/tests/test_transport.py +81 -0
  110. pyxcp/tests/test_utils.py +30 -0
  111. pyxcp/timing.py +60 -0
  112. pyxcp/transport/__init__.py +10 -0
  113. pyxcp/transport/base.py +440 -0
  114. pyxcp/transport/base_transport.hpp +0 -0
  115. pyxcp/transport/can.py +441 -0
  116. pyxcp/transport/eth.py +219 -0
  117. pyxcp/transport/sxi.py +135 -0
  118. pyxcp/transport/transport_wrapper.cpp +0 -0
  119. pyxcp/transport/usb_transport.py +213 -0
  120. pyxcp/types.py +1000 -0
  121. pyxcp/utils.py +127 -0
  122. pyxcp/vector/__init__.py +0 -0
  123. pyxcp/vector/map.py +82 -0
  124. pyxcp-0.22.33.dist-info/LICENSE +165 -0
  125. pyxcp-0.22.33.dist-info/METADATA +107 -0
  126. pyxcp-0.22.33.dist-info/RECORD +128 -0
  127. pyxcp-0.22.33.dist-info/WHEEL +6 -0
  128. 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