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

Skip to content

Commit 9d5a0f0

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')
1 parent 71580d8 commit 9d5a0f0

File tree

5 files changed

+249
-143
lines changed

5 files changed

+249
-143
lines changed

can/interfaces/canalystii.py

Lines changed: 110 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -1,96 +1,35 @@
1-
from ctypes import *
1+
import collections
2+
import warnings
3+
from ctypes import c_ubyte
24
import logging
3-
import platform
4-
from can import BusABC, Message
5+
import canalystii as driver
6+
import time
7+
from can import BusABC, Message, CanError
58

69
logger = logging.getLogger(__name__)
710

811

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-
7512
class CANalystIIBus(BusABC):
7613
def __init__(
7714
self,
78-
channel,
15+
channel=(0, 1),
7916
device=0,
8017
bitrate=None,
8118
Timing0=None,
8219
Timing1=None,
8320
can_filters=None,
21+
rx_queue_size=None,
8422
**kwargs,
8523
):
8624
"""
8725
88-
:param channel: channel number
89-
:param device: device number
90-
:param bitrate: CAN network bandwidth (bits/s)
26+
:param channel: channel number, list/tuple of multiple channels, or comma separated string of channels
27+
:param device: USB device number
28+
:param bitrate: CAN bitrate
9129
:param Timing0: customize the timing register if bitrate is not specified
9230
:param Timing1:
9331
:param can_filters: filters for packet
32+
:param rx_queue_size: if set, software received message queue can only grow to this many messages (for all channels) before older messages are dropped
9433
"""
9534
super().__init__(channel=channel, can_filters=can_filters, **kwargs)
9635

@@ -102,103 +41,137 @@ def __init__(
10241
# Assume comma separated string of channels
10342
self.channels = [int(ch.strip()) for ch in channel.split(",")]
10443

105-
self.device = device
44+
self.rx_queue = collections.deque(maxlen=rx_queue_size)
45+
46+
if bitrate is None and Timing0 is None and Timing1 is None:
47+
raise ValueError(
48+
"Either bitrate or both Timing0 and Timing1 must be specified"
49+
)
50+
if bitrate is None and (Timing0 is None or Timing1 is None):
51+
raise ValueError(
52+
"If bitrate is not set then both Timing0 and Timing1 must be set"
53+
)
54+
if bitrate is not None and (Timing0 is not None or Timing1 is not None):
55+
raise ValueError(
56+
"If bitrate is set then Timing0 and Timing1 must not be set"
57+
)
10658

10759
self.channel_info = "CANalyst-II: device {}, channels {}".format(
108-
self.device, self.channels
60+
device, self.channels
10961
)
11062

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)
121-
122-
if CANalystII.VCI_OpenDevice(VCI_USBCAN2, self.device, 0) == STATUS_ERR:
123-
logger.error("VCI_OpenDevice Error")
124-
63+
self.device = driver.CanalystDevice(device_index=device)
12564
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
65+
self.device.init(channel, bitrate=bitrate, timing0=Timing0, timing1=Timing1)
13866

13967
def send(self, msg, timeout=None):
140-
"""
68+
"""Send a CAN message to the bus
14169
14270
:param msg: message to send
143-
:param timeout: timeout is not used here
144-
:return:
71+
:param timeout: timeout (in seconds) to wait for the TX queue to clear
72+
:return: None
14573
"""
146-
extern_flag = 1 if msg.is_extended_id else 0
147-
raw_message = VCI_CAN_OBJ(
74+
raw_message = driver.Message(
14875
msg.arbitration_id,
149-
0,
150-
0,
151-
SENDTYPE_ALLOW_RETRY,
76+
0, # timestamp
77+
1, # time_flag
78+
0, # send_type
15279
msg.is_remote_frame,
153-
extern_flag,
80+
msg.is_extended_id,
15481
msg.dlc,
15582
(c_ubyte * 8)(*msg.data),
156-
(c_byte * 3)(*[0, 0, 0]),
15783
)
15884

15985
if msg.channel is not None:
16086
channel = msg.channel
16187
elif len(self.channels) == 1:
16288
channel = self.channels[0]
16389
else:
164-
raise ValueError("msg.channel must be set when using multiple channels.")
90+
raise ValueError(
91+
"Message channel must be set when using multiple channels."
92+
)
16593

166-
CANalystII.VCI_Transmit(
167-
VCI_USBCAN2, self.device, channel, byref(raw_message), 1
94+
send_result = self.device.send(channel, [raw_message], timeout)
95+
if timeout is not None and not send_result:
96+
raise CanError(f"Send timed out after {timeout} seconds")
97+
98+
def _recv_from_queue(self):
99+
"""Return a message from the internal receive queue"""
100+
channel, raw_msg = self.rx_queue.popleft()
101+
102+
# Protocol timestamps are in units of 100us, convert to seconds as float
103+
timestamp = raw_msg.timestamp * 100e-6
104+
105+
return (
106+
Message(
107+
channel=channel,
108+
timestamp=timestamp,
109+
arbitration_id=raw_msg.can_id,
110+
is_extended_id=raw_msg.extended,
111+
is_remote_frame=raw_msg.remote,
112+
dlc=raw_msg.data_len,
113+
data=bytes(raw_msg.data),
114+
),
115+
False,
168116
)
169117

118+
def poll_received_messages(self):
119+
"""Poll new messages from the device into the rx queue but don't
120+
return any message to the caller
121+
122+
Calling this function isn't necessary as polling the device is done
123+
automatically when calling recv(). This function is for the situation
124+
where an application needs to empty the hardware receive buffer without
125+
consuming any message.
126+
"""
127+
for channel in self.channels:
128+
self.rx_queue.extend(
129+
(channel, raw_msg) for raw_msg in self.device.receive(channel)
130+
)
131+
170132
def _recv_internal(self, timeout=None):
171133
"""
172134
173135
:param timeout: float in seconds
174136
:return:
175137
"""
176-
raw_message = VCI_CAN_OBJ()
177138

178-
timeout = -1 if timeout is None else int(timeout * 1000)
139+
if self.rx_queue:
140+
return self._recv_from_queue()
179141

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
185-
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-
)
142+
deadline = None
143+
while deadline is None or time.time() < deadline:
144+
if deadline is None and timeout is not None:
145+
deadline = time.time() + timeout
198146

199-
def flush_tx_buffer(self):
147+
self.poll_received_messages()
148+
149+
if self.rx_queue:
150+
return self._recv_from_queue()
151+
152+
# If blocking on a timeout, add a sleep before we loop again
153+
# to reduce CPU usage.
154+
#
155+
# The timeout is deliberately kept low to avoid the possibility of a
156+
# hardware buffer overflow. This value was determined
157+
# experimentally, but the ideal value will depend on the specific
158+
# system.
159+
if deadline is None or deadline - time.time() > 0.050:
160+
time.sleep(0.020)
161+
162+
return (None, False)
163+
164+
def flush_tx_buffer(self, channel=None):
165+
"""Flush the TX buffer of the device.
166+
167+
Note that because of protocol limitations this function returning
168+
doesn't mean that messages have been sent, it may also mean they
169+
failed to send.
170+
"""
200171
for channel in self.channels:
201-
CANalystII.VCI_ClearBuffer(VCI_USBCAN2, self.device, channel)
172+
self.device.flush_tx_buffer(channel, float("infinity"))
202173

203174
def shutdown(self):
204-
CANalystII.VCI_CloseDevice(VCI_USBCAN2, self.device)
175+
for channel in self.channels:
176+
self.device.stop(channel)
177+
self.device = None

doc/interfaces/canalystii.rst

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,32 @@
11
CANalyst-II
22
===========
33

4-
CANalyst-II(+) is a USB to CAN Analyzer. The controlcan library is originally developed by
5-
`ZLG ZHIYUAN Electronics`_.
4+
CANalyst-II is a USB to CAN Analyzer device produced by Chuangxin Technology.
65

7-
.. note::
6+
Install: ``pip install "python-can[canalystii]"``
87

9-
Use of this interface requires the ``ControlCAN.dll`` (Windows) or ``libcontrolcan.so`` vendor library to be placed in the Python working directory.
8+
Supported platform
9+
------------------
10+
11+
Windows, Linux and Mac.
12+
13+
Note: Since ``pyusb`` with ``libusb0`` as backend is used, ``libusb-win32`` USB driver is required to be installed in Windows.
14+
15+
Limitations
16+
-----------
17+
18+
Multiple Channels
19+
^^^^^^^^^^^^^^^^^
20+
21+
The USB protocol transfers messages grouped by channel. Messages received on channel 0 and channel 1 may be returned by software out of order between the two channels (although inside each channel, all messages are in order). The timestamp field of each message comes from the hardware and shows the exact time each message was received. To compare ordering of messages on channel 0 vs channel 1, sort the received messages by the timestamp field first.
22+
23+
Backend Driver
24+
--------------
25+
26+
The backend driver module `canalystii <https://pypi.org/project/canalystii>` must be installed to use this interface. This open source driver is unofficial and based on reverse engineering. Earlier versions of python-can required a binary library from the vendor for this functionality.
1027

1128
Bus
1229
---
1330

1431
.. autoclass:: can.interfaces.canalystii.CANalystIIBus
1532

16-
17-
.. _ZLG ZHIYUAN Electronics: http://www.zlg.com/can/can/product/id/42.html

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"seeedstudio": ["pyserial>=3.0"],
2929
"serial": ["pyserial~=3.0"],
3030
"neovi": ["filelock", "python-ics>=2.12"],
31+
"canalystii": ["canalystii>=0.1.0"],
3132
"cantact": ["cantact>=0.0.7"],
3233
"gs_usb": ["gs_usb>=0.2.1"],
3334
"nixnet": ["nixnet>=0.3.1"],

0 commit comments

Comments
 (0)
0