Fixes to precision mode. README improvements.

pull/8/head
Peter Hinch 2021-06-22 09:01:40 +01:00
rodzic 8d3171334c
commit 696dcb68a8
8 zmienionych plików z 163 dodań i 98 usunięć

112
README.md
Wyświetl plik

@ -1314,10 +1314,12 @@ focus, `increase` and `decrease` buttons adjust the value. Brief presses cause
small changes, longer presses cause accelerating change. A long press of
`select` invokes high precision mode.
The callback receives an initial arg being the slider instance followed by any
user supplied args. They can be a bound methods, typically of a `Screen`
subclass. The callback runs whenever the value changes enabling dynamic color
change. See `gui/demos/active.py`.
### Callback
The callback receives an initial arg being the widget instance followed by any
user supplied args. The callback can be a bound method, typically of a `Screen`
subclass. The callback runs when the widget is instantiated and whenever the
value changes. This enables dynamic color change. See `gui/demos/active.py`.
###### [Contents](./README.md#0-contents)
@ -1339,14 +1341,15 @@ The `Scale` may be `active` or `passive`. A description of the user interface
in the `active` case may be found in
[Floating Point Widgets](./README.md#112-floating-point-widgets).
Legends for the scale are created dynamically as it scrolls past the window.
The user may control this by means of a callback. The example `lscale.py` in
`nano-gui` illustrates a variable with range 88.0 to 108.0, the callback
ensuring that the display legends match the user variable. A further callback
enables the scale's color to change over its length or in response to other
circumstances.
The scale handles floats in range `-1.0 <= V <= 1.0`, however data values may
be scaled to match any given range.
The scale handles floats in range `-1.0 <= V <= 1.0`.
Legends for the scale are created dynamically as it scrolls past the window.
The user may control this by means of a callback. Example code may be found
[in nano-gui](https://github.com/peterhinch/micropython-nano-gui/blob/master/gui/demos/scale.py) which has a `Scale` whose value range is 88.0 to 108.0.
A callback ensures that the display legends match the user variable. A further
callback can enable the scale's color to change over its length or in response
to other circumstances.
Constructor mandatory positional args:
1. `writer` The `Writer` instance (defines font) to use.
@ -1355,26 +1358,27 @@ Constructor mandatory positional args:
Optional keyword only arguments:
* `ticks=200` Number of "tick" divisions on scale. Must be divisible by 2.
* `legendcb=None` Callback for populating scale legends (see below).
* `tickcb=None` Callback for setting tick colors (see below).
* `height=0` Pass 0 for a minimum height based on the font height.
* `value=0.0` Initial value.
* `height=0` Default is a minimum height based on the font height.
* `width=100`
* `fgcolor=None` Color of foreground (the control itself). If `None` the
`Writer` foreground default is used.
* `bgcolor=None` Background color of object. If `None` the `Writer` background
default is used.
* `bdcolor=False` Color of border. If `False` no border will be drawn. If a
color is provided, a border line will be drawn around the control.
* `bdcolor=None` Color of border, default `fgcolor`. If `False` no border will
be drawn. If a color is provided, a border line will be drawn around the
control.
* `prcolor=None` If `active`, in precision mode the white focus border changes
to yellow to for a visual indication. An alternative color can be provided.
`WHITE` will defeat this change.
* `pointercolor=None` Color of pointer. Defaults to `.fgcolor`.
* `fontcolor=None` Color of legends. Default `fgcolor`.
* `callback=dolittle` Callback function which runs whenever the control's
value changes. If the control is `active` it also runs on instantiation. This
enables dynamic color changes. Default is a null function.
* `legendcb=None` Callback for populating scale legends (see below).
* `tickcb=None` Callback for setting tick colors (see below).
* `callback=dolittle` Callback function which runs when the user moves the
scale or the value is changed programmatically. If the control is `active` it
also runs on instantiation. Default is a null function.
* `args=[]` A list/tuple of arguments for above callback.
* `value=0.0` Initial value.
* `active=False` By default the widget is passive. By setting `active=True`
the widget can acquire focus; its value can then be adjusted with the
`increase` and `decrease` buttons.
@ -1390,12 +1394,21 @@ Methods:
For example code see `gui/demos/active.py`.
### Control algorithm
If instantiated as `active`, the floating point widget behaves as per
[section 1.12](./README.md#112-floating-point-widgets). When the widget has
focus, `increase` and `decrease` buttons adjust the value. Brief presses cause
small changes, longer presses cause accelerating change. A long press of
`select` invokes high precision mode.
### Callback
The callback receives an initial arg being the widget instance followed by any
user supplied args. The callback can be a bound method, typically of a `Screen`
subclass. The callback runs when the widget is instantiated and whenever the
value changes. This enables dynamic color change.
### Callback legendcb
The display window contains 20 ticks comprising two divisions; by default a
@ -1404,8 +1417,8 @@ whose text is defined by the `legendcb` callback. If no user callback is
supplied, legends will be of the form `0.3`, `0.4` etc. User code may override
these to cope with cases where a user variable is mapped onto the control's
range. The callback takes a single `float` arg which is the value of the tick
(in range -1.0 <= v <= 1.0). It must return a text string. An example from the
`lscale.py` demo shows FM radio frequencies:
(in range -1.0 <= v <= 1.0). It must return a text string. An example from
[ths nano-gui demo](https://github.com/peterhinch/micropython-nano-gui/blob/master/gui/demos/scale.py) shows FM radio frequencies:
```python
def legendcb(f):
return '{:2.0f}'.format(88 + ((f + 1) / 2) * (108 - 88))
@ -1453,13 +1466,14 @@ from gui.widgets.scale_log import ScaleLog
```
![Image](./images/log_scale.JPG)
This enables the input and/or display of floating point values with extremely
wide dynamic range. This is done by means of a base 10 logarithmic scale. In
other respects the concept is that of the `Scale` class.
This displays floating point values with extremely wide dynamic range and
optionally enables their input. The dynamic range is handled by means of a base
10 logarithmic scale. In other respects the concept is that of the `Scale`
class.
The control is modelled on old radios where a large scale scrolls past a small
window having a fixed pointer. The use of a logarithmic scale enables the
display and input of a value which can change by many orders of magnitude.
value to span a range of multiple orders of magnitude.
The `Scale` may be `active` or `passive`. A description of the user interface
in the `active` case may be found in
@ -1471,7 +1485,7 @@ decades to be traversed quickly.
Legends for the scale are created dynamically as it scrolls past the window,
with one legend for each decade. The user may control this by means of a
callback, for example to display units, e.g. `10nF`. A further callback
callback, for example to display units, e.g. `10MHz`. A further callback
enables the scale's color to change over its length or in response to other
circumstances.
@ -1489,14 +1503,15 @@ Keyword only arguments (all optional):
* `decades=5` Defines the control's maximum value (i.e. `10**decades`).
* `value=1.0` Initial value for control. Will be constrained to
`1.0 <= value <= 10**decades` if outside this range.
* `height=0` Pass 0 for a minimum height based on the font height.
* `height=0` Default is a minimum height based on the font height.
* `width=160`
* `fgcolor=None` Color of foreground (the control itself). If `None` the
`Writer` foreground default is used.
* `bgcolor=None` Background color of object. If `None` the `Writer` background
default is used.
* `bdcolor=False` Color of border. If `False` no border will be drawn. If a
color is provided, a border line will be drawn around the control.
* `bdcolor=None` Color of border, default `fgcolor`. If `False` no border will
be drawn. If a color is provided, a border line will be drawn around the
control.
* `prcolor=None` If `active`, in precision mode the white focus border changes
to yellow to for a visual indication. An alternative color can be provided.
`WHITE` will defeat this change.
@ -1504,11 +1519,11 @@ Keyword only arguments (all optional):
* `fontcolor=None` Color of legends. Default `WHITE`.
* `legendcb=None` Callback for populating scale legends (see below).
* `tickcb=None` Callback for setting tick colors (see below).
* `callback=dolittle` Callback function which will run when the user moves the
scale or the value is changed programmatically. Default is a null function.
* `callback=dolittle` Callback function which runs when the user moves the
scale or the value is changed programmatically. If the control is `active` it
also runs on instantiation. Default is a null function.
* `args=[]` A list/tuple of arguments for above callback. The callback's
arguments are the `ScaleLog` instance, followed by any user supplied args.
* `value=1.0` Initial value.
* `delta=0.01` This determines the smallest amount of change which can be
achieved with a brief button press. See Control Algorithm below.
* `active=False` Determines whether the widget accepts user input.
@ -1517,7 +1532,7 @@ Methods:
* `value=None` Set or get the current value. Always returns the current value.
A passed `float` is constrained to the range `1.0 <= V <= 10**decades` and
becomes the control's current value. The `ScaleLog` is updated. Always returns
the control's current value. See note below on precision.
the control's current value.
* `greyed_out` Optional Boolean argument `val=None`. If `None` returns the
current 'greyed out' status of the control. Otherwise enables or disables it,
showing it in its new state.
@ -1543,10 +1558,10 @@ reduced by a factor of 10.
### Callback
This receives an initial arg being the widget instance followed by any user
supplied args. They can be bound methods, typically of a `Screen` subclass.
`cb_move` runs when the value changes but before the update is processed,
enabling dynamic color change.
The callback receives an initial arg being the widget instance followed by any
user supplied args. The callback can be a bound method, typically of a `Screen`
subclass. The callback runs when the widget is instantiated and whenever the
value changes. This enables dynamic color change.
### Callback legendcb
@ -1630,7 +1645,7 @@ Keyword only args:
foreground color will be used. If `False` is passed, no pip will be drawn. The
pip is suppressed if the shortest pointer would be hard to see.
Methods:
Method:
1. `text` Updates the label if present (otherwise throws a `ValueError`). Args:
* `text=None` The text to display. If `None` displays last value.
@ -1639,7 +1654,6 @@ Methods:
* `bgcolor=None` Background color, as per foreground.
* `bdcolor=None` Border color. As per above except that if `False` is
passed, no border is displayed. This clears a previously drawn border.
2. `show` No args. (Re)draws the control. Primarily for internal use by GUI.
When a `Pointer` is instantiated it is assigned to the `Dial` by the `Pointer`
constructor.
@ -1678,7 +1692,7 @@ async def run(dial):
mins.value(0 + 0.9j, YELLOW)
dm = cmath.exp(-1j * cmath.pi / 30) # Rotate by 1 minute
dh = cmath.exp(-1j * cmath.pi / 1800) # Rotate hours by 1 minute
# Twiddle the hands: see aclock.py for an actual clock
# Twiddle the hands: see vtest.py for an actual clock
while True:
await asyncio.sleep_ms(200)
mins.value(mins.value() * dm, RED)
@ -1717,23 +1731,24 @@ Constructor mandatory positional args:
Optional keyword only arguments:
* `height=70` Dimension of the square bounding box.
* `arc=TWOPI` Movement available. Default 2*PI radians (360 degrees).
* `arc=TWOPI` Movement available. Default 2*PI radians (360 degrees). May be
reduced, e.g. to provide a 270° range of movement.
* `ticks=9` Number of graduations around the dial.
* `value=0.0` Initial value. By default the knob will be at its most
counter-clockwise position.
* `fgcolor=None` Color of foreground (the control itself). If `None` the
`Writer` foreground default is used.
* `bgcolor=None` Background color of object. If `None` the `Writer` background
default is used.
* `color=None` Fill color for the control knob. Default: no fill.
* `bdcolor=False` Color of border. If `False` no border will be drawn. If a
color is provided, a border line will be drawn around the control.
* `prcolor=None` If `active`, in precision mode the white focus border changes
to yellow to for a visual indication. An alternative color can be provided.
`WHITE` will defeat this change.
* `color=None` Fill color for the control knob. Default: no fill.
* `callback=dolittle` Callback function runs when the user moves the knob or
the value is changed programmatically.
* `args=[]` A list/tuple of arguments for above callback.
* `value=0.0` Initial value. By default the knob will be at its most
counter-clockwise position.
* `active=True` Enable user input via the `increase` and `decrease` buttons.
Methods:
@ -1744,6 +1759,13 @@ Methods:
correspond to the new value. The move callback will run. The method constrains
the range to 0.0 to 1.0. Always returns the control's value.
### Callback
The callback receives an initial arg being the widget instance followed by any
user supplied args. The callback can be a bound method, typically of a `Screen`
subclass. The callback runs when the widget is instantiated and whenever the
value changes. This enables dynamic color change.
###### [Contents](./README.md#0-contents)
# 22. Graph Plotting

Wyświetl plik

@ -21,7 +21,7 @@ display = None # Singleton instance
ssd = None
gc.collect()
__version__ = (0, 1, 0)
__version__ = (0, 1, 1)
# Null function
dolittle = lambda *_ : None
@ -688,21 +688,24 @@ class Widget:
class LinearIO(Widget):
def __init__(self, writer, row, col, height, width,
fgcolor, bgcolor, bdcolor,
value=None, active=True,
value=None, active=True, prcolor=False,
min_delta=0.01, max_delta=0.1):
self.min_delta = min_delta
self.max_delta = max_delta
super().__init__(writer, row, col, height, width,
fgcolor, bgcolor, bdcolor,
value, active)
# Handle variable precision
# Handle variable precision. Start normal
self.precision = False
# 1 sec long press to set precise
self.lpd = Delay_ms(self.precise, (True,))
# Precision mode can only be entered when the active control has focus.
# In this state it will have a white border. By default this turns yellow
# but subclass can be defeat this with None or another color
self.prcolor = YELLOW
self.do_precision = prcolor is not False
if self.do_precision:
# Subclass supports precision mode
# 1 sec long press to set precise
self.lpd = Delay_ms(self.precise, (True,))
# Precision mode can only be entered when the active control has focus.
# In this state it will have a white border. By default this turns yellow
# but subclass can be defeat this with WHITE or another color
self.prcolor = YELLOW if prcolor is None else prcolor
def do_up(self, button):
asyncio.create_task(self.btnhan(button, 1))
@ -717,7 +720,7 @@ class LinearIO(Widget):
maxd = self.max_delta
else:
d = self.min_delta
maxd = d * 4 # Why move fast in slow mode?
maxd = d * 4 # Why move fast in precision mode?
self.value(self.value() + up * d)
t = ticks_ms()
while not button():
@ -729,17 +732,17 @@ class LinearIO(Widget):
def precise(self, v): # Timed out while button pressed
self.precision = v
if self.prcolor is not None:
self.draw = True
self.draw = True
def do_sel(self): # Select button was pushed
if self.precision: # Already in mode
self.precise(False)
else: # Require a long press to enter mode
self.lpd.trigger()
if self.do_precision: # Subclass handles precision mode
if self.precision: # Already in mode
self.precise(False)
else: # Require a long press to enter mode
self.lpd.trigger()
def unsel(self): # Select button was released
self.lpd.stop()
self.do_precision and self.lpd.stop()
def leave(self): # Control has lost focus
self.precise(False)

Wyświetl plik

@ -42,13 +42,13 @@ class BaseScreen(Screen):
self.lbl = Label(wri, row, col, 70, bdcolor=RED)
self.vslider = Slider(wri, 2, 2, callback=self.slider_cb,
bdcolor=RED, slotcolor=BLUE, prcolor=CYAN,
bdcolor=RED, slotcolor=BLUE,
legends=('0.0', '0.5', '1.0'), value=0.5)
col = 80
row = 15
self.hslider = HorizSlider(wri, row, col, callback=self.slider_cb,
bdcolor=YELLOW, slotcolor=BLUE,
bdcolor=GREEN, slotcolor=BLUE,
legends=('0.0', '0.5', '1.0'), value=0.7)
row += 30
self.scale = Scale(wri, row, col, width = 150, tickcb = tickcb,
@ -70,7 +70,7 @@ class BaseScreen(Screen):
def cb(self, obj):
self.lbl.value('{:4.2f}'.format(obj.value()))
self.lbl.value('{:5.3f}'.format(obj.value()))
def cbcb(self, cb):
val = cb.value()

Wyświetl plik

@ -16,7 +16,7 @@ class Knob(LinearIO):
def __init__(self, writer, row, col, *, height=70, arc=TWOPI, ticks=9, value=0.0,
fgcolor=None, bgcolor=None, color=None, bdcolor=None, prcolor=None,
callback=dolittle, args=[], active=True):
super().__init__(writer, row, col, height, height, fgcolor, bgcolor, bdcolor, value, active)
super().__init__(writer, row, col, height, height, fgcolor, bgcolor, bdcolor, value, active, prcolor)
super()._set_callbacks(callback, args)
radius = height / 2
self.arc = min(max(arc, 0), TWOPI) # Usable angle of control
@ -28,10 +28,8 @@ class Knob(LinearIO):
self.ticks = max(ticks, 2) # start and end of travel
self.color = color
self.draw = True # Ensure a redraw on next refresh
if active: # Run callback (e.g. to set dynamic colors)
if prcolor is not None:
self.prcolor = prcolor # Option for different bdcolor in precision mode
self.callback(self, *self.args)
# Run callback (e.g. to set dynamic colors)
self.callback(self, *self.args)
def show(self):
if super().show(False): # Honour bgcolor

Wyświetl plik

@ -37,9 +37,8 @@ class Scale(LinearIO):
else:
ctrl_ht = height - min_ht # adjust ticks for greater height
width &= 0xfffe # Make divisible by 2: avoid 1 pixel pointer offset
super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor, self._to_int(value), active)
if active:
super()._set_callbacks(callback, args)
super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor, self._to_int(value), active, prcolor)
super()._set_callbacks(callback, args)
self.minval = -1.0 # By default scales run from -1.0 to +1.0
self.fontcolor = fontcolor if fontcolor is not None else self.fgcolor
self.x0 = col + 2
@ -57,10 +56,8 @@ class Scale(LinearIO):
self.ldl = ctrl_ht # Large tick
self.ldy0 = ycl - self.ldl // 2
self.draw = True # Ensure a redraw on next refresh
if active: # Run callback (e.g. to set dynamic colors)
if prcolor is not None:
self.prcolor = prcolor # Option for different bdcolor in precision mode
self.callback(self, *self.args)
# Run callback (e.g. to set dynamic colors)
self.callback(self, *self.args)
def show(self):
wri = self.writer

Wyświetl plik

@ -48,9 +48,8 @@ class ScaleLog(LinearIO):
else:
ctrl_ht = height - min_ht # adjust ticks for greater height
width &= 0xfffe # Make divisible by 2: avoid 1 pixel pointer offset
super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor, self._constrain(value), active)
if active:
super()._set_callbacks(callback, args)
super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor, self._constrain(value), active, prcolor)
super()._set_callbacks(callback, args)
self.fontcolor = fontcolor if fontcolor is not None else self.fgcolor
self.x0 = col + 2
@ -69,10 +68,8 @@ class ScaleLog(LinearIO):
self.ldy0 = ycl - self.ldl // 2
self.dw = (self.x1 - self.x0) // 2 # Pixel width of a decade
self.draw = True # Ensure a redraw on next refresh
if active: # Run callback (e.g. to set dynamic colors)
if prcolor is not None:
self.prcolor = prcolor # Option for different bdcolor in precision mode
self.callback(self, *self.args)
# Run callback (e.g. to set dynamic colors)
self.callback(self, *self.args)
# Pre calculated log10(x) for x in range(1, 10)
def show(self, logs=(0.0, 0.3010, 0.4771, 0.6021, 0.6990, 0.7782, 0.8451, 0.9031, 0.9542)):

Wyświetl plik

@ -24,9 +24,8 @@ class Slider(LinearIO):
slotcolor=None, prcolor=None,
callback=dolittle, args=[], value=0.0, active=True):
width &= 0xfe # ensure divisible by 2
super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor, value, active)
if active:
super()._set_callbacks(callback, args)
super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor, value, active, prcolor)
super()._set_callbacks(callback, args)
self.divisions = divisions
self.legends = legends
self.fontcolor = self.fgcolor if fontcolor is None else fontcolor
@ -42,10 +41,8 @@ class Slider(LinearIO):
self.slot_y0 = row + _SLIDE_DEPTH // 2
self.slot_h = height - _SLIDE_DEPTH - 1
self.draw = True # Ensure a redraw on next refresh
if active: # Run callback (e.g. to set dynamic colors)
if prcolor is not None:
self.prcolor = prcolor # Option for different bdcolor in precision mode
self.callback(self, *self.args)
# Run callback (e.g. to set dynamic colors)
self.callback(self, *self.args)
def show(self):
# Blank slot, ticks and slider
@ -97,9 +94,8 @@ class HorizSlider(LinearIO):
slotcolor=None, prcolor=None,
callback=dolittle, args=[], value=0.0, active=True):
height &= 0xfe # ensure divisible by 2
super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor, value, active)
if active:
super()._set_callbacks(callback, args)
super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor, value, active, prcolor)
super()._set_callbacks(callback, args)
self.divisions = divisions
self.legends = legends
self.fontcolor = self.fgcolor if fontcolor is None else fontcolor
@ -115,10 +111,8 @@ class HorizSlider(LinearIO):
centre = row + height // 2
self.slot_y0 = centre - _HALF_SLOT_WIDTH
self.draw = True # Ensure a redraw on next refresh
if active: # Run callback (e.g. to set dynamic colors)
if prcolor is not None:
self.prcolor = prcolor # Option for different bdcolor in precision mode
self.callback(self, *self.args)
# Run callback (e.g. to set dynamic colors)
self.callback(self, *self.args)
def show(self):
# Blank slot, ticks and slider

Wyświetl plik

@ -0,0 +1,54 @@
# ili9341_pyb.py Customise for your hardware config
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2021 Peter Hinch
# As written, supports:
# ili9341 240x320 displays on Pyboards. On a Pyboard 1.1 frozen bytecode
# is required.
# Edit the driver import for other displays.
# Demo of initialisation procedure designed to minimise risk of memory fail
# when instantiating the frame buffer. The aim is to do this as early as
# possible before importing other modules.
# WIRING
# PB Display
# GPIO Pin
# Vin Vin
# X6 CLK Hardware SPI0
# X8 DATA (AKA SI MOSI)
# Y9 DC
# Y10 Rst
# Gnd Gnd
# Y11 CS
# Pushbuttons are wired between the pin and Gnd
# PB pin Meaning
# X1 Operate current control
# X2 Decrease value of current control
# X3 Select previous control
# X4 Select next control
# X5 Increase value of current control
from machine import Pin, SPI, freq
import gc
from drivers.ili93xx.ili9341 import ILI9341 as SSD
# Create and export an SSD instance
pdc = Pin('Y9', Pin.OUT, value=0) # Arbitrary pins
prst = Pin('Y10', Pin.OUT, value=1)
pcs = Pin('Y11', Pin.OUT, value=1)
spi = SPI(1, baudrate=30_000_000)
gc.collect() # Precaution before instantiating framebuf
ssd = SSD(spi, pcs, pdc, prst, usd=True)
from gui.core.ugui import Display
# Create and export a Display instance
# Define control buttons
nxt = Pin('X4', Pin.IN, Pin.PULL_UP) # Move to next control
sel = Pin('X1', Pin.IN, Pin.PULL_UP) # Operate current control
prev = Pin('X3', Pin.IN, Pin.PULL_UP) # Move to previous control
increase = Pin('X5', Pin.IN, Pin.PULL_UP) # Increase control's value
decrease = Pin('X2', Pin.IN, Pin.PULL_UP) # Decrease control's value
display = Display(ssd, nxt, sel, prev, increase, decrease)