I'm making an application that reads serial data coming from the sensors on an arduino board. Problems arise when trying to use the matplotlib.animation class to make a live graph of said data. The GUI widgets become unresponsive when the plotting is taking place. As far as i've understood, making the serial reading process run on its own thread could potentially solve the issue. I'm having trouble understanding how this could be made so that it is compatible with the FuncAnimation-subclass.
def read_serial_data(port, bauds=9600):
s = serial.Serial(port, bauds)
line = s.readline()[0:-2]
return line
def getPorts():
return [port.device for port in serial.tools.list_ports.comports(include_links=False)]
class GUI():
def __init__(self):
self.root = Tk.Tk()
self._fig = plt.figure()
self.root.title('Measurement Dashboard')
self.root.state('normal')
self.root.config(background='#ffffff')
self._canvas = FigureCanvasTkAgg(self._fig, self.root)
self._canvas.get_tk_widget().grid(column = 1, row = 1)
self._canvas.draw()
self._animate = None
self._ax = self._fig.add_subplot(111)
self._ax.yaxis.grid(True, color = 'black', linestyle='--')
self._ax.xaxis.grid(True, color = 'black', linestyle='--')
self._ax.set_xlabel('time')
self._ax.set_ylabel('CO2')
self.filename = Tk.StringVar()
self.entry = ttk.Entry(self.root, textvariable = self.filename)
self.entry.grid(column = 2, row = 2)
self.info_var = Tk.StringVar()
self.info_entry = ttk.Entry(self.root, textvariable = self.info_var)
self.info_entry.grid(column = 2, row = 3)
self.port = Tk.StringVar()
self.ports = getPorts()
self._cb = ttk.Combobox(self.root, textvariable= self.port, values = self.ports)
self._cb.grid(column = 2, row = 1)
self.start_button = Tk.Button(self.root, text = 'Start', command = self.plot)
self.start_button.grid(column = 1, row = 2)
self.save_button = Tk.Button(self.root, text = 'Save info', command = self.save_info)
self.save_button.grid(column = 2, row = 4)
def save_info(self):
global info
info = self.info_var.get()
def start(self):
self.root.mainloop()
def plot(self):
if self._animate is None:
self.scope = Scope(self._ax, self.filename.get())
self._canvas.draw_idle()
self._animate = animation.FuncAnimation(self._fig, self.scope.animate, frames = self.update, interval=2000, blit=False)
def update(self):
line = read_serial_data(self.port.get())
data = line.decode('utf-8')
yield data
time = datetime.now()
duration = time - start_time
measurement = {'time': time, 'dur': duration.seconds, 'CO2': data, 'info': info}
write_csv_line(self.filename.get(), measurement)
self.root.after(10000, self.update)
if __name__ == "__main__":
gui = GUI()
gui.start()
thread = Thread(target=read_serial_data,args=(gui.port,))
thread.start()
CodePudding user response:
You don't really need another thread but can do this using non-blocking IO on th eserial port and make use of the Tkinter after
call to manage the poll interval. pyserial provices inWaiting
to test if the device has input waiting to be read. If there are bytes waiting, read just those.
Here is an example reader class to read lines from a serial device and post them to the application handler method once a complete line is read.
class Reader(Serial):
def __init__(self, *args, **kwargs):
self.buffer = ''
super(Reader, self).__init__(*args, **kwargs)
def monitor(self, w, interval=10):
n = self.inWaiting()
if n != 0:
data = self.read(n).decode('ascii')
self.buffer = self.buffer data
ndx = self.buffer.find("\n")
if ndx != -1:
line = self.buffer[:ndx 1]
self.buffer = self.buffer[ndx 1:]
w.after_idle(lambda: w.parse_input(line))
w.after(interval, lambda: self.monitor(w, interval))
Used like:
app = <tkinter application class instance>
reader = Reader(port, baudrate, timeout=0)
reader.flushInput()
reader.monitor(app, 10)
app.mainloop()
In this case, it will call a parse_input
method on the app
instance whenever a line is read (delimited by a newline).
If you decide to use a thread, then you need a Queue to pass the data to the Tkinter UI thread and must ensure you don't call Tkinter methods from the worker thread.