Home > Back-end >  Threading tkinter serial reading function
Threading tkinter serial reading function

Time:03-24

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.

  • Related