线缓冲串口输入

8

我有一个串行设备,正在尝试从中读取输入。我向它发送了一个字符串 "ID\r",然后它返回 "ID XX\r"(其中 \r 是 ASCII 回车,十六进制为 0x0d)。

由于串行.readline 上的 eol 选项不再受支持,我正在使用 TextIOWrapper 从串行端口读取并一次返回一行。

我的问题是,它不是在看到回车符后立即返回我的字符串,而是等待我打开串行端口时设置的两倍超时时间。我希望它在读取完整行后立即返回字符串,因为我可能需要将数百个这些命令发送到设备,并且不想每次都等待超时时间。如果将超时时间设置为 0,则根本没有输出(可能是因为我的脚本在设备有机会输出任何内容之前就停止等待),而如果将超时时间设置为 None,则脚本将永远阻塞。

这是一个简单的测试脚本:

import serial
import io
import time

ser = serial.Serial("/dev/ttyUSB0", baudrate=9600,
                    bytesize=8, parity='N', stopbits=1,
                    xonxoff=0, rtscts=1, timeout=5)

sio = io.TextIOWrapper(io.BufferedRWPair(ser, ser),
                       newline=None)


sio.write(unicode("ID\r"))
sio.flush()

print "reading..."

x = sio.readline()

print len(x)
print x

脚本总是在显示“reading”和打印从串口读取的“ID XX”字符串之间花费10秒钟。
我确认设备正在输出回车符,因为我已经使用strace监视了读取过程。
select(4, [3], [], [], {5, 0})          = 1 (in [3], left {4, 991704})
read(3, "I", 8192)                      = 1
select(4, [3], [], [], {5, 0})          = 1 (in [3], left {4, 999267})
read(3, "D", 8191)                      = 1
select(4, [3], [], [], {5, 0})          = 1 (in [3], left {4, 999420})
read(3, " ", 8190)                      = 1
select(4, [3], [], [], {5, 0})          = 1 (in [3], left {4, 999321})
read(3, "X", 8189)                      = 1
select(4, [3], [], [], {5, 0})          = 1 (in [3], left {4, 999355})
read(3, "X", 8188)                      = 1
select(4, [3], [], [], {5, 0})          = 1 (in [3], left {4, 999171})
read(3, "\r", 8187)                     = 1
select(4, [3], [], [], {5, 0})          = 0 (Timeout)
select(4, [3], [], [], {5, 0})          = 0 (Timeout)

您可以看到有两个select()超时,导致了10秒的延迟,但您也可以清晰地看到回车符被读取。我已经尝试将newline参数设置为'None'和''(应自动允许\r、\n和\r\n),以及'\r',但每次结果都相同。
我还尝试将BufferedRWPair()调用中的buffer_size设置为“1”,以防止缓冲输入,但没有任何区别。
您有什么想法我做错了什么吗?
如果我无法让它正常工作,我的下一步将是使用serial.read()逐个字符读取并进行自己的行缓冲,但我想先尝试使用textiowrapper的“正确”方法。

你确定 print 语句不会触发输出缓冲吗?试着加上 -u 参数来运行它。 - Burhan Khalid
4个回答

8

今天我在这个问题上浪费了几个小时。事实证明,io.BufferedReader会一直读取数据,直到填满缓冲区,然后将缓冲区传递给io.TextIOWrapper。默认的缓冲区大小是8192,因此根据您的设备性能,这可能需要一些时间。

正确的示例代码应该是:

# buffer size is 1 byte, so directly passed to TextIOWrapper
sio = io.TextIOWrapper(io.BufferedRWPair(ser, ser, 1), encoding='ascii')
print sio.readline()[:-1]

3

注意:我正在Mac上使用Python 3.4,所以你的情况可能不同,但我相信通过将TextIOWrapper回溯到Python 2.7,Python 2.7(和其他操作系统)的情况基本与我以下描述的相同。

主要问题是io.TextIOWrapper本身使用缓冲机制,由未记录的_CHUNK_SIZE属性控制。这非常不愉快。所以你有两个选择:

  1. Use a timeout as you tried out. This is what is hinted on in the documentation of readline on the pyserial documentation page. However, if you use a large value (as you did), when there is not enough data to fill the buffer of TextIOWrapper, your code will block until the timeout is reached. This is what you are experiencing essentially (I did not go after why you have to wait double the timeout value, but I think that this could be sorted out by looking at the implementation of TextIOWrapper and ultimately is irrelevant to your question).
  2. The second choice is to change _CHUNK_SIZE to 1. In your case, simply add the line

    sio._CHUNK_SIZE = 1
    

    to your code right after you initialized sio. This has the perhaps unpleasant effect that the buffering within TextIOWrapper itself will be turned off (this is used for the incremental decoding of the input). If performance is not an issue, this is the simplest solution. If performance is an issue, you can set a low value of timeout, not toucing _CHUNK_SIZE. However, in this case be prepared to get an empty string from readline() (if the device sends you an empty line, that will come through as '\n', so it can be distinguished from the empty string that you will get when a read runs out of the alloted time).

你的代码还存在另一个问题:当sio被移除时,ser的close方法将被调用两次,这将导致程序即将完成时出现异常(至少在我的电脑上是这样)。你应该创建两个serial.Serial实例,并将它们传递给BufferedRWPair。
我还创建了一个基于TextIOWrapper的包装类,如果有兴趣的话,我也可以发布它,只是我不想在响应中添加一些额外的代码,严格来说,这些代码是不需要的。
PS:与此同时,我在Ubuntu上尝试了该代码。虽然在我的Mac上,我没有看到设置io.BufferedRWPair缓冲区大小为1的必要性,但在Ubuntu上,我也不得不这样做,此外还要将_CHUNK_SIZE设置为1。

1
感谢你的代码,Keith。但是我想保持这段代码的可移植性,所以我想坚持使用默认的“serial”包。
此外,由于我还在学习Python,我想尝试学习如何按照预期使用TextIOWrapper。
我放弃了尝试让serial.readline()工作,所以现在我只会使用一个简单的“readLine”函数来逐个字符读取并查找回车终止符。虽然如果我遇到更多串行奇怪的问题,我可能会重新考虑使用你的代码。
谢谢!
def readLine(ser):
    str = ""
    while 1:
        ch = ser.read()
        if(ch == '\r' or ch == ''):  
            break
        str += ch

    #"print "str = " + str

    return str

0
“如果没有实际在现场检查,这将是很难调试的。但请尝试使用我的tty模块。”

http://code.google.com/p/pycopia/source/browse/trunk/aid/pycopia/tty.py

尝试使用SerialPort对象。我已经成功地使用它与串行仪器进行交互,而“那个其他的串行模块”存在许多类似于您描述的问题。这个对象还可以告诉您FIFO中是否有数据。

让我知道进展如何。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接