Question

I am looking for a solution to emulate the behavior of the UI of an electronic component and the user interaction (which should be pushing buttons) with LEDs reporting an internal state of the electronic component.

I am using python and the tKinter module to do so.

My code runs and my GUI window displays correctly. However, when I push several times on buttons the behavior is not as expected.

I have 4 possible state for each LED (OFF, ON, (Blinking) SLOW, (Blinking) FAST). I have 4 buttons which can have an impact on the state. Each button has an interaction function defined in the widget class I have defined, and each of this function, once called, redefines the internal state of the widget.

In order to control the blinking of the LED, I use a single loop and the self.after( ..) function. This function is the following:

def toggleLeds(self):
    for led in [self.ledTxIP, self.ledRxIP, self.ledTxRS, self.ledRxRS, self.ledPower, self.ledRun, self.ledStatus, self.ledConfig]:
        if (((led[1] == "SLOW") and (self._FastBlinking == 0)) or (led[1] =="FAST")):
            bg = led[0].cget("background")
            bg = "green" if bg == "black" else "black"
            led[0].configure(background=bg)
        elif((led[1] == "OFF") and (self._update == 1)):
            led[0].configure(background="black")
            self._update = 0
        elif (self._update == 1):
            led[0].configure(background="green")
            self._update = 0
    self._FastBlinking = (self._FastBlinking + 1)%2
    self.update_idletasks()
    self.after(self._FastBlinkTime, self.toggleLeds)

This one is called recursively through the self.after function, and at the end of the interaction function I have defined for each button.

Here is how I have defined a single LED:

    self.ledTxIP     = [tk.Label(self, width=1, borderwidth=2, relief="groove"),"OFF"]

And here is an example of the button interaction function:

def pushMode(self):
    if (re.search("Reset",self.state) == None):
        if (self.clickModCnt == 0):
            self.state = "Status"
            self._stateTimer = int(time.gmtime()[5])
        elif (self.clickModCnt == 1):
            if(int(time.gmtime()[5]) - self._stateTimer < 3):
                self.state = "Config"
            else:
                self.state = "RunMode"
        else:
            self.state = "RunMode"
    self.clickModCnt = (self.clickModCnt + 1)%3
    self._update = 1
    self.updateLedState()

If anybody has an advice on this, it would be more than welcome.

Was it helpful?

Solution

I don't know why this didn't jump out at me sooner, but I think the problem is listed in your own question text, referring to the toggleLeds method:

This one is called recursively through the self.after function, and at the end of the interaction function I have defined for each button.

When the program initially runs, I'm assuming that you call toggleLeds somewhere to kick off the initial pattern for the LEDs. That sets up a single recursive loop via the self.after call at the end of the method. However, if you also call that same method every time you click a button to change state, you're setting up a new loop with every button click, and each new loop may or may not be in sync with your initial loop.

There are a couple ways that I can think of to handle this possible conflict. One is to avoid making new calls to toggleLeds, but that way there could be a delay between the button click and the new LED pattern. If you don't mind that delay, that's probably the best solution.

If you want the light/blink pattern to change immediately, you need to interrupt the current loop and start a new one with the new light/blink states. According to the Tkinter reference produced by New Mexico Tech, the after method:

...returns an integer “after identifier” that can be passed to the .after_cancel() method if you want to cancel the callback.

Here's how you could take advantage of that. First make sure that you're storing that identifier when calling the after method:

self.after_id = self.after(self._FastBlinkTime, self.toggleLeds)

Then change your toggleLeds method definition to accept an optional "interrupt" argument, and to cancel the existing after loop if that argument is True:

def toggleLeds(self, interrupt=False):
    if interrupt:
        self.after_cancel(self.after_id)
    # Existing code follows

Finally, pass True to that argument when calling the method after a button has been clicked:

# Existing button processing code here
self.toggleLeds(interrupt=True)

With these changes in place, each button click would cancel the current after cycle and start a new one, preventing more than one cycle from running at once, which should keep the LEDs in sync.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top