5

I'm trying to write a program that gets data from a serial port connection and automatically updates the Tkinter window in real time based on that data.

I tried to create a separate thread for the window that periodically gets the current data from the main thread and updates the window, like this:

serialdata = []
data = True

class SensorThread(threading.Thread):
    def run(self):
        serial = serial.Serial('dev/tty.usbmodem1d11', 9600)
        try:
            while True:
                serialdata.append(serial.readline())
        except KeyboardInterrupt:
            serial.close()
            exit()

class GuiThread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
        self.root = Tk()
        self.lbl = Label(self.root, text="")

    def run(self):
        self.lbl(pack)
        self.lbl.after(1000, self.updateGUI)
        self.root.mainloop()

    def updateGUI(self):
        msg = "Data is True" if data else "Data is False"
        self.lbl["text"] = msg
        self.root.update()
        self.lbl.after(1000, self.updateGUI)

if __name == "__main__":
    SensorThread().start()
    GuiThread().start()

    try:
        while True:
            # A bunch of analysis that sets either data = True or data = False based on serialdata
    except KeyboardInterrupt:
        exit()

Running it gives me this error:

Exception in thread Thread-2: Traceback (most recent call last): File "/System/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/threading.py", line 522, in __bootstrap_inner self.run() File "analysis.py", line 52, in run self.lbl1.pack() File "/System/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/lib-tk/Tkinter.py", line 1764, in pack_configure + self._options(cnf, kw)) RuntimeError: main thread is not in main loop

When I google this error, I mostly get posts where people are trying to interact with the window from two different threads, but I don't think I'm doing that. Any ideas? Thanks so much!

user1363445
  • 313
  • 2
  • 3
  • 6
  • 1
    Did you try running the TK part not in a thread? Ie just run the serial port stuff in a thread and the TK stuff can stay in the main process. I suspect that might work... – Nick Craig-Wood May 13 '12 at 20:22
  • Like one thread for getting the serial port data and another thread for the data analysis loop? I'll give that a shot. – user1363445 May 13 '12 at 20:39

2 Answers2

7

Don't run the TK gui from a thread - run it from the main process. I mashed your example into something that demonstrates the principle

from time import sleep
import threading
from Tkinter import *

serialdata = []
data = True

class SensorThread(threading.Thread):
    def run(self):
        try:
            i = 0
            while True:
                serialdata.append("Hello %d" % i)
                i += 1
                sleep(1)
        except KeyboardInterrupt:
            exit()

class Gui(object):
    def __init__(self):
        self.root = Tk()
        self.lbl = Label(self.root, text="")
        self.updateGUI()
        self.readSensor()

    def run(self):
        self.lbl.pack()
        self.lbl.after(1000, self.updateGUI)
        self.root.mainloop()

    def updateGUI(self):
        msg = "Data is True" if data else "Data is False"
        self.lbl["text"] = msg
        self.root.update()
        self.lbl.after(1000, self.updateGUI)

    def readSensor(self):
        self.lbl["text"] = serialdata[-1]
        self.root.update()
        self.root.after(527, self.readSensor)

if __name__ == "__main__":
    SensorThread().start()
    Gui().run()
Nick Craig-Wood
  • 52,955
  • 12
  • 126
  • 132
  • 1
    you should use a thread-safe `Queue` object to communicate between the threads instead of using a simple list variable. – Bryan Oakley May 13 '12 at 21:26
  • 1
    That would be better yes, but my aim was to show the OP the solution to the problem, not teach them pythons IPC mechanisms ;-) – Nick Craig-Wood May 13 '12 at 22:25
2

You need to put the GUI in the main thread, and use a separate thread to poll the serial port. When you read data off of the serial port you can push it onto a Queue object.

In the main GUI thread you can set up polling to check the queue periodically, by using after to schedule the polling. Call a function which drains the queue and then calls itself with after to effectively emulate an infinite loop.

If the data that comes from the sensor comes at a fairly slow rate, and you can poll the serial port without blocking, you can do that all in the main thread -- instead of pushing and pulling from the queue, your main thread can just see if there's data available and read it if there is. You can only do this if it's possible to read without blocking, otherwise your GUI will freeze while it waits for data.

For example, you could make it work like this:

def poll_serial_port(self):
    if serial.has_data():
        data = serial.readline()
        self.lbl.configure(text=data)
    self.after(100, self.poll_serial_port)

The above will check the serial port 10 times per second, pulling one item off at a time. You'll have to adjust that for your actual data conditions of course. This assumes that you have some method like has_data that can return True if and only if a read won't block.

Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685