Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Intermittent Python thread error, "main thread is not in main loop"

Middle aged dad (electrical engineer not programmer by trade) trying to teach my 13 year old daughter electronics and programming. So far, I love Python. I am building a program to display temperatures throughout our house with tkinter GUI and DS18B20 sensors.

We've pieced together the program below from reading books, online research and using Stack Overflow for trouble-shooting bugs (this site rocks!).

Now we're stumped, we keep getting an intermittent error, when we run the program the first time after loading idle on our Raspberry, it works fine.

The second time, and all subsequent times, we get this error message:

Traceback (most recent call last):
  File "/home/pi/Code-working-library/stackoverflow-paste.py", line 140, in <module>
    app.equipTemp.set(tempread)
  File "/usr/lib/python2.7/lib-tk/Tkinter.py", line 203, in set
    return self._tk.globalsetvar(self._name, value)
RuntimeError: main thread is not in main loop

Note, our understanding is that in order to have a static window and update labels updated temps read off our sensor (DS18B20) we needed to use a thread. The sample code we started with has the _init_ statements with only one underscore preceding and trailing - not sure why, if I add a second underscore, I get error messages. The updating window code we used as our basis came from Raspberry Pi forum

Here is our code:

from Tkinter import *
import tkFont
import os
import glob
import time
import subprocess
import re
import sys
import time
import threading
import Image 
import ImageTk

os.system('modprobe w1-gpio')
os.system('modprobe w1-therm')

#28-000005c6ba08

sensors = ['28-000005c6ba08'] 
sensors1 = ['28-000005c70f69'] 

def read_temp_raw():
    catdata = subprocess.Popen(['cat',device_file], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    out,err = catdata.communicate()
    out_decode = out.decode('utf-8')
    lines = out_decode.split('\n')
    return lines

def read_temp():
    lines = read_temp_raw()
    while lines[0].strip()[-3:] != 'YES':
        time.sleep(0.2)
        lines = read_temp_raw()
    equals_pos = lines[1].find('t=')
    if equals_pos != -1:
        temp_string = lines[1][equals_pos+2:]
        temp_c = float(temp_string) / 1000.0
        temp_f = temp_c * 9.0 / 5.0 + 32.0
        return temp_f

###########  build window  ###################

bground="grey"


class App(threading.Thread):

    def _init_(self):    
        threading.Thread._init_(self)
        self.start()

    def callback(self):
        self.root.quit()        


    def run(self):

        #Make the window
        self.root = Tk() 
        self.root.wm_title("Home Management System")
        self.root.minsize(1440,1000)

        self.equipTemp = StringVar()   
        self.equipTemp1 = StringVar()
        self.equipTemp2 = StringVar()       

        self.customFont = tkFont.Font(family="Helvetica", size=16)

        #   1st floor Image
        img = Image.open("HOUSE-PLANS-01.png") 
        photo = ImageTk.PhotoImage(img)

        Label1=Label(self.root, image=photo)
        Label1.place(x=100, y=100)

        #   2nd floor
        img2 = Image.open("HOUSE-PLANS-02.png")
        photo2 = ImageTk.PhotoImage(img2)

        Label1=Label(self.root, image=photo2)
        Label1.place(x=600, y=100)

        #   Basement image
        img3 = Image.open("HOUSE-PLANS-03.png")
        photo3 = ImageTk.PhotoImage(img3)

        Label1=Label(self.root, image=photo3)
        Label1.place(x=100, y=500)

        #   Attic Image
        img4 = Image.open("HOUSE-PLANS-04.png")
        photo4 = ImageTk.PhotoImage(img4)

        Label1=Label(self.root, image=photo4)
        Label1.place(x=600, y=500)

        #   House Isometric Image
        img5 = Image.open("house-iso.png")
        photo5 = ImageTk.PhotoImage(img5)

        Label1=Label(self.root, image=photo5)
        Label1.place(x=1080, y=130)

        #Garage Temp Label
        Label2=Label(self.root, textvariable=self.equipTemp, width=6, justify=RIGHT, font=self.customFont)
        Label2.place(x=315, y=265)



        print "start monitoring and updating the GUI"

        self.root.mainloop() #start monitoring and updating the GUI



###########  Start Loop    ###################

print "starting app"

app = App()
app.start()

print "app started"


###################  Begin ds18b20 function  ##############

while True:

    #   28-000005c6ba08
    i = "28-000005c6ba08"
    base_dir = '/sys/bus/w1/devices/'
    device_folder = glob.glob(base_dir + i)[0]
    device_file = device_folder + '/w1_slave'

    tempread=round(read_temp(),1)


    app.equipTemp.set(tempread)
    time.sleep(5)

    ##################### END ds18b20 Function  ######
like image 833
user3808752 Avatar asked Aug 17 '14 16:08

user3808752


2 Answers

You need to run the GUI code in the main thread, and your temperature reading code needs to be in the background thread. It's only safe to update the GUI in the main thread, so you can pass the temperature data you're reading from the background thread back to the main thread via a Queue, and have the main thread periodically check for data in the queue using self.root.after():

from Tkinter import *
import tkFont
import os
import glob
import time
import threading
import Image 
import Queue


def update_temp(queue):
    """ Read the temp data. This runs in a background thread. """
    while True:
        #   28-000005c6ba08
        i = "28-000005c6ba08"
        base_dir = '/sys/bus/w1/devices/'
        device_folder = glob.glob(base_dir + i)[0]
        device_file = device_folder + '/w1_slave'

        tempread=round(read_temp(),1)

        # Pass the temp back to the main thread.
        queue.put(tempread)
        time.sleep(5)

class Gui(object):
    def __init__(self, queue):
        self.queue = queue

        #Make the window
        self.root = Tk() 
        self.root.wm_title("Home Management System")
        self.root.minsize(1440,1000)

        self.equipTemp = StringVar()   
        self.equipTemp1 = StringVar()
        self.equipTemp2 = StringVar()       

        self.customFont = tkFont.Font(family="Helvetica", size=16)

        #   1st floor Image
        img = Image.open("HOUSE-PLANS-01.png") 
        photo = ImageTk.PhotoImage(img)

        Label1=Label(self.root, image=photo)
        Label1.place(x=100, y=100)

        #   2nd floor
        img2 = Image.open("HOUSE-PLANS-02.png")
        photo2 = ImageTk.PhotoImage(img2)

        Label1=Label(self.root, image=photo2)
        Label1.place(x=600, y=100)

        #   Basement image
        img3 = Image.open("HOUSE-PLANS-03.png")
        photo3 = ImageTk.PhotoImage(img3)

        Label1=Label(self.root, image=photo3)
        Label1.place(x=100, y=500)

        #   Attic Image
        img4 = Image.open("HOUSE-PLANS-04.png")
        photo4 = ImageTk.PhotoImage(img4)

        Label1=Label(self.root, image=photo4)
        Label1.place(x=600, y=500)

        #   House Isometric Image
        img5 = Image.open("house-iso.png")
        photo5 = ImageTk.PhotoImage(img5)

        Label1=Label(self.root, image=photo5)
        Label1.place(x=1080, y=130)

        #Garage Temp Label
        Label2=Label(self.root, textvariable=self.equipTemp, width=6, justify=RIGHT, font=self.customFont)
        Label2.place(x=315, y=265)

        print "start monitoring and updating the GUI"

        # Schedule read_queue to run in the main thread in one second.
        self.root.after(1000, self.read_queue)

    def read_queue(self):
        """ Check for updated temp data"""
        try:
            temp = self.queue.get_nowait()
            self.equipTemp.set(temp)
        except Queue.Empty:
            # It's ok if there's no data to read.
            # We'll just check again later.
            pass
        # Schedule read_queue again in one second.
        self.root.after(1000, self.read_queue)

if __name__ == "__main__":
    queue = Queue.Queue()
    # Start background thread to get temp data
    t = threading.Thread(target=update_temp, args=(queue,))
    t.start()
    print "starting app"
    # Build GUI object
    gui = Gui(queue)
    # Start mainloop
    gui.root.mainloop()

Edit:

After actually taking a look at the tkinter source code, as well as the Python bug tracker, it appears that unlike almost every other GUI library out there, tkinter is intended to be thread-safe, as long you run the mainloop in the main thread of the application. See the answer I added here for more info, or go straight to the resolved issue about tkinter's thread safety on the Python bug tracker here. If the tkinter source and Python's bug tracker are correct, that would mean that as long as you run the mainloop in the main thread, you can happily call gui.equipTemp.set() directly from your temperature reading thread - no Queue required. And in my testing, that did indeed work just fine.

like image 128
dano Avatar answered Nov 11 '22 07:11

dano


GUI toolkits are not threadsafe. You can only built and change your GUI from the main thread. Since reading the temperature does not take that long, you can remove all the threading code and use the after-method from Tk.

Your read_temp_raw function is very complicated:

def read_temp_raw():
    with open(device_file) as temp:
        return temp.read().split('\n')
like image 25
Daniel Avatar answered Nov 11 '22 08:11

Daniel