Non-blocking TM1697 countdown

The official pyboard running MicroPython.
This is the reference design and main target board for MicroPython.
You can buy one at the store.
Target audience: Users with a pyboard.
Post Reply
oracledude
Posts: 4
Joined: Thu Mar 26, 2020 1:09 pm

Non-blocking TM1697 countdown

Post by oracledude » Thu Mar 26, 2020 1:27 pm

I'm hoping to use a PyBoard as a replacement air hockey table scoreboard. The plan is to have a 4x7seg clock countdown while a couple photo resistors sitting behind the goal lines look for light decreases, signalling a goal. I'm using the TM1697 library to drive the display successfully, and can detect the goals when testing. The problem I'm having is that it's all in a single loop, so it misses any fast dips of the sensors. I've added in several 'checks' to prevent false positives, but I'm sure there is a better way to do the whole thing.

I was originally trying to use uasync to multi-thread the countdown, but had difficulty implementing it. This is my first real attempt at Python, minus a shell script or two.

Is there a way to have one of the onboard timers handle the countdown in a non-blocking fashion, so I can check the other sensors reliably. Eventually, I want to add a relay and control the fan, starting up with game start and turning it off at 0:00.

Code: Select all

from machine import Pin, Timer
import tm1637
import utime as t

#i2c for tm1637
Y1 = pyb.Pin.board.Y1
Y2 = pyb.Pin.board.Y2
tm = tm1637.TM1637(clk = Y1, dio = Y2)

#select button
X1 = pyb.Pin.board.X1
button = pyb.Pin(X1, pyb.Pin.IN)

#goal lines (use ADC to limit photoresistors to 64k)
X2 = pyb.Pin.board.X2
X3 = pyb.Pin.board.X3
goal1 = machine.ADC(X2)
goal2 = machine.ADC(X3)
# minimum ambient sensor value for goal detection (<64k)
g1threshold = 64*1024
g2threshold = 64*1024

# number of seconds to not allow goals, to avoid false positives
goalDelay = 3
 
goalTime = 0

# clock 
startClock = False
min = 0
sec = 5
enableColon = True    

while True:
    print("waiting for button press")
    if not button.value() and not startClock:  
        endTime = 60 * min + sec + t.time()
        print("Button press")
        startClock = True
        # set thresholds at 10% drop.
        g1threshold = int(goal1.read_u16() * .90)
        if (g1threshold == 0):
            showError("ERR INIT 1")
        g2threshold = int(goal2.read_u16() * .90)
        if (g2threshold == 0):
            showError("ERR INIT 2")
        break
    t.sleep_ms(250)
    
while True:
    score1 = score2 = 0
    while startClock:
        # update the 4x7seg with the time remaining
        min = abs(int((endTime - t.time()) / 60))
        sec = (endTime - t.time()) % 60
        if (sec % 5 == 3):
            print("Update Scoreboard")
            tm.numbers(score1, score2)
        print(str(min), str(sec), sep=':' )
        enableColon = not enableColon  # alternately blink the colon
        tm.numbers(min, sec, colon = enableColon)
        #### Rethink this - don't score if just scored
        if (int(goal1.read_u16()) < g1threshold) 
            # prevent false positive if both sensors trigger or either report zero
            if (int(goal2.read_u16()) < g2threshold):
                showError("S2 PRB")
            if (int(goal1.read_u16() == 0):
                showError("S1 ERR 0")
            if (int(goal2.read_u16() == 0):
                showError("S2 ERR 0")
            if t.time() > (goalTime + goalDelay):
                goalTime = t.time()
                print("Player 1 Scored")
                score1 += 1
        if (int(goal2.read_u16()) < g2threshold):
            # prevent false positive if both sensors trigger or either report zero
            if (int(goal1.read_u16()) < g1threshold):
                showError("S1 PRB")
            if (int(goal1.read_u16() == 0):
                showError("S1 ERR 0")
            if (int(goal2.read_u16() == 0):
                showError("S2 ERR 0")
            if t.time() > (goalTime + goalDelay):
                goalTime = t.time()
                print("Player 2 Scored")
                score2 += 1
                
        if (min + sec == 0):  # once both reach zero, turn off relay & break  
            tm.numbers(score1, score2)
            t.sleep(3)
            tm.numbers(0, 00)
            print("**************")
            print(score1, score2, sep=':')
            print("GAME OVER")
            startClock = False
            break
        t.sleep_us(100)
    break

def showError(string)
    print(string)
    tm.scroll(string, delay=250)
    exit()  

oracledude
Posts: 4
Joined: Thu Mar 26, 2020 1:09 pm

Re: Non-blocking TM1697 countdown

Post by oracledude » Tue Mar 31, 2020 5:38 am

Am I not asking the right questions here or is there somewhere else I can look? Reddit pointed me to Discord, Discord pointed me here.

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

Re: Non-blocking TM1697 countdown

Post by pythoncoder » Tue Mar 31, 2020 9:13 am

It's the right place to ask, but maybe the wrong type of question. People are busy and may not have the time to trawl through reams of code. I think you need to quantify what you're trying to detect. How long are the pulses? You could measure them with a simple test program. This would guide us towards a solution. It might also help if you pointed us at some data for the TM1697. Googling it produced nothing useful.
Peter Hinch

User avatar
jimmo
Posts: 1440
Joined: Tue Aug 08, 2017 1:57 am
Location: Sydney, Australia
Contact:

Re: Non-blocking TM1697 countdown

Post by jimmo » Tue Mar 31, 2020 12:27 pm

Yes, sorry about the delay. Exactly what Peter said. I try and get to all the tricky ones eventually (you should see how many forum tabs I have queued up).

This is a tricky problem -- that display driver by it's nature is going to be blocking because it has to bit bang the data and sleep between each bit. (I'm not 100% sure why it can't use SPI, but haven't read the datasheet in enough detail).

I kind of wonder if the answer might be to solve this electronically rather than in software. Can you build a simple comparator circuit for the light sensors, giving you a nice binary signal. (LM393 or something similar). Then you can even have a potentiometer as the comparator reference allowing for easy adjustment.

That way you can use a simple edge-triggered interrupt on the software side rather than having to poll the ADC.

oracledude
Posts: 4
Joined: Thu Mar 26, 2020 1:09 pm

Re: Non-blocking TM1697 countdown

Post by oracledude » Tue Mar 31, 2020 5:51 pm

I'll handle the input comparator and the display output later, no problem. I just need help at this point getting a timer working in its own thread. I'll settle for a print("start") and print("end") as long as I can do anything else and not interrupt the timer.

I keep seeing the examples that do tick, sleep, tock, but that locks up the processor during the sleep call, right?

pseudo:
start async_timer
while time > 0
call sub 1
call sub 2
if time = 0 call sub 3

User avatar
jimmo
Posts: 1440
Joined: Tue Aug 08, 2017 1:57 am
Location: Sydney, Australia
Contact:

Re: Non-blocking TM1697 countdown

Post by jimmo » Tue Mar 31, 2020 10:52 pm

You can still handle interrupts while sleeping. And you can sleep in a thread and other threads will continue to execute. (Although I don't really recommend using threads on (Micro)Python)

This is why asyncio is so good. You can use "await asyncio.sleep(...)" and your other tasks will execute in the background.

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

Re: Non-blocking TM1697 countdown

Post by pythoncoder » Wed Apr 01, 2020 8:44 am

jimmo wrote:
Tue Mar 31, 2020 10:52 pm
... (Although I don't really recommend using threads on (Micro)Python)...
I'd extend that to anywhere, unless there is absolutely no choice. See https://glyph.twistedmatrix.com/2014/02/unyielding.html for an eloquent exposition of the horrors of threading. Then use uasyncio.

Although it's not entirely without merit. I used to earn good money tracking down the "once per blue moon" and "3am on a Tuesday when there's an R in the month but only on site" bugs in multi threaded, multi processor systems. ;)
Peter Hinch

oracledude
Posts: 4
Joined: Thu Mar 26, 2020 1:09 pm

Re: Non-blocking TM1697 countdown

Post by oracledude » Wed Apr 01, 2020 2:24 pm

(u)asyncio is the precise root cause of this topic. I wasn't getting any traction on StackOverflow, Reddit, or Discord, so I switched vernaculars and asked about non-blocking timers/threads instead.

On March 20, I posted a uasyncio q on Stack (https://stackoverflow.com/questions/607 ... -a-pyboard) and was only redirected here.

I couldn't figure out where to do the async calls properly.

this is a snip of my original code.

Code: Select all

........
async def countdown():
    print("started countdown")
    # init min/sec to any int > 0
    min = 5
    sec = 0
    endTime = 60 * min + sec + utime.time()
    enableColon = True        
    while True:
        # update the 4x7seg with the time remaining
        print("Hit while True")
        min = abs(int((endTime - utime.time()) / 60))
        sec = (endTime - utime.time()) % 60
        print(str(min), str(sec), sep=':' )
        enableColon = not enableColon  # alternately blink the colon
        tm.numbers(min, sec, colon = enableColon)
        if(min + sec == 0):  # once both reach zero, turn off relay & break  
            ## stopFanRelay()
            print("MinSec0 Hit")
            break
        await uasyncio.sleep(1)
.......
# start time via select button
while True:
    # Only run the clock once, regardless of button presses
    if not button.value() and not startClock:  # Start
        print("Button press")
        startClock = True
        ## startFanRelay()
        loop = uasyncio.get_event_loop()
        print("get_event_loop")
        #loop.create_task(countdown())
        print("create_task")
        loop.run_until_complete(countdown()) #??
        print("run_until_complete")
        break

### thread countdown clock 
#loop = uasyncio.get_event_loop()  #??
#loop.create_task(countdown())   #??

User avatar
jimmo
Posts: 1440
Joined: Tue Aug 08, 2017 1:57 am
Location: Sydney, Australia
Contact:

Re: Non-blocking TM1697 countdown

Post by jimmo » Wed Apr 01, 2020 2:49 pm

Have you seen Peter's (@pythoncoder) tutorial at https://github.com/peterhinch/micropyth ... UTORIAL.md

The code you posted to StackOverflow won't work because it never runs the asyncio loop. Looks like you've fixed that in the version you've posted here. Also that asyncio.sleep() is in seconds not milliseconds.

It looks like you're doing it right though? What is the issue you see with that code.

Use asyncio.create_task(foo()) to start a background task
Use await foo() to run something "synchronously"

I'd be clearer if you just had a single "main" task, started in one place, rather than restarting the event loop in another outer loop.

Code: Select all

async def main():
  while True:
    if ... some button state ? ..:
      await countdown()
      asyncio.get_event_loop().create_task(background_stuff())
      await play_game()

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

Post Reply