Fix exit behaviour. Add driver pico_epaper_42_v2.py.

pull/48/head
Peter Hinch 2024-07-19 14:01:08 +01:00
rodzic 4a644ab756
commit 3b6d265810
5 zmienionych plików z 431 dodań i 33 usunięć

Wyświetl plik

@ -1471,6 +1471,9 @@ comprise the currently visible button followed by its arguments.
Constructor argument:
* `callback=dolittle` The callback function. Default does nothing.
* `new_cb=False` When a button is pressed, determines whether the callback run
is that of the button visible when pressed, or that which becomes visible after
the press.
Methods:
* `add_button` Adds a button to the `ButtonList`. Arguments: as per the
@ -1491,7 +1494,9 @@ Counter intuitively, running the callback of the previous button is normal
behaviour. Consider a `ButtonList` consisting of ON and OFF buttons. If ON is
visible this implies that the machine under control is off. Pressing `select`
causes the ON callback to run, starting the machine. The new button displayed
now reads OFF.
now reads OFF. There are situations in which the opposite behaviour is required
such as when choosing an option from a list: in this case the callback from the
newly visible button might be expected to run.
Typical usage is as follows:
```python

Wyświetl plik

@ -0,0 +1,354 @@
# pico_epaper_42_v2.py
# Materials used for discovery can be found here
# https://www.waveshare.com/wiki/4.2inch_e-Paper_Module_Manual#Introduction
# Note, at the time of writing this, none of the source materials have working
# code that works with partial refresh, as the C code has a bug and all the other
# materials use that reference material as the source of truth.
# *****************************************************************************
# * | File : pico_epaper_42_v2.py
# * | Author : michael surdouski
# * | Function : Electronic paper driver
# *----------------
# * | This version: rev2.2
# * | Date : 2024-05-22
# -----------------------------------------------------------------------------
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documnetation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
# Waveshare URLs
# Main page: https://www.waveshare.com/pico-epaper-4.2.htm
# Wiki: https://www.waveshare.com/wiki/Pico-ePaper-4.2
# Code: https://github.com/waveshareteam/Pico_ePaper_Code/blob/main/python/Pico-ePaper-4.2_V2.py
from machine import Pin, SPI
import framebuf
import time
import asyncio
from drivers.boolpalette import BoolPalette
def asyncio_running():
try:
_ = asyncio.current_task()
except:
return False
return True
# Display resolution
_EPD_WIDTH = const(400)
_BWIDTH = _EPD_WIDTH // 8
_EPD_HEIGHT = const(300)
_RST_PIN = 12
# changed default to 7, as this can be confusing on pico -- pin 8 for SPI1 is the Rx, which overrides DC pin if miso is set to none
_DC_PIN = 7
_CS_PIN = 9
_BUSY_PIN = 13
# Invert: EPD is black on white
# 337/141 us for 2000 bytes (125/250MHz)
@micropython.viper
def _linv(dest: ptr32, source: ptr32, length: int):
n: int = length - 1
z: uint32 = int(0xFFFFFFFF)
while n >= 0:
dest[n] = source[n] ^ z
n -= 1
class EPD(framebuf.FrameBuffer):
# A monochrome approach should be used for coding this. The rgb method ensures
# nothing breaks if users specify colors.
@staticmethod
def rgb(r, g, b):
return int((r > 127) or (g > 127) or (b > 127))
def __init__(self, spi=None, cs=None, dc=None, rst=None, busy=None):
self._rst = Pin(_RST_PIN, Pin.OUT) if rst is None else rst
self._busy_pin = Pin(_BUSY_PIN, Pin.IN, Pin.PULL_UP) if busy is None else busy
self._cs = Pin(_CS_PIN, Pin.OUT) if cs is None else cs
self._dc = Pin(_DC_PIN, Pin.OUT) if dc is None else dc
self._spi = SPI(1, sck=Pin(10), mosi=Pin(11), miso=Pin(28)) if spi is None else spi
self._spi.init(baudrate=4_000_000)
# Busy flag: set immediately on .show(). Cleared when busy pin is logically false.
self._busy = False
# Async API
self.updated = asyncio.Event()
self.complete = asyncio.Event()
self.maxblock = 25
# partial refresh
self._partial = False
# Public bound variables required by nanogui.
# Dimensions in pixels as seen by nanogui
self.width = _EPD_WIDTH
self.height = _EPD_HEIGHT
# Other public bound variable.
# Special mode enables demos written for generic displays to run.
self.demo_mode = False
self.blank_on_exit = True
self._buf = bytearray(_EPD_HEIGHT * _BWIDTH)
self._mvb = memoryview(self._buf)
self._ibuf = bytearray(1000) # Buffer for inverted pixels
mode = framebuf.MONO_HLSB
self.palette = BoolPalette(mode) # Enable CWriter.
super().__init__(self._buf, _EPD_WIDTH, _EPD_HEIGHT, mode)
self.init()
time.sleep_ms(500)
# Hardware reset
def reset(self):
for v in (1, 0, 1):
self._rst(v)
time.sleep_ms(20)
def _command(self, command, data=None):
self._dc(0)
self._cs(0)
self._spi.write(command)
self._cs(1)
if data is not None:
self._data(data)
def _data(self, data):
self._dc(1)
self._cs(0)
self._spi.write(data)
self._cs(1)
def _display_on(self):
if self._partial:
self._command(b"\x22")
self._data(b"\xFF")
self._command(b"\x20")
else:
self._command(b"\x22")
self._data(b"\xF7")
self._command(b"\x20")
# Called by constructor. Application use is deprecated.
def init(self):
self.reset() # hardware reset
self._command(b"\x12") # software reset
self.wait_until_ready()
self.set_full()
self._display_on()
# Common API
def set_full(self):
self._partial = False
self._command(b"\x21") # Display update control
self._data(b"\x40")
self._data(b"\x00")
self._command(b"\x3C") # BorderWaveform
self._data(b"\x05")
self._command(b"\x11") # data entry mode
self._data(b"\x03") # X-mode
self._set_window()
self._set_cursor()
self.wait_until_ready()
def set_partial(self):
self._partial = True
self._command(b"\x21") # Display update control
self._data(b"\x00")
self._data(b"\x00")
self._command(b"\x3C") # BorderWaveform
self._data(b"\x80")
self._command(b"\x11") # data entry mode
self._data(b"\x03") # X-mode
self._set_window()
self._set_cursor()
self.wait_until_ready()
@micropython.native
def _bsend(self, start, nbytes): # Invert b<->w, buffer and send nbytes source bytes
buf = self._ibuf # Invert and buffer is done 32 bits at a time, hence >> 2
_linv(buf, self._mvb[start:], nbytes >> 2)
self._dc(1)
self._cs(0)
self._spi.write(buf)
self._cs(1)
# Send the frame buffer. If running asyncio, return whenever MAXBLOCK ms elapses
# so that caller can yield to the scheduler.
# Returns no. of bytes outstanding.
def _send_bytes(self):
fbidx = 0 # Index into framebuf
nbytes = len(self._ibuf) # Bytes to send
nleft = len(self._buf) # Size of framebuf
asyn = asyncio_running()
def inner():
nonlocal fbidx
nonlocal nbytes
nonlocal nleft
ts = time.ticks_ms()
while nleft > 0:
self._bsend(fbidx, nbytes) # Invert, buffer and send nbytes
fbidx += nbytes # Adjust for bytes already sent
nleft -= nbytes
nbytes = min(nbytes, nleft)
if asyn and time.ticks_diff(time.ticks_ms(), ts) > self.maxblock:
return nbytes # Probably not all done; quit. Caller yields, calls again
return 0 # All done
return inner
# micro-gui API; asyncio is running.
async def do_refresh(self, split): # split = 5
assert not self._busy, "Refresh while busy"
if self._partial:
await self._as_show_partial()
else:
await self._as_show_full()
def shutdown(self, clear=False):
time.sleep(1) # Empirically necessary (ugh)
self.fill(0)
self.set_full()
if clear or self.blank_on_exit:
self.show()
self.wait_until_ready()
self.sleep()
# nanogui API
def show(self):
if self._busy:
raise RuntimeError("Cannot refresh: display is busy.")
if self._partial:
self._show_partial()
else:
self._show_full()
if not self.demo_mode:
# Immediate return to avoid blocking the whole application.
# User should wait for ready before calling refresh()
return
self.wait_until_ready()
time.sleep_ms(2000) # Demo mode: give time for user to see result
def _show_full(self):
self._busy = True # Immediate busy flag. Pin goes low much later.
if asyncio_running():
self.updated.clear()
self.complete.clear()
asyncio.create_task(self._as_show_full())
return
# asyncio is not running, hence sb() will not time out.
self._command(b"\x24")
sb = self._send_bytes() # Instantiate closure
sb() # Run to completion
self._command(b"\x26")
sb = self._send_bytes() # Create new instance
sb()
self._busy = False
self._display_on()
async def _as_show_full(self):
self._command(b"\x24")
sb = self._send_bytes() # Instantiate closure
while sb():
await asyncio.sleep_ms(0) # Timed out. Yield and continue.
self._command(b"\x26")
sb = self._send_bytes() # New closure instance
while sb():
await asyncio.sleep_ms(0)
self.updated.set()
self._display_on()
while self._busy_pin():
await asyncio.sleep_ms(0)
self._busy = False
self.complete.set()
def _show_partial(self):
self._busy = True
if asyncio_running():
self.updated.clear()
self.complete.clear()
asyncio.create_task(self._as_show_partial())
return
self._command(b"\x24")
sb = self._send_bytes() # Instantiate closure
sb()
self._busy = False
self._display_on()
async def _as_show_partial(self):
self._command(b"\x24")
sb = self._send_bytes() # Instantiate closure
while sb():
await asyncio.sleep_ms(0)
self.updated.set()
self._display_on()
while self._busy_pin():
await asyncio.sleep_ms(0)
self._busy = False
self.complete.set()
# nanogui API
def wait_until_ready(self):
while not self.ready():
time.sleep_ms(100)
def ready(self):
return not (self._busy or self._busy_pin()) # 1 == busy
def sleep(self):
self._command(b"\x10") # deep sleep
self._data(b"\x01")
# window and cursor always the same for 4.2"
def _set_window(self):
self._command(b"\x44")
self._data(b"\x00")
self._data(b"\x31")
self._command(b"\x45")
self._data(b"\x00")
self._data(b"\x00")
self._data(b"\x2B")
self._data(b"\x01")
def _set_cursor(self):
self._command(b"\x4E")
self._data(b"\x00")
self._command(b"\x4F")
self._data(b"\x00")
self._data(b"\x00")

Wyświetl plik

@ -389,6 +389,13 @@ class Screen:
ins_new.after_open() # Optional subclass method
if ins_old is None: # Initialising
loop.run_until_complete(cls.monitor()) # Starts and ends uasyncio
# asyncio is no longer running
if hasattr(ssd, "shutdown"):
ssd.shutdown() # An EPD with a special shutdown method.
else:
ssd.fill(0)
ssd.show()
cls.current_screen = None # Ensure another demo can run
# Don't do asyncio.new_event_loop() as it prevents re-running
# the same app.
@ -403,9 +410,6 @@ class Screen:
# Screen instance will be discarded: no need to worry about .tasks
entry[0].cancel()
await asyncio.sleep_ms(0) # Allow task cancellation to occur.
display.clr_scr()
ssd.show()
cls.current_screen = None # Ensure another demo can run
# 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

Wyświetl plik

@ -3,7 +3,7 @@
# Use with setup_examples/pico_epaper_42_pico.py
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2023 Peter Hinch
# Copyright (c) 2023-2024 Peter Hinch
# Initialise hardware and framebuf before importing modules.
# Create SSD instance. Must be done first because of RAM use.
@ -16,6 +16,8 @@ import gui.fonts.freesans20 as large
from gui.core.colors import *
# Option to leave image in place on exit:
# ssd.blank_on_exit = False
# Widgets
from gui.widgets import (
Label,
@ -47,10 +49,12 @@ async def full_refresh():
await Screen.rfsh_done.wait() # Wait for a single full refresh to end
ssd.set_partial()
async def set_partial(): # Ensure 1st refresh is a full refresh
await Screen.rfsh_done.wait() # Wait for first refresh to end
ssd.set_partial()
class FooScreen(Screen):
def __init__(self):
buttons = []

Wyświetl plik

@ -8,14 +8,30 @@ from gui.core.ugui import Screen, Widget, display
from gui.primitives.delay_ms import Delay_ms
from gui.core.colors import *
dolittle = lambda *_ : None
dolittle = lambda *_: None
class Button(Widget):
lit_time = 1000
def __init__(self, writer, row, col, *, shape=RECTANGLE, height=20, width=50,
fgcolor=None, bgcolor=None, bdcolor=False, textcolor=None, litcolor=None, text='',
callback=dolittle, args=[]):
def __init__(
self,
writer,
row,
col,
*,
shape=RECTANGLE,
height=20,
width=50,
fgcolor=None,
bgcolor=None,
bdcolor=False,
textcolor=None,
litcolor=None,
text="",
callback=dolittle,
args=[]
):
sl = writer.stringlen(text)
if shape == CIRCLE: # Only height need be specified
width = max(sl, height)
@ -40,7 +56,7 @@ class Button(Widget):
y = self.row
w = self.width
h = self.height
if not self.visible: # erase the button
if not self.visible: # erase the button
display.usegrey(False)
display.fill_rect(x, y, w, h, BGCOLOR)
return
@ -55,16 +71,20 @@ class Button(Widget):
else:
xc = x + w // 2
yc = y + h // 2
if self.shape == RECTANGLE: # rectangle
if self.shape == RECTANGLE: # rectangle
display.fill_rect(x, y, w, h, self.bgcolor)
display.rect(x, y, w, h, self.fgcolor)
if len(self.text):
display.print_centred(self.writer, xc, yc, self.text, self.textcolor, self.bgcolor)
elif self.shape == CLIPPED_RECT: # clipped rectangle
display.print_centred(
self.writer, xc, yc, self.text, self.textcolor, self.bgcolor
)
elif self.shape == CLIPPED_RECT: # clipped rectangle
display.fill_clip_rect(x, y, w, h, self.bgcolor)
display.clip_rect(x, y, w, h, self.fgcolor)
if len(self.text):
display.print_centred(self.writer, xc, yc, self.text, self.textcolor, self.bgcolor)
display.print_centred(
self.writer, xc, yc, self.text, self.textcolor, self.bgcolor
)
async def shownormal(self):
# Handle case where screen changed while timer was active: delay repaint
@ -75,13 +95,14 @@ class Button(Widget):
self.bgcolor = self.def_bgcolor
self.draw = True # Redisplay
def do_sel(self): # Select was pushed
self.callback(self, *self.callback_args) # CB takes self as 1st arg.
def do_sel(self): # Select was pushed
self.callback(self, *self.callback_args) # CB takes self as 1st arg.
if self.litcolor is not None and self.has_focus(): # CB may have changed focus
self.bgcolor = self.litcolor
self.draw = True # Redisplay
self.delay.trigger(Button.lit_time)
# Preferred way to close a screen or dialog. Produces an X button at the top RHS.
# Note that if the bottom screen is closed, the application terminates.
class CloseButton(Button):
@ -89,30 +110,38 @@ class CloseButton(Button):
scr = Screen.current_screen
# Calculate the button width if not provided. Button allows
# 5 pixels either side.
wd = width if width else (writer.stringlen('X') + 10)
wd = width if width else (writer.stringlen("X") + 10)
self.user_cb = callback
self.user_args = args
super().__init__(writer, *scr.locn(4, scr.width - wd - 4),
width = wd, height = wd, bgcolor = bgcolor,
callback = self.cb, text = 'X')
super().__init__(
writer,
*scr.locn(4, scr.width - wd - 4),
width=wd,
height=wd,
bgcolor=bgcolor,
callback=self.cb,
text="X"
)
def cb(self, _):
self.user_cb(self, *self.user_args)
Screen.back()
# Group of buttons, typically at same location, where pressing one shows
# the next e.g. start/stop toggle or sequential select from short list
class ButtonList:
def __init__(self, callback=dolittle):
def __init__(self, callback=dolittle, new_cb=False):
self.user_callback = callback
self._new_cb = new_cb
self.lstbuttons = []
self.current = None # No current button
self.current = None # No current button
self._greyed_out = False
def add_button(self, *args, **kwargs):
button = Button(*args, **kwargs)
self.lstbuttons.append(button)
active = self.current is None # 1st button added is active
active = self.current is None # 1st button added is active
button.visible = active
button.callback = self._callback
if active:
@ -128,12 +157,12 @@ class ButtonList:
new.visible = True
new.draw = True # Redisplay without changing currency
# Args for user callback: button instance followed by any specified.
# Normal behaviour is to run cb of old button: this mimics a button press.
# Optionally programmatic value changes can run the cb of new button.
if new_cb: # Forced value change, callback is that of new button
# Normal behaviour is to run cb of old button (see docs).
# This may be overridden for programmatic value changes, or for
# physical button presses via constructor arg. See docs.
if new_cb or self._new_cb:
self.user_callback(new, *new.callback_args)
else: # A button was pressed
# Callback context is button just pressed, not the new one
else:
self.user_callback(old, *old.callback_args)
return self.current
@ -153,8 +182,10 @@ class ButtonList:
old.visible = False
new.visible = True
Screen.select(new) # Move currency and redisplay
# Callback context is button just pressed, not the new one
self.user_callback(old, *old.callback_args)
if self._new_cb:
self.user_callback(new, *new.callback_args)
else:
self.user_callback(old, *old.callback_args)
# Group of buttons at different locations, where pressing one shows
@ -163,7 +194,7 @@ class RadioButtons:
def __init__(self, highlight, callback=dolittle, selected=0):
self.user_callback = callback
self.lstbuttons = []
self.current = None # No current button
self.current = None # No current button
self.highlight = highlight
self.selected = selected
self._greyed_out = False
@ -198,4 +229,4 @@ class RadioButtons:
else:
but.bgcolor = but.def_bgcolor
but.draw = True
self.user_callback(button, *args) # user gets button with args they specified
self.user_callback(button, *args) # user gets button with args they specified