pycyphal2 2.0.0.dev0__py3-none-any.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.
- pycyphal2/__init__.py +89 -0
- pycyphal2/_api.py +604 -0
- pycyphal2/_hash.py +204 -0
- pycyphal2/_header.py +349 -0
- pycyphal2/_node.py +1472 -0
- pycyphal2/_publisher.py +427 -0
- pycyphal2/_subscriber.py +430 -0
- pycyphal2/_transport.py +92 -0
- pycyphal2/can/__init__.py +43 -0
- pycyphal2/can/_interface.py +131 -0
- pycyphal2/can/_reassembly.py +158 -0
- pycyphal2/can/_transport.py +525 -0
- pycyphal2/can/_wire.py +376 -0
- pycyphal2/can/pythoncan.py +261 -0
- pycyphal2/can/socketcan.py +225 -0
- pycyphal2/py.typed +0 -0
- pycyphal2/udp.py +1000 -0
- pycyphal2-2.0.0.dev0.dist-info/METADATA +58 -0
- pycyphal2-2.0.0.dev0.dist-info/RECORD +22 -0
- pycyphal2-2.0.0.dev0.dist-info/WHEEL +5 -0
- pycyphal2-2.0.0.dev0.dist-info/licenses/LICENSE +20 -0
- pycyphal2-2.0.0.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Linux SocketCAN backend for :mod:`pycyphal2.can`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import errno
|
|
7
|
+
from collections.abc import Iterable
|
|
8
|
+
import logging
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import socket
|
|
11
|
+
import struct
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
from .._api import ClosedError, Instant
|
|
15
|
+
from ._interface import Filter, Interface, TimestampedFrame
|
|
16
|
+
|
|
17
|
+
if sys.platform != "linux" or not hasattr(socket, "AF_CAN"):
|
|
18
|
+
raise ImportError("SocketCAN is available only on Linux with AF_CAN support")
|
|
19
|
+
|
|
20
|
+
_logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
_CAN_FILTER_CAPACITY = 64
|
|
23
|
+
_CAN_INTERFACE_TYPE = 280
|
|
24
|
+
_CAN_CLASSIC_MTU = 16
|
|
25
|
+
_CAN_FD_MTU = 72
|
|
26
|
+
_CANFD_FDF = getattr(socket, "CANFD_FDF", 0)
|
|
27
|
+
_CAN_FRAME_STRUCT = struct.Struct("=IB3x8s")
|
|
28
|
+
_CANFD_FRAME_STRUCT = struct.Struct("=IBBBB64s")
|
|
29
|
+
_CAN_FILTER_STRUCT = struct.Struct("=II")
|
|
30
|
+
_TRANSIENT_TX_ERRNO = {errno.EAGAIN, errno.EWOULDBLOCK, errno.ENOBUFS, errno.ENOMEM, errno.EBUSY}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SocketCANInterface(Interface):
|
|
34
|
+
def __init__(self, name: str) -> None:
|
|
35
|
+
self._name = str(name)
|
|
36
|
+
self._sock = socket.socket(socket.PF_CAN, socket.SOCK_RAW, socket.CAN_RAW)
|
|
37
|
+
self._sock.setblocking(False)
|
|
38
|
+
self._sock.setsockopt(socket.SOL_CAN_RAW, socket.CAN_RAW_LOOPBACK, 1)
|
|
39
|
+
self._sock.bind((self._name,))
|
|
40
|
+
self._fd = self._read_iface_mtu() >= _CAN_FD_MTU
|
|
41
|
+
if self._fd:
|
|
42
|
+
self._sock.setsockopt(socket.SOL_CAN_RAW, socket.CAN_RAW_FD_FRAMES, 1)
|
|
43
|
+
self._closed = False
|
|
44
|
+
self._failure: BaseException | None = None
|
|
45
|
+
self._tx_seq = 0
|
|
46
|
+
self._tx_queue: asyncio.PriorityQueue[tuple[int, int, int, bytes]] = asyncio.PriorityQueue()
|
|
47
|
+
self._tx_task: asyncio.Task[None] | None = None
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def name(self) -> str:
|
|
51
|
+
return self._name
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def fd(self) -> bool:
|
|
55
|
+
return self._fd
|
|
56
|
+
|
|
57
|
+
def filter(self, filters: Iterable[Filter]) -> None:
|
|
58
|
+
self._raise_if_closed()
|
|
59
|
+
flt = list(filters)
|
|
60
|
+
if len(flt) > _CAN_FILTER_CAPACITY:
|
|
61
|
+
flt = Filter.coalesce(flt, _CAN_FILTER_CAPACITY)
|
|
62
|
+
packed = bytearray()
|
|
63
|
+
for item in flt:
|
|
64
|
+
packed.extend(
|
|
65
|
+
_CAN_FILTER_STRUCT.pack(
|
|
66
|
+
socket.CAN_EFF_FLAG | (item.id & socket.CAN_EFF_MASK),
|
|
67
|
+
# Keep CAN_RTR_FLAG in the mask so the kernel rejects RTR frames at the filter layer.
|
|
68
|
+
socket.CAN_EFF_FLAG | socket.CAN_RTR_FLAG | (item.mask & socket.CAN_EFF_MASK),
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
self._sock.setsockopt(socket.SOL_CAN_RAW, socket.CAN_RAW_FILTER, bytes(packed))
|
|
72
|
+
|
|
73
|
+
def enqueue(self, id: int, data: Iterable[memoryview], deadline: Instant) -> None:
|
|
74
|
+
self._raise_if_closed()
|
|
75
|
+
if self._tx_task is None:
|
|
76
|
+
self._tx_task = asyncio.get_running_loop().create_task(self._tx_loop())
|
|
77
|
+
for chunk in data:
|
|
78
|
+
self._tx_seq += 1
|
|
79
|
+
self._tx_queue.put_nowait((id, self._tx_seq, deadline.ns, bytes(chunk)))
|
|
80
|
+
|
|
81
|
+
def purge(self) -> None:
|
|
82
|
+
if self._closed:
|
|
83
|
+
return
|
|
84
|
+
dropped = 0
|
|
85
|
+
try:
|
|
86
|
+
while True:
|
|
87
|
+
self._tx_queue.get_nowait()
|
|
88
|
+
dropped += 1
|
|
89
|
+
except asyncio.QueueEmpty:
|
|
90
|
+
pass
|
|
91
|
+
if dropped > 0:
|
|
92
|
+
_logger.debug("SocketCAN purge iface=%s dropped=%d", self._name, dropped)
|
|
93
|
+
|
|
94
|
+
async def receive(self) -> TimestampedFrame:
|
|
95
|
+
self._raise_if_closed()
|
|
96
|
+
loop = asyncio.get_running_loop()
|
|
97
|
+
recv_size = _CAN_FD_MTU if self._fd else _CAN_CLASSIC_MTU
|
|
98
|
+
while True:
|
|
99
|
+
try:
|
|
100
|
+
raw = await loop.sock_recv(self._sock, recv_size)
|
|
101
|
+
except asyncio.CancelledError:
|
|
102
|
+
raise
|
|
103
|
+
except OSError as ex:
|
|
104
|
+
self._fail(ex)
|
|
105
|
+
raise ClosedError(f"SocketCAN interface {self._name} receive failed") from ex
|
|
106
|
+
frame = self._decode(raw)
|
|
107
|
+
if frame is not None:
|
|
108
|
+
return frame
|
|
109
|
+
|
|
110
|
+
def close(self) -> None:
|
|
111
|
+
if self._closed:
|
|
112
|
+
return
|
|
113
|
+
self._closed = True
|
|
114
|
+
if self._tx_task is not None:
|
|
115
|
+
self._tx_task.cancel()
|
|
116
|
+
self._tx_task = None
|
|
117
|
+
self._sock.close()
|
|
118
|
+
|
|
119
|
+
def __repr__(self) -> str:
|
|
120
|
+
return f"{type(self).__name__}({self._name!r}, fd={self._fd})"
|
|
121
|
+
|
|
122
|
+
async def _tx_loop(self) -> None:
|
|
123
|
+
loop = asyncio.get_running_loop()
|
|
124
|
+
while not self._closed:
|
|
125
|
+
try:
|
|
126
|
+
identifier, seq, deadline_ns, payload = await self._tx_queue.get()
|
|
127
|
+
except asyncio.CancelledError:
|
|
128
|
+
raise
|
|
129
|
+
if self._closed:
|
|
130
|
+
return
|
|
131
|
+
if Instant.now().ns >= deadline_ns:
|
|
132
|
+
_logger.debug("SocketCAN tx drop expired iface=%s id=%08x", self._name, identifier)
|
|
133
|
+
continue
|
|
134
|
+
frame = self._encode(identifier, payload)
|
|
135
|
+
timeout = max(0.0, (deadline_ns - Instant.now().ns) * 1e-9)
|
|
136
|
+
if timeout <= 0.0:
|
|
137
|
+
_logger.debug("SocketCAN tx drop expired iface=%s id=%08x", self._name, identifier)
|
|
138
|
+
continue
|
|
139
|
+
try:
|
|
140
|
+
await asyncio.wait_for(loop.sock_sendall(self._sock, frame), timeout=timeout)
|
|
141
|
+
except asyncio.TimeoutError:
|
|
142
|
+
self._tx_queue.put_nowait((identifier, seq, deadline_ns, payload))
|
|
143
|
+
await asyncio.sleep(0.001)
|
|
144
|
+
except OSError as ex:
|
|
145
|
+
if self._is_transient_tx_error(ex):
|
|
146
|
+
_logger.debug("SocketCAN tx retry iface=%s err=%s", self._name, ex)
|
|
147
|
+
self._tx_queue.put_nowait((identifier, seq, deadline_ns, payload))
|
|
148
|
+
await asyncio.sleep(0.001)
|
|
149
|
+
continue
|
|
150
|
+
self._fail(ex)
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
def _read_iface_mtu(self) -> int:
|
|
154
|
+
return int(Path(f"/sys/class/net/{self._name}/mtu").read_text().strip())
|
|
155
|
+
|
|
156
|
+
def _fail(self, ex: BaseException) -> None:
|
|
157
|
+
if self._failure is None:
|
|
158
|
+
self._failure = ex
|
|
159
|
+
_logger.error("SocketCAN interface %s failed: %s", self._name, ex)
|
|
160
|
+
self.close()
|
|
161
|
+
|
|
162
|
+
def _raise_if_closed(self) -> None:
|
|
163
|
+
if self._closed:
|
|
164
|
+
if self._failure is not None:
|
|
165
|
+
raise ClosedError(f"SocketCAN interface {self._name} failed") from self._failure
|
|
166
|
+
raise ClosedError(f"SocketCAN interface {self._name} closed")
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def _is_transient_tx_error(ex: OSError) -> bool:
|
|
170
|
+
return ex.errno in _TRANSIENT_TX_ERRNO
|
|
171
|
+
|
|
172
|
+
def _encode(self, identifier: int, data: bytes) -> bytes:
|
|
173
|
+
if len(data) > 8:
|
|
174
|
+
if not self._fd:
|
|
175
|
+
raise ClosedError(f"SocketCAN interface {self._name} is not CAN FD-capable")
|
|
176
|
+
return _CANFD_FRAME_STRUCT.pack(
|
|
177
|
+
socket.CAN_EFF_FLAG | (identifier & socket.CAN_EFF_MASK),
|
|
178
|
+
len(data),
|
|
179
|
+
_CANFD_FDF,
|
|
180
|
+
0,
|
|
181
|
+
0,
|
|
182
|
+
data.ljust(64, b"\x00"),
|
|
183
|
+
)
|
|
184
|
+
return _CAN_FRAME_STRUCT.pack(
|
|
185
|
+
socket.CAN_EFF_FLAG | (identifier & socket.CAN_EFF_MASK),
|
|
186
|
+
len(data),
|
|
187
|
+
data.ljust(8, b"\x00"),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
@staticmethod
|
|
191
|
+
def _decode(raw: bytes) -> TimestampedFrame | None:
|
|
192
|
+
if len(raw) < _CAN_CLASSIC_MTU:
|
|
193
|
+
_logger.debug("SocketCAN drop short len=%d", len(raw))
|
|
194
|
+
return None
|
|
195
|
+
if len(raw) >= _CAN_FD_MTU:
|
|
196
|
+
can_id, length, _flags, _reserved0, _reserved1, data = _CANFD_FRAME_STRUCT.unpack(raw[:_CAN_FD_MTU])
|
|
197
|
+
payload = data[: min(length, 64)]
|
|
198
|
+
else:
|
|
199
|
+
can_id, length, data = _CAN_FRAME_STRUCT.unpack(raw[:_CAN_CLASSIC_MTU])
|
|
200
|
+
payload = data[: min(length, 8)]
|
|
201
|
+
if (can_id & socket.CAN_EFF_FLAG) == 0 or (can_id & (socket.CAN_RTR_FLAG | socket.CAN_ERR_FLAG)) != 0:
|
|
202
|
+
_logger.debug("SocketCAN drop non-extended or non-data id=%08x", can_id)
|
|
203
|
+
return None
|
|
204
|
+
return TimestampedFrame(
|
|
205
|
+
id=can_id & socket.CAN_EFF_MASK,
|
|
206
|
+
data=payload,
|
|
207
|
+
timestamp=Instant.now(),
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def list_interfaces() -> list[str]:
|
|
212
|
+
out: list[str] = []
|
|
213
|
+
base = Path("/sys/class/net")
|
|
214
|
+
try:
|
|
215
|
+
for item in sorted(base.iterdir()):
|
|
216
|
+
try:
|
|
217
|
+
if int((item / "type").read_text().strip()) == _CAN_INTERFACE_TYPE:
|
|
218
|
+
out.append(item.name)
|
|
219
|
+
except OSError:
|
|
220
|
+
continue
|
|
221
|
+
except ValueError:
|
|
222
|
+
continue
|
|
223
|
+
except OSError:
|
|
224
|
+
pass
|
|
225
|
+
return out
|
pycyphal2/py.typed
ADDED
|
File without changes
|