Skip to content

Commit 6b59513

Browse files
committed
Merge branch 'develop' into feature/bus_fd_field
# Conflicts: # can/__init__.py # can/interfaces/canalystii.py # can/interfaces/cantact.py # can/interfaces/etas/__init__.py # can/interfaces/gs_usb.py # can/interfaces/ics_neovi/neovi_bus.py # can/interfaces/iscan.py # can/interfaces/ixxat/canlib_vcinpl.py # can/interfaces/ixxat/canlib_vcinpl2.py # can/interfaces/kvaser/canlib.py # can/interfaces/neousys/neousys.py # can/interfaces/nican.py # can/interfaces/nixnet.py # can/interfaces/pcan/pcan.py # can/interfaces/robotell.py # can/interfaces/serial/serial_can.py # can/interfaces/slcan.py # can/interfaces/systec/ucanbus.py # can/interfaces/udp_multicast/bus.py # can/interfaces/usb2can/usb2canInterface.py # can/interfaces/vector/canlib.py # test/serial_test.py # test/test_interface_canalystii.py
2 parents eadfa83 + 1188c57 commit 6b59513

File tree

120 files changed

+792
-533
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

120 files changed

+792
-533
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ jobs:
9595
- name: mypy 3.11
9696
run: |
9797
mypy --python-version 3.11 .
98+
- name: ruff
99+
run: |
100+
ruff check can
98101
- name: pylint
99102
run: |
100103
pylint --rcfile=.pylintrc \

.pylintrc

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,8 @@ known-standard-library=
438438
# Force import order to recognize a module as part of a third party library.
439439
known-third-party=enchant
440440

441+
# Allow explicit reexports by alias from a package __init__
442+
allow-reexport-from-package=no
441443

442444
[CLASSES]
443445

@@ -498,5 +500,5 @@ min-public-methods=2
498500

499501
# Exceptions that will emit a warning when being caught. Defaults to
500502
# "BaseException, Exception".
501-
overgeneral-exceptions=BaseException,
502-
Exception
503+
overgeneral-exceptions=builtins.BaseException,
504+
builtins.Exception

can/__init__.py

Lines changed: 87 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,47 +6,105 @@
66
"""
77

88
import logging
9-
from typing import Dict, Any
9+
from typing import Any, Dict
1010

1111
__version__ = "4.1.0"
12+
__all__ = [
13+
"ASCReader",
14+
"ASCWriter",
15+
"AsyncBufferedReader",
16+
"BitTiming",
17+
"BitTimingFd",
18+
"BLFReader",
19+
"BLFWriter",
20+
"broadcastmanager",
21+
"BufferedReader",
22+
"Bus",
23+
"BusABC",
24+
"BusState",
25+
"CanError",
26+
"CanInitializationError",
27+
"CanInterfaceNotImplementedError",
28+
"CanOperationError",
29+
"CanProtocol",
30+
"CanTimeoutError",
31+
"CanutilsLogReader",
32+
"CanutilsLogWriter",
33+
"CSVReader",
34+
"CSVWriter",
35+
"CyclicSendTaskABC",
36+
"detect_available_configs",
37+
"interface",
38+
"LimitedDurationCyclicSendTaskABC",
39+
"Listener",
40+
"Logger",
41+
"LogReader",
42+
"ModifiableCyclicTaskABC",
43+
"Message",
44+
"MessageSync",
45+
"Notifier",
46+
"Printer",
47+
"RedirectReader",
48+
"RestartableCyclicTaskABC",
49+
"set_logging_level",
50+
"SizedRotatingLogger",
51+
"SqliteReader",
52+
"SqliteWriter",
53+
"ThreadSafeBus",
54+
"typechecking",
55+
"TRCFileVersion",
56+
"TRCReader",
57+
"TRCWriter",
58+
"util",
59+
"VALID_INTERFACES",
60+
]
1261

1362
log = logging.getLogger("can")
1463

1564
rc: Dict[str, Any] = {}
1665

17-
from .listener import Listener, BufferedReader, RedirectReader, AsyncBufferedReader
18-
66+
from . import typechecking # isort:skip
67+
from . import util # isort:skip
68+
from . import broadcastmanager, interface
69+
from .bit_timing import BitTiming, BitTimingFd
70+
from .broadcastmanager import (
71+
CyclicSendTaskABC,
72+
LimitedDurationCyclicSendTaskABC,
73+
ModifiableCyclicTaskABC,
74+
RestartableCyclicTaskABC,
75+
)
76+
from .bus import BusABC, BusState, CanProtocol
1977
from .exceptions import (
2078
CanError,
21-
CanInterfaceNotImplementedError,
2279
CanInitializationError,
80+
CanInterfaceNotImplementedError,
2381
CanOperationError,
2482
CanTimeoutError,
2583
)
26-
27-
from .util import set_logging_level
28-
29-
from .message import Message
30-
from .bus import BusABC, BusState, CanProtocol
31-
from .thread_safe_bus import ThreadSafeBus
32-
from .notifier import Notifier
33-
from .interfaces import VALID_INTERFACES
34-
from . import interface
3584
from .interface import Bus, detect_available_configs
36-
from .bit_timing import BitTiming, BitTimingFd
37-
38-
from .io import Logger, SizedRotatingLogger, Printer, LogReader, MessageSync
39-
from .io import ASCWriter, ASCReader
40-
from .io import BLFReader, BLFWriter
41-
from .io import CanutilsLogReader, CanutilsLogWriter
42-
from .io import CSVWriter, CSVReader
43-
from .io import SqliteWriter, SqliteReader
44-
from .io import TRCReader, TRCWriter, TRCFileVersion
45-
46-
from .broadcastmanager import (
47-
CyclicSendTaskABC,
48-
LimitedDurationCyclicSendTaskABC,
49-
ModifiableCyclicTaskABC,
50-
MultiRateCyclicSendTaskABC,
51-
RestartableCyclicTaskABC,
85+
from .interfaces import VALID_INTERFACES
86+
from .io import (
87+
ASCReader,
88+
ASCWriter,
89+
BLFReader,
90+
BLFWriter,
91+
CanutilsLogReader,
92+
CanutilsLogWriter,
93+
CSVReader,
94+
CSVWriter,
95+
Logger,
96+
LogReader,
97+
MessageSync,
98+
Printer,
99+
SizedRotatingLogger,
100+
SqliteReader,
101+
SqliteWriter,
102+
TRCFileVersion,
103+
TRCReader,
104+
TRCWriter,
52105
)
106+
from .listener import AsyncBufferedReader, BufferedReader, Listener, RedirectReader
107+
from .message import Message
108+
from .notifier import Notifier
109+
from .thread_safe_bus import ThreadSafeBus
110+
from .util import set_logging_level

can/bit_timing.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# pylint: disable=too-many-lines
22
import math
3-
from typing import List, Mapping, Iterator, cast
3+
from typing import Iterator, List, Mapping, cast
44

5-
from can.typechecking import BitTimingFdDict, BitTimingDict
5+
from can.typechecking import BitTimingDict, BitTimingFdDict
66

77

88
class BitTiming(Mapping):

can/broadcastmanager.py

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,39 @@
55
:meth:`can.BusABC.send_periodic`.
66
"""
77

8-
from typing import Optional, Sequence, Tuple, Union, Callable, TYPE_CHECKING
8+
import abc
9+
import logging
10+
import sys
11+
import threading
12+
import time
13+
from typing import TYPE_CHECKING, Callable, Optional, Sequence, Tuple, Union
14+
15+
from typing_extensions import Final
916

1017
from can import typechecking
18+
from can.message import Message
1119

1220
if TYPE_CHECKING:
1321
from can.bus import BusABC
1422

15-
from can.message import Message
16-
17-
import abc
18-
import logging
19-
import threading
20-
import time
2123

2224
# try to import win32event for event-based cyclic send task (needs the pywin32 package)
25+
USE_WINDOWS_EVENTS = False
2326
try:
2427
import win32event
2528

26-
HAS_EVENTS = True
29+
# Python 3.11 provides a more precise sleep implementation on Windows, so this is not necessary.
30+
# Put version check here, so mypy does not complain about `win32event` not being defined.
31+
if sys.version_info < (3, 11):
32+
USE_WINDOWS_EVENTS = True
2733
except ImportError:
28-
HAS_EVENTS = False
34+
pass
2935

3036
log = logging.getLogger("can.bcm")
3137

38+
NANOSECONDS_IN_SECOND: Final[int] = 1_000_000_000
39+
NANOSECONDS_IN_MILLISECOND: Final[int] = 1_000_000
40+
3241

3342
class CyclicTask(abc.ABC):
3443
"""
@@ -64,6 +73,7 @@ def __init__(
6473
# Take the Arbitration ID of the first element
6574
self.arbitration_id = messages[0].arbitration_id
6675
self.period = period
76+
self.period_ns = int(round(period * 1e9))
6777
self.messages = messages
6878

6979
@staticmethod
@@ -246,7 +256,7 @@ def __init__(
246256
)
247257
self.on_error = on_error
248258

249-
if HAS_EVENTS:
259+
if USE_WINDOWS_EVENTS:
250260
self.period_ms = int(round(period * 1000, 0))
251261
try:
252262
self.event = win32event.CreateWaitableTimerEx(
@@ -261,7 +271,7 @@ def __init__(
261271
self.start()
262272

263273
def stop(self) -> None:
264-
if HAS_EVENTS:
274+
if USE_WINDOWS_EVENTS:
265275
win32event.CancelWaitableTimer(self.event.handle)
266276
self.stopped = True
267277

@@ -272,7 +282,7 @@ def start(self) -> None:
272282
self.thread = threading.Thread(target=self._run, name=name)
273283
self.thread.daemon = True
274284

275-
if HAS_EVENTS:
285+
if USE_WINDOWS_EVENTS:
276286
win32event.SetWaitableTimer(
277287
self.event.handle, 0, self.period_ms, None, None, False
278288
)
@@ -281,10 +291,11 @@ def start(self) -> None:
281291

282292
def _run(self) -> None:
283293
msg_index = 0
294+
msg_due_time_ns = time.perf_counter_ns()
295+
284296
while not self.stopped:
285297
# Prevent calling bus.send from multiple threads
286298
with self.send_lock:
287-
started = time.perf_counter()
288299
try:
289300
self.bus.send(self.messages[msg_index])
290301
except Exception as exc: # pylint: disable=broad-except
@@ -294,13 +305,19 @@ def _run(self) -> None:
294305
break
295306
else:
296307
break
308+
msg_due_time_ns += self.period_ns
297309
if self.end_time is not None and time.perf_counter() >= self.end_time:
298310
break
299311
msg_index = (msg_index + 1) % len(self.messages)
300312

301-
if HAS_EVENTS:
302-
win32event.WaitForSingleObject(self.event.handle, self.period_ms)
303-
else:
304-
# Compensate for the time it takes to send the message
305-
delay = self.period - (time.perf_counter() - started)
306-
time.sleep(max(0.0, delay))
313+
# Compensate for the time it takes to send the message
314+
delay_ns = msg_due_time_ns - time.perf_counter_ns()
315+
316+
if delay_ns > 0:
317+
if USE_WINDOWS_EVENTS:
318+
win32event.WaitForSingleObject(
319+
self.event.handle,
320+
int(round(delay_ns / NANOSECONDS_IN_MILLISECOND)),
321+
)
322+
else:
323+
time.sleep(delay_ns / NANOSECONDS_IN_SECOND)

can/bus.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,17 @@
22
Contains the ABC bus implementation and its documentation.
33
"""
44

5-
from typing import cast, Any, Iterator, List, Optional, Sequence, Tuple, Union
6-
7-
import can.typechecking
8-
9-
from abc import ABC, ABCMeta, abstractmethod
10-
import can
5+
import contextlib
116
import logging
127
import threading
13-
from time import time
8+
from abc import ABC, ABCMeta, abstractmethod
149
from enum import Enum, auto
10+
from time import time
11+
from typing import Any, Iterator, List, Optional, Sequence, Tuple, Union, cast
1512

16-
from can.broadcastmanager import ThreadBasedCyclicSendTask, CyclicSendTaskABC
13+
import can
14+
import can.typechecking
15+
from can.broadcastmanager import CyclicSendTaskABC, ThreadBasedCyclicSendTask
1716
from can.message import Message
1817

1918
LOG = logging.getLogger(__name__)
@@ -52,13 +51,15 @@ class BusABC(metaclass=ABCMeta):
5251
#: Log level for received messages
5352
RECV_LOGGING_LEVEL = 9
5453

54+
_is_shutdown: bool = False
55+
5556
@abstractmethod
5657
def __init__(
5758
self,
5859
channel: Any,
5960
protocol: CanProtocol = CanProtocol.CAN_20,
6061
can_filters: Optional[can.typechecking.CanFilters] = None,
61-
**kwargs: object
62+
**kwargs: object,
6263
):
6364
"""Construct and open a CAN bus instance of the specified type.
6465
@@ -110,7 +111,6 @@ def recv(self, timeout: Optional[float] = None) -> Optional[Message]:
110111
time_left = timeout
111112

112113
while True:
113-
114114
# try to get a message
115115
msg, already_filtered = self._recv_internal(timeout=time_left)
116116

@@ -126,7 +126,6 @@ def recv(self, timeout: Optional[float] = None) -> Optional[Message]:
126126
# try next one only if there still is time, and with
127127
# reduced timeout
128128
else:
129-
130129
time_left = timeout - (time() - start)
131130

132131
if time_left > 0:
@@ -320,6 +319,10 @@ def stop_all_periodic_tasks(self, remove_tasks: bool = True) -> None:
320319
:param remove_tasks:
321320
Stop tracking the stopped tasks.
322321
"""
322+
if not hasattr(self, "_periodic_tasks"):
323+
# avoid AttributeError for partially initialized BusABC instance
324+
return
325+
323326
for task in self._periodic_tasks:
324327
# we cannot let `task.stop()` modify `self._periodic_tasks` while we are
325328
# iterating over it (#634)
@@ -434,9 +437,15 @@ def flush_tx_buffer(self) -> None:
434437

435438
def shutdown(self) -> None:
436439
"""
437-
Called to carry out any interface specific cleanup required
438-
in shutting down a bus.
440+
Called to carry out any interface specific cleanup required in shutting down a bus.
441+
442+
This method can be safely called multiple times.
439443
"""
444+
if self._is_shutdown:
445+
LOG.debug("%s is already shut down", self.__class__)
446+
return
447+
448+
self._is_shutdown = True
440449
self.stop_all_periodic_tasks()
441450

442451
def __enter__(self):
@@ -445,6 +454,14 @@ def __enter__(self):
445454
def __exit__(self, exc_type, exc_val, exc_tb):
446455
self.shutdown()
447456

457+
def __del__(self) -> None:
458+
if not self._is_shutdown:
459+
LOG.warning("%s was not properly shut down", self.__class__)
460+
# We do some best-effort cleanup if the user
461+
# forgot to properly close the bus instance
462+
with contextlib.suppress(AttributeError):
463+
self.shutdown()
464+
448465
@property
449466
def state(self) -> BusState:
450467
"""

0 commit comments

Comments
 (0)