Interrupting a flashing LED program

My daughter gave me the LED flashing Xmas tree HAT for RPi for XMas!
I have set up a number of continuous flashing programs under control of a tkinter button interface. This works fine, but I want to be able to switch the programs with a button click; currently each of the programs run until completion (e.g. five cycles of the flashing combination). Rather I would like to leave the thing flashing on one program, and change to another on a button click.
Is there a way to interrupt a flashing program by a button click on a different button??
ChatGPT says I have to use threading!? This would be an entirely new skill, and looks challengingā€¦.
Thoughts?

2 Likes

Hey @David197028,

Would it be acceptable to have the program change after it finishes the current loop? Adding interrupt checks to the code would be how I would handle this. This would introduce a small but hopefully not noticeable delay and is much simpler than implementing threading.

Would you be able to share the current code you are using or link to it so we can get a better idea of the programs you are wanting to switch between?

1 Like

Sure - Iā€™ve just rejigged it to create a new object-oriented format, but the annotations should be clear enough.
Thanks, David

import tkinter
import threading
import time
import random
from colorzero import Color, Hue
from tree import RGBXmasTree

COLOURS = [Color('green'), Color('red'), Color('blue'), Color('orange')]

window = tkinter.Tk()


class FlashingProgram:
	def __init__(self, name, present_colour):
		self.name = name
		self.running = False
		self.present_colour = present_colour
		
	def cycle(self):
		global tree
		#get the next colour
		colour_now = COLOURS.index(self.present_colour)
		colour_now += 1
		if colour_now == 4:
			colour_now = 0
		self.present_colour = COLOURS[colour_now]
		tree.color = self.present_colour
		step = 1
		bright=0
		for counter in range (0,21): #fluctuates the brightness once
			tree.brightness = bright/10
			bright += step
			if bright >10:
				step = -step
				bright = 9
			elif bright <0:
				step = -step
				bright = 0
			time.sleep(0.01)
			
	def random_sparkles(self):
		global tree
		tree.brightness = 0.1
		pixel = random.choice(tree)
		r = random.random()
		g = random.random()
		b = random.random()
		pixel.color = (r, g, b)
		
	def random_colours(self):
		global tree
		tree.brightness = 0.1
		pixel = random.choice(tree)
		pixel.color = random.choice(COLOURS)
	
	def hue_cycle(self):
		global tree
		tree.brightness = 0.1
		self.present_colour += Hue(deg=1)
		tree.color = self.present_colour
		
	def start(self):
		self.running = True
		while self.running:
			if self.name == "cycle":
				#do one interation of the flassh program:
				self.cycle()
				#check if button has been clicked; if so, return
			if self.name == "random_sparkles":
				#do one interation of the flassh program:
				self.random_sparkles()
				#check if button has been clicked; if so, return
			if self.name == "random_colours":
				#do one interation of the flassh program:
				self.random_colours()
				#check if button has been clicked; if so, return
			if self.name == "hue_cycle":
				#do one interation of the flassh program:
				self.hue_cycle()
				#check if button has been clicked; if so, return
		else:
			return

def set_flag():
	#Maybe here would go an interrupt flag?
	#Probably to take a different arg from each button?
	flash_state_lbl.config(text = "Clicked")
	print("Clicked")
	window.update()


tree = RGBXmasTree()

#Initiate the various flashing sequence objects
flasher1 = FlashingProgram("cycle", Color('red'))
flasher2 = FlashingProgram("random_sparkles", None)
flasher3 = FlashingProgram("random_colours", None)
flasher4 = FlashingProgram("hue_cycle", Color('red'))

#Buttons setup
cycle_btn = tkinter.Button(window, text="Cycle", command=set_flag)
cycle_btn.grid(row=0, column=0)
random_colours_btn = tkinter.Button(window, text="Rndom colours", command = set_flag)
random_colours_btn.grid(row=0, column=1)
hue_cycle_btn = tkinter.Button(window, text="Hue cycle", command=set_flag)
hue_cycle_btn.grid(row=0, column=2)
random_sparkles_btn = tkinter.Button(window, text="Random sparkles", command=set_flag)
random_sparkles_btn.grid(row=0, column=3)

#Label setup
flash_state_lbl = tkinter.Label(window, text="Off")
flash_state_lbl.grid(row=1, column=0, columnspan=4)

window.update()

try:
    flasher3.start()
except KeyboardInterrupt:
    tree.off()
    tree.close()

window.mainloop()
2 Likes

Hey @David197028,

I would implement a global ā€œhas the button been pressedā€ variable and put an if statement check for this inside of the for loop in the cycle function.

This will give you some kind of a delay but this way it will only be the delay associated with one brightness fluctuation cycle instead of the entire functions duration.

1 Like

I think that would work for a physical button, but Iā€™m not sure how to implement if for a tkinter button. Whilst the loop is continuously looping (including checking for a button press), it is not monitoring the buttons to change the state of the ā€œhas the button been pressedā€ variable.

For a tkinter button the procedure is the same, except that the global variable is set in the method you construct for the button to call into whenever the button press is detected. Using the example provided, it would look something like this:

from tkinter import *
top = Tk()
pressed = false
top.geometry("100x100")
def helloCallBack():
   pressed = True
B = Button(top, text ="Hello", command = helloCallBack)
B.place(x=50,y=50)
top.mainloop()
  ...
  if pressed = True:
    [change the pattern]
    pressed = False
 ...
1 Like

Thanks Jeff. I gave that a go, but while the program is busy running the flashing cycle, the button press is not registered and the ā€˜pressedā€™ variable (equivalent) is not changed, so I canā€™t find a way to exit the loop.

Hi @David197028,

If you are feeling up to attempting this project using threading the following code should work as a scaffold to set up your lighting functions so that they change quickly on the button press.

import tkinter
import threading
import time
import random
from colorzero import Color, Hue
from tree import RGBXmasTree

COLOURS = [Color('green'), Color('red'), Color('blue'), Color('orange')]

window = tkinter.Tk()

class FlashingProgram:
    def __init__(self, name, present_colour=None):
        self.name = name
        self.running = False
        self.present_colour = present_colour
        self.thread = None

    def start(self):
        self.running = True
        self.thread = threading.Thread(target=self.run)
        self.thread.start()

    def stop(self):
        self.running = False
        if self.thread is not None:
            self.thread.join()

    def run(self):
        while self.running:
            if self.name == "cycle":
                self.cycle()
            elif self.name == "random_sparkles":
                self.random_sparkles()
            elif self.name == "random_colours":
                self.random_colours()
            elif self.name == "hue_cycle":
                self.hue_cycle()
        self.thread = None

    def cycle(self):
        global tree
        # Lighting changes for cycle
        time.sleep(0.01)

    def random_sparkles(self):
        global tree
        # Lighting changes for random_sparkles
        time.sleep(0.01) 

    def random_colours(self):
        global tree
        # Lighting changes for random_colours
        time.sleep(0.01) 

    def hue_cycle(self):
        global tree
        # Lighting changes for hue_cycle
        time.sleep(0.01) 


def set_flag(flasher):
    global running_flasher
    if running_flasher:
        running_flasher.stop()
    flasher.start()
    running_flasher = flasher

tree = RGBXmasTree()
running_flasher = None

flasher1 = FlashingProgram("cycle", Color('red'))
flasher2 = FlashingProgram("random_sparkles")
flasher3 = FlashingProgram("random_colours")
flasher4 = FlashingProgram("hue_cycle", Color('red'))

cycle_btn = tkinter.Button(window, text="Cycle", command=lambda: set_flag(flasher1))
cycle_btn.grid(row=0, column=0)

random_colours_btn = tkinter.Button(window, text="Random colours", command=lambda: set_flag(flasher2))
random_colours_btn.grid(row=0, column=1)

hue_cycle_btn = tkinter.Button(window, text="Hue cycle", command=lambda: set_flag(flasher3))
hue_cycle_btn.grid(row=0, column=2)

random_sparkles_btn = tkinter.Button(window, text="Random sparkles", command=lambda: set_flag(flasher4))
random_sparkles_btn.grid(row=0, column=3)

flash_state_lbl = tkinter.Label(window, text="Off")
flash_state_lbl.grid(row=1, column=0, columnspan=4)

try:
    window.mainloop()
finally:
    if running_flasher:
        running_flasher.stop()
    tree.off()
    tree.close()

I havenā€™t been able to test this code with a physical setup that matches yours so it may need some modification to work for you. I also havenā€™t added any particular lighting patterns into the flashing programs.

Hopefully, this gives you some idea of how threading could be implemented into this project!

Let me know how you go or if this code gives you any weird errors. :slight_smile:

1 Like

Have you determined whether the problem is that the press is not registered, or the variable is not tested? To confirm that the button press is registered you would put a message box in helloCallBack. To determine the state of the variable you would print it at the start of the loop.

(I presume you noted my syntax error - if Iā€™m not typing semicolons I regress :face_with_diagonal_mouth:)

1 Like

Sam, that is so brilliant! Thanks - Iā€™ll have to spend hours working through how it works, but it absolutely works brilliantly.
Have made a few cosmetic changes, but for any interested, hereā€™s the final code:

import tkinter
import threading
import time
import random
from colorzero import Color, Hue
from tree import RGBXmasTree

COLOURS = [Color('green'), Color('red'), Color('blue'), Color('orange')]
BTN_WIDTH=15
BTN_HEIGHT=2
FONT=("Arial", 18, "bold")

window = tkinter.Tk()
window.title("Christmas tree lights")
window.geometry("900x600")

class FlashingProgram:
    def __init__(self, name, present_colour=None):
        self.name = name
        self.running = False
        self.present_colour = present_colour
        self.thread = None

    def start(self):
        self.running = True
        self.thread = threading.Thread(target=self.run)
        self.thread.start()

    def stop(self):
        self.running = False
        if self.thread is not None:
            self.thread.join()

    def run(self):
        while self.running:
            if self.name == "cycle":
                self.cycle()
            elif self.name == "random_sparkles":
                self.random_sparkles()
            elif self.name == "random_colours":
                self.random_colours()
            elif self.name == "hue_cycle":
                self.hue_cycle()
            elif self.name=="Off":
                pass #tree has already been turned off
        self.thread = None

    def cycle(self):
        global tree
        #get the next colour
        colour_now = COLOURS.index(self.present_colour)
        colour_now += 1
        if colour_now == 4:
            colour_now = 0
        self.present_colour = COLOURS[colour_now]
        tree.color = self.present_colour
        step = 1
        bright=0
        for counter in range (0,21): #fluctuates the brightness once
            tree.brightness = bright/10
            bright += step
            if bright >10:
                step = -step
                bright = 9
            elif bright <0:
                step = -step
                bright = 0
            time.sleep(0.01)
            
    def random_sparkles(self):
        global tree
        tree.brightness = 0.1
        pixel = random.choice(tree)
        r = random.random()
        g = random.random()
        b = random.random()
        pixel.color = (r, g, b)

    def random_colours(self):
        global tree
        tree.brightness = 0.1
        pixel = random.choice(tree)
        pixel.color = random.choice(COLOURS)

    def hue_cycle(self):
        global tree
        tree.brightness = 0.1
        self.present_colour += Hue(deg=1)
        tree.color = self.present_colour



def set_flag(flasher):
    global running_flasher
    if running_flasher:
        running_flasher.stop()
    #Clear tree lights
    global tree
    tree.color = (0, 0, 0)
    #Update label
    if flasher==flasher1:
        label_txt = "Cycle"
    elif flasher==flasher2:
        label_txt = "Random colours"
    elif flasher==flasher3:
        label_txt = "Random sparkles"
    elif flasher==flasher4:
        label_txt = "Hue cycle"
    elif flasher==flasher0:
        label_txt = "Off"
    flash_state_lbl.config(text=label_txt)
    #Start flasher
    flasher.start()
    running_flasher = flasher



tree = RGBXmasTree()
running_flasher = None

flasher0 = FlashingProgram("Off")
flasher1 = FlashingProgram("cycle", Color('red'))
flasher2 = FlashingProgram("random_colours")
flasher3 = FlashingProgram("random_sparkles")
flasher4 = FlashingProgram("hue_cycle", Color('red'))


off_btn = tkinter.Button(window, text="Off", width=BTN_WIDTH, height=BTN_HEIGHT, font=FONT, command=lambda: set_flag(flasher0))
off_btn.grid(row=0, column=0)
cycle_btn = tkinter.Button(window, text="Cycle", width=BTN_WIDTH, height=BTN_HEIGHT, font=FONT, command=lambda: set_flag(flasher1))
cycle_btn.grid(row=1, column=0)
random_colours_btn = tkinter.Button(window, text="Random colours", width=BTN_WIDTH, height=BTN_HEIGHT,font=FONT, command=lambda: set_flag(flasher2))
random_colours_btn.grid(row=1, column=1)
hue_cycle_btn = tkinter.Button(window, text="Hue cycle", width=BTN_WIDTH, height=BTN_HEIGHT,font=FONT, command=lambda: set_flag(flasher4))
hue_cycle_btn.grid(row=1, column=2)
random_sparkles_btn = tkinter.Button(window, text="Random sparkles", width=BTN_WIDTH, height=BTN_HEIGHT,font=FONT, command=lambda: set_flag(flasher3))
random_sparkles_btn.grid(row=1, column=3)
flash_state_lbl = tkinter.Label(window, text="Off", width=BTN_WIDTH, height=BTN_HEIGHT, font=FONT)
flash_state_lbl.grid(row=2, column=0, columnspan=4)

try:
    window.mainloop()
finally:
    if running_flasher:
        running_flasher.stop()
    tree.off()
    tree.close()

1 Like

Hi @David197028

Great to hear that you got it all working!

1 Like