Skip to content

Commit b16f8aa

Browse files
authored
Improve slcan.py (#1490)
* Improve slcan.py 1. Improve receiving performance 2. Fix an issue that the first read may blocking even if the `timeout` parameter is not `None`. * use `read_all` to read out serial port data. * fix when the `timeout` parameter is 0, it cannot enter the while loop. * For performance reason, revert to using `read` to read serial data. * add `size=1` and renamed `new_data` to `new_byte` to make the code easier to understand and read. * fix the issue of returning `None` before receiving the full SLCAN message. * Due to the simplification of the control flow, the class variable `self._buffer` can be replaced by the local variable `buffer`. * Revert "Due to the simplification of the control flow," This reverts commit 3eb80b9. * Simplify the control flow for finding the end of a SLCAN message. * improve the `timeout` handling of slcan.py * Change the plain format calls to f-strings. * using `bytearray.fromhex` to parse the frame's data. * change `del self._buffer[:]` to `self._buffer.clear` since it's more readable. * Simplify the timeout handling * fix the issue when the returned string is `None`. * using `pyserial.reset_input_buffer` to discard the data in the input buffer. * fix failing PyPy test * fix failing PyPy test * improve the comments
1 parent 814051e commit b16f8aa

File tree

2 files changed

+81
-99
lines changed

2 files changed

+81
-99
lines changed

can/interfaces/slcan.py

+57-90
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ def __init__(
6464
btr: Optional[str] = None,
6565
sleep_after_open: float = _SLEEP_AFTER_SERIAL_OPEN,
6666
rtscts: bool = False,
67+
timeout: float = 0.001,
6768
**kwargs: Any,
6869
) -> None:
6970
"""
@@ -82,7 +83,8 @@ def __init__(
8283
Time to wait in seconds after opening serial connection
8384
:param rtscts:
8485
turn hardware handshake (RTS/CTS) on and off
85-
86+
:param timeout:
87+
Timeout for the serial or usb device in seconds (default 0.001)
8688
:raise ValueError: if both ``bitrate`` and ``btr`` are set or the channel is invalid
8789
:raise CanInterfaceNotImplementedError: if the serial module is missing
8890
:raise CanInitializationError: if the underlying serial connection could not be established
@@ -98,7 +100,10 @@ def __init__(
98100

99101
with error_check(exception_type=CanInitializationError):
100102
self.serialPortOrig = serial.serial_for_url(
101-
channel, baudrate=ttyBaudrate, rtscts=rtscts
103+
channel,
104+
baudrate=ttyBaudrate,
105+
rtscts=rtscts,
106+
timeout=timeout,
102107
)
103108

104109
self._buffer = bytearray()
@@ -150,46 +155,34 @@ def _write(self, string: str) -> None:
150155
self.serialPortOrig.flush()
151156

152157
def _read(self, timeout: Optional[float]) -> Optional[str]:
158+
_timeout = serial.Timeout(timeout)
153159

154160
with error_check("Could not read from serial device"):
155-
# first read what is already in receive buffer
156-
while self.serialPortOrig.in_waiting:
157-
self._buffer += self.serialPortOrig.read()
158-
# if we still don't have a complete message, do a blocking read
159-
start = time.time()
160-
time_left = timeout
161-
while not (
162-
ord(self._OK) in self._buffer or ord(self._ERROR) in self._buffer
163-
):
164-
self.serialPortOrig.timeout = time_left
165-
byte = self.serialPortOrig.read()
166-
if byte:
167-
self._buffer += byte
168-
# if timeout is None, try indefinitely
169-
if timeout is None:
170-
continue
171-
# try next one only if there still is time, and with
172-
# reduced timeout
173-
else:
174-
time_left = timeout - (time.time() - start)
175-
if time_left > 0:
176-
continue
161+
while True:
162+
# Due to accessing `serialPortOrig.in_waiting` too often will reduce the performance.
163+
# We read the `serialPortOrig.in_waiting` only once here.
164+
in_waiting = self.serialPortOrig.in_waiting
165+
for _ in range(max(1, in_waiting)):
166+
new_byte = self.serialPortOrig.read(size=1)
167+
if new_byte:
168+
self._buffer.extend(new_byte)
177169
else:
178-
return None
170+
break
171+
172+
if new_byte in (self._ERROR, self._OK):
173+
string = self._buffer.decode()
174+
self._buffer.clear()
175+
return string
176+
177+
if _timeout.expired():
178+
break
179179

180-
# return first message
181-
for i in range(len(self._buffer)):
182-
if self._buffer[i] == ord(self._OK) or self._buffer[i] == ord(self._ERROR):
183-
string = self._buffer[: i + 1].decode()
184-
del self._buffer[: i + 1]
185-
break
186-
return string
180+
return None
187181

188182
def flush(self) -> None:
189-
del self._buffer[:]
183+
self._buffer.clear()
190184
with error_check("Could not flush"):
191-
while self.serialPortOrig.in_waiting:
192-
self.serialPortOrig.read()
185+
self.serialPortOrig.reset_input_buffer()
193186

194187
def open(self) -> None:
195188
self._write("O")
@@ -204,7 +197,7 @@ def _recv_internal(
204197
canId = None
205198
remote = False
206199
extended = False
207-
frame = []
200+
data = None
208201

209202
string = self._read(timeout)
210203

@@ -215,14 +208,12 @@ def _recv_internal(
215208
canId = int(string[1:9], 16)
216209
dlc = int(string[9])
217210
extended = True
218-
for i in range(0, dlc):
219-
frame.append(int(string[10 + i * 2 : 12 + i * 2], 16))
211+
data = bytearray.fromhex(string[10 : 10 + dlc * 2])
220212
elif string[0] == "t":
221213
# normal frame
222214
canId = int(string[1:4], 16)
223215
dlc = int(string[4])
224-
for i in range(0, dlc):
225-
frame.append(int(string[5 + i * 2 : 7 + i * 2], 16))
216+
data = bytearray.fromhex(string[5 : 5 + dlc * 2])
226217
elif string[0] == "r":
227218
# remote frame
228219
canId = int(string[1:4], 16)
@@ -242,7 +233,7 @@ def _recv_internal(
242233
timestamp=time.time(), # Better than nothing...
243234
is_remote_frame=remote,
244235
dlc=dlc,
245-
data=frame,
236+
data=data,
246237
)
247238
return msg, False
248239
return None, False
@@ -252,15 +243,15 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None:
252243
self.serialPortOrig.write_timeout = timeout
253244
if msg.is_remote_frame:
254245
if msg.is_extended_id:
255-
sendStr = "R%08X%d" % (msg.arbitration_id, msg.dlc)
246+
sendStr = f"R{msg.arbitration_id:08X}{msg.dlc:d}"
256247
else:
257-
sendStr = "r%03X%d" % (msg.arbitration_id, msg.dlc)
248+
sendStr = f"r{msg.arbitration_id:03X}{msg.dlc:d}"
258249
else:
259250
if msg.is_extended_id:
260-
sendStr = "T%08X%d" % (msg.arbitration_id, msg.dlc)
251+
sendStr = f"T{msg.arbitration_id:08X}{msg.dlc:d}"
261252
else:
262-
sendStr = "t%03X%d" % (msg.arbitration_id, msg.dlc)
263-
sendStr += "".join(["%02X" % b for b in msg.data])
253+
sendStr = f"t{msg.arbitration_id:03X}{msg.dlc:d}"
254+
sendStr += msg.data.hex().upper()
264255
self._write(sendStr)
265256

266257
def shutdown(self) -> None:
@@ -295,29 +286,17 @@ def get_version(
295286
cmd = "V"
296287
self._write(cmd)
297288

298-
start = time.time()
299-
time_left = timeout
300-
while True:
301-
string = self._read(time_left)
302-
303-
if not string:
304-
pass
305-
elif string[0] == cmd and len(string) == 6:
306-
# convert ASCII coded version
307-
hw_version = int(string[1:3])
308-
sw_version = int(string[3:5])
309-
return hw_version, sw_version
310-
# if timeout is None, try indefinitely
311-
if timeout is None:
312-
continue
313-
# try next one only if there still is time, and with
314-
# reduced timeout
315-
else:
316-
time_left = timeout - (time.time() - start)
317-
if time_left > 0:
318-
continue
319-
else:
320-
return None, None
289+
string = self._read(timeout)
290+
291+
if not string:
292+
pass
293+
elif string[0] == cmd and len(string) == 6:
294+
# convert ASCII coded version
295+
hw_version = int(string[1:3])
296+
sw_version = int(string[3:5])
297+
return hw_version, sw_version
298+
299+
return None, None
321300

322301
def get_serial_number(self, timeout: Optional[float]) -> Optional[str]:
323302
"""Get serial number of the slcan interface.
@@ -331,24 +310,12 @@ def get_serial_number(self, timeout: Optional[float]) -> Optional[str]:
331310
cmd = "N"
332311
self._write(cmd)
333312

334-
start = time.time()
335-
time_left = timeout
336-
while True:
337-
string = self._read(time_left)
338-
339-
if not string:
340-
pass
341-
elif string[0] == cmd and len(string) == 6:
342-
serial_number = string[1:-1]
343-
return serial_number
344-
# if timeout is None, try indefinitely
345-
if timeout is None:
346-
continue
347-
# try next one only if there still is time, and with
348-
# reduced timeout
349-
else:
350-
time_left = timeout - (time.time() - start)
351-
if time_left > 0:
352-
continue
353-
else:
354-
return None
313+
string = self._read(timeout)
314+
315+
if not string:
316+
pass
317+
elif string[0] == cmd and len(string) == 6:
318+
serial_number = string[1:-1]
319+
return serial_number
320+
321+
return None

test/test_slcan.py

+24-9
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,26 @@
22

33
import unittest
44
import can
5+
from .config import IS_PYPY
6+
7+
8+
"""
9+
Mentioned in #1010 & #1490
10+
11+
> PyPy works best with pure Python applications. Whenever you use a C extension module,
12+
> it runs much slower than in CPython. The reason is that PyPy can't optimize C extension modules since they're not fully supported.
13+
> In addition, PyPy has to emulate reference counting for that part of the code, making it even slower.
14+
15+
https://realpython.com/pypy-faster-python/#it-doesnt-work-well-with-c-extensions
16+
"""
17+
TIMEOUT = 0.5 if IS_PYPY else 0.001 # 0.001 is the default set in slcanBus
518

619

720
class slcanTestCase(unittest.TestCase):
821
def setUp(self):
9-
self.bus = can.Bus("loop://", interface="slcan", sleep_after_open=0)
22+
self.bus = can.Bus(
23+
"loop://", interface="slcan", sleep_after_open=0, timeout=TIMEOUT
24+
)
1025
self.serial = self.bus.serialPortOrig
1126
self.serial.read(self.serial.in_waiting)
1227

@@ -15,7 +30,7 @@ def tearDown(self):
1530

1631
def test_recv_extended(self):
1732
self.serial.write(b"T12ABCDEF2AA55\r")
18-
msg = self.bus.recv(0)
33+
msg = self.bus.recv(TIMEOUT)
1934
self.assertIsNotNone(msg)
2035
self.assertEqual(msg.arbitration_id, 0x12ABCDEF)
2136
self.assertEqual(msg.is_extended_id, True)
@@ -33,7 +48,7 @@ def test_send_extended(self):
3348

3449
def test_recv_standard(self):
3550
self.serial.write(b"t4563112233\r")
36-
msg = self.bus.recv(0)
51+
msg = self.bus.recv(TIMEOUT)
3752
self.assertIsNotNone(msg)
3853
self.assertEqual(msg.arbitration_id, 0x456)
3954
self.assertEqual(msg.is_extended_id, False)
@@ -51,7 +66,7 @@ def test_send_standard(self):
5166

5267
def test_recv_standard_remote(self):
5368
self.serial.write(b"r1238\r")
54-
msg = self.bus.recv(0)
69+
msg = self.bus.recv(TIMEOUT)
5570
self.assertIsNotNone(msg)
5671
self.assertEqual(msg.arbitration_id, 0x123)
5772
self.assertEqual(msg.is_extended_id, False)
@@ -68,7 +83,7 @@ def test_send_standard_remote(self):
6883

6984
def test_recv_extended_remote(self):
7085
self.serial.write(b"R12ABCDEF6\r")
71-
msg = self.bus.recv(0)
86+
msg = self.bus.recv(TIMEOUT)
7287
self.assertIsNotNone(msg)
7388
self.assertEqual(msg.arbitration_id, 0x12ABCDEF)
7489
self.assertEqual(msg.is_extended_id, True)
@@ -85,23 +100,23 @@ def test_send_extended_remote(self):
85100

86101
def test_partial_recv(self):
87102
self.serial.write(b"T12ABCDEF")
88-
msg = self.bus.recv(0)
103+
msg = self.bus.recv(TIMEOUT)
89104
self.assertIsNone(msg)
90105

91106
self.serial.write(b"2AA55\rT12")
92-
msg = self.bus.recv(0)
107+
msg = self.bus.recv(TIMEOUT)
93108
self.assertIsNotNone(msg)
94109
self.assertEqual(msg.arbitration_id, 0x12ABCDEF)
95110
self.assertEqual(msg.is_extended_id, True)
96111
self.assertEqual(msg.is_remote_frame, False)
97112
self.assertEqual(msg.dlc, 2)
98113
self.assertSequenceEqual(msg.data, [0xAA, 0x55])
99114

100-
msg = self.bus.recv(0)
115+
msg = self.bus.recv(TIMEOUT)
101116
self.assertIsNone(msg)
102117

103118
self.serial.write(b"ABCDEF2AA55\r")
104-
msg = self.bus.recv(0)
119+
msg = self.bus.recv(TIMEOUT)
105120
self.assertIsNotNone(msg)
106121

107122
def test_version(self):

0 commit comments

Comments
 (0)