Add realtime synchronisation option.

pull/8/head
Peter Hinch 2021-10-23 09:56:24 +01:00
rodzic dcd907c69b
commit 77b5c7203e
4 zmienionych plików z 371 dodań i 9 usunięć

Wyświetl plik

@ -95,7 +95,8 @@ there is a workround if it's impossible to upgrade. See
4.2 [Constructor](./README.md#42-constructor)
4.3 [Callback methods](./README.md#43-callback-methods) Methods which run in response to events.
4.4 [Method](./README.md#44-method) Optional interface to uasyncio code.
4.5 [Usage](./README.md#45-usage) Accessing data created in a screen.
4.5 [Bound variable](./README.md#45-bound-variable)
4.6 [Usage](./README.md#46-usage) Accessing data created in a screen.
5. [Window class](./README.md#5-window-class)
5.1 [Constructor](./README.md#51-constructor)
5.2 [Class method](./README.md#52-class-method)
@ -130,6 +131,7 @@ there is a workround if it's impossible to upgrade. See
     23.3.2 [Class PolarCurve](./README.md#2332-class-polarcurve)
23.4 [Class TSequence](./README.md#234-class-tsequence) Plotting realtime, time sequential data.
24. [Old firmware](./README.md#24-old-firmware) For users of color displays who can't run current firmware.
25. [Realtime applications](./README.md#25-realtime-applications) Accommodating tasks requiring fast RT performance.
[Appendix 1 Application design](./README.md#appendix-1-application-design) Tab order, button layout, encoder interface, use of graphics primitives
# 1. Basic concepts
@ -866,7 +868,14 @@ base screen are cancelled.
For finer control, applications can ignore this method and handle cancellation
explicitly in code.
## 4.5 Usage
## 4.5 Bound variable
* `pause_ms=0` Screen refreshes are performed by a looping task. This
refreshes the screen before pausing. The default of 0ms allows other tasks to
be scheduled and suffices in the vast majority of cases. In some applications
with difficult realtime requirements a longer pause can offer benefits.
## 4.6 Usage
The `Screen.change()` classmethod returns immediately. This has implications
where the new, top screen sets up data for use by the underlying screen. One
@ -2657,6 +2666,44 @@ run V1.17 or later it is possible to run under V1.15+. This involves copying
to `gui/core/writer.py`. This uses Python code to render text if the firmware
or driver are unable to support fast rendering.
# 25. Realtime applications
Screen refresh is performed in a continuous loop with yields to the scheduler.
In normal applications this works well, however a significant proportion of
processor time is spent performing a blocking refresh. A means of synchronising
refresh to other tasks is provided, enabling the application to control the
screen refresh. This is done by means of two `Event` instances. The refresh
task operates as below (code simplified to illustrate this mechanism).
```python
class Screen:
rfsh_start = Event() # Refresh pauses until set (set by default).
rfsh_done = Event() # Flag a user task that a refresh was done.
@classmethod
async def auto_refresh(cls):
cls.rfsh_start.set()
while True:
await cls.rfsh_start.wait()
ssd.show() # Synchronous (blocking) refresh.
# Flag user code.
cls.rfsh_done.set()
await asyncio.sleep_ms(0) # Let user code respond to event
```
By default the `rfsh_start` event is permanently set, allowing refresh to free
run. User code can clear this event to delay refresh. The `rfsh_done` event can
signal to user code that refresh is complete. As an example of simple usage,
the following, if awaited, pauses until a refresh is complete and prevents
another from occurring.
```python
async def refresh_and_stop(self):
Screen.rfsh_start.set() # Allow refresh
Screen.rfsh_done.clear() # Enable completion flag
await Screen.rfsh_done.wait() # Wait for a refresh to end
Screen.rfsh_start.clear() # Prevent another.
```
The demo `gui/demos/audio.py` provides example usage.
###### [Contents](./README.md#0-contents)
# Appendix 1 Application design

Wyświetl plik

@ -203,6 +203,10 @@ class Display:
class Screen:
current_screen = None
is_shutdown = Event()
# These events enable user code to synchronise display refresh
# to a realtime process.
rfsh_start = Event() # Refresh pauses until set (set by default).
rfsh_done = Event() # Flag a user task that a refresh was done.
@classmethod
def ctrl_move(cls, _, v):
@ -290,22 +294,27 @@ class Screen:
# If the display driver has an async refresh method, determine the split
# value which must be a factor of the height. In the unlikely event of
# no factor, do_refresh confers no benefit, so use synchronous code.
@staticmethod
async def auto_refresh():
arfsh = hasattr(ssd, 'do_refresh') # Refresh can be asynchronous
@classmethod
async def auto_refresh(cls):
arfsh = hasattr(ssd, 'do_refresh') # Refresh can be asynchronous.
# By default rfsh_start is permanently set. User code can clear this.
cls.rfsh_start.set()
if arfsh:
h = ssd.height
split = max(y for y in (1,2,3,5,7) if not h % y)
if split == 1:
arfsh = False
while True:
await cls.rfsh_start.wait()
Screen.show(False) # Update stale controls. No physical refresh.
# Now perform physical refresh.
if arfsh:
await ssd.do_refresh(split)
else:
ssd.show() # Synchronous (blocking) refresh.
await asyncio.sleep_ms(0)
# Flag user code.
cls.rfsh_done.set()
await asyncio.sleep_ms(0) # Let user code respond to event
@classmethod
def back(cls):

Wyświetl plik

@ -9,8 +9,11 @@ from machine import I2S
from machine import Pin
import pyb
# ***************
# Do allocations early
BUFSIZE = 1024*20 # 5.8ms/KiB
BUFSIZE = 1024*20 # 5.8ms/KiB 8KiB occasional dropouts
WAVSIZE = 1024*2
root = "/sd/music" # Location of directories containing albums
@ -20,7 +23,7 @@ pyb.Pin("EN_3V3").on() # provide 3.3V on 3V3 output pin
I2S_ID = 1
# allocate sample array once
wav_samples = bytearray(BUFSIZE)
wav_samples = bytearray(WAVSIZE)
# The proper way is to parse the WAV file as per
# https://github.com/miketeachman/micropython-i2s-examples/blob/master/examples/wavplayer.py
@ -128,6 +131,7 @@ class BaseScreen(Screen):
CloseButton(wri) # Quit the application
# self.reg_task(asyncio.create_task(self.report()))
async def report(self):
while True:
gc.collect()
@ -184,13 +188,29 @@ class BaseScreen(Screen):
self.show_song()
#self.play_album()
def show_song(self):
def show_song(self): # 13ms
song = self.songs[self.song_idx]
ns = song.find(SelectScreen.album)
ne = song[ns:].find('/') + 1
end = song[ns + ne:].find(".wav")
self.lblsong.value(song[ns + ne: ns + ne + end])
async def refresh_and_stop(self):
Screen.rfsh_start.set() # Allow refresh
Screen.rfsh_done.clear()
await Screen.rfsh_done.wait() # Wait for a refresh to end
Screen.rfsh_start.clear() # Prevent another.
async def refresh_ctrl(self): # Enter with refresh paused
await asyncio.sleep_ms(100) # Time for initial buffer fill
try:
while True:
await self.refresh_and_stop() # Allow one screen refresh
await asyncio.sleep_ms(20) # Time for buffer top-up
finally: # Allow refresh to free-run
Screen.rfsh_start.set()
async def album_task(self):
self.playing = True # Prevent other instances
self.stop_play = False
@ -198,7 +218,12 @@ class BaseScreen(Screen):
songs = self.songs[self.song_idx :] # Start from current index
for song in songs:
self.show_song()
await self.refresh_and_stop() # Pause until refresh is stopped.
# Delay refresh to ensure buffer is filled
rc = asyncio.create_task(self.refresh_ctrl())
await asyncio.sleep_ms(0)
await self.play_song(song)
rc.cancel() # Restore normal display refresh
if self.stop_play:
break # A callback has stopped playback
self.song_idx += 1
@ -206,6 +231,7 @@ class BaseScreen(Screen):
self.song_idx = 0 # Played to completion.
self.show_song()
self.playing = False
rc.cancel() # Restore normal display refresh
# Open and play a binary wav file
async def play_song(self, song):
@ -223,6 +249,7 @@ class BaseScreen(Screen):
# HACK awaiting https://github.com/micropython/micropython/pull/7868
swriter.out_buf = wav_samples_mv[:num_read]
await swriter.drain()
# wav_samples is now empty
self.offset += size
def test():

Wyświetl plik

@ -0,0 +1,279 @@
# audio.py
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2021 Peter Hinch
import hardware_setup # Create a display instance
from gui.core.ugui import Screen, ssd
from machine import I2S
from machine import Pin
import pyb
# ***************
# Do allocations early
BUFSIZE = 1024*20 # 5.8ms/KiB 8KiB occasional dropouts
WAVSIZE = 1024*2
root = "/sd/music" # Location of directories containing albums
pyb.Pin("EN_3V3").on() # provide 3.3V on 3V3 output pin
# ======= I2S CONFIGURATION =======
I2S_ID = 1
# allocate sample array once
wav_samples = bytearray(WAVSIZE)
# The proper way is to parse the WAV file as per
# https://github.com/miketeachman/micropython-i2s-examples/blob/master/examples/wavplayer.py
# Here for simplicity we assume stereo files ripped from CD's.
config = {
'sck' : Pin('W29'),
'ws' : Pin('W16'),
'sd' : Pin('Y4'),
'mode' : I2S.TX,
'bits' : 16, # Sample size in bits/channel
'format' : I2S.STEREO,
'rate' : 44100, # Sample rate in Hz
'ibuf' : BUFSIZE, # Buffer size
}
audio_out = I2S(I2S_ID, **config)
# *** MONITOR ***
from machine import UART
import monitor
# Define interface to use
monitor.set_device(UART(2, 1_000_000)) # UART must be 1MHz
trig2 = monitor.trigger(2)
# trig4 = monitor.trigger(4)
# ======= GUI =======
from gui.widgets.label import Label
from gui.widgets.buttons import Button, CloseButton, CIRCLE
from gui.widgets.sliders import HorizSlider
from gui.widgets.listbox import Listbox
from gui.core.writer import CWriter
# Font for CWriter
import gui.fonts.arial10 as arial10
import gui.fonts.icons as icons
from gui.core.colors import *
import os
import gc
import uasyncio as asyncio
import sys
# Initial check on ilesystem
try:
subdirs = [x[0] for x in os.ilistdir(root) if x[1] == 0x4000]
if len(subdirs):
subdirs.sort()
else:
print("No albums found in ", root)
sys.exit(1)
except OSError:
print(f"Expected {root} directory not found.")
sys.exit(1)
class SelectScreen(Screen):
songs = []
album = ""
def __init__(self, wri):
super().__init__()
Listbox(wri, 2, 2, elements = subdirs, dlines = 8, width=100, callback = self.lbcb)
def lbcb(self, lb): # sort
directory = ''.join((root, '/', lb.textvalue()))
songs = [x[0] for x in os.ilistdir(directory) if x[1] != 0x4000]
songs.sort()
SelectScreen.songs = [''.join((directory, '/', x)) for x in songs]
SelectScreen.album = lb.textvalue()
Screen.back()
class BaseScreen(Screen):
def __init__(self):
self.swriter = asyncio.StreamWriter(audio_out)
args = {
'bdcolor' : RED,
'slotcolor' : BLUE,
'legends' : ('-48dB', '-24dB', '0dB'),
'value' : 0.5,
'height' : 15,
}
buttons = {
'shape' : CIRCLE,
'fgcolor' : GREEN,
}
super().__init__()
# Audio status
self.playing = False # Track is playing
self.stop_play = False # Command
self.paused = False
self.songs = [] # Paths to songs in album
self.song_idx = 0 # Current index into .songs
self.offset = 0 # Offset into file
self.volume = -3
wri = CWriter(ssd, arial10, GREEN, BLACK, False)
wri_icons = CWriter(ssd, icons, WHITE, BLACK, False)
Button(wri_icons, 2, 2, text='E', callback=self.new, args=(wri,), **buttons) # New
Button(wri_icons, row := 30, col := 2, text='D', callback=self.replay, **buttons) # Replay
Button(wri_icons, row, col := col + 25, text='F', callback=self.play_cb, **buttons) # Play
Button(wri_icons, row, col := col + 25, text='B', callback=self.pause, **buttons) # Pause
Button(wri_icons, row, col := col + 25, text='A', callback=self.stop, **buttons) # Stop
Button(wri_icons, row, col + 25, text='C', callback=self.skip, **buttons) # Skip
row = 60
col = 2
self.lbl = Label(wri, row, col, 120)
self.lblsong = Label(wri, self.lbl.mrow + 2, col, 120)
row = 110
col = 14
HorizSlider(wri, row, col, callback=self.slider_cb, **args)
CloseButton(wri) # Quit the application
# self.reg_task(asyncio.create_task(self.report()))
# *** MONITOR ***
monitor.init()
asyncio.create_task(monitor.hog_detect())
async def report(self):
while True:
gc.collect()
print(gc.mem_free())
await asyncio.sleep(20)
def slider_cb(self, s):
self.volume = round(8 * (s.value() - 1))
def play_cb(self, _):
self.play_album()
def pause(self, _):
self.stop_play = True
self.paused = True
self.show_song()
def stop(self, _): # Abandon album
self.stop_play = True
self.paused = False
self.song_idx = 0
self.show_song()
def replay(self, _):
if self.stop_play:
self.song_idx = max(0, self.song_idx - 1)
else:
self.stop_play = True # Replay from start
self.paused = False
self.show_song()
#self.play_album()
def skip(self, _):
self.stop_play = True
self.paused = False
self.song_idx = min(self.song_idx + 1, len(self.songs) -1)
self.show_song()
#self.play_album()
def new(self, _, wri):
self.stop_play = True
self.paused = False
Screen.change(SelectScreen, args=[wri,])
def play_album(self):
if not self.playing:
self.reg_task(asyncio.create_task(self.album_task()))
def after_open(self):
self.songs = SelectScreen.songs
self.lbl.value(SelectScreen.album)
if self.songs:
self.song_idx = 0 # Start on track 0
self.show_song()
#self.play_album()
@monitor.sync(5)
def show_song(self): # 13ms
song = self.songs[self.song_idx]
ns = song.find(SelectScreen.album)
ne = song[ns:].find('/') + 1
end = song[ns + ne:].find(".wav")
self.lblsong.value(song[ns + ne: ns + ne + end])
@monitor.asyn(4)
async def refresh_and_stop(self):
Screen.rfsh_start.set() # Allow refresh
Screen.rfsh_done.clear()
await Screen.rfsh_done.wait() # Wait for a refresh to end
Screen.rfsh_start.clear() # Prevent another.
async def refresh_ctrl(self): # Enter with refresh paused
await asyncio.sleep_ms(100) # Time for initial buffer fill
try:
while True:
await self.refresh_and_stop() # Allow one screen refresh
await asyncio.sleep_ms(20) # Time for buffer top-up
finally: # Allow refresh to free-run
Screen.rfsh_start.set()
async def album_task(self):
self.playing = True # Prevent other instances
self.stop_play = False
# Leave paused status unchanged
songs = self.songs[self.song_idx :] # Start from current index
for song in songs:
self.show_song()
await self.refresh_and_stop() # Pause until refresh is stopped.
# Delay refresh to ensure buffer is filled
rc = asyncio.create_task(self.refresh_ctrl())
await asyncio.sleep_ms(0)
await self.play_song(song)
rc.cancel() # Restore normal display refresh
if self.stop_play:
break # A callback has stopped playback
self.song_idx += 1
else:
self.song_idx = 0 # Played to completion.
self.show_song()
self.playing = False
rc.cancel() # Restore normal display refresh
# Open and play a binary wav file
@monitor.asyn(1)
async def play_song(self, song):
wav_samples_mv = memoryview(wav_samples)
size = len(wav_samples)
if not self.paused:
# advance to first byte of Data section in WAV file. This is not
# correct for all WAV files. See link above.
self.offset = 44
swriter = self.swriter
with open(song, "rb") as wav:
_ = wav.seek(self.offset)
while (num_read := wav.readinto(wav_samples_mv)) and not self.stop_play:
I2S.shift(buf=wav_samples_mv[:num_read], bits=16, shift=self.volume)
# HACK awaiting https://github.com/micropython/micropython/pull/7868
swriter.out_buf = wav_samples_mv[:num_read]
trig2(False)
await swriter.drain()
# wav_samples is now empty
trig2(True) # Ready for data
self.offset += size
def test():
print('Audio demo.')
try:
Screen.change(BaseScreen) # A class is passed here, not an instance.
finally:
audio_out.deinit()
print("========== CLOSE AUDIO ==========")
test()