README changes. Add new demos.

pull/8/head
Peter Hinch 2021-06-23 18:05:14 +01:00
rodzic 97300f86ad
commit dc51cf61cf
10 zmienionych plików z 271 dodań i 133 usunięć

Wyświetl plik

@ -48,9 +48,10 @@ target and a C device driver (unless you can acquire a suitable binary).
# Project status
Code has been tested on ESP32 and Pi Pico. The API shuld be stable. I'm not
aware of any bugs but code is new and issues are likely. This document is
likely to have errors, typos and omissions. It is under review.
Code has been tested on ESP32, Pi Pico and Pyboard. The API shuld be stable.
Code is new and issues are likely: please report any found. This document is
under review. I plan to add further demos and to upgrade the performance of
some display drivers.
# 0. Contents
@ -160,16 +161,20 @@ The currently selected `Widget` is identified by a white border: the `focus`
moves between widgets via `Next` and `Prev`. Only `active` `Widget` instances
(those that can accept input) can receive the `focus`. Widgets are defined as
`active` or `passive` in the constructor, and this status cannot be changed. In
some cases the state can be specified as a constructor arg. An `active` widget
can be disabled and re-enabled at runtime. A disabled `active` widget is shown
"greyed-out" and, until re-enabled, cannot accept the focus.
some cases the state can be specified as a constructor arg, but other widgets
have a predefined state. An `active` widget can be disabled and re-enabled at
runtime. A disabled `active` widget is shown "greyed-out" and, until
re-enabled, cannot accept the `focus`.
## 1.5 Hardware definition
A file `hardware_setup.py` must exist in the GUI root directory. This defines
the connections to the display, the display driver, and pins used for the
pushbuttons. Example files may be found in the `setup_examples` directory. The
following is a typical example for a Raspberry Pi Pico driving an ILI9341
pushbuttons. Example files may be found in the `setup_examples` directory.
Further examples (without pin definitions) are in this
[nano-gui directory](https://github.com/peterhinch/micropython-nano-gui/tree/master/setup_examples).
The following is a typical example for a Raspberry Pi Pico driving an ILI9341
display:
```python
@ -242,8 +247,8 @@ If installing to the device's filesystem it is necessary to maintain the
directory structure. The `drivers` and `gui` directories (with subdirectories
and contents) should be copied, along with `hardware_setup.py`. Filesystem
space may be conserved by copying only the display driver in use. Unused
widgets, fonts and demos can also be trimmed, but directory structure must be
kept.
widgets, fonts and demos can also be trimmed, but the directory structure must
be kept.
There is scope for speeding loading and saving RAM by using frozen bytecode.
Once again, directory structure must be maintained.
@ -326,20 +331,36 @@ require a large (320x240) display. Demos are run by issuing (for example):
```python
>>> import gui.demos.simple
```
* `simple.py` Minimal demo discussed below.
* `active.py` Demonstrates `active` controls providing floating point input.
* `plot.py` Graph plotting.
* `screens.py` Listbox, dropdown and dialog boxes.
#### Demos
These will run on screens of 128x128 pixels or above. The initial ones are
minimal and aim to demonstrate a single technique.
* `simple.py` Minimal demo discussed below. `Button` presses print to REPL.
* `checkbox` A `Checkbox` controlling an `LED`.
* `slider.py` A `Slider` whose color varies with its value.
* `slider_label.py` A `Slider` updating a `Label`. Good for trying precision
mode.
* `screen_change.py` A `Pushbutton` causing a screen change.
* `tbox.py` Text boxes and user-controlled scrolling.
* `various.py` Assorted widgets including the different types of pushbutton.
* `vtest.py` Clock and compass styles of vector display.
#### Test scripts
Some of these require larger screens. Required sizes are specified as
(height x width).
* `active.py` Demonstrates `active` controls providing floating point input
(240x320).
* `plot.py` Graph plotting (128x200).
* `screens.py` Listbox, dropdown and dialog boxes (128x240).
* `various.py` Assorted widgets including the different types of pushbutton
(240x320).
* `vtest.py` Clock and compass styles of vector display (240x320).
## 1.12 Floating Point Widgets
The challenge is to devise a way, with just two pushbuttons, of adjusting a
data value which may have an extremely large dynamic range. This is the ratio
of the data value's total range to the smallest adjustment that can be made.
The mechanism as currently implemented enables a precision of 0.05%.
The mechanism currently implemented enables a precision of 0.05%.
Floating point widgets respond to a brief press of the `increase` or `decrease`
buttons by adjusting the value by a small amount. A continued press causes the
@ -359,6 +380,10 @@ of the visual appearance of the widget: fine changes can be too small to see.
Options are to use the [Scale widget](./README.md#18-scale-widget) or to have a
linked `Label` showing the widget's exact value.
The callback runs whenever the widget's value changes. This causes the callback
to run repeatedly while the user adjusts the widget. This is required if there
is a linked `Label` to update.
###### [Contents](./README.md#0-contents)
# 2. Usage
@ -479,9 +504,10 @@ PALE_YELLOW = create_color(12, 150, 150, 0) # index, r, g, b
If a 4-bit driver is in use, the color `rgb(150, 150, 0)` will be assigned to
"spare" color number 12. Any color number in range `0 <= n <= 15` may be
used, implying that predefined colors may be reassigned. It is recommended
that `BLACK` (0) and `WHITE` (15) are not changed. If an 8-bit or larger driver
is in use, the first `index` arg is ignored and there is no restriction on the
number of colors that may be created.
that `BLACK` (0) and `WHITE` (15) are not changed; `GREY` (6) and `YELLOW` (5)
are also GUI defaults. If an 8-bit or larger driver is in use, the first
`index` arg is ignored and there is no restriction on the number of colors that
may be created.
Regardless of the display driver the `PALE_YELLOW` variable may be used to
refer to the color. An example of custom color definition may be found in

Wyświetl plik

@ -0,0 +1,37 @@
# checkbox.py Minimal micro-gui demo showing a Checkbox updating an LED.
# hardware_setup must be imported before other modules because of RAM use.
import hardware_setup # Create a display instance
from gui.core.ugui import Screen, ssd
from gui.widgets.buttons import CloseButton
from gui.widgets.checkbox import Checkbox
from gui.widgets.led import LED
from gui.core.writer import CWriter
# Font for CWriter
import gui.fonts.arial10 as arial10
from gui.core.colors import *
class BaseScreen(Screen):
def __init__(self):
super().__init__()
wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False)
col = 2
row = 2
self.cb = Checkbox(wri, row, col, callback=self.cbcb)
col+= 40
self.led = LED(wri, row, col, color=YELLOW, bdcolor=GREEN)
CloseButton(wri)
def cbcb(self, cb):
self.led.value(cb.value())
def test():
print('Checkbox demo.')
Screen.change(BaseScreen)
test()

Wyświetl plik

@ -0,0 +1,48 @@
# linked_sliders.py Minimal micro-gui demo one Slider controlling two others.
# hardware_setup must be imported before other modules because of RAM use.
import hardware_setup # Create a display instance
from gui.core.ugui import Screen, ssd
from gui.widgets.buttons import CloseButton
from gui.widgets.sliders import Slider
from gui.core.writer import CWriter
# Font for CWriter
import gui.fonts.arial10 as arial10
from gui.core.colors import *
class BaseScreen(Screen):
def __init__(self):
args = {
'bdcolor' : RED,
'slotcolor' : BLUE,
'legends' : ('0.0', '0.5', '1.0'),
'value' : 0.5,
}
super().__init__()
wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False)
col = 2
row = 2
dc = 45
# Note: callback runs now, but other sliders have not yet been instantiated.
self.s0 = Slider(wri, row, col, callback=self.slider_cb, **args)
col += dc
self.s1 = Slider(wri, row, col, **args)
col += dc
self.s2 = Slider(wri, row, col, **args)
CloseButton(wri)
def slider_cb(self, s):
v = s.value()
if hasattr(self, 's1'): # If s1 & s2 have been instantiated
self.s1.value(v)
self.s2.value(v)
def test():
print('Linked sliders. Leftmost one controls others.')
Screen.change(BaseScreen)
test()

Wyświetl plik

@ -0,0 +1,51 @@
# screen_change.py Minimal micro-gui demo showing a Button causing a screen change.
# hardware_setup must be imported before other modules because of RAM use.
import hardware_setup # Create a display instance
from gui.core.ugui import Screen, ssd
from gui.widgets.buttons import Button, CloseButton
from gui.widgets.label import Label
from gui.core.writer import CWriter
# Font for CWriter
import gui.fonts.font10 as font10
from gui.core.colors import *
# Defining a button in this way enables it to be re-used on
# multiple Screen instances. Note that a Screen class is
# passed, not an instance.
def fwdbutton(wri, row, col, cls_screen, text='Next'):
def fwd(button):
Screen.change(cls_screen) # Callback
Button(wri, row, col, height = 30, callback = fwd,
fgcolor = BLACK, bgcolor = GREEN,
text = text, shape = RECTANGLE, width = 100)
wri = CWriter(ssd, font10, GREEN, BLACK, verbose=False)
# This screen overlays BaseScreen.
class BackScreen(Screen):
def __init__(self):
super().__init__()
Label(wri, 2, 2, 'New screen.')
CloseButton(wri)
class BaseScreen(Screen):
def __init__(self):
super().__init__()
Label(wri, 2, 2, 'Base screen.')
fwdbutton(wri, 40, 2, BackScreen)
CloseButton(wri)
def test():
print('Screen change demo.')
Screen.change(BaseScreen) # Pass class, not instance!
test()

Wyświetl plik

@ -1,6 +1,6 @@
# screens.py micro-gui demo of multiple screens, dropdowns etc
# Create SSD instance. Must be done first because of RAM use.
# hardware_setup must be imported before other modules because of RAM use.
import hardware_setup # Create a display instance
from gui.core.ugui import Screen, Window, ssd

Wyświetl plik

@ -1,6 +1,6 @@
# simple.py Minimal micro-gui demo.
# Initialise hardware and framebuf before importing modules.
# Import SSD and Display instances. Must be done first because of RAM use.
# hardware_setup must be imported before other modules because of RAM use.
import hardware_setup # Create a display instance
from gui.core.ugui import Screen, ssd
@ -33,7 +33,7 @@ class BaseScreen(Screen):
CloseButton(wri) # Quit the application
def test():
print('Testing micro-gui...')
Screen.change(BaseScreen)
print('Simple demo: button presses print to REPL.')
Screen.change(BaseScreen) # A class is passed here, not an instance.
test()

Wyświetl plik

@ -0,0 +1,42 @@
# slider.py Minimal micro-gui demo showing a Slider with variable color.
# hardware_setup must be imported before other modules because of RAM use.
import hardware_setup
from gui.core.ugui import Screen, ssd
from gui.widgets.buttons import CloseButton
from gui.widgets.sliders import Slider
from gui.core.writer import CWriter
# Font for CWriter
import gui.fonts.arial10 as arial10
from gui.core.colors import *
class BaseScreen(Screen):
def __init__(self):
super().__init__()
wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False)
col = 2
row = 2
Slider(wri, row, col, callback=self.slider_cb,
bdcolor=RED, slotcolor=BLUE,
legends=('0.0', '0.5', '1.0'), value=0.5)
CloseButton(wri)
def slider_cb(self, s):
v = s.value()
if v < 0.2:
s.color(BLUE)
elif v > 0.8:
s.color(RED)
else:
s.color(GREEN)
def test():
print('Slider demo.')
Screen.change(BaseScreen)
test()

Wyświetl plik

@ -0,0 +1,41 @@
# slider_label.py Minimal micro-gui demo showing a Slider controlling a Label.
# hardware_setup must be imported before other modules because of RAM use.
import hardware_setup # Create a display instance
from gui.core.ugui import Screen, ssd
from gui.widgets.buttons import CloseButton
from gui.widgets.sliders import Slider
from gui.widgets.label import Label
from gui.core.writer import CWriter
# Font for CWriter
import gui.fonts.arial10 as arial10
from gui.core.colors import *
class BaseScreen(Screen):
def __init__(self):
super().__init__()
wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False)
col = 2
row = 2
self.lbl = Label(wri, row + 45, col + 50, 35, bdcolor=RED, bgcolor=DARKGREEN)
# Instntiate Label first, because Slider callback will run now.
# See linked_sliders.py for another approach.
Slider(wri, row, col, callback=self.slider_cb,
bdcolor=RED, slotcolor=BLUE,
legends=('0.0', '0.5', '1.0'), value=0.5)
CloseButton(wri)
def slider_cb(self, s):
v = s.value()
self.lbl.value('{:5.3f}'.format(v))
def test():
print('Slider Label demo. Long press select for precision mode.')
Screen.change(BaseScreen)
test()

Wyświetl plik

@ -114,7 +114,7 @@ def test():
if ssd.height < 128 or ssd.width < 128:
print(' This test requires a display of at least 128x128 pixels.')
else:
print('Testing micro-gui...')
print('Textbox demo.')
Screen.change(MainScreen)
test()

Wyświetl plik

@ -1,107 +0,0 @@
# pushbutton.py
# Copyright (c) 2018-2021 Peter Hinch
# Released under the MIT License (MIT) - see LICENSE file
import uasyncio as asyncio
import utime as time
from . import launch
from gui.primitives.delay_ms import Delay_ms
# An alternative Pushbutton solution with lower RAM use is available here
# https://github.com/kevinkk525/pysmartnode/blob/dev/pysmartnode/utils/abutton.py
class Pushbutton:
debounce_ms = 50
long_press_ms = 1000
double_click_ms = 400
def __init__(self, pin, suppress=False, sense=None):
self.pin = pin # Initialise for input
self._supp = suppress
self._dblpend = False # Doubleclick waiting for 2nd click
self._dblran = False # Doubleclick executed user function
self._tf = False
self._ff = False
self._df = False
self._ld = False # Delay_ms instance for long press
self._dd = False # Ditto for doubleclick
self.sense = pin.value() if sense is None else sense # Convert from electrical to logical value
self.state = self.rawstate() # Initial state
asyncio.create_task(self.buttoncheck()) # Thread runs forever
def press_func(self, func=False, args=()):
self._tf = func
self._ta = args
def release_func(self, func=False, args=()):
self._ff = func
self._fa = args
def double_func(self, func=False, args=()):
self._df = func
self._da = args
if func: # If double timer already in place, leave it
if not self._dd:
self._dd = Delay_ms(self._ddto)
else:
self._dd = False # Clearing down double func
def long_func(self, func=False, args=()):
if func:
if self._ld:
self._ld.callback(func, args)
else:
self._ld = Delay_ms(func, args)
else:
self._ld = False
# Current non-debounced logical button state: True == pressed
def rawstate(self):
return bool(self.pin.value() ^ self.sense)
# Current debounced state of button (True == pressed)
def __call__(self):
return self.state
def _ddto(self): # Doubleclick timeout: no doubleclick occurred
self._dblpend = False
if self._supp and not self.state:
if not self._ld or (self._ld and not self._ld()):
launch(self._ff, self._fa)
async def buttoncheck(self):
while True:
state = self.rawstate()
# State has changed: act on it now.
if state != self.state:
self.state = state
if state: # Button pressed: launch pressed func
if self._tf:
launch(self._tf, self._ta)
if self._ld: # There's a long func: start long press delay
self._ld.trigger(Pushbutton.long_press_ms)
if self._df:
if self._dd(): # Second click: timer running
self._dd.stop()
self._dblpend = False
self._dblran = True # Prevent suppressed launch on release
launch(self._df, self._da)
else:
# First click: start doubleclick timer
self._dd.trigger(Pushbutton.double_click_ms)
self._dblpend = True # Prevent suppressed launch on release
else: # Button release. Is there a release func?
if self._ff:
if self._supp:
d = self._ld
# If long delay exists, is running and doubleclick status is OK
if not self._dblpend and not self._dblran:
if (d and d()) or not d:
launch(self._ff, self._fa)
else:
launch(self._ff, self._fa)
if self._ld:
self._ld.stop() # Avoid interpreting a second click as a long push
self._dblran = False
# Ignore state changes until switch has settled
await asyncio.sleep_ms(Pushbutton.debounce_ms)