Search code examples
pythontkintermodbusminimalmodbus

Tkinter: multiprocessing on events that happen per second


I have made a Tkinter program that reads from a modbus slave device. Every second it reads the device and displays the output onto labels. However, I have multiple tabs that run the same code for each device connected. While a device is being read, the whole GUI freezes so you can't move the program or press buttons until it is done reading. Would multiprocessing help with freezing? If so, how could I implement it?

Here is my code:

import tkinter as tk
from tkinter import *
from tkinter import ttk
from time import time
import minimalmodbus
import serial
minimalmodbus.CLOSE_PORT_AFTER_EACH_CALL = True

class Page(tk.Frame):
    def __init__(self, *args, **kwargs):
        tk.Frame.__init__(self, *args, **kwargs)
    def show(self):
        self.lift()

class Page1(Page):
    def __init__(self, *args, **kwargs):
       Page.__init__(self, *args, **kwargs)

       self.gas = minimalmodbus.Instrument('COM3', 1)
       self.gas.serial.baudrate = 9600
       self.gas.serial.bytesize = 8
       self.gas.serial.parity = serial.PARITY_NONE
       self.gas.serial.stopbits = 1
       self.gas.serial.timeout = 0.25
       self.gas.mode = minimalmodbus.MODE_RTU

       self.value_display = tk.Label(self, text='value', width=10)
       self.value_display.pack(side="top")

       self.unit_display = tk.Label(self, text='unit', width=10)
       self.unit_display.pack(side="top")

       self.gas_display = tk.Label(self, text='temp', width=10)
       self.gas_display.pack(side="top")

       self.status_display = tk.Label(self, text='status', width=10)
       self.status_display.pack(side="top")

       self.command_display = tk.Label(self, text='command', width=10)
       self.command_display.pack(side="top")

       self.pressure_display = tk.Label(self, text='pressure', width=10)
       self.pressure_display.pack(side="top")

       self.timer_button = tk.Button(self, text='Start', command=self.toggle)
       self.timer_display = tk.Label(self, text='00:00', width=10)
       self.timer_button.pack(side="top")
       self.timer_display.pack(side="top")
       self.paused = True

    def gas_meth(self):
        try:
            gas_value = self.gas.read_registers(0,42)

            self.value_display.config(text=gas_value[0])
            self.unit_display.config(text=gas_value[1])
            self.gas_display.config(text=gas_value[2])
            self.status_display.config(text=gas_value[3])
            self.command_display.config(text=gas_value[4])
            self.pressure_display.config(text=gas_value[5])

        except IOError:
            self.gas_display.config(text="Lost con.")
        except ValueError:
            self.gas_display.config(text="RTU error")
        self.gas_display.after(1000, self.gas_meth)

    def toggle(self):
        if self.paused:
            self.paused = False
            self.timer_button.config(text='Stop')
            self.oldtime = time()
            self.run_timer()
            self.gas_meth()
        else:
            self.paused = True
            self.oldtime = time()
            self.timer_button.config(text='Start')

    def run_timer(self):
        if self.paused:
            return
        delta = int(time() - self.oldtime)
        timestr = '{:02}:{:02}'.format(*divmod(delta, 60))
        self.timer_display.config(text=timestr)
        self.timer_display.after(500, self.run_timer)

class MainView(tk.Frame):
    def __init__(self, *args, **kwargs):
        tk.Frame.__init__(self, *args, **kwargs)
        p1 = Page1(self)

        buttonframe = tk.Frame(self)
        container = tk.Frame(self)
        buttonframe.pack(side="top", fill="x", expand=False)
        container.pack(side="top", fill="both", expand=True)

        p1.place(in_=container, x=0, y=0, relwidth=1, relheight=1)
        b1 = tk.Button(buttonframe, text="Page 1", command=p1.lift)
        b1.pack(side="left")
        p1.show()

if __name__ == "__main__":
    root = tk.Tk()
    main = MainView(root)
    main.pack(side="top", fill="both", expand=True)
    root.wm_geometry("1000x600")
    root.mainloop()

Solution

  • The general idea is to put the code that reads from the sensors in a thread, and have that code communicate with the GUI thread via a queue.

    Here's a real quick hack to demonstrate the technique:

    import tkinter as tk
    import threading
    import queue
    import random
    import time
    
    class Example(object):
        def __init__(self):
            self.root = tk.Tk()
            self.sensor_vars = []
    
            self.root.grid_columnconfigure(1, weight=1)
            for row, i in enumerate(range(3)):
                var = tk.StringVar()
                self.sensor_vars.append(var)
                label = tk.Label(self.root, text="Sensor %d:" % i)
                value = tk.Label(self.root, textvariable=var, width=4)
                label.grid(row=row, column=0, sticky="e")
                value.grid(row=row, column=1, sticky="w")
    
            # create a queue for communication
            self.queue = queue.Queue()
    
            # create some sensors
            self.sensors = []
            for i in range(3):
                sensor = Sensor(self.queue, i)
                self.sensors.append(sensor)
                sensor.setName("Sensor %d" % i)
    
            # start polling the queue
            self.poll_queue()
    
        def start(self):
            # start the sensors
            for sensor in self.sensors:
                sensor.start()
    
            # start the GUI loop
            self.root.mainloop()
    
            # wait for the threads to finish
            for sensor in self.sensors:
                sensor.stop()
                sensor.join()
    
        def poll_queue(self):
            if not self.queue.empty():
                message = self.queue.get()
                index = message["index"]
                self.sensor_vars[index].set(message["value"])
    
            self.root.after(100, self.poll_queue)
    
    class Sensor(threading.Thread):
        def __init__(self, queue, index):
            threading.Thread.__init__(self)
            self.queue = queue
            self.index = index
            self.stop_requested = False
    
        def stop(self):
            self.stop_requested = True
    
        def run(self):
            for i in range(10):
                if self.stop_requested:
                    break
                value = random.randint(10, 100)
                self.queue.put({"index": self.index, "value": value})
                time.sleep(1)
    
    
    
    if __name__ == "__main__":
        app = Example()
        app.start()