0

I'm having a function/GUI issue. I'm coding a light function that starts a routine and checks if the time is between 8am and some stop time. This routine starts at 8am and ends at that arbitrary time. The issue is that once I hit start on this routine, the GUI won't let me leave the window with that start button because it's stuck inside the timing routine. I'd really like to be able to set the timer to run in the background and then be able to leave the window of that GUI. It looks like threading is the key to doing that but I'm unsure.

I have the GUI code in a separate file, so as to simplify my work. I haven't explored any GUI interrupt tools yet.

Here is my code

import datetime
from threading import Timer
import tkinter as tk
import time

# =============================================================================
# userInput takes a formatted input and passes it back to main.
# =============================================================================
def userInput():
        try:
            startTime = datetime.datetime.strptime(input('When would you like the routine to start in HH:MM 24 hour format: '), "%H:%M").time()
            print (startTime.strftime("%H:%M"))
        except:
            print ("Please enter correct time in HHMM format")

        try:
            redTime = datetime.datetime.strptime(input('When would you to start the red shift in HH:MM 24 hour format: '), "%H:%M").time()
            print (redTime.strftime("%H:%M"))
        except:
            print ("Please enter correct time in HHMM format")

        return startTime, redTime

# =============================================================================
# timeComparator is a function which, if the user changes any settings or chooses
# start in the middle of a cycle, implements the correct routine depending on 
# where in the cycle it's in. Right now, it's being utitilized to just test.
# The actual function of this will be adjusted later.
# =============================================================================            
def timeComparator(startTimered, redTime):
    now = datetime.datetime.now().time()
    #this obtains the current time
    #if statement compares input from 
    print("The current time is: ", now)
    if (now < startTime):
        print ("hello human")
    elif (now > startTime):
        print ("hello plant")

# =============================================================================
# This routine is intended to be controlled by the user in the manual portion
# of the GUI. This receives the start time and returns secs to initiate the 
# timer.
# =============================================================================
def manualRoutine(startTime, redTime):

    now = datetime.datetime.now().time()
    nowSeconds = (((now.hour * 60) + now.minute) * 60) + now.second

    startSeconds = ((startTime.hour * 60) + startTime.minute) * 60
    secsStart = startSeconds - nowSeconds

    redSeconds = ((redTime.hour * 60) + redTime.minute) * 60
    nowSeconds = (((now.hour * 60) + now.minute) * 60) + now.second
    secsRed = redSeconds - nowSeconds

    return secsStart, secsRed

# =============================================================================
# This function references 8am and 8pm and checks to see if the current time is
# between these two time anchors. If it is, it'll implement the lights in the
# while loop. This is meant to be implemented for both the plant life and
# the automatic human routine.
# =============================================================================
def autoRoutine():
    now = datetime.datetime.now().time()
    autoStart = now.replace(hour=8, minute=0)

    stoptime = datetime.datetime.now().time()
    autoStop = stoptime.replace(hour=12, minute=29)
    #WS2812 code here that  the autoStart and autoStop

    if (now > autoStart and now < autoStop):
            keepprint = False 
#    
    while (keepprint == False):

        nowloop = datetime.datetime.now().time()
        #nowloop keeps track of the current time through each loop

        print("the lights are starting")
        time.sleep (1.0)
        if (nowloop >= autoStop):
            keepprint = True
        #this breaks the loop after the stop time

    print(autoStart.strftime("%H:%M"))

    return autoStart
# =============================================================================
# blueFade is the function call for the beginning of the day light start up and
# midday light continuity. This function will end at the end of the cycle and 
# will immediately be followed by the orangeFade() function. Also, this will 
# receive the redTime to determine when to stop the function right before the 
# red shift
# =============================================================================
def blueFade(redTime):    

    print("sanity")
    keepprint = False

    while (keepprint == False):
            nowloop = datetime.datetime.now().time()

            print("manual routine lights are on")
            time.sleep(1.0)
            if (nowloop >= redTime):
                keepprint = True

    print("the manual routine has stopped")

    #WS2812 code here
    #redTime will be used to end this code before redFade begins




# =============================================================================
# redFade is a function in where the fade from blue to a more reddish hue starts.
# Depending on when the user stated they wanted the red shift to start, this will
# begin at that time and then fade from blue, to a reddish hue, then to completely
# off. This will take exactly 30 minutes
# =============================================================================
def redFade():

    print("the red hue is being imprinted")

# =============================================================================
# Main function. Will be used to call all other functions. The 
# =============================================================================
if __name__=="__main__":

#    autoRoutine()

    startTime, redTime = userInput()

    timeComparator(startTime, redTime)

    secsBlue, secsRed = manualRoutine(startTime, redTime)

    bluelights = Timer(secsBlue, lambda: blueFade(redTime))
    redlights = Timer(secsRed, redTime)

    bluelights.start()
    redlights.start()

in the code above, I'd like to run autoRoutine() in the background as well as bluelights and redlights. Right now, if i knew how to run autoRoutine() in the back I could probably fix it up to do both blue and the redlights timer. Thanks in advance.

martineau
  • 119,623
  • 25
  • 170
  • 301
igomez
  • 39
  • 5
  • 1
    The code in your question doesn't appear to be using any GUI at all. That said, in general, GUI-frameworks are generally user event-driven, which is true of Python's `tkinter` module. See [Tkinter, executing functions over time](https://stackoverflow.com/questions/9342757/tkinter-executing-functions-over-time). Most however provide way(s) of periodically checking the status of arbitrary code executing in the "background", such as in separate threads or processes. – martineau Apr 08 '19 at 19:28
  • Yes, the GUI is implemented in another code. They're seperated for sanity sake. At the end of the day I'm just trying to save startTime and redTime – igomez Apr 08 '19 at 20:22
  • Suggest you read [Trying to fix tkinter GUI freeze-ups (using threads)](https://stackoverflow.com/questions/53525746/trying-to-fix-tkinter-gui-freeze-ups-using-threads). – martineau Apr 08 '19 at 20:53

1 Answers1

0

You run things in the background by implementing a threading.Thread() that gets passed whatever arguments are needed. Most the time I find that a while True: loop works best so the thread will run forever, but there are some instances you may want to .join() the thread back into your main execution flow. I usually do this with while some_global_variable == 1 and then you can edit it from outside the function, or just wait for the thread to reach the end of the assigned function on its own. The main flow will look something like this:

import threading # standard library, no pip-install
import tkinter as tk
import time

start = time.time()

win = tk.Tk()
label = tk.Label()
label.grid()

def worker_function():
    while True:
        now = time.time()
        elapsed = now - start
        label.configure(text='{:5.2}'.format(elapsed))

t = threading.Thread(target=worker_function, args=[])
t.daemon = True
t.start() # our thread will run until the end of the assigned function (worker_function here)
# this will never end because there is a while True: within worker_function

win.mainloop()

Additionally you could supply arguments to your thread within the [] provided (it needs a list even if there is only 1 argument). So if our function was:

def worker_function(a, b, c, d):

Our thread would be created as:

t = threading.Thread(target=worker_function, args=[a, b, c, d])

Also note it is target=worker_function and not target=worker_function()

Reedinationer
  • 5,661
  • 1
  • 12
  • 33
  • would threading fix the storing variables issue even if I exit the window that implements the function and variable storage, and go to a different window? – igomez Apr 08 '19 at 20:30
  • @igomez that depends on your implementation. It's likely that yes it would solve it, since once the variables are passed to your thread it won't forget them until it reaches the end of its assigned function (never in a `while True` loop). However, if your thread is trying to alter a window that no longer exists you will encounter an exception! – Reedinationer Apr 08 '19 at 20:36
  • In general—with a more realistic GUI with more than one widget—this approach will likely _not_ work because `tkinter` doesn't support multithreading in the sense that only the one thread (usually the main one) can update the GUI. To use multithreading, some other means of inter-thread communication must be used, such as a `Queue` along with the `tkinter` widget `after()` method to periodically check its contents (and possibly update the GUI if necessary). – martineau Apr 08 '19 at 20:45
  • From a strictly thread programming point-of-view, to allow the main thread to exit while the `worker_function` thread is still running, you should set `t.daemon = True` before calling `t.start()`. – martineau Apr 08 '19 at 20:49
  • @martineau good point about being a daemon. I said it would solve his problem because it sounded to me like he wanted the thread to run when the window was closed, which I interpreted as OP not wanting to update a window from the thread. You are correct though that only the main thread can update the GUI. However, it is my understanding that each thread created from the main thread counts as a main thread, and only when a new *process* is involved do `Queue`'s become necessary...or at least that's when I've encountered those problems. – Reedinationer Apr 08 '19 at 20:54
  • Reedinationer: Since threads in Python for the most part don't really run concurrently (due to the GIL), you may be able to get away with using them is some cases. However that's not universally true depending on exactly what the thread(s) are doing. – martineau Apr 08 '19 at 20:59