使用pyserial、matplotlib和tkinter GUI进行实时数据打印



我目前正在进行一个项目,在该项目中,我使用两个电位计和一个Arduino Uno来收集实时(x,y)定位数据(一个电位计用于x,一个用于y)。我目前可以与Arduino建立串行连接,并调出一个带有图形和两个按钮(一个用于开始绘图,一个用于停止绘图)的tkinter GUI,但一旦我尝试读取串行数据并同时更新图形,串行连接就会冻结,不再更新值。没有出现任何错误,所以我不确定发生了什么。串行数据是由两个值(x和y)组成的字符串,用空格分隔。我已经意识到,如果我注释掉canvas.draw()行,串行通信就可以正常工作,并且可以在控制台中实时更新。我在下面附上了我的相关代码。

import matplotlib
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import pyautogui as gui
import tkinter as tk
import numpy as np
import serial as sr
matplotlib.use('TkAgg')
# ------------Global Variables ------------#
data_x = np.array([])
data_y = np.array([])
screen_width, screen_height = gui.size()
cond = False

def mapValues(num, x_or_y):
global screen_width, screen_height
maxValX = 1023
maxValY = 1023
if x_or_y == 1:
proportion = num / maxValX
new_val = round(screen_width * proportion)
if new_val > screen_width:
new_val = screen_width
else:
proportion = num / maxValY
new_val = round(screen_height * proportion)
if new_val > screen_height:
new_val = screen_height
if new_val < 0:
new_val = 0
return new_val

def plot_data():
global cond, data_x, data_y, s
len_line = 4
if cond:  # to prevent continuous data logging
data = s.readline()  # read and decode the serial data
data = data.decode()
try:
coordinates = data.split()  # split the incoming str data into the x and y components
x_val = coordinates[0]
y_val = coordinates[1]
# print(x_val, y_val, type(x_val))
x_val = mapValues(int(x_val), 1)  # map the values to an arbitrary range
y_val = mapValues(int(y_val), 2)
print(x_val, y_val)
# print('length of array: ', len(data_x))
# print(data_x, 'n', data_y)
if len(data_x) < len_line:  # keep the length of the x,y data consistently at whatever value we want
data_x = np.append(data_x, x_val)
data_y = np.append(data_y, 320 - y_val)
else:  # shift all the values back in the list and add the new one to the end
data_x[0:len_line-1] = data_x[1:len_line]
data_x[len_line-1] = x_val
data_y[0:len_line-1] = data_y[1:len_line]
data_y[len_line-1] = y_val
# update the x and y data of the line
line1.set_xdata(data_x)
line1.set_ydata(data_y)
print('Line data: ', line1.get_xydata())
# update the canvas
canvas.draw()
canvas.flush_events()
except Exception as e:
print(e)
root.after(1, plot_data)

def start_plot():
global s, cond
s.reset_input_buffer()
cond = True

def stop_plot():
global cond, s
cond = False

if __name__ == '__main__':
print('Starting...')
# ----------main GUI code --------------------#
root = tk.Tk()
root.title('Real Time Motus Hand')
root.configure(background='black')
app_width = 1200
app_height = 800
x_offset = (screen_width - app_width) / 2
y_offset = (screen_height - app_height) / 2
root.geometry(f'{app_width}x{app_height}+{int(x_offset)}+{int(y_offset)}')
# ---------- create plot object on figure ----------#
fig = Figure()
# customize parameters
ax = fig.add_subplot(111)
ax.set_xlabel('x-axis')
ax.set_ylabel('y-axis')
ax.set_xlim(0, screen_width)
ax.set_ylim(0, screen_height)
#  create line object to control data
line1 = ax.plot([], [])[0]
#  define figure height and width and placement
plot_width = 800
plot_height = 600
canvas = FigureCanvasTkAgg(fig, master=root)
canvas.get_tk_widget().place(x=(app_width - plot_width) / 2, y=(app_height - plot_height) / 2,
width=plot_width, height=plot_height)
canvas.draw()
# ----------create start and stop buttons -------------
start = tk.Button(root, text='start drawing', font=('calbiri', 12), command=lambda: start_plot())
start.place(x=100, y=app_height - 50)
stop = tk.Button(root, text='stop drawing', font=('calbiri', 12), command=lambda: stop_plot())
stop.place(x=300, y=app_height - 50)
# --------start serial communication ---------
s = sr.Serial('/dev/cu.usbmodem142201', 9600)
s.reset_input_buffer()
root.after(1, plot_data)
root.mainloop()

这可能是线程问题。不久前,在使用TKinter-ui时遇到了类似的情况——考虑创建一个"全局"类来存储TKinter框架中显示的信息,并实例化一个线程来更新这些值,而不是试图使用主线程/进程同时做几件事,尤其是更新视觉元素。如果这是一个总是更新或至少频繁更新的事件,它会锁定试图写入UI的主进程,而不是继续该进程,因此创建一个类来存储数据并由一个线程负责这样做可以减轻主处理线程的一些负担,因为一个线程,甚至主线程,一次只能做一件事。

只要重读你的描述,根据你在评论画布时观察到的行为,我更加确信这是一个线程问题。。。

如果需要例子,让我知道,我会看看我能做什么

此外,你似乎参与了某种物理或工程项目——祝你好运!

---线程示例:文档可在此处找到:https://docs.python.org/3/library/threading.html但我发现这种语言很复杂,所以我将跳到最基本的部分:

import threading
thread = threading.Thread(target=FUNCTION,args=[Parameter1,Parameter2,...,ParameterN])
thread.start()
thread.join()

这里有几个重要的部分,主要是1)FUNCTION是你定义的一些函数,在这种情况下,我想你可以写一个函数来收集当前数据并将值设置为tkinter-ui;只需给出函数的名称,就像我在那里得到的一样,不要添加你的理解或参数,因为第二个数字是2)args=[]是函数中所有变量的列表,重要的是你这样做是因为列表或线程混淆了3)然后你用thread.start()实例化操作,但是你也可以选择执行thread.join(),这将停止你的程序继续运行,直到线程完成,这可能不是你想要的。完全可选。

我建议将其作为一种工具来使用,看看它如何最适合。我再次建议创建一些泛型类(我通常称之为MyGlobals或一些变体)来存储数据/值,因为除非在函数中明确说明,否则线程将无法访问全局变量。此外,如果您有几个线程用于几个不同的操作,那么没有什么可以阻止您将它们制作成一个列表,然后使用迭代来连续启动/连接所有这些线程。许多不同的路线,但遗憾的是,这是你的冒险。出去玩吧!