Python - 如何从串口完全读取数据?

3

我正在尝试使用基于PySerial库的以下代码读取写入到串口的JSON字符串:

while True:
    if serial_port.in_waiting > 0:
        buffer = serial_port.readline()
        print('buffer=', buffer)
        ascii = buffer.decode('ascii')
        print('ascii=', ascii)

我试图嗅探端口上的数据,并确信该数据被完整地写入,没有任何散乱。
jpnevulator --ascii --tty "/dev/ttyACM1" --read
7B 22 30 22 3A 31 7D                            {"0":1}
7B 22 30 22 3A 32 7D                            {"0":2}
7B 22 30 22 3A 33 7D                            {"0":3}
7B 22 30 22 3A 34 7D                            {"0":4}
7B 22 30 22 3A 35 7D                            {"0":5}
7B 22 30 22 3A 36 7D                            {"0":6}

然而,所使用的代码会导致数据散乱读取,因此显示以下结果:
buffer= b'{"0'
ascii= {"0
buffer= b'":1}'
ascii= ":1}
buffer= b'{"'
ascii= {"
buffer= b'0":2'
ascii= 0":2
buffer= b'}'
ascii= }

此外,当我使用 read() 而不是 readline() 时,我得到相同的行为:
buffer= b'{'
data_str= {
buffer= b'"'
data_str= "
buffer= b'3'
data_str= 3
buffer= b'"'
data_str= "
buffer= b':'
data_str= :
buffer= b'1'
data_str= 1
buffer= b'}'
data_str= }

我甚至尝试使用另一份代码,它也使用了同样的库,但是遇到了相同的问题。

我不确定为什么会出现这种情况。


2
Readline()可能不是你想要的,因为它似乎没有以换行符"\n"终止你的数据,所以尝试使用read()。同样,你可能只是太着急了,串口在仅接收到第一个字符时就会显示有数据存在,因此当只输入1或2个字符时,你的buffer.decode可能会表现得很奇怪。 - Reroute
@Reroute 正如我在编辑中指出的那样,使用read()并不能解决问题。 - user6039980
1
正如@Reroute在他们的评论中建议的那样。一旦有任何输入可用(.in_waiting> 0),您立即采取行动。有没有办法告诉您已经获得了完整的“消息”并仅在那时读取?或者...您仍然可以逐个读取,但必须自己组装所需的内容。即收集直到闭合括号,然后启动新的ascii。您的输入是否具有固定大小?这将使您的生活更轻松。;) - Ondrej K.
2个回答

1

这是数据通信中常见的问题:何时接收到完整的帧?

如果您控制双方,建议在协议中添加额外的帧定界。否则,您需要逐个字符地检查是否已经收到完整的JSON对象。

一个非常简单的协议是ndjson,它只需要在帧之间添加'\n',并且不要在有效负载中插入换行符。如果JSON字符串中有实际的换行符,则使用JSON库时它们将自动转义。

示例编写器:

# Writing ndjson frames
frames = [
    {"0": "1"},
    {"0": "2"},
]
for frame in frames:
    port.write(json.dumps(frame, separators=(',', ':')))  # No extra whitespace or newlines
    port.write('\n')  # ndjson protocol separator

使用pyserial.readline()可以读取 ndjson 格式的流。

请注意,您需要指定超时时间才能退出程序,否则 readline() 将永久阻塞。

以下是一个示例读取器,假设使用超时打开了port

while True:
    data = port.readline()  # Blocks until a complete frame is received or timeout
    if data:
        d = json.loads(data)
        print("Received object: %r" % d)
    else:
        if should_exit:
            break

1
我来试着解决这个问题 :) 你的循环等待任何输入可用 serial_port.in_waiting > 0。因此,你看到的就是这种行为。读取将在可以获得任何内容时开始。似乎 PySerial 没有任何额外的抽象可用于让你知道最后一个准备好的字节是 ASCII 卷曲括号字符(我只是浏览了一下文档)。你总是可以应用一个通用的解决方案,在 Python 脚本中逐步读取并理解它。

不过,先有一个问题。你的输入示例表明你将处理相同大小的字符串 / JSON?我们真的那么幸运吗?如果是这样,你可以等待那些或更多字节可用,并将所需大小读入你的 buffer 中。

否则,你可以根据你的代码进行变化:

buffer = bytes()  # .read() returns bytes right?
while True:
    if serial_port.in_waiting > 0:
        buffer += serial_port.read(serial_port.in_waiting)
        try:
            complete = buffer[:buffer.index(b'}')+1]  # get up to '}'
            buffer = buffer[buffer.index(b'}')+1:]  # leave the rest in buffer
        except ValueError:
            continue  # Go back and keep reading
        print('buffer=', complete)
        ascii = buffer.decode('ascii')
        print('ascii=', ascii)

注意1:我认为serial_port.in_waiting在if和read之间可能会发生变化,但我也认为未读取的字节仅留在缓冲区中,我们没有问题。
注意2:这种方法有点天真,没有考虑到您可能还读取了两个JSON代码块。
注意3:如果是这样的话,它也没有考虑您的JSON中嵌套的映射。
希望这仍然有帮助。底线是,除非处理固定大小的输入或以其他方式获取pySerial按所需分块提供内容,否则必须在脚本中读取并处理它。

更新:为了反映评论中的讨论。

你的问题实际上是你只看到(流的)串口字节。在那个层次上,没有任何有用的对通过传递的数据的理解。你需要一个更高级别的(应用程序或中间层),从中获得意义。换句话说,解析封装传输数据的协议。

事实上,如果我们知道表示JSON的字符串(一堆字节)是通过传递的(作为协议,表示/封装数据(结构)的方式),那么这可以工作,但重新组装需要在原始串行通信之上发生。我们的应用程序(或库/模块)可以读取原始串行数据,理解它并将其提供给更高层次。


感谢您的回答。输入大小不固定,需要一次性检索。不幸的是,当不同的写操作在近期发生时(即来自不同缓冲区的重叠散乱读取),这种连接方法将失败。 - user6039980
1
在这种情况下,您需要一个不同的触发器来执行读取,而不仅仅是 serial_port.in_waiting 变为非零。您需要知道不仅有任何/第一个字节可用,而且完整的数据“块”已准备就绪。 - Ondrej K.
现在每次读取只显示一个字符 - 这是我编写的代码,用于解决您所指的问题:https://gist.github.com/kaisbe/281b5eef1edffa82d1427b0d7b913e6c。 - user6039980
1
我刚才浏览了它的文档,没有看到任何其他信令设施的迹象。但这确实是有道理的。如果你只看串行接口,你会得到(一系列)字节。没有额外的信息,一些协议封装你的内容,说:我从这里开始,我在这里完成,你什么也做不了。这将是应用程序特定的,并在应用程序中处理。例如,打开和关闭大括号可以起到这个目的。但我不知道你的设置,可能不仅仅是一个流,我可以随意读取和解析。 - Ondrej K.
1
换句话说,如果没有更多关于设置的详细信息和一些创意,我恐怕您将无法仅使用.read()获取代表一个JSON的字符串。 - Ondrej K.
显示剩余2条评论

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