0

Purpose: Multi menu program that when a mode is selected it will execute that mode indefinitely within it's own loop with a visible timer of prefixed time for example 60sec. It will be used in a Raspberry Pi to control some automation. I have succeeded in making everything except the timer. I tried with tk timer, countdown, whiles and fors, with partial or no success. It's probably due to my inexperience and the fact that I'm not clear about when or where the variables are declared.

Any help is appreciated, code follows.

import tkinter as tk
from tkinter import *
import sys
import os
import time

if os.environ.get('DISPLAY','') == '':
    print('no display found. Using :0.0')
    os.environ.__setitem__('DISPLAY', ':0.0')

def mode1():
        print("Mode 1")
        #do stuff

def mode2():
        print("Mode 2")
        #do stuff

def mode3():
        print("Mode 3")
        #do stuff

master = tk.Tk()
master.attributes('-fullscreen', True)
master.title("tester")
master.geometry("800x480")

label1 = tk.Label(master, text='Choose Mode',font=30)
label1.pack()

switch_frame = tk.Frame(master)

switch_frame.pack()

switch_variable = tk.StringVar()
off_button = tk.Radiobutton(switch_frame, bg="red", text="Off", variable=switch_variable,
                            indicatoron=False, value="off", width=20, command=quit)
m1_button = tk.Radiobutton(switch_frame, selectcolor="green", text="Mode 1", variable=switch_variable,
                            indicatoron=False, value="m1", width=20, height=10, command=mode1)
m2_button = tk.Radiobutton(switch_frame, selectcolor="green", text="Mode 2", variable=switch_variable,
                            indicatoron=False, value="m2", width=20, height=10, command=mode2)
m3_button = tk.Radiobutton(switch_frame, selectcolor="green", text="Mode 3", variable=switch_variable,
                             indicatoron=False, value="m3", width=20, height=10, command=mode3)
off_button.pack(side="bottom")
m1_button.pack(side="left")
m2_button.pack(side="left")
m3_button.pack(side="left")

timertext = tk.Label(master, text="Next execution in:")
timertext.place (x=10, y=150)
#timerlabel = tk.Label(master, text=countdown)
#timerlabel.place (x=200, y=150)

master.mainloop()

I tried including this timer to my script, but instead of showing the timer in a separate window, I tried to include it in the parent window.

import tkinter as tk
from tkinter import *
import sys
import os
import time


class ExampleApp(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)
        self.label = tk.Label(self, text="", width=10)
        self.label.pack()
        self.remaining = 0
        self.countdown(10)

    def countdown(self, remaining = None):
        if remaining is not None:
            self.remaining = remaining

        if self.remaining <= 0:
            self.label.configure(text="Doing stuff!")
            self.update_idletasks()
            time.sleep(0.3)
            os.system('play -nq -t alsa synth {} sine {}'.format(0.5, 440))
            #do stuff
            self.remaining = 10
            self.countdown()
        else:
            self.label.configure(text="%d" % self.remaining)
            self.remaining = self.remaining - 1
            self.after(1000, self.countdown)


if __name__ == "__main__":
    app = ExampleApp()
    app.mainloop()
`

  • Have you tried to the time or datetime library? Typically the method is to start a loop and set a variable to the time that it started, then have the loop also get the time every time it loops into another variable, then use the difference between the two variables and compare it to a time limit, say 300ms to break the loop. – Pokebab Jul 30 '20 at 10:16
  • Hi, thanks for the reply. Would I be able to somehow show the countdown until next execution with that method? I guess with the second variable you mentioned? – John Boudouris Jul 30 '20 at 10:24
  • Take a look at this - maybe this will help you: https://www.pluralsight.com/blog/software-development/pluralsight-live-countdown-clock There are many websites out there with information regarding how to use timers this way with Python and Raspberry Pis. https://www.google.com/search?q=timers+raspberry+pi+python&rlz=1C1MSIM_enGB875GB875&oq=timers+raspberry+pi+python&aqs=chrome..69i57j0l2.6949j0j4&sourceid=chrome&ie=UTF-8 – Pokebab Jul 30 '20 at 10:35
  • Hi again, thanks for replying. I edited the original post to include a timer I tried to combine with my code, obviously with no success. I tried to make the countdown show up in the main window, thus the ```code.timertext = tk.Label(master, text="Next execution in:") timertext.place (x=10, y=150)``` My issue is not to find a timer script, but rather how to implement it in my code. – John Boudouris Jul 30 '20 at 10:59
  • I want to help but I don't feel like your code is followable. From what I gather from the first segment, you have made a window with 4 radio buttons m1,m2,m3 and off. For some reason you have chosen to use place after using the pack method for timer text. Next, in the second segment, this roughly looks like a timer script although I'm not sure using the time.sleep method is particularly nice but whatever, but I can't see that the class method ```self.after``` even exists? I think you need to go back and edit your post, with your full code, with decent comments on your code, and explain more. – Pokebab Jul 30 '20 at 12:07

1 Answers1

0

Using time.sleep is not practical within tkinter app - basically it will block the whole application rendering it unresponsive. To avoid this, one would use threading and enclose blocking parts in a thread so the tkinter's mainloop is not blocked. You can see some solutions here.

Though the tkinter's self.after can be used as another alternative - as long as the underlying tasks you want to run are as quick as possible - eg. (almost) non-blocking, it will still slowly throw your timer off but that can be worked with (using time-aware scheduler instead of countdown).

For you, this means that the play command should either return immediately or take less time to execute then your countdown (or in my example, scheduling time of one loop).

import functools
import os
import time
import tkinter as tk

BUTTON_ACTIVE_BG_COLOR = "green"
BUTTON_INACTIVE_BG_COLOR = "grey"

BASE_COMMAND = "echo {}"  # 'play -nq -t alsa synth {} sine {}'.format(0.5, 440)
CMD_LONG_BLOCKING = "sleep 5"

# mode_name, mode_command
MODES = [
    ("Mode 1", BASE_COMMAND.format("mode1")),
    ("Mode 2", BASE_COMMAND.format("mode2")),
    ("Mode 3", BASE_COMMAND.format("mode3")),
    ("Mode BLOCKING TEST", CMD_LONG_BLOCKING),
]

SCHEDULER_DELTA = 10  # run every N s


def get_next_sched_time():
    return time.time() + SCHEDULER_DELTA


class ExampleApp(tk.Tk):
    running = False
    current_mode_command = None
    current_active_button = None
    scheduler_next = time.time() + SCHEDULER_DELTA

    # helper for start/stop button to remember remaining time of last scheduler loop
    scheduler_last_remaining_time = SCHEDULER_DELTA

    def __init__(self):
        tk.Tk.__init__(self)
        self.label = tk.Label(self, text=self.running, width=10)
        self.label.pack(fill=tk.X, pady=5)

        self.create_mode_buttons()

        self.stop_button = tk.Button(
            self, text="Stopped", command=self.run_toggle, width=30, bg=None
        )
        self.stop_button.pack(pady=100)

        self.after(0, self.scheduler)

    def create_mode_buttons(self):
        for mode_name, mode_cmd in MODES:
            mode_button = tk.Button(
                self, text=mode_name, width=15, bg=BUTTON_INACTIVE_BG_COLOR
            )

            mode_button.configure(
                command=functools.partial(
                    self.button_set_mode, cmd=mode_cmd, button=mode_button
                )
            )
            mode_button.pack(pady=5)

    def run_toggle(self):
        """
        Method for toggling timer.
        """
        self.running = not self.running
        self.stop_button.configure(text="Running" if self.running else "Stopped")
        self.update_idletasks()

        if self.running:
            # False => True
            self.scheduler_next = time.time() + self.scheduler_last_remaining_time
        else:
            # True => False
            # save last remaining time
            self.scheduler_last_remaining_time = self.scheduler_next - time.time()

    def color_active_mode_button(self, last_active, active):
        if last_active:
            last_active.configure(bg=BUTTON_INACTIVE_BG_COLOR)

        active.configure(bg=BUTTON_ACTIVE_BG_COLOR)
        self.update_idletasks()

    def button_set_mode(self, cmd, button):
        """
        Method for changing the 'mode' of next execution.

        Clicking the buttons only changes what command will be run next.
        Optionally it can (should?) reset the timer.
        """
        if self.current_active_button == button:
            return

        self.color_active_mode_button(
            last_active=self.current_active_button, active=button
        )
        self.current_active_button = button
        print("Mode changed to:", button["text"])

        # self.scheduler_next = get_next_sched_time()  # Reset countdown
        self.current_mode_command = cmd

    def scheduler(self):
        if self.running:
            time_now = time.time()
            if time_now >= self.scheduler_next:
                # TIME TO RUN
                # this can block the whole app
                print("Executing mode: ", self.current_mode_command)
                os.system(self.current_mode_command)

                # Reusing the time before running the command instead of new/current `time.time()`.
                # Like this the time it took to execute that command won't interfere with scheduling.
                # If the current "now" time would be used instead, then next scheduling time(s)
                # would be additionally delayed by the time it took to execute the command.
                self.scheduler_next = time_now + SCHEDULER_DELTA

                # Be wary that the time it took to execute the command
                # should not be greater then SCHEDULER_DELTA
                # or the >scheduler won't keep up<.

            self.label.configure(
                text="Remaining: {:06.2f}".format(self.scheduler_next - time.time())
            )
            self.update_idletasks()
        # This will re"schedule the scheduler" within tkinter's mainloop.
        self.after(10, self.scheduler)


if __name__ == "__main__":
    app = ExampleApp()
    app.mainloop()

Once running, try the "Mode" with blocking sleep, once it executes the label with remaining time will stop and the UI freeze because whole tkinter app is now blocked. If this isn't enough for your use case then you will need threads.

Wereii
  • 330
  • 5
  • 16
  • Oh my, it looks like a full rewrite. It's working, but I sure need to look and understand what and why you did what you did. Thank you sir! – John Boudouris Jul 30 '20 at 15:09
  • Ok I played around with your code and I understand the blocking you are referring to. That isn't a problem in my case, the timer isn't something to be as accurate as Big Ben, it's just to give time between executions. I do have a question though, if I wanted to execute several commands for a mode, how could I syntax them? I see the execution is BASE_COMMAND.format() and the BASE_COMMAND=. I'd appreciate it If you could give an example of running let's say 3 commands, sleep 3, sleep 2 and sleep 1. Thanks again for all your effort. – John Boudouris Jul 30 '20 at 16:23
  • ok I think I got it, adding \n I can write any system commands I need. Thanks again. – John Boudouris Jul 30 '20 at 17:08
  • The BASE_COMMANDs are just string variables with the command that will be executed - look for [os.system](https://docs.python.org/3/library/os.html#os.system) in my snippet. The important part is probably just the `ExampleApp.scheduler` method, simply said the `self.after` at the end always schedules the method again but it executes the command only once the current time is **after** the delay. Different modes are only handled by exchanging the command that should be run after the delay. You are welcome. – Wereii Jul 30 '20 at 23:27
  • I'm trying to add to the program a counter that simply shows according to modes 1 2 or 3, a simple addition. If mode 1 is executed +1 to a counter, if mode 2 +2 to the counter. I'm having trouble doing it. I've tried approaching it through global IntVar or simple var and I still can't figure it out. I would appreciate any help with some small feedback to understand how it's working. Thanks. – John Boudouris Sep 10 '20 at 11:12
  • Just add `self.scheduler_next += 1` into `button_set_mode` method:) somewhere after the `if self.current_active_button ... : return` (but not into the if statement body, after it) – Wereii Sep 11 '20 at 10:26