diff --git a/README.md b/README.md index 0e30992..36a4947 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/gui/core/ugui.py b/gui/core/ugui.py index 4c25459..a2ee710 100644 --- a/gui/core/ugui.py +++ b/gui/core/ugui.py @@ -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) diff --git a/gui/demos/active.py b/gui/demos/active.py index c5713e3..fd37208 100644 --- a/gui/demos/active.py +++ b/gui/demos/active.py @@ -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() diff --git a/gui/widgets/knob.py b/gui/widgets/knob.py index f63ff43..2ac2256 100644 --- a/gui/widgets/knob.py +++ b/gui/widgets/knob.py @@ -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 diff --git a/gui/widgets/scale.py b/gui/widgets/scale.py index c50991c..0b3d7a7 100644 --- a/gui/widgets/scale.py +++ b/gui/widgets/scale.py @@ -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 diff --git a/gui/widgets/scale_log.py b/gui/widgets/scale_log.py index 2f7a058..d804988 100644 --- a/gui/widgets/scale_log.py +++ b/gui/widgets/scale_log.py @@ -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)): diff --git a/gui/widgets/sliders.py b/gui/widgets/sliders.py index d4bd2dd..211ff93 100644 --- a/gui/widgets/sliders.py +++ b/gui/widgets/sliders.py @@ -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 diff --git a/setup_examples/ili9341_pyb.py b/setup_examples/ili9341_pyb.py new file mode 100644 index 0000000..0d654b6 --- /dev/null +++ b/setup_examples/ili9341_pyb.py @@ -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)