GreenSaber
GreenSaber

Reputation: 1148

Tkinter: Separating Processes

I am trying to build a Tkinter program that supports multiprocessing. I need to read from multiple Modbus devices and display the output onto the GUI.

I have successfully done this with a command line by using processes, but in Tkinter, my GUI freezes every time I attempt to read.

Here is my code:

import os
from multiprocessing import Process
import threading
import queue
import tkinter as tk
from tkinter import *
from tkinter import ttk
import time
import time as ttt
import minimalmodbus
import serial
minimalmodbus.CLOSE_PORT_AFTER_EACH_CALL = True


class Application(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master)
        self.gas = minimalmodbus.Instrument('COM3', 1)
        self.gas.serial.baudrate = 9600
        self.gas.serial.parity = serial.PARITY_NONE
        self.gas.serial.bytesize = 8
        self.gas.serial.stopbits = 1
        self.gas.serial.timeout = 0.25
        self.gas.mode = minimalmodbus.MODE_RTU

        self.pack()
        self.create_widgets()

    def create_widgets(self):
        self.first_gas_labelframe = LabelFrame(self, text="Gas 1", width=100)
        self.first_gas_labelframe.pack()

        self.value_label = Label(self.first_gas_labelframe, text="Value")
        self.value_label.pack()

        self.unit_label = Label(self.first_gas_labelframe, text="Unit")
        self.unit_label.pack()

        self.temp_label = Label(self.first_gas_labelframe, text="Temp")
        self.temp_label.pack()


        self.timer_button = tk.Button(self, text='Start', command=self.process)

        self.quit = tk.Button(self, text="QUIT", fg="red", command=root.destroy)
        self.quit.pack()

        self.gas_list = [self.gas]

    def reader():
        self.read = gas_num.read_registers(0,42)
        self.value_label.config(text=self.read[0])
        self.unit_label.config(text=self.read[1])
        self.temp_label.config(text=self.read[2])

    def process(self):
        for sen in self.gas_list:
                self.proc = Process(target=self.reader, args=(sen,))
                self.proc.start()
                self.proc.join()


if __name__ == '__main__':
        root = tk.Tk()
        app = Application()
        app.mainloop()

When I press the start button the program will freeze until the process is done. How do I correctly set up a system to have the GUI operational while having processes run?

Upvotes: 3

Views: 3526

Answers (1)

double_j
double_j

Reputation: 1706

The simplest solution would be to put this all on a separate thread. The reason your GUI freezes up is because the process method has consumed the Main Thread and until it completes, Tkinter won't update anything.

Here's a simple example:

self.timer_button = tk.Button(self, text='Start', command=lambda: threading.Thread(target=self.process).start())

However, this doesn't stop the user from clicking the button twice. You could create a new method which controls this. Example:

import os
from multiprocessing import Process
import threading
import queue
import tkinter as tk
from tkinter import *
from tkinter import ttk
import time
import time as ttt
import minimalmodbus
import serial
minimalmodbus.CLOSE_PORT_AFTER_EACH_CALL = True
THREAD_LOCK = threading.Lock()

class Application(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master)
        self.gas = minimalmodbus.Instrument('COM3', 1)
        self.gas.serial.baudrate = 9600
        self.gas.serial.parity = serial.PARITY_NONE
        self.gas.serial.bytesize = 8
        self.gas.serial.stopbits = 1
        self.gas.serial.timeout = 0.25
        self.gas.mode = minimalmodbus.MODE_RTU

        self.pack()
        self.create_widgets()

    def create_widgets(self):
        self.first_gas_labelframe = LabelFrame(self, text="Gas 1", width=100)
        self.first_gas_labelframe.pack()

        self.value_label = Label(self.first_gas_labelframe, text="Value")
        self.value_label.pack()

        self.unit_label = Label(self.first_gas_labelframe, text="Unit")
        self.unit_label.pack()

        self.temp_label = Label(self.first_gas_labelframe, text="Temp")
        self.temp_label.pack()


        self.timer_button = tk.Button(self, text='Start', command=self.start_thread)

        self.quit = tk.Button(self, text="QUIT", fg="red", command=root.destroy)
        self.quit.pack()

        self.gas_list = [self.gas]

    def check_thread(self):
        if self.thread.is_alive():
            root.after(50, self.check_thread)
        else:
            # Thread completed
            self.timer_button.config(state='normal')

    def start_thread(self):
        self.timer_button.config(state='disabled')
        self.thread = threading.Thread(target=self.process)
        self.thread.start()
        root.after(50, self.check_thread)

    def reader(self):
        self.read = gas_num.read_registers(0,42)
        self.value_label.config(text=self.read[0])
        self.unit_label.config(text=self.read[1])
        self.temp_label.config(text=self.read[2])

    def process(self):
        with THREAD_LOCK:
            for sen in self.gas_list:
                self.proc = Process(target=self.reader, args=(sen,))
                self.proc.start()
                self.proc.join()


if __name__ == '__main__':
    root = tk.Tk()
    app = Application()
    app.mainloop()

The Lock is to ensure no two threads are running at the same time. I've also disables the button so that the user can't click until it's done. By using the root.after method, you can create callbacks that wait for a period of time before running.

As far as the multiprocessing, you are running the processes on a separate process, but only one at a time. If you want to run many at one time, then you need to move the join call somewhere else. I'm not sure how many processes are running at once but you could do something like this:

processes = []
for sen in self.gas_list:
    proc = Process(target=self.reader, args=(sen,))
    processes.append(proc)
    proc.start()
[x.join() for x in processes]

In this implementation, I've removed assigning proc as a class variable.

I do not have the libraries or data to properly test all this, but it should work...

EDIT

This will initiate a pool of 6 processes that loop through self.gas_list, passing the item to self.reader. When those complete, it will check to make sure a second has gone by (waiting if it hasn't) and restarting the above process. This will run forever or until an exception is raised. You will need to import Pool from the multiprocessing module.

def process(self):
    with THREAD_LOCK:
        pool = Pool(6)
        while 1:
            start_time = time.time()
            pool.map(self.reader, self.gas_list)
            execution_time = time.time() - start_time
            if execution_time < 1:
                time.sleep(1-execution_time)

Upvotes: 2

Related Questions