Skip to content

Commit 4f56e65

Browse files
[Cyphal/CAN] PythonCAN: Remove send executor; improve development documentation on type checking (#293)
Co-authored-by: Pavel Kirienko <pavel.kirienko@gmail.com>
1 parent d2ee6c5 commit 4f56e65

File tree

7 files changed

+114
-9
lines changed

7 files changed

+114
-9
lines changed

.gitmodules

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[submodule "public_regulated_data_types_for_testing"]
22
path = demo/public_regulated_data_types
3-
url = https://github.com/UAVCAN/public_regulated_data_types
3+
url = https://github.com/OpenCyphal/public_regulated_data_types

CHANGELOG.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
Changelog
44
=========
55

6+
v1.15
7+
-----
8+
9+
- Made PythonCAN support better.
10+
611
v1.14
712
-----
813

CONTRIBUTING.rst

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,49 @@ To abort on first error::
197197

198198
nox -x -- -x
199199

200+
Running MyPy
201+
.........................
202+
203+
Warning, this might be obsolete.
204+
205+
Sometimes it is useful to run MyPy directly, for instance, to check the types without waiting for a very long time
206+
for the tests to finish.
207+
Here's how to do it on Windows::
208+
209+
.nox\test-3-10\Scripts\activate
210+
pip install mypy
211+
mypy --strict pycyphal tests .nox\test-3-10\tmp\.compiled
212+
213+
214+
Running pylint
215+
.........................
216+
217+
Warning, this might be obsolete.
218+
219+
Sometimes it is useful to run pylint directly, for instance, to check the code quality without waiting
220+
for a very long time for the tests to finish.
221+
222+
Here's how to do it on Windows::
223+
224+
.nox\test-3-10\Scripts\activate
225+
pip install pylint
226+
pylint pycyphal tests .nox\test-3-10\tmp\.compiled
227+
228+
229+
Running black
230+
.........................
231+
232+
Warning, this might be obsolete.
233+
234+
Sometimes it is useful to run black directly, for instance, to check the code formatting
235+
without waiting for a very long time for the tests to finish.
236+
It is better, however, to configure the IDE to invoke Black automatically on save.
237+
238+
Here's how to do it on Windows::
239+
240+
pip install black
241+
black pycyphal tests .nox\test-3-10\tmp\.compiled
242+
200243

201244
Running a subset of tests
202245
.........................

demo/public_regulated_data_types

docs/pages/faq.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,23 @@ I am getting ``ModuleNotFoundError: No module named 'uavcan'``. Do I need to ins
3333
Imports fail with ``AttributeError: module 'uavcan...' has no attribute '...'``. What am I doing wrong?
3434
Remove the legacy library: ``pip uninstall -y uavcan``.
3535
Read the :ref:`installation` guide for details.
36+
37+
38+
I am experiencing poor SLCAN read/write performance on Windows. What can I do?
39+
Increasing the process priority to REALTIME
40+
(available if the application has administrator privileges) will help.
41+
Without administrator privileges, the HIGH priority set by this code,
42+
will still help with delays in SLCAN performance.
43+
Here's an example::
44+
45+
if sys.platform.startswith("win"):
46+
import ctypes, psutil
47+
48+
# Reconfigure the system timer to run at a higher resolution. This is desirable for the real-time tests.
49+
t = ctypes.c_ulong()
50+
ctypes.WinDLL("NTDLL.DLL").NtSetTimerResolution(5000, 1, ctypes.byref(t))
51+
p = psutil.Process(os.getpid())
52+
p.nice(psutil.REALTIME_PRIORITY_CLASS)
53+
elif sys.platform.startswith("linux"):
54+
p = psutil.Process(os.getpid())
55+
p.nice(-20)

pycyphal/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.14.4"
1+
__version__ = "1.15.0"

pycyphal/transport/can/media/pythoncan/_pythoncan.py

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33
# Author: Alex Kiselev <a.kiselev@volz-servos.com>, Pavel Kirienko <pavel@opencyphal.org>
44

55
from __future__ import annotations
6+
import queue
67
import time
78
import typing
89
import asyncio
910
import logging
1011
import threading
11-
import functools
12+
from functools import partial
1213
import dataclasses
1314
import collections
14-
import concurrent.futures
1515
import warnings
1616

1717
import can
@@ -22,6 +22,14 @@
2222
_logger = logging.getLogger(__name__)
2323

2424

25+
@dataclasses.dataclass(frozen=True)
26+
class _TxItem:
27+
msg: can.Message
28+
timeout: float
29+
future: asyncio.Future[None]
30+
loop: asyncio.AbstractEventLoop
31+
32+
2533
@dataclasses.dataclass(frozen=True)
2634
class PythonCANBusOptions:
2735
hardware_loopback: bool = False
@@ -188,7 +196,9 @@ def __init__(
188196
self._closed = False
189197
self._maybe_thread: typing.Optional[threading.Thread] = None
190198
self._rx_handler: typing.Optional[Media.ReceivedFramesHandler] = None
191-
self._background_executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
199+
# This is for communication with a thread that handles the call to _bus.send
200+
self._tx_queue: queue.Queue[_TxItem | None] = queue.Queue()
201+
self._tx_thread = threading.Thread(target=self.transmit_thread_worker, daemon=True)
192202

193203
params: typing.Union[_FDInterfaceParameters, _ClassicInterfaceParameters]
194204
if self._is_fd:
@@ -231,6 +241,7 @@ def is_fd(self) -> bool:
231241
return self._is_fd
232242

233243
def start(self, handler: Media.ReceivedFramesHandler, no_automatic_retransmission: bool) -> None:
244+
self._tx_thread.start()
234245
if self._maybe_thread is None:
235246
self._rx_handler = handler
236247
self._maybe_thread = threading.Thread(
@@ -254,6 +265,24 @@ def configure_acceptance_filters(self, configuration: typing.Sequence[FilterConf
254265
_logger.debug("%s: Acceptance filters activated: %s", self, ", ".join(map(str, configuration)))
255266
self._bus.set_filters(filters)
256267

268+
def transmit_thread_worker(self) -> None:
269+
try:
270+
while not self._closed:
271+
tx = self._tx_queue.get(block=True)
272+
if self._closed or tx is None:
273+
break
274+
try:
275+
self._bus.send(tx.msg, tx.timeout)
276+
tx.loop.call_soon_threadsafe(partial(tx.future.set_result, None))
277+
except Exception as ex:
278+
tx.loop.call_soon_threadsafe(partial(tx.future.set_exception, ex))
279+
except Exception as ex:
280+
_logger.critical(
281+
"Unhandled exception in transmit thread, transmission thread stopped and transmission is no longer possible: %s",
282+
ex,
283+
exc_info=True,
284+
)
285+
257286
async def send(self, frames: typing.Iterable[Envelope], monotonic_deadline: float) -> int:
258287
num_sent = 0
259288
loopback: typing.List[typing.Tuple[Timestamp, Envelope]] = []
@@ -269,10 +298,16 @@ async def send(self, frames: typing.Iterable[Envelope], monotonic_deadline: floa
269298
)
270299
try:
271300
desired_timeout = monotonic_deadline - loop.time()
272-
await loop.run_in_executor(
273-
self._background_executor,
274-
functools.partial(self._bus.send, message, timeout=max(desired_timeout, 0)),
301+
received_future: asyncio.Future[None] = asyncio.Future()
302+
self._tx_queue.put_nowait(
303+
_TxItem(
304+
message,
305+
max(desired_timeout, 0),
306+
received_future,
307+
asyncio.get_running_loop(),
308+
)
275309
)
310+
await received_future
276311
except (asyncio.TimeoutError, can.CanError): # CanError is also used to report timeouts (weird).
277312
break
278313
else:
@@ -287,6 +322,8 @@ async def send(self, frames: typing.Iterable[Envelope], monotonic_deadline: floa
287322
def close(self) -> None:
288323
self._closed = True
289324
try:
325+
self._tx_queue.put(None)
326+
self._tx_thread.join(timeout=self._MAXIMAL_TIMEOUT_SEC * 10)
290327
if self._maybe_thread is not None:
291328
self._maybe_thread.join(timeout=self._MAXIMAL_TIMEOUT_SEC * 10)
292329
self._maybe_thread = None

0 commit comments

Comments
 (0)