8000 canalystii: Switch from binary library to pure Python driver · projectgus/python-can@18f3462 · GitHub
[go: up one dir, main page]

Skip to content

Commit 18f3462

Browse files
committed
canalystii: Switch from binary library to pure Python driver
- Project 'canalystii' hosted at https://github.com/projectgus/python-canalystii Compared to previous implementation: - No more binary library, can support MacOS as well as Windows and Linux - Can receive on all enabled channels (previous implementation received on one channel only) - Performance is slightly slower but still easily possible to receive at max message rate on both channels - send timeout is now based on CAN layer send not USB layer send - User configurable software rx message queue size - Adds tests (mocking at the layer of the underlying driver 'canalystii') - Adds type annotations - Replaces Timing0 & Timing1 parameters (for BTR0 & BTR1) with the BitTiming class Closes hardbyte#726
1 parent bb78312 commit 18f3462

File tree

5 files changed

+291
-162
lines changed

5 files changed

+291
-162
lines changed

can/interfaces/canalystii.py

Lines changed: 162 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -1,204 +1,209 @@
1-
from ctypes import *
1+
import collections
2+
from ctypes import c_ubyte
23
import logging
3-
import platform
4-
from can import BusABC, Message
4+
import canalystii as driver
5+
import time
6+
import warnings
7+
from typing import Any, Dict, Optional, Deque, Sequence, Tuple, Union
8+
from can import BitTiming, BusABC, Message
9+
from can.exceptions import CanTimeoutError
10+
from can.typechecking import CanFilters
511

612
logger = logging.getLogger(__name__)
713

814

9-
class VCI_INIT_CONFIG(Structure):
10-
_fields_ = [
11-
("AccCode", c_int32),
12-
("AccMask", c_int32),
13-
("Reserved", c_int32),
14-
("Filter", c_ubyte),
15-
("Timing0", c_ubyte),
16-
("Timing1", c_ubyte),
17-
("Mode", c_ubyte),
18-
]
19-
20-
21-
class VCI_CAN_OBJ(Structure):
22-
_fields_ = [
23-
("ID", c_uint),
24-
("TimeStamp", c_int),
25-
("TimeFlag", c_byte),
26-
("SendType", c_byte),
27-
("RemoteFlag", c_byte),
28-
("ExternFlag", c_byte),
29-
("DataLen", c_byte),
30-
("Data", c_ubyte * 8),
31-
("Reserved", c_byte * 3),
32-
]
33-
34-
35-
SENDTYPE_ALLOW_RETRY = 0 # Retry send if there is bus contention
36-
SENDTYPE_ONE_SHOT = 1 # Drop the message if transmission fails first time
37-
38-
VCI_USBCAN2 = 4
39-
40-
STATUS_OK = 0x01
41-
STATUS_ERR = 0x00
42-
43-
TIMING_DICT = {
44-
5000: (0xBF, 0xFF),
45-
10000: (0x31, 0x1C),
46-
20000: (0x18, 0x1C),
47-
33330: (0x09, 0x6F),
48-
40000: (0x87, 0xFF),
49-
50000: (0x09, 0x1C),
50-
66660: (0x04, 0x6F),
51-
80000: (0x83, 0xFF),
52-
83330: (0x03, 0x6F),
53-
100000: (0x04, 0x1C),
54-
125000: (0x03, 0x1C),
55-
200000: (0x81, 0xFA),
56-
250000: (0x01, 0x1C),
57-
400000: (0x80, 0xFA),
58-
500000: (0x00, 0x1C),
59-
666000: (0x80, 0xB6),
60-
800000: (0x00, 0x16),
61-
1000000: (0x00, 0x14),
62-
}
63-
64-
try:
65-
if platform.system() == "Windows":
66-
CANalystII = WinDLL("./ControlCAN.dll")
67-
else:
68-
CANalystII = CDLL("./libcontrolcan.so")
69-
logger.info("Loaded CANalystII library")
70-
except OSError as e:
71-
CANalystII = None
72-
logger.info("Cannot load CANalystII library")
73-
74-
7515
class CANalystIIBus(BusABC):
7616
def __init__(
7717
self,
78-
channel,
79-
device=0,
80-
bitrate=None,
81-
Timing0=None,
82-
Timing1=None,
83-
can_filters=None,
84-
**kwargs,
18+
channel: Union[int, Sequence[int], str] = (0, 1),
19+
device: int = 0,
20+
bitrate: Optional[int] = None,
21+
bit_timing: Optional[BitTiming] = None,
22+
can_filters: Optional[CanFilters] = None,
23+
rx_queue_size: Optional[int] = None,
24+
**kwargs: Dict[str, Any],
8525
):
8626
"""
8727
88-
:param channel: channel number
89-
:param device: device number
90-
:param bitrate: CAN network bandwidth (bits/s)
91-
:param Timing0: customize the timing register if bitrate is not specified
92-
:param Timing1:
93-
:param can_filters: filters for packet
28+
:param channel:
29+
Optional channel number, list/tuple of multiple channels, or comma
30+
separated string of channels. Default is to configure both
31+
channels.
32+
:param device:
33+
Optional USB device number. Default is 0 (first device found).
34+
:param bitrate:
35+
CAN bitrate in bits/second. Required unless the bit_timing argument is set.
36+
:param bit_timing:
37+
Optional BitTiming instance to use for custom bit timing setting.
38+
If this argument is set then it overrides the bitrate argument.
39+
:param can_filters:
40+
Optional filters for received CAN messages.
41+
:param rx_queue_size:
42+
If set, software received message queue can only grow to this many
43+
messages (for all channels) before older messages are dropped
9444
"""
9545
super().__init__(channel=channel, can_filters=can_filters, **kwargs)
9646

97-
if isinstance(channel, (list, tuple)):
98-
self.channels = channel
99-
elif isinstance(channel, int):
100-
self.channels = [channel]
101-
else:
47+
if not (bitrate or bit_timing):
48+
raise ValueError("Either bitrate or bit_timing argument is required")
49+
50+
if isinstance(channel, str):
10251
# Assume comma separated string of channels
10352
self.channels = [int(ch.strip()) for ch in channel.split(",")]
53+
elif isinstance(channel, int):
54+
self.channels = [channel]
55+
else: # Sequence[int]
56+
self.channels = [c for c in channel]
10457

105-
self.device = device
106-
107-
self.channel_info = "CANalyst-II: device {}, channels {}".format(
108-
self.device, self.channels
109-
)
110-
111-
if bitrate is not None:
112-
try:
113-
Timing0, Timing1 = TIMING_DICT[bitrate]
114-
except KeyError:
115-
raise ValueError("Bitrate is not supported")
116-
117-
if Timing0 is None or Timing1 is None:
118-
raise ValueError("Timing registers are not set")
119-
120-
self.init_config = VCI_INIT_CONFIG(0, 0xFFFFFFFF, 0, 1, Timing0, Timing1, 0)
58+
self.rx_queue = collections.deque(
59+
maxlen=rx_queue_size
60+
) # type: Deque[Tuple[int, driver.Message]]
12161

122-
if CANalystII.VCI_OpenDevice(VCI_USBCAN2, self.device, 0) == STATUS_ERR:
123-
logger.error("VCI_OpenDevice Error")
62+
self.channel_info = f"CANalyst-II: device {device}, channels {self.channels}"
12463

64+
self.device = driver.CanalystDevice(device_index=device)
12565
for channel in self.channels:
126-
status = CANalystII.VCI_InitCAN(
127-
VCI_USBCAN2, self.device, channel, byref(self.init_config)
128-
)
129-
if status == STATUS_ERR:
130-
logger.error("VCI_InitCAN Error")
131-
self.shutdown()
132-
return
133-
134-
if CANalystII.VCI_StartCAN(VCI_USBCAN2, self.device, channel) == STATUS_ERR:
135-
logger.error("VCI_StartCAN Error")
136-
self.shutdown()
137-
return
138-
139-
def send(self, msg, timeout=None):
140-
"""
66+
if bit_timing:
67+
try:
68+
if bit_timing.f_clock != 8_000_000:
69+
warnings.warn(
70+
f"bit_timing.f_clock value {bit_timing.f_clock} "
71+
"doesn't match expected device f_clock 8MHz."
72+
)
73+
except ValueError:
74+
pass # f_clock not specified
75+
self.device.init(
76+
channel, timing0=bit_timing.btr0, timing1=bit_timing.btr1
77+
)
78+
else:
79+
self.device.init(channel, bitrate=bitrate)
80+
81+
# Delay to use between each poll for new messages
82+
#
83+
# The timeout is deliberately kept low to avoid the possibility of
84+
# a hardware buffer overflow. This value was determined
85+
# experimentally, but the ideal value will depend on the specific
86+
# system.
87+
RX_POLL_DELAY = 0.020
88+
89+
def send(self, msg: Message, timeout: Optional[float] = None) -> None:
90+
"""Send a CAN message to the bus
14191
14292
:param msg: message to send
143-
:param timeout: timeout is not used here
144-
:return:
93+
:param timeout: timeout (in seconds) to wait for the TX queue to clear.
94+
If set to ``None`` (default) the function returns immediately.
95+
96+
Note: Due to limitations in the device firmware and protocol, the
97+
timeout will not trigger if there are problems with CAN arbitration,
98+
but only if the device is overloaded with a backlog of too many
99+
messages to send.
145100
"""
146-
extern_flag = 1 if msg.is_extended_id else 0
147-
raw_message = VCI_CAN_OBJ(
101+
raw_message = driver.Message(
148102
msg.arbitration_id,
149-
0,
150-
0,
151-
SENDTYPE_ALLOW_RETRY,
103+
0, # timestamp
104+
1, # time_flag
105+
0, # send_type
152106
msg.is_remote_frame,
153-
extern_flag,
107+
msg.is_extended_id,
154108
msg.dlc,
155109
(c_ubyte * 8)(*msg.data),
156-
(c_byte * 3)(*[0, 0, 0]),
157110
)
158111

159112
if msg.channel is not None:
160113
channel = msg.channel
161114
elif len(self.channels) == 1:
162115
channel = self.channels[0]
163116
else:
164-
raise ValueError("msg.channel must be set when using multiple channels.")
117+
raise ValueError(
118+
"Message channel must be set when using multiple channels."
119+
)
165120

166-
CANalystII.VCI_Transmit(
167-
VCI_USBCAN2, self.device, channel, byref(raw_message), 1
121+
send_result = self.device.send(channel, [raw_message], timeout)
122+
if timeout is not None and not send_result:
123+
raise CanTimeoutError(f"Send timed out after {timeout} seconds")
124+
125+
def _recv_from_queue(self) -> Tuple[Message, bool]:
126+
"""Return a message from the internal receive queue"""
127+
channel, raw_msg = self.rx_queue.popleft()
128+
129+
# Protocol timestamps are in units of 100us, convert to seconds as
130+
# float
131+
timestamp = raw_msg.timestamp * 100e-6
132+
133+
return (
134+
Message(
135+
channel=channel,
136+
timestamp=timestamp,
137+
arbitration_id=raw_msg.can_id,
138+
is_extended_id=raw_msg.extended,
139+
is_remote_frame=raw_msg.remote,
140+
dlc=raw_msg.data_len,
141+
data=bytes(raw_msg.data),
142+
),
143+
False,
168144
)
169145

170-
def _recv_internal(self, timeout=None):
146+
def poll_received_messages(self) -> None:
147+
"""Poll new messages from the device into the rx queue but don't
148+
return any message to the caller
149+
150+
Calling this function isn't necessary as polling the device is done
151+
automatically when calling recv(). This function is for the situation
152+
where an application needs to empty the hardware receive buffer without
153+
consuming any message.
154+
"""
155+
for channel in self.channels:
156+
self.rx_queue.extend(
157+
(channel, raw_msg) for raw_msg in self.device.receive(channel)
158+
)
159+
160+
def _recv_internal(
161+
self, timeout: Optional[float] = None
162+
) -> Tuple[Optional[Message], bool]:
171163
"""
172164
173165
:param timeout: float in seconds
174166
:return:
175167
"""
176-
raw_message = VCI_CAN_OBJ()
177168

178-
timeout = -1 if timeout is None else int(timeout * 1000)
169+
if self.rx_queue:
170+
return self._recv_from_queue()
179171

180-
status = CANalystII.VCI_Receive(
181-
VCI_USBCAN2, self.device, self.channels[0], byref(raw_message), 1, timeout
182-
)
183-
if status <= STATUS_ERR:
184-
return None, False
172+
deadline = None
173+
while deadline is None or time.time() < deadline:
174+
if deadline is None and timeout is not None:
175+
deadline = time.time() + timeout
176+
177+
self.poll_received_messages()
178+
179+
if self.rx_queue:
180+
return self._recv_from_queue()
181+
182+
# If blocking on a timeout, add a sleep before we loop again
183+
# to reduce CPU usage.
184+
if deadline is None or deadline - time.time() > 0.050:
185+
time.sleep(self.RX_POLL_DELAY)
186+
187+
return (None, False)
188+
189+
def flush_tx_buffer(self, channel: Optional[int] = None) -> None:
190+
"""Flush the TX buffer of the device.
191+
192+
:param channel:
193+
Optional channel number to flush. If set to None, all initialized
194+
channels are flushed.
195+
196+
Note that because of protocol limitations t E17C his function returning
197+
doesn't mean that messages have been sent, it may also mean they
198+
failed to send.
199+
"""
200+
if channel:
201+
self.device.flush_tx_buffer(channel, float("infinity"))
185202
else:
186-
return (
187-
Message(
188-
timestamp=raw_message.TimeStamp if raw_message.TimeFlag else 0.0,
189-
arbitration_id=raw_message.ID,
190-
is_remote_frame=raw_message.RemoteFlag,
191-
is_extended_id=raw_message.ExternFlag,
192-
channel=0,
193-
dlc=raw_message.DataLen,
194-
data=raw_message.Data,
195-
),
196-
False,
197-
)
203+
for ch in self.channels:
204+
self.device.flush_tx_buffer(ch, float("infinity"))
198205

199-
def flush_tx_buffer(self):
206+
def shutdown(self) -> None:
200207
for channel in self.channels:
201-
CANalystII.VCI_ClearBuffer(VCI_USBCAN2, self.device, channel)
202-
203-
def shutdown(self):
204-
CANalystII.VCI_CloseDevice(VCI_USBCAN2, self.device)
208+
self.device.stop(channel)
209+
self.device = None

0 commit comments

Comments
 (0)
0