peterhinch / micropython-vs1053

Synchronous and asynchronous drivers for VS1053b MP3/FLAC player
MIT License
25 stars 7 forks source link

Rpi Pico #3

Open OlafM2015 opened 1 year ago

OlafM2015 commented 1 year ago

Hi, I like to use the vs1053 in combination with the Pico. Do you have recommendations before I start? Many thanks, Olaf

peterhinch commented 1 year ago

This is a combination that I haven't yet tested but it should work. The first step is getting an SD card working. It's probably best to start with the synchronous driver and MP3 files, creating a modified pbaudio_syn.py to match the RP2.

If you get this working please let me know the details and your code and I'll document it (crediting you, of course).

OlafM2015 commented 1 year ago

Hi, I have it working. Needed to change pyb module and adding the sdcard module and the pins. I managed to get I2S out of the VS1053, such that I can use a digital amp (Max98357). Up till now, I get a mp3 bitrate of max 128 kbps. I would like to achieve 192 kbps. When I look at the dreq, I see that it stays a long time high, so, it seems the VS1053 speed of data in is not enough. Furthermore, the SD card stops sometimes. Which is probably also a speed setting issue. (I tested the HW with a "standard" library, there it worked ok). So still some work to do. What would be your recommendation worth increasing the speed/ bitrate? Thanks

peterhinch commented 1 year ago

Congratulations on getting it working.

the SD card stops sometimes

That sounds serious. I suggest you ask in the forum about the SD card. It's very likely that others have got this working and would have suggestions about maximising performance. I would aim to get the SD card working as reliably and as fast as possible first, because without that you have no chance of achieving good results at higher bitrates.

OlafM2015 commented 1 year ago

Indeed, checked the SDcard HW. Made the wiring much shorter and good ground. Now it plays for hours without any error :-) at 128 kbps. In the current config, I see that the dreq is about 5 ms low and then high again. I would like to get it up to 192 kbps, I tried to double the numbers double _DATA_BAUDRATE ,_SCI_BAUDRATE , however, seems that it is more complex. Does it make sense to start experimenting? Probably I need to change also _SCI_CLOCKF. not sure. What would be your advise?

peterhinch commented 1 year ago

I'll have a look at this - it clearly makes sense for me to test and document its use with RP2. You could try overclocking the Pico - I find they work fine at 250MHz but of course this isn't guaranteed. The _DATA_BAUDRATE is the nominal rate used to read the SD card. This could be increased but it all depends on the quality of the SD card itself. However given that, in my testing, 9-10MHz was adequate for 250Kbps MP3 it really shouldn't be necessary to increase this.

It's a while since I last did anything with the VS1053b but I can't see why the RP2 shouldn't be comparable with Pyboards in this application.

Hopefully I'll have something to report by the weekend.

peterhinch commented 1 year ago

I've set up the hardware and can replicate your problem. I'll get some testgear on the case on Thursday.

OlafM2015 commented 1 year ago

For your information, tested with the pbaudio and the Adafruit_vs_1053.h . See attached pictures. (both on same HW) I have difficulty to understand fully the details of the difference. The sound is for 128 kbps ok for both. At 192 and 256 and 320 the Adafruit is ok. It would be great that the 192 would work for the pbaudio. But maybe/probably the speed of the micropython is not enough. Hopefully it can be optimized.

AdaFruit_vs1053h_128kbps AdaFruit_vs1053h_320kbps pbaudio128kbps pbaudio192kbps

OlafM2015 commented 1 year ago

By the way, we do this work for a music player for people with dementia. We are a foundation, working with volunteers to design and produce this music player. We have now produced 100 pieces, based upon the Raspberry pi Zero 2 and an interface board (with Tough interface and backlight leds). The Audio path is I2S to a 2 x Max 98357 amp and two good speakers.
As the Raspberries are not available and we want to start producing 200-300 pieces we started the Pico + VS1053 route. See for more info foundation (sorry for the Dutch) www.stichtingoradio.nl and https://www.youtube.com/watch?v=LoT91xpWNf4 We have the intention to make an Open source kit available, such that people (youngsters) can make the music player for them parents/grandparents. see www.oradio.tech However, we cannot promote this as the Rpi Zero2 is not available. The Pico + VS1053 + interface board would to the trick. Especially with the micropython code it would be a good platform to learn and further increase the Open source activities. Just a brief motivation :-) ToughInterfaceBoard1 ToughInterfaceBoard2 OradioFront )

OlafM2015 commented 1 year ago

The code I use to test:

main.zip

peterhinch commented 1 year ago

An interesting and worthwhile project. I will study this today. I need to be sure I understand exactly what you're doing.

I hadn't appreciated that you're using the asynchronous driver. I'm puzzled by the LA traces. With the ones labelled "Adafruit" are you using the Adafruit CircuitPython driver? I take it the other traces use my vs1053.py asynchronous driver.

Am I right in assuming that all traces use a Pico and the Adafruit VS1053 adaptor?

OlafM2015 commented 1 year ago

For the VS1053 I use the Adafruit Music maker, as this one has connections for the I2S of the VS1053. As I had troubles with the SD card, I used another board (the red one) with only resistors as and not the level shift buffers on the adafruit board. The LA traces are as indicated as in your pbaudio script. The Adafruit measurements are from acactly the same HW, but than with the Ardiuno lib and the IDE, in which the driver is in C(++) made. Hope that this helps IMG_0776 IMG_0775 .

peterhinch commented 1 year ago

I've had some success. I've pushed an updated vs1053.py to a pico branch.

The problem is that the play loop needs to iterate very fast. The old code uses awrite which does a buffer copy. Some time ago I submitted this uasyncio PR for a non-allocating write, but Damien has not implemented it. The solution is a hack, at least until the PR is accepted. I've also used micropython.native: the loop can now iterate in 720us as measured with micropython-monitor.

I will do some more work on this (e.g. to check whether micropython.native is doing anything useful) before merging it with the main branch, but I thought you could make use of a preview.

My test script is

from machine import SPI, Pin
from vs1053 import *
import uasyncio as asyncio

reset = Pin(15, Pin.OUT, value=1)  # Active low hardware reset
sdcs = Pin(5, Pin.OUT, value=1)  # SD card CS
xcs = Pin(14, Pin.OUT, value=1)  # Labelled CS on PCB, xcs on chip datasheet
xdcs = Pin(2, Pin.OUT, value=1)  # Data chip select xdcs in datasheet
dreq = Pin(3, Pin.IN)  # Active high data request
player = VS1053(SPI(0), reset, dreq, xdcs, xcs, sdcs, '/fc')

async def heartbeat():
    led = Pin(25, Pin.OUT)
    while(True):
        led(not(led()))
        await asyncio.sleep_ms(500)

async def main():
    player.volume(-10, -10)  # -10dB (0dB is loudest)
    locn = '/fc/192kbps/'
    asyncio.create_task(heartbeat())
    with open(locn + '01.mp3', 'rb') as f:
        await player.play(f)
    # player.mode_set(SM_EARSPEAKER_LO | SM_EARSPEAKER_HI)  # You decide.
    # player.response(bass_freq=150, bass_amp=15)  # This is extreme.
    with open(locn + '02.mp3', 'rb') as f:
        await player.play(f)
    with open(locn + '03.mp3', 'rb') as f:
        await player.play(f)
    print('All done.')

asyncio.run(main())
OlafM2015 commented 1 year ago

Great, could I test it? I am not sure how to test/ find the files? If seen that sdcard.py is updated so I uploaded it. But where can I find the vs1053.py ? (I am not so experienced with Github)

peterhinch commented 1 year ago

Scrub that - there was a problem. It seems that when you overclock the RP2 the clock frequency survives a soft reset. Consequently my testing was inadvertently done at 250MHz - so the build I pushed did not work after a power cycle.

I now have a build which does work at stock frequency, but I had to replace the StreamWriter code with a synchronous write. Tomorrow I'll do a bit more work on optimising it and push an update with instructions on how to get it.

seen that sdcard.py is updated so I uploaded it

? Not since May 2020 according to my git log.

OlafM2015 commented 1 year ago

Ok, thanks for update

peterhinch commented 1 year ago

I've now pushed an update. To access it online you can change to the pico branch using the dropdown at the top left which initially contains master. You can then copy the code. You'll also find my test scripts, pico.py being the normal one. There are also scripts that use monitor.py and logic analyser images.

An alternative way to access the code is to clone the repo to your PC and issue

$ git checkout pico

You will then find the updated code, test scripts and two logic analyser images in the images directory. These illustrate behaviour when playing a 192Kbps MP3 file. A point worth noting is that dreq spends quite a lot of time False. This is always worth checking: if it spends long periods True it probably means that the VS1053's internal buffer has underrun - distortion is likely. The images show the time taken reading the SD card, writing to the VS1053, and also the points where the driver yields to the scheduler.

I will need to do more testing before merging, notably cancellation and checking whether it can handle more demanding files such as VBR and FLAC. I'll also need to document its use with the Pico.

Please let me know how you get on.

peterhinch commented 1 year ago

Perhaps worth adding that I'm using the Adafruit 1381 card with an SD card in its slot. I've used this in all my testing including with other hosts: I don't think there is any issue with its design.

The problem is entirely down to the performance of this code (drawn from the play() method in the original version):

       while s.readinto(buf):  # Read <=32 bytes
            cnt += 1
            await sw.awrite(buf)

Use of stream writing had a problem whereby a buffer copy takes place. Even with a mod to prevent copying, it was still slower than a synchronous write. I also found it necessary to use micropython.native. With these changes it is able to play 192Kbps files with the Pico running at stock clock rate. Judging by the behaviour of dreq there is a good margin. The critical part of the revised code is:

        while s.readinto(buf):  # Read <=32 bytes
            cnt += 1
            # Yield when forced to wait or after N iterations
            while (not dreq()) or cnt > 10:
                await asyncio.sleep_ms(0)
                cnt = 0
            self.write(buf)

There are two optimisations: writing is now synchronous and the frequency of yielding to the scheduler is reduced. If it is waiting on dreq it yields. If dreq has remained True for too long (about 6ms) it yields. Margins could be improved further by increasing the cnt value at which it yields. If you have no tasks running which require low latency it could be raised to (say) 30 (~18ms). Possibly it's best viewed as a backstop to prevent .play from stalling the scheduler in the event that dreq stays True.

However .play is quite demanding: you need a good quality SD card. You also need to ensure that any other tasks you create do not use up too much CPU time. In your final code I suggest you monitor dreq to judge whether there is a good margin.

I don't think there is any merit in adjusting SPI clock rates. The docs show that there is a good margin. I checked the actual rate on the Pico and it is over 10MHz.

OlafM2015 commented 1 year ago

Great, this works perfect. :-) Did a quick check tested 192, 256 and 320. Even 320, shows a considerable margin on DREQ (see picture). Will further study and like to use the monitor.py to check the yield of the scripts. After that, I will start the design of the first proto of the HW, consisting of a Pico + VS1053 and SDcard slot (and power conditioning). With the I2S output and interfacing to the Tough PCB.
Just for now, this is a really great step for the project. pbaudioV2-320kbps

peterhinch commented 1 year ago

Excellent!

I originally used the ioread mechanism because it is the uasyncio way of doing stream I/O. You prompted me to do some measurements and figure out how to maximise speed. In this instance ioread is not ideal - abandoning it will benefit all platforms.

I pushed an update last night that fixes bugs in cancellation and the sine test - these resulted from abandoning the ioread mechanism. I have now pushed a further update which provides an enable_i2s method. I have no means of testing this so I'd be grateful if you could review and test. Run with default args to replicate your 48KHz usage.

There will be further updates, but none which will affect speed - I think I've optimised .play as far as it can be taken. I also need to look at the synchronous driver. I'll document use with the Pico and merge.

A test of a VBR MP3 file was OK at stock clock speed with dreq inactive for a good proportion of the time. I attempted to play FLAC files. It almost works if I overclock to 250MHz but dreq sometimes stays True and it eventually fails. These are very demanding with a data rate of 50% of CD audio (705Kbps).

OlafM2015 commented 1 year ago

Tested with player.enable_i2s(rate=48, mclock=False) Works excellent, much easier than all the_write_reg. See attached picture of the I2S clock at 48. I2S_CLK_48

OlafM2015 commented 1 year ago

Tried to make a version, in which a button can switch the music on and another button to switch it off (cancel). Was a bit a struggle. Used the following (not so elegant) construction. What would be your advise, to make this ? while not PlayMusic: await asyncio.sleep_ms(100) # waiting until PlayMusic == True No music with open(RandomFile, 'rb') as f: FilePlay = asyncio.create_task(player.play(f)) while not FilePlay.done() and PlayMusic: # Play Music loop until file is playied or PlayMusic is False await asyncio.sleep_ms(100) else: await player.cancel()

peterhinch commented 1 year ago

Have you seen my Switch and Pushbutton classes here? For your purposes a Switch will do. Then you might write:

from primitives import Switch
play = Switch(Pin(10, Pin.IN, Pin.PULL_UP))
play.close_func(None)  # Creates a bound Event close
can = Switch(Pin(11, Pin.IN, Pin.PULL_UP))
can.close_func(None)

async def play_loop():
   while True:
        play.close.clear()  # Ignore any pending switch closures
        await play.close.wait()  # Wait on Play switch closure
        with open(RandomFile, 'rb') as f:
            await player.play(f)

async def cancel_loop():
   while True:
        can.close.clear()
        await can.close.wait()  # Wait on Cancel switch closure
        await player.cancel()

Both these loops run forever. Obviously I haven't tested this, but I don't believe any interlocks are needed. If .cancel is run when no music is playing, nothing will happen (the driver checks for this state). Pressing "Play" while a song is playing will have no effect because the Event is cleared down after the song ends.

To keep you in the loop I'm still working on the driver with the aim of achieving FLAC file playback on the Pico. I plan to use a 1024 byte buffer. The buffer is topped up from the SD card file when dreq is False. 32 byte blocks are taken from the buffer and fed to the VS1053 when dreq is True. I believe this will deliver even faster performance.

I have pushed an update containing the current version code: there are some minor fixes and improvements you might like to grab. Ignore the pico.py test script: I was experimenting with a second SD card on the other SPI bus. It offered no benefit.

OlafM2015 commented 1 year ago

The Switch works really nice, see code below. I added a while PlayMusic loop to make that the music keeps on laying and that the 5 switches can control the stop of the loop. For a proof of concept, for now, it works excellent. Thanks for the guidenance. Next step is to monitor the yield with the monitor.py tools.

async def play_sel1_loop():
   global MusicDirPointer, MusicDir, PlayMusic 
   while True:
        play_sel1.close.clear()  # Ignore any pending switch closures
        await play_sel1.close.wait()  # Wait on Play switch closure
        PlayMusic = False
        MusicDirPointer = 0
        await player.cancel()
        PlayMusic = True
        while PlayMusic and MusicDirPointer == 0:
            RandomFile = MusicDir[MusicDirPointer] + '/' + getRandomFile(MusicDir[MusicDirPointer])
            allLedsOff()
            Led_sel1.on()
            print ("Playing",RandomFile)
            with open(RandomFile, 'rb') as f:
                await player.play(f)
peterhinch commented 1 year ago

I have now merged the pico branch and pushed an update so please use the master branch. The code is little changed from what you are running. I have removed monitored versions and test images.

I abandoned the buffered .play method: it added complexity for little benefit. The problems I was experiencing with FLAC files proved to be down to an SD card which couldn't take the pace. The Pico can play FLAC files with both the synchronous and asynchronous driver.

OlafM2015 commented 1 year ago

I use now the async v21053.py from the master branch. Running duration test to check stability. Runs excellent. Looking at dreq, with the LA, first impression is that it is also more efficient. Nice work. Will test FLAC files later. First, I like to finalize the proto HW design, as it takes 4-6 weeks to get it the components and produced.

OlafM2015 commented 1 year ago

First design of PCB: Pico + VS1053b + SD card + HW watchdog. Any recommendations/ suggestions are welcome. PCB_Design_1_0 PCB_Design_1_0_bottom PCB_Design_1_0_top

peterhinch commented 1 year ago

I haven't done any hardware design at chip level with the vs1053b so I haven't much to contribute here. A couple of general observations.

I would provide for use of micropython-monitor by bringing out to test points either a UART txd pin or three arbitrary GPIO pins (plus a gnd). That gives you the option to use monitor if the need arises.

I2S is quite sensitive to layout. I had problems using standard jumper wires to link an amplifier to I2S - these went away when I designed a PCB. I think the issue is that amplifier chips have a phase locked loop which uses the edges of the I2S clock for synchronisation, so the edges have to be quite clean. Without a schematic I can't see the location of your audio amplifier but if it's off-board you may need to take precautions.

Re the driver the changes cause problems with ESP32. These are down to a firmware bug with my use of the micropython.native decorator on a coroutine. The solution I have adopted (but not yet pushed) is to provide an option to do buffered reading. This is via an additional constructor arg. From the docs:

Optional args:

This means that your application will run unchanged with the default False value.

OlafM2015 commented 1 year ago

"I would provide for use of micropython-monitor by bringing out to test points either a UART txd pin" Just to make sure, the Txd pin is pin 1 (GPIO 0)? Can I use also another UART0 for example pin 16 ( GPIO 12)?image

Ps. In the description of monitor.py I read that pin 2 is used for Txd? How should I interpreted this? image

OlafM2015 commented 1 year ago

"I2S is quite sensitive to layout. I had problems using standard jumper wires to link an amplifier to I2S "
Indeed, good point tested in HW the effect of additional capacitive load on the line, soon it began to deform. On the reference (with RPI zero2) , I checked the signal and that is much cleaner and sharper edges. It is difficult for now to judge the reason, first check the first proto. And in the layout, I will make sure the cap load is minimised.

peterhinch commented 1 year ago

I guess this reference is ambiguous. My intention is that txd on the device under test is connected to pin 2 (GPIO 1, UART0 Rx) on the Pico running the monitor firmware. The DUT may be any hardware, and you can use any UART so the doc can't specify a pin for that. In your case where the DUT is a Pico you could use pin 1, UART0 Tx. Your image suggests other pins can be specified here, in which case bring out any legal UART Tx pin.

I'll clarify the doc.

Re I2S I was surprised by its sensitivity. It's the first time I've hit trouble using standard jumper leads on a digital signal. Mike Teachman is the expert and I did some testing with him when he was developing the I2S support. His comments on wiring are here.

[EDIT] I have now pushed V0.1.5 which includes the optional buffered mode. The docs have been updated to detail the capability of the driver improvements we have implemented.

OlafM2015 commented 1 year ago

Another subject, hope it is still ok for this thread. I tried to detect a long press (for playlist selection) using the Pushbutton . However, can not get it work. I studied describtion, probably I miss something fundamentel. See below the parts of the script

from primitives import Pushbutton
play_sel1 = Pushbutton(Pin(6, Pin.IN, Pin.PULL_UP), suppress=True)
play_sel1.press_func(None)
play_sel1.long_func(None)

async def play_sel1_long():
   print('check started') 
   while True:
        play_sel1.long.clear()  # Ignore any pending  press
        await play_sel1.long.wait()  # Wait on Play_sel1 long  press
        print('Sel1 long pressed')

async def main():
  asyncio.create_task(play_sel1_long())

  while(True):
       await asyncio.sleep_ms(100)

asyncio.run(main())
peterhinch commented 1 year ago

I've tested this on a Pyboard and it works correctly. Have you checked for electrical problems with pin 6 on your board?

OlafM2015 commented 1 year ago

Indeed, I was so focussed on SW that I forgot to check HW, mixed two lines. Works now perfect. Can detect Short and long press with this concept nice :-)

OlafM2015 commented 1 year ago

We are now further designing the Oradio with the vs1053.py, primitives and asyncio, which works all very well. A new feature would be, that the caretaker can update the music on the SDcard, such that the people with dementia can have a personalized music list. Mechanically, the removal and placement of the SDcard is not so easy (when attache do the electronics). So, it would be nice, if the SD card is accessible via the micro USB on the Pico (when the VS1053 is on hold to release the SPI bus). Would this be possible? (Did a search but could not find the appropriate direction)

peterhinch commented 1 year ago

If I understand you correctly you want to host a memory device on the Pico's USB bus. As far as I know USB host mode is not yet supported. You might want to raise a query in discussions as others may know of plans to support this.

OlafM2015 commented 1 year ago

Indeed, posted question/feature

peterhinch commented 1 year ago

Another thought. It is possible to connect an external micro-sd card socket to the Pico. Perhaps this could be mounted somewhere more accessible. I have tested the VS1053 in that mode. If this is of any interest I can supply more details.

OlafM2015 commented 1 year ago

Another thought. It is possible to connect an external micro-sd card socket to the Pico. Perhaps this could be mounted somewhere more accessible. I have tested the VS1053 in that mode. If this is of any interest I can supply more details.

That could also be practical. I am worried about the SPI bus and the long lines to get it to an external mico-sd card socket? Can give more details what you tested / have in mind?

OlafM2015 commented 1 year ago

Indeed, I was so focussed on SW that I forgot to check HW, mixed two lines. Works now perfect. Can detect Short and long press with this concept nice :-)

The long press works perfect. Now I would like to have on the same switch two "long" press levels: 1) of 250 ms to suppress unintended touches 2) of 8000 ms to select another playlist Would that be possible? How to make two instances long_func with specific long_press_ms ?

peterhinch commented 1 year ago

That would be a significant change to my driver. I'm not keen on doing this as it has never been requested before. I'm reluctant to increase the code size because many applications have multiple button instances.

Options are:

  1. Create your own fork with this change.
  2. Fake it with some asynchronous code. When you detect a long press you start a task. This records the start time and repeatedly polls the button. When it detects a button release the task quits, but before doing this it checks how long it's been running. If it's over 8000ms it sets an Event or runs a callback.
OlafM2015 commented 1 year ago

Ok clear, Followed the 2) "Fake" route, which works proper:

async def play_sel3_loop()):
   ThresholdPressbutton = 25 # 10th of ms needed to activate actions
   while True:
        n = 0
        play_sel3.press.clear()  # Ignore any pending switch closures
        await play_sel3.press.wait()  # Wait on Play switch closure
        while play_sel3.rawstate():   # loop while button pressed
            await asyncio.sleep_ms(10)
            n = n +1
            if n > ThresholdPressbutton:
               Led_sel3.on()
               OTHER ACTIONS
               break
peterhinch commented 1 year ago

[EDIT] Another option. This complete example was tested (on a Pyboard):

import uasyncio as asyncio
from primitives import Pushbutton, Delay_ms
from pyb import Pin, LED

red, green, blue = LED(1), LED(2), LED(4)
btn = Pushbutton(Pin('Y1', Pin.IN, Pin.PULL_UP))
btn.press_func(None)  # Use event interface
btn.release_func(None)

async def flash(led):
    led.on()
    await asyncio.sleep(1)
    led.off()

async def heartbeat():
    while True:
        blue.toggle()
        await asyncio.sleep_ms(200)

async def main():
    asyncio.create_task(heartbeat())
    ntim = Delay_ms(duration=1000)  # Fairly long press
    ltim = Delay_ms(duration=8000)  # Very long press
    while True:
        ltim.stop()  # Stop any running timers
        ntim.stop()
        btn.press.clear()  # Ignore any pending press
        btn.release.clear()
        await btn.press.wait()
        ntim.trigger()  # Button pressed, start timers, await release
        ltim.trigger()
        await btn.release.wait()
        # Button released: check for any running timers
        if not ltim():  # Very long press timer timed out before button was released
            asyncio.create_task(flash(red))
        elif not ntim():
            asyncio.create_task(flash(green))
        # If both timers running it was a brief press

asyncio.run(main())

This avoids the polling and should be efficient in terms of processor and scheduler usage.

If you aren't using any special features of Pushbutton and the button is normally open and wired to gnd you could use the simpler Switch class.

OlafM2015 commented 1 year ago

Thanks, getting to understand the principles a bit better with these examples, will test next week.

peterhinch commented 1 year ago

The example above works, but may not be quite what you want. This is because it waits for a button release before action occurs. The example below runs code when the timer times out. This means that a very long press will first trigger the normal long press code. The choice is yours - or you may want to adapt these.

This was tested on a Pyboard so you'll need to adapt the Pin and LED for RP2.

To run this sample you will need to update the following from primitives: __init__.py and Delay_ms.py.

import uasyncio as asyncio
from primitives import Pushbutton, Delay_ms, wait_any
from pyb import Pin, LED

red, green, blue = LED(1), LED(2), LED(4)
btn = Pushbutton(Pin('X17', Pin.IN, Pin.PULL_UP))
btn.press_func(None)  # Use event interface
btn.release_func(None)

async def flash(led):
    led.on()
    await asyncio.sleep(1)
    led.off()

async def heartbeat():
    while True:
        blue.toggle()
        await asyncio.sleep_ms(200)

async def main():
    asyncio.create_task(heartbeat())
    ntim = Delay_ms(duration=1000)  # Fairly long press
    ltim = Delay_ms(duration=8000)  # Very long press
    while True:
        ltim.stop()  # Stop any running timers and clear their event
        ntim.stop()
        await btn.press.wait()
        btn.press.clear()
        ntim.trigger()  # Button pressed, start timers, await release
        ltim.trigger()  # Run any press code
        ev = await wait_any((btn.release, ntim))
        if ev == ntim: # Normal timer timed out
            asyncio.create_task(flash(green))  # long press code
            ev = await wait_any((btn.release, ltim))
            if ev == ltim:  # Long timer timed out
                asyncio.create_task(flash(red))
        # Must await release otherwise the event is cleared before release
        # occurs, setting the release event before the next press event.
        await btn.release.wait()
        btn.release.clear()

asyncio.run(main())

The new wait_any primitive is documented here.

OlafM2015 commented 1 year ago

This is because it waits for a button release before action occurs.

Indeed, in current design , it waits untill the timer is ended and than starts the actions. This is what your new code is now doing :-). Using the delay_ms and wait_any makes nice clean code. Could the switch class also be used?

peterhinch commented 1 year ago

Yes, Switch supports the Event interface. I'd recommend it as a simpler class if your button is normally open and linked to gnd with a pullup. Note the naming of "open" and "close" rather than "press" and "release".

I am actively working on event based coding and the API for wait_any is changing. It will become a class named WaitAny and it will be called as follows

ev = await wait_any((btn.release, ltim)).wait()

The object here is to make WaitAny behave like an Event as described in the doc I referenced.

I'll let you know when I push this update.

OlafM2015 commented 1 year ago

Excellent, just from curiosity, how does these events work on lower level, are these still polled, or via interrupts ? (Sorry for this basic question) can you give some guidance what I should study to understand.

peterhinch commented 1 year ago

The WaitAll and WaitAny primitives do not use polling and use no cpu time while paused.

The drivers that access hardware Pin objects do poll. I do plan to re-examine whether soft IRQ's might make sense. In the past I have measured the overhead of polling and it is quite low unless you have a very large number of buttons. Here is a poll method:

    async def _poll(self, dt):  # Poll the button
        while True:
            if (s := self._pin() ^ self._lopen) != self._state:
                self._state = s
                self._of() if s else self._cf()
            await asyncio.sleep_ms(dt)  # Wait out bounce

In the normal state where nothing is happening the if statement will fail so the loop just keeps yielding to the scheduler.

I have now pushed an update which enables WaitAll and WaitAny primitives to be nested. These are renamed because they are now classes. I have also pushed very minimal classes for switch (ESwitch) and pushbutton (EButton) interfacing which support only the Event interface. There is a test suite here.

I haven't yet had time to properly document these additions.

You need to update the following from the primitives directory: __init__.py delay_ms.py and add events.py. The following demos your requirement. Note that the ESwitch constructor now takes an lopen arg which is the state of the switch when open or button when not pressed. As written it runs on a Pyboard and uses the usr button for input.

import uasyncio as asyncio
from primitives import Delay_ms, WaitAny, ESwitch
from pyb import Pin, LED

red, green, blue = LED(1), LED(2), LED(4)
btn = ESwitch(Pin('X17', Pin.IN, Pin.PULL_UP), lopen=0)  # Note lopen arg!

async def flash(led):
    led.on()
    await asyncio.sleep(1)
    led.off()

async def heartbeat():
    while True:
        blue.toggle()
        await asyncio.sleep_ms(200)

async def main():
    asyncio.create_task(heartbeat())
    ntim = Delay_ms(duration = 1000)  # Fairly long press
    ltim = Delay_ms(duration = 8000)  # Very long press
    while True:
        ltim.stop()  # Stop any running timers and clear their event
        ntim.stop()
        await btn.close.wait()
        btn.close.clear()
        ntim.trigger()  # Button pressed, start timers, await release
        ltim.trigger()  # Run any press code
        ev = await WaitAny((btn.open, ntim)).wait()
        if ev is ntim: # Normal timer timed out
            asyncio.create_task(flash(green))  # long press code
            ev = await WaitAny((btn.open, ltim)).wait()
            if ev is ltim:  # Long timer timed out
                asyncio.create_task(flash(red))
        # Must await release otherwise the event is cleared before release
        # occurs, setting the release event before the next press event.
        await btn.open.wait()
        btn.open.clear()

asyncio.run(main())
peterhinch commented 1 year ago

To properly answer your question the reason I poll switches and pushbuttons may be found in this article.

When a switch contact bounces it can produce voltages that do not correspond to valid logic levels. If you read a pin at that point in time, you can be confident that the Pin object will return either 1 or 0. However I don't believe you can be confident of the behaviour of an interrupt. To achieve confidence you'd need to do a lot of testing on the ever-increasing range of MicroPython hardware.

If I were designing hardware to interface a switch to a pin, and that pin was to raise an interrupt, I'd use a CR network to limit rate followed by a schmitt trigger to ensure valid logic levels.

OlafM2015 commented 1 year ago

You need to update the following from the primitives directory: __init__.py delay_ms.py and add events.py. The following demos your requirement.

Done, works excellent. See part of code below. Tested with ntim = 100 ms and ltim = 8000 ms. Does the job perfect. See also picture of LA. (Still need to restructure the variables a bit and streamline code)


from primitives import Delay_ms, WaitAny, ESwitch

play_sel3 = ESwitch(Pin(4, Pin.IN, Pin.PULL_UP), lopen=0)

async def play_sel3_ESwitch_loop():
   global MusicDirPointer, MusicDir, PlayMusic, VolAnn, ThresholdPressbutton
   ntim = Delay_ms(duration = 100)  # "short" press with some delay to supress "false" activation
   ltim = Delay_ms(duration = 8000)  # Very long press
   while True:
        ltim.stop()  # Stop any running timers and clear their event
        ntim.stop()
        await play_sel3.close.wait()
        play_sel3.close.clear()
        ntim.trigger()  # Button pressed, start timers, await release
        ltim.trigger()  # Run any press code
        ev = await WaitAny((play_sel3.open, ntim)).wait()
        if ev is ntim: # Normal timer timed out
            allLedsOff()
            Led_sel3.on()
            asyncio.create_task(play_switch_sound())  # play switch sound to confirm button press
            print('short press detected')
            MusicDirPointer = 2 
            PlayMusic = True
            ev = await WaitAny((play_sel3.open, ltim)).wait()
            if ev is ltim:  # Long timer timed out
                print('long press detected')
                asyncio.create_task(playpointer_update()) # increase MusicDirSubPointer with 1 (until >2)
        await play_sel3.open.wait()
        play_sel3.open.clear()

100ms_short_delay