yl_
yl_

Reputation: 3

Tkinter .after runs forever and .mainloop never runs

I'm creating a python program on my RPi3 that changes the frequency of a GPIO pin depending on a Tkinter scale.

Here is my code:

import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BOARD)
from Tkinter import *
import time

freq = 1.0

master = Tk()

def update():
    period = 1.0/float(freq)

    GPIO.output(8, GPIO.HIGH)
    time.sleep(period/2.0)
    GPIO.output(8, GPIO.LOW)
    time.sleep(period/2.0)

    master.after(0, update)

scale = Scale(master, from_=1, to=20000, orient=HORIZONTAL, variable=freq)
scale.pack()

GPIO.setup(8, GPIO.OUT)

master.after(0, update)
master.mainloop()

GPIO.cleanup()

For some reason, master.after(0, update) runs forever and master.mainloop() never runs. I can tell because the scale never shows up and pin 8 turns on for half a second, then turns off for half a second, and the cycle repeats.
If I press Ctrl+C then master.after(0, update) stops running and master.mainloop() starts running, the scale appears, but nothing happens when I drag the slider left and right.

I ran the program by typing sudo python tone.py in the terminal then pressing enter.

Fix/Alternative?

Upvotes: 0

Views: 374

Answers (2)

Bryan Oakley
Bryan Oakley

Reputation: 385910

Processing events

You are making two somewhat common mistakes: you shouldn't do after(0, ...), and you shouldn't call sleep.

  1. after(0, ...) means that every time you process the event, you immediately add another event. The event loop never has a chance to process other events in the queue, including events to handle the slider, screen updates, etc.

  2. When you call sleep, the GUI does exactly that: it sleeps. While it is sleeping it can't process any events.

The solution is to use only after with a reasonable time span, and not call sleep at all.

For example:

def update():

    ...
    # set the pin high
    GPIO.output(8, GPIO.HIGH)

    # set the pin low in half the period
    master.after(period/2, GPIO.output, 8, GPIO.LOW)

    # do this again based on the period
    master.after(period, update)

Another way, if you continually want to toggle the pin every half second, would be this:

def update(value=GPIO.HIGH):
    GPIO.output(8, value)
    next_value = GPIO.LOW if value == GPIO.HIGH else GPIO.HIGH
    master.after(500, update, next_value)

Using the slider

When you use the variable attribute of the slider, the variable must be an instance of one of the special tkinter variables, such as IntVar. You will then need to call get or set to get or set the value.

For example:

freq = IntVar()
freq.set(1)

def update(value=GPIO.HIGH):
    period = 1.0/float(freq.get())
    ...

Upvotes: 1

Delioth
Delioth

Reputation: 1554

mainloop() is running, but Tkinter won't update views unless it is idle. KeyboardInterrupt will tell it to cleanup, at which point it finishes its event queue (which includes updating interface) and exits.

Solution: Give mainloop time to be idle- you really just need to change your after(0, update) to have some milliseconds inside OR tell master to .update_idletasks() to update the GUI.

A slightly better solution would be to make your high/low parts into their own functions which call each after the needed delay- sleeping in the mainloop gui is a bad idea, as your GUI cannot update whatsoever if the program is sleeping. (nor can it take input et al.) You would have millisecond frames to change input before it updated again, while using two functions that call each other after the chosen milliseconds would let you adjust the timings et al while it's waiting to flip to the other on/off.

Upvotes: 0

Related Questions