-
Notifications
You must be signed in to change notification settings - Fork 632
Improve slcan.py #1490
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Improve slcan.py #1490
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
2c9e1e6
Improve slcan.py
mikisama f95ca7d
Merge branch 'hardbyte:develop' into develop
mikisama 7fa9f5c
use `read_all` to read out serial port data.
mikisama c581cc9
fix when the `timeout` parameter is 0, it cannot enter the while loop.
mikisama 8722a11
For performance reason, revert to using `read` to read serial data.
mikisama fa62a63
add `size=1` and renamed `new_data` to `new_byte`
mikisama 73ce3dd
fix the issue of returning `None` before receiving
mikisama 3eb80b9
Due to the simplification of the control flow,
mikisama 2bbee49
Revert "Due to the simplification of the control flow,"
mikisama 1635381
Simplify the control flow for finding the end of a SLCAN message.
mikisama 42db106
improve the `timeout` handling of slcan.py
mikisama 6f994cc
Merge branch 'hardbyte:develop' into develop
mikisama 3c8a9e3
Change the plain format calls to f-strings.
mikisama ce3b9a4
using `bytearray.fromhex` to parse the frame's data.
mikisama 2c22b4e
change `del self._buffer[:]` to `self._buffer.clear` since it's more …
mikisama 09e1248
Simplify the timeout handling
mikisama e3a388b
fix the issue when the returned string is `None`.
mikisama 370a2ff
using `pyserial.reset_input_buffer` to discard
mikisama 68c9c71
fix failing PyPy test
mikisama cec01d7
fix failing PyPy test
mikisama 6c3b13a
improve the comments
mikisama File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -64,6 +64,7 @@ def __init__( | |
btr: Optional[str] = None, | ||
sleep_after_open: float = _SLEEP_AFTER_SERIAL_OPEN, | ||
rtscts: bool = False, | ||
timeout: float = 0.001, | ||
**kwargs: Any, | ||
) -> None: | ||
""" | ||
|
@@ -82,7 +83,8 @@ def __init__( | |
Time to wait in seconds after opening serial connection | ||
:param rtscts: | ||
turn hardware handshake (RTS/CTS) on and off | ||
|
||
:param timeout: | ||
Timeout for the serial or usb device in seconds (default 0.001) | ||
:raise ValueError: if both ``bitrate`` and ``btr`` are set or the channel is invalid | ||
:raise CanInterfaceNotImplementedError: if the serial module is missing | ||
:raise CanInitializationError: if the underlying serial connection could not be established | ||
|
@@ -98,7 +100,10 @@ def __init__( | |
|
||
with error_check(exception_type=CanInitializationError): | ||
self.serialPortOrig = serial.serial_for_url( | ||
channel, baudrate=ttyBaudrate, rtscts=rtscts | ||
channel, | ||
baudrate=ttyBaudrate, | ||
rtscts=rtscts, | ||
timeout=timeout, | ||
) | ||
|
||
self._buffer = bytearray() | ||
|
@@ -150,46 +155,34 @@ def _write(self, string: str) -> None: | |
self.serialPortOrig.flush() | ||
|
||
def _read(self, timeout: Optional[float]) -> Optional[str]: | ||
_timeout = serial.Timeout(timeout) | ||
|
||
with error_check("Could not read from serial device"): | ||
# first read what is already in receive buffer | ||
while self.serialPortOrig.in_waiting: | ||
self._buffer += self.serialPortOrig.read() | ||
# if we still don't have a complete message, do a blocking read | ||
start = time.time() | ||
time_left = timeout | ||
while not ( | ||
ord(self._OK) in self._buffer or ord(self._ERROR) in self._buffer | ||
): | ||
self.serialPortOrig.timeout = time_left | ||
byte = self.serialPortOrig.read() | ||
if byte: | ||
self._buffer += byte | ||
# if timeout is None, try indefinitely | ||
if timeout is None: | ||
continue | ||
# try next one only if there still is time, and with | ||
# reduced timeout | ||
else: | ||
time_left = timeout - (time.time() - start) | ||
if time_left > 0: | ||
continue | ||
while True: | ||
# Due to accessing `serialPortOrig.in_waiting` too often will reduce the performance. | ||
# We read the `serialPortOrig.in_waiting` only once here. | ||
in_waiting = self.serialPortOrig.in_waiting | ||
for _ in range(max(1, in_waiting)): | ||
new_byte = self.serialPortOrig.read(size=1) | ||
if new_byte: | ||
self._buffer.extend(new_byte) | ||
else: | ||
return None | ||
break | ||
|
||
if new_byte in (self._ERROR, self._OK): | ||
string = self._buffer.decode() | ||
self._buffer.clear() | ||
return string | ||
|
||
if _timeout.expired(): | ||
break | ||
|
||
# return first message | ||
for i in range(len(self._buffer)): | ||
if self._buffer[i] == ord(self._OK) or self._buffer[i] == ord(self._ERROR): | ||
string = self._buffer[: i + 1].decode() | ||
del self._buffer[: i + 1] | ||
break | ||
return string | ||
return None | ||
|
||
def flush(self) -> None: | ||
del self._buffer[:] | ||
self._buffer.clear() | ||
with error_check("Could not flush"): | ||
while self.serialPortOrig.in_waiting: | ||
self.serialPortOrig.read() | ||
self.serialPortOrig.reset_input_buffer() | ||
|
||
def open(self) -> None: | ||
self._write("O") | ||
|
@@ -204,7 +197,7 @@ def _recv_internal( | |
canId = None | ||
remote = False | ||
extended = False | ||
frame = [] | ||
data = None | ||
|
||
string = self._read(timeout) | ||
|
||
|
@@ -215,14 +208,12 @@ def _recv_internal( | |
canId = int(string[1:9], 16) | ||
dlc = int(string[9]) | ||
extended = True | ||
for i in range(0, dlc): | ||
frame.append(int(string[10 + i * 2 : 12 + i * 2], 16)) | ||
data = bytearray.fromhex(string[10 : 10 + dlc * 2]) | ||
elif string[0] == "t": | ||
# normal frame | ||
canId = int(string[1:4], 16) | ||
dlc = int(string[4]) | ||
for i in range(0, dlc): | ||
frame.append(int(string[5 + i * 2 : 7 + i * 2], 16)) | ||
data = bytearray.fromhex(string[5 : 5 + dlc * 2]) | ||
elif string[0] == "r": | ||
# remote frame | ||
canId = int(string[1:4], 16) | ||
|
@@ -242,7 +233,7 @@ def _recv_internal( | |
timestamp=time.time(), # Better than nothing... | ||
is_remote_frame=remote, | ||
dlc=dlc, | ||
data=frame, | ||
data=data, | ||
) | ||
return msg, False | ||
return None, False | ||
|
@@ -252,15 +243,15 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: | |
self.serialPortOrig.write_timeout = timeout | ||
if msg.is_remote_frame: | ||
if msg.is_extended_id: | ||
sendStr = "R%08X%d" % (msg.arbitration_id, msg.dlc) | ||
sendStr = f"R{msg.arbitration_id:08X}{msg.dlc:d}" | ||
else: | ||
sendStr = "r%03X%d" % (msg.arbitration_id, msg.dlc) | ||
sendStr = f"r{msg.arbitration_id:03X}{msg.dlc:d}" | ||
else: | ||
if msg.is_extended_id: | ||
sendStr = "T%08X%d" % (msg.arbitration_id, msg.dlc) | ||
sendStr = f"T{msg.arbitration_id:08X}{msg.dlc:d}" | ||
else: | ||
sendStr = "t%03X%d" % (msg.arbitration_id, msg.dlc) | ||
sendStr += "".join(["%02X" % b for b in msg.data]) | ||
sendStr = f"t{msg.arbitration_id:03X}{msg.dlc:d}" | ||
sendStr += msg.data.hex().upper() | ||
self._write(sendStr) | ||
|
||
def shutdown(self) -> None: | ||
|
@@ -295,29 +286,17 @@ def get_version( | |
cmd = "V" | ||
self._write(cmd) | ||
|
||
start = time.time() | ||
time_left = timeout | ||
while True: | ||
string = self._read(time_left) | ||
|
||
if not string: | ||
pass | ||
elif string[0] == cmd and len(string) == 6: | ||
# convert ASCII coded version | ||
hw_version = int(string[1:3]) | ||
sw_version = int(string[3:5]) | ||
return hw_version, sw_version | ||
# if timeout is None, try indefinitely | ||
if timeout is None: | ||
continue | ||
# try next one only if there still is time, and with | ||
# reduced timeout | ||
else: | ||
time_left = timeout - (time.time() - start) | ||
if time_left > 0: | ||
continue | ||
else: | ||
return None, None | ||
string = self._read(timeout) | ||
|
||
if not string: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
pass | ||
elif string[0] == cmd and len(string) == 6: | ||
# convert ASCII coded version | ||
hw_version = int(string[1:3]) | ||
sw_version = int(string[3:5]) | ||
return hw_version, sw_version | ||
|
||
return None, None | ||
|
||
def get_serial_number(self, timeout: Optional[float]) -> Optional[str]: | ||
"""Get serial number of the slcan interface. | ||
|
@@ -331,24 +310,12 @@ def get_serial_number(self, timeout: Optional[float]) -> Optional[str]: | |
cmd = "N" | ||
self._write(cmd) | ||
|
||
start = time.time() | ||
time_left = timeout | ||
while True: | ||
string = self._read(time_left) | ||
|
||
if not string: | ||
pass | ||
elif string[0] == cmd and len(string) == 6: | ||
serial_number = string[1:-1] | ||
return serial_number | ||
# if timeout is None, try indefinitely | ||
if timeout is None: | ||
continue | ||
# try next one only if there still is time, and with | ||
# reduced timeout | ||
else: | ||
time_left = timeout - (time.time() - start) | ||
if time_left > 0: | ||
continue | ||
else: | ||
return None | ||
string = self._read(timeout) | ||
|
||
if not string: | ||
pass | ||
elif string[0] == cmd and len(string) == 6: | ||
serial_number = string[1:-1] | ||
return serial_number | ||
|
||
return None |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you remove this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Setting the serial port timeout in the while loop slow down the RX performance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a way to ensure, that the function respects the
timeout
parameter?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
timeout
parameterThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With this proposed change I think there's still a scenario where the timeout is not enforced.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
read_all
sounds good to me.The serial port must be instantiated with
timeout=0
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Tbruno25
read
method only copies it out. Only when the data bytes in the buffer are smaller than the parameter__size
, theread
method will blocking until the data bytes in the buffer is__size
, then copy it out.in_waiting
parameter shows the current number of bytes in the buffer, blocking will not occur. (The time to copy data out is negligibly small)MAX
receive buffer size in win10 is16384
, and ubuntu 20.04 is4095
. I'm not sure whether it can be changed. But I think there will not beinfinite
junk.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@zariiii9003
Looks good. Just a few minor changes requested.
According to my test, the
read_all
method returnsb''
if there is no data that can be read out.So the
if new_data is not None:
should be changed toif new_data:
, otherwise it will never dotime.sleep
, which will cause high CPU usage.By the way, I did not initialize the
timeout
parameter ofserialPortOrig
at all and it defaults toNone
. It seems still works for me.I'm curious. If you don't set the
timeout
to 0, what will happen to you?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nothing, i don't have an interface to test this 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting! And I agree -- I wasn't considering the physical limitations of the hardware/kernel. Good catch 🙂