Traffic lights with a button interrupt - how would you have done it?

General discussions and questions abound development of code with MicroPython that is not hardware specific.
Target audience: MicroPython Users.
twodoctors
Posts: 19
Joined: Tue Jun 28, 2022 11:24 am

Traffic lights with a button interrupt - how would you have done it?

Post by twodoctors » Thu Jul 21, 2022 2:56 pm

Hi all,

Just playing with my Pico and practising doing some codes without looking up stuff. Thought I would tackle something "simple" like a traffic light sequence. I expanded it to a pedestrian crossing lights as well. And to make it even more complex, I added a pedestrian request button so that it will shorten the green light duration for the traffic (so pedestrian can cross).

In the video, the 1st sequence is "normal". 2nd sequence is pressing button early, but traffic still has built in 5 sec green time minimum. 3rd sequence is pressing button after the 5 seconds will immediately force a change from traffic green to traffic yellow and red.

(The kit I bought didn't have a green LED!)

https://youtu.be/ZDPbHfOmbAU

And this is my code. Not the most elegant, not OOP, but it works as intended.

Code: Select all

from machine import Pin
from utime import sleep

t_red = Pin(0, Pin.OUT)
t_yel = Pin(1, Pin.OUT)
t_grn = Pin(2, Pin.OUT)
p_red = Pin(3, Pin.OUT)
p_grn = Pin(4, Pin.OUT)
btn_irq = Pin(15, Pin.IN, Pin.PULL_UP)
button_down = False

ped = 0 #pedestrian or traffic go green
w = 1 # wait time before traffic change from green to yellow

# car traffic sequence
def traffic():
    t_red.on()
    sleep(5)
    t_yel.on()
    sleep(2)
    t_red.off()
    t_yel.off()
    t_grn.on()
    sleep(5) # minimium 5 secs green time before change to yellow
    global w
    sleep(w) # points to allow IRQ to shorten green to yellow time
    print("5")
    sleep(w)
    print("4")
    sleep(w)
    print("3")
    sleep(w)
    print("2")
    sleep(w)
    print("1")
    t_grn.off()
    t_yel.on()
    sleep(2)
    t_yel.off()
    t_red.on()
    sleep(5)
    ped = 1

# pedestrian traffic sequence
def pedes():
    p_red.off()
    p_grn.on()
    sleep(10)
    for x in range(20):
        p_grn.toggle()
        sleep(0.3)
    p_grn.off()
    p_red.on()
    global w
    w = 1
    ped = 0

# pedestrian crossing button to speed up change from traffic green to yellow and then red
def Interrupt(pin):
    print("IRQ")
    global w
    w = 0 # sleep time becomes zero during traffic green, forcing it to change to yellow and red on IRQ
    print(w)

# debounce
if btn_irq.value() == False and not button_down:
    button_down = True
    if btn.irq.value() == True and button_down:
        button_down = False
        
btn_irq.irq(trigger=Pin.IRQ_RISING, handler=Interrupt)

t_yel.off()
t_grn.off()
p_red.on()
p_grn.off()

while ped == 0:
    traffic()
    pedes()
Purely for my learning (only a 3 week old python novice). My question is, how else could I have interrupted the "sleep" between traffic green to traffic yellow? Can IRQ interrupt the loop while it is on "sleep(x)"? I can't seem to be able to manage it, hence adding multiple short "sleep" to sleep with "w" variable. I'm sure there's a more elegant way to write this code.

Thanks!

Adrian

User avatar
karfas
Posts: 193
Joined: Sat Jan 16, 2021 12:53 pm
Location: Vienna, Austria

Re: Traffic lights with a button interrupt - how would you have done it?

Post by karfas » Thu Jul 21, 2022 4:43 pm

I don't think there is an easy way to stop sleep().

However, you could create your own stoppable sleep function:

Code: Select all

stop_mysleep = 0
def mysleep(seconds)
    global stop_mysleep
    stop_mysleep = 0
    for _ in range(seconds*10)
          if stop_mysleep:
               return
          sleep(0.1)

Then, set stop_mysleep in the interrupt function.

Writing this in the subway - not tested, but you should get the idea.

A better approach to a problem like this is for sure a finite state machine (FSM). Will post a small example when I get home.
A few hours of debugging might save you from minutes of reading the documentation! :D
My repositories: https://github.com/karfas

User avatar
karfas
Posts: 193
Joined: Sat Jan 16, 2021 12:53 pm
Location: Vienna, Austria

Re: Traffic lights with a button interrupt - how would you have done it?

Post by karfas » Thu Jul 21, 2022 9:32 pm

A simple FSM approach, using only green+red.
In a real program, I would most likely create a definition of all state changes and transitions using a dictionary.

Code: Select all

# simple FSM. should be RED for 1 second, GREEN for 1/2 second.
# setting event to EVT_STOP turns everything off and ends the loop.
from time import sleep

red = True
green = False

STAT_RED = 1
STAT_GREEN = 2
STAT_END = 99

EVT_NONE = 0
EVT_STOP = 1

event = EVT_NONE
state = STAT_GREEN
count = 0
for _ in range(100): # TESTING. Would most likely be while state <> STAT_END: in a real app.
    sleep(0.1)
    count += 1
    # regardless of state, process the STOP event
    if event == EVT_STOP:
        state = STAT_END
        red = false
        green = false       
    if state == STAT_RED:
        if count > 10:
            state = STAT_GREEN
            red = False
            green = True
            count = 0
    if state == STAT_GREEN:
        if count > 5:
            state = STAT_RED
            red = True
            green = False
            count = 0
    if red:
        print("RED")
    if green:
        print("GREEN")
    if not (red or green):
        print("STOP")
        break
A few hours of debugging might save you from minutes of reading the documentation! :D
My repositories: https://github.com/karfas

twodoctors
Posts: 19
Joined: Tue Jun 28, 2022 11:24 am

Re: Traffic lights with a button interrupt - how would you have done it?

Post by twodoctors » Thu Jul 21, 2022 11:03 pm

I understand the "def sleep():" and how I could have saved lines and lines of codes if the traffic green time is long.

I don't understand your FSM approach.

Code: Select all

if event == EVT_STOP:
        state = STAT_END
        red = false
        green = false 
Is that both red and green off? Traffic lights don't behave like that. I think you are trying to teach me some new concept but I don't understand! 🤷🏻‍♂️

User avatar
karfas
Posts: 193
Joined: Sat Jan 16, 2021 12:53 pm
Location: Vienna, Austria

Re: Traffic lights with a button interrupt - how would you have done it?

Post by karfas » Fri Jul 22, 2022 7:56 am

twodoctors wrote:
Thu Jul 21, 2022 11:03 pm
Is that both red and green off? Traffic lights don't behave like that.
Well, my traffic light can be switched off :D.

Thanks for the great example of complex logic for a (perceived) simple problem.

My program above is too short to show all cases you showed in your first post (and I'm too lazy to decode all this stuff).
You might want to read the Wikipedia article on https://en.wikipedia.org/wiki/Finite-state_machine .

The concept here is:
  • each status of (all !) traffic lights is represented by a "state" of the FSM.
  • the "events" are a) the timer tick, b) your pushbutton, c) my switch-off button (maybe)
  • For each "event", in each "state", your code decides whether you switch to another state or do nothing.
  • The code "does" nothing unless a state switch happens.
I admit that this requires A LOT of states when you want to use my simple machine also for blinking yellow or blinking green, but these functions should go to some class anyway (outline below).

Writing this because I see a lot of complex and not understandable if .. else cascades.
FSMs are a great way to
- give structure to the decisions in your code
- handle things asynchronously (without the complexities required for asyncio and the like)

Code: Select all

class Light(): # can: on(seconds), blink(seconds), off(), tick()
	pass

red = Light(...)
green = Light(...)
yellow = Light(...)


STAT_RED = 1
STAT_RED_END = 2
STAT_GREEN = 3
STAT_GREEN_END = 4
...

# initial: red
state = STAT_RED
red.on(10)
green.off()
yellow.off()

def run():
	while true:
		sleep(0.1)
		for light in [red,green,yellow]:
			event = light.tick()      # switches output on/off, maybe blinks. returns true if a timed on or blink is done.
			if event:
				if state == STAT_RED:
					state = STAT_RED_END 		# a few seconds red+yellow
					red.on(3)
					yellow.on(3)
				elif state == STAT_RED_END:
					state = STAT_GREEN
					green.on(10)
				elif state == STAT_GREEN:              
					state = STAT_GREEN_END
					green.blink(5)
				elif state == STAT_GREEN_END:	# in Austria, we have green blinking....
					state = STAT_YELLOW
					yellow.on(5)
				elif ....
				
				# We only handle the event of ONE light here....
				break
A few hours of debugging might save you from minutes of reading the documentation! :D
My repositories: https://github.com/karfas

DeaD_EyE
Posts: 19
Joined: Sun Jul 17, 2022 12:57 pm
Contact:

Re: Traffic lights with a button interrupt - how would you have done it?

Post by DeaD_EyE » Fri Jul 22, 2022 11:30 am

You can cancel tasks with asyncio. It's not easy to understand for beginners because synchronous code and asynchronous code is mixed. Often it's not direct clear whether a function is a normal function or a Coroutine. You must await for Coroutine and running Tasks in background could be cancelled. This example does solve the problem with state. In this case, it just starts with Red again after PIN_RESET was first low and then high.

A class is a better for this approach instead of the function traffic_light_program.
The class can keep the state, and you don't have to use global.

Code: Select all

import gc
import uasyncio as asyncio
from machine import Pin, Signal
from micropython import const


PIN_RST = const(17)
PIN_RED = const(10)
PIN_YELLOW = const(12)
PIN_GREEN = const(13)


async def traffic_light_program(
    red: Pin,
    yellow: Pin,
    green: Pin,
    ):
    
    # initial state
    red.off()
    yellow.off()
    green.off()

    while True:
        print("Red")
        red.on()
        await asyncio.sleep(5)
        red.off()
        
        print("Yellow")
        yellow.on()
        await asyncio.sleep(1)
        yellow.off()
        
        print("Green")
        green.on()
        await asyncio.sleep(5)
        green.off()
        
        print("Yellow")
        yellow.on()
        await asyncio.sleep(1)
        yellow.off()


async def main_program():
    rst_pin = Signal(
        Pin(PIN_RST, mode=Pin.IN, pull=Pin.PULL_UP),
        invert=True,
    )
    leds = [
        Pin(x, mode=Pin.OUT, value=0)
        for x in (PIN_RED, PIN_YELLOW, PIN_GREEN)
    ]
    
    while True:
        print("Starting Task")
        task = asyncio.create_task(traffic_light_program(*leds))
        
        # wait until reset signal is True (Pin is low)
        while not rst_pin():
            await asyncio.sleep(0.1) 
        
        task.cancel()
        print("Task was cancelled")
        gc.collect()

        # wait until reset signal is False (Pin is high)
        while not rst_pin():
            await asyncio.sleep(0.1)


def main():
    asyncio.run(main_program())


if __name__ == "__main__":
    main()

Docs:

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Asynchronous programming

Post by pythoncoder » Fri Jul 22, 2022 2:30 pm

As @DeaD_EyE says, the best way to do this kind of thing is with asynchronous programming: asyncio in CPython and uasyncio in MicroPython. It allows a more scalable object oriented approach: if you write an asynchronous TrafficLight class, you can implement N of them simply by creating N instances. Each one will operate with timing independent of the others.

There is a significant learning curve, but it's well worth doing as most serious firmware is written using cooperative multi-tasking. There is a tutorial here which is specifically for MicroPython firmware applications. That doc points you to classes for interfacing switches and pushbuttons. These use asynchronous code to run callbacks. These are easier to use than interrupts. Interrupts have vital uses, but they are rarely the best way to interface switches and buttons.
Peter Hinch
Index to my micropython libraries.

User avatar
karfas
Posts: 193
Joined: Sat Jan 16, 2021 12:53 pm
Location: Vienna, Austria

Re: Traffic lights with a button interrupt - how would you have done it?

Post by karfas » Fri Jul 22, 2022 3:55 pm

pythoncoder wrote:
Fri Jul 22, 2022 2:30 pm
the best way to do this kind of thing is with asynchronous programming: asyncio
I stronly disagree, at least for this application.
asyncio is great for doing many different things in parallel, and it's definitely worth learning.

The traffic lights for a real-world crossroad don't require a gazillion of parallel "instances".
They are (at least I hope so, every time I need to cross a street :D) carefully synchronized.

In my opinion, a few functions for button/timer handling don't justify the additional effort to
- synchronize all your tasks
- understand what all these "processes" are doing right now
- decorate each end every function in sight with "async", so they may call^H^H^H^H (sorry, await) each other
- run constantly into bugs when you simply "call" (but not await) the thing.

But maybe I had used asyncio wrong in the past ?
A few hours of debugging might save you from minutes of reading the documentation! :D
My repositories: https://github.com/karfas

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

uasyncio every time.

Post by pythoncoder » Sat Jul 23, 2022 7:58 am

Each to their own.

As someone who has been doing asynchronous programming since the days of Intel 8080 assembler, cooperative multi-tasking is the natural way to write firmware. Once you have a handle on the concept it is as much part of programming as OOP. And, in my view, it is as important to learn.

Using interrupts and hardware timers is overkill for many applications. It is not scalable and introduces needless hardware dependencies. Hard ISR's introduce programming restrictions and potential concurrency issues not present in uasyncio callbacks. With uasyncio large and complex applications (by microcontroller standards) work unchanged when ported to new hardware like RP2.

The biggest benefit is that the logic of a well-written uasyncio application is easier to understand compared to a tangle of timer callbacks and ISR's.
Peter Hinch
Index to my micropython libraries.

jomas
Posts: 59
Joined: Mon Dec 25, 2017 1:48 pm
Location: Netherlands

Re: Traffic lights with a button interrupt - how would you have done it?

Post by jomas » Sat Jul 23, 2022 12:56 pm

In this example I would define the traffic light always as a couple (tlc) because that is how traffic lights work. So
one traffic light couple (tlc) is a light for the pedestrian and one for the traffic. Then it will always follow the same sequence which can be programmed in a simple while loop. No need for canceling tasks, interrupts, debouncing buttons or state machines.

for example (a bit simplified from the topic starter):

Code: Select all

import time
from machine import Pin

def tlc():
    but = Pin(4, Pin.IN, Pin.PULL_UP)
    tl = "G"
    pl = "R"
    print("Trafic: Pedestrian")
    while True:
          print(tl, pl)
          for i in range(50):
              time.sleep_ms(100)
              if but.value()==0:
                  break
          time.sleep(5)
          tl="Y"
          print(tl, pl)
          time.sleep(2)
          tl="R"
          print(tl, pl)
          time.sleep(1)
          pl="G"
          print(tl, pl)
          time.sleep(3)
          pl = "Gblink"
          print(tl, pl)
          time.sleep(1)
          pl = "R"
          print(tl, pl)
          time.sleep(1)
          tl = "G"

tlc()


But if you need more traffic light (couples) I agree with Peter that asyncio is the way to go. But even then the loop will be almost the same, no need to cancel tasks.

Post Reply