From 35064c35015ea52ef83cbd0835eba3a4c03030a7 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 8 Nov 2021 11:35:06 +0000 Subject: [PATCH] Add Adjuster widget and demos. --- README.md | 118 +++++++++++++++++++++++++++++----------- gui/demos/adjust_vec.py | 52 ++++++++++++++++++ gui/demos/adjuster.py | 58 ++++++++++++++++++++ gui/widgets/adjuster.py | 49 +++++++++++++++++ hardware_setup.py | 2 +- 5 files changed, 246 insertions(+), 33 deletions(-) create mode 100644 gui/demos/adjust_vec.py create mode 100644 gui/demos/adjuster.py create mode 100644 gui/widgets/adjuster.py diff --git a/README.md b/README.md index 2fe38ae..b75a597 100644 --- a/README.md +++ b/README.md @@ -117,21 +117,22 @@ there is a workround if it's impossible to upgrade. See 19. [ScaleLog widget](./README.md#19-scalelog-widget) Wide dynamic range float entry and display. 20. [Dial widget](./README.md#20-dial-widget) Display multiple vectors. 21. [Knob widget](./README.md#21-knob-widget) Rotary potentiometer float entry. -22. [Menu class](./README.md#22-menu-class) -23. [Graph plotting](./README.md#22-graph-plotting) Widgets for Cartesian and polar graphs. - 23.1 [Concepts](./README.md#231-concepts) -      23.1.1 [Graph classes](./README.md#2311-graph-classes) -      23.1.2 [Curve classes](./README.md#2312-curve-classes) -      23.1.3 [Coordinates](./README.md#2313-coordinates) - 23.2 [Graph classes](./README.md#231-graph-classes) -      23.2.1 [Class CartesianGraph](./README.md#2321-class-cartesiangraph) -      23.2.2 [Class PolarGraph](./README.md#2322-class-polargraph) - 23.3 [Curve classes](./README.md#233-curve-classes) -      23.3.1 [Class Curve](./README.md#2331-class-curve) -      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. +22. [Adjuster widget](./README.md#22-adjuster-widget) Space saving way to enter floats. +23. [Menu class](./README.md#23-menu-class) +24. [Graph plotting](./README.md#24-graph-plotting) Widgets for Cartesian and polar graphs. + 24.1 [Concepts](./README.md#241-concepts) +      24.1.1 [Graph classes](./README.md#2411-graph-classes) +      24.1.2 [Curve classes](./README.md#2412-curve-classes) +      24.1.3 [Coordinates](./README.md#2413-coordinates) + 24.2 [Graph classes](./README.md#242-graph-classes) +      24.2.1 [Class CartesianGraph](./README.md#2421-class-cartesiangraph) +      24.2.2 [Class PolarGraph](./README.md#2422-class-polargraph) + 24.3 [Curve classes](./README.md#243-curve-classes) +      24.3.1 [Class Curve](./README.md#2431-class-curve) +      24.3.2 [Class PolarCurve](./README.md#2432-class-polarcurve) + 24.4 [Class TSequence](./README.md#244-class-tsequence) Plotting realtime, time sequential data. +25. [Old firmware](./README.md#25-old-firmware) For users of color displays who can't run current firmware. +26. [Realtime applications](./README.md#26-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 @@ -520,6 +521,8 @@ minimal and aim to demonstrate a single technique. * `tbox.py` Text boxes and user-controlled scrolling. * `tstat.py` A demo of the `Meter` class with data sensitive regions. * `menu.py` A multi-level menu. + * `adjuster.py` Simple demo of the `Adjuster` control. + * `adjust_vec.py` A pair of `Adjuster`s vary a vector. ### 1.11.2 Test scripts @@ -2272,8 +2275,8 @@ Optional keyword only arguments: * `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. + to yellow for a visual indication. An alternative color can be provided. + `WHITE` defeats this change; `False` disables precision mode. * `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. @@ -2296,7 +2299,58 @@ value changes. This enables dynamic color change. ###### [Contents](./README.md#0-contents) -# 22 Menu class +# 22. Adjuster widget + +```python +from gui.widgets.adjuster import Adjuster +``` +The `Adjuster` is a space saving version of the `Knob`. It is normally paired +with a `Label` which provides user feedback of the value. It has a range of +0.0 to 1.0 and a visual arc of 270°. User code can provide arbitrary scaling +or nonlinear operation. This is demonstrated in `demos/adjuster.py`. The +widget was inspired by discussions with the author of +[this project](https://www.instructables.com/Poor-Mans-Waveform-Generator-Based-on-RP2040-Raspb/). + + +Constructor mandatory positional args: + 1. `writer` The `Writer` instance. This defines the control's height. + 2. `row` Location on screen. + 3. `col` + +Optional keyword only arguments: + * `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. + * `prcolor=None` In precision mode the white focus border changes to yellow + for a visual indication. An alternative color can be provided. `WHITE` defeats + the change; `False` disables precision mode. + * `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. + +Methods: + * `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. + * `value` Optional argument `val`. If set, adjusts the pointer to + 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. Typically the callback will adjust the text displayed on a +linked label. + +###### [Contents](./README.md#0-contents) + +# 23 Menu class ```python from gui.widgets.menu import Menu @@ -2404,7 +2458,7 @@ different callback if the application required it. ###### [Contents](./README.md#0-contents) -# 23. Graph Plotting +# 24. Graph Plotting ```python from gui.widgets.graph import PolarGraph, PolarCurve, CartesianGraph, Curve, TSequence @@ -2417,7 +2471,7 @@ from gui.widgets.graph import PolarGraph, PolarCurve, CartesianGraph, Curve, TSe For example code see `gui/demos/plot.py`. -## 23.1 Concepts +## 24.1 Concepts Data for Cartesian graphs constitutes a sequence of x, y pairs, for polar graphs it is a sequence of complex `z` values. The module supports three @@ -2427,13 +2481,13 @@ common cases: 3. One or more `y` values arrive gradually. The `X` axis represents time. This is a simplifying case of 2. -### 23.1.1 Graph classes +### 24.1.1 Graph classes A user program first instantiates a graph object (`PolarGraph` or `CartesianGraph`). This creates an empty graph image upon which one or more curves may be plotted. Graphs are passive widgets so cannot accept user input. -### 23.1.2 Curve classes +### 24.1.2 Curve classes The user program then instantiates one or more curves (`Curve` or `PolarCurve`) as appropriate to the graph. Curves may be assigned colors to @@ -2448,7 +2502,7 @@ Where it is required to plot realtime data as it arrives, this is achieved via calls to the curve's `point` method. If a prior point exists it causes a line to be drawn connecting the point to the last one drawn. -### 23.1.3 Coordinates +### 24.1.3 Coordinates `PolarGraph` and `CartesianGraph` objects are subclassed from `Widget` and are positioned accordingly by `row` and `col` with a 2-pixel outside border. The @@ -2464,9 +2518,9 @@ unit circle but will be clipped to the rectangular graph boundary. ###### [Contents](./README.md#0-contents) -## 23.2 Graph classes +## 24.2 Graph classes -### 23.2.1 Class CartesianGraph +### 24.2.1 Class CartesianGraph Constructor. Mandatory positional arguments: @@ -2493,7 +2547,7 @@ Keyword only arguments (all optional): Method: * `show` No args. Redraws the empty graph. Used when plotting time sequences. -### 23.2.2 Class PolarGraph +### 24.2.2 Class PolarGraph Constructor. Mandatory positional arguments: @@ -2518,9 +2572,9 @@ Method: ###### [Contents](./README.md#0-contents) -## 23.3 Curve classes +## 24.3 Curve classes -### 23.3.1 Class Curve +### 24.3.1 Class Curve The Cartesian curve constructor takes the following positional arguments: @@ -2560,7 +2614,7 @@ To plot x values from 1000 to 4000 we would set the `origin` x value to 1000 and the `excursion` x value to 3000. The `excursion` values scale the plotted values to fit the corresponding axis. -### 23.3.2 Class PolarCurve +### 24.3.2 Class PolarCurve The constructor takes the following positional arguments: @@ -2594,7 +2648,7 @@ Complex points should lie within the unit circle to be drawn within the grid. ###### [Contents](./README.md#0-contents) -## 23.4 Class TSequence +## 24.4 Class TSequence A common task is the acquisition and plotting of real time data against time, such as hourly temperature and air pressure readings. This class facilitates @@ -2658,7 +2712,7 @@ class TSeq(Screen): ``` ###### [Contents](./README.md#0-contents) -# 24. Old firmware +# 25. Old firmware Current firmware is highly recommended. For users of color displays who cannot run V1.17 or later it is possible to run under V1.15+. This involves copying @@ -2666,7 +2720,7 @@ 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 +# 26. 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 diff --git a/gui/demos/adjust_vec.py b/gui/demos/adjust_vec.py new file mode 100644 index 0000000..82ab077 --- /dev/null +++ b/gui/demos/adjust_vec.py @@ -0,0 +1,52 @@ +# adjust_vec.py Demo of Adjusters linked to a dial + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + +# hardware_setup must be imported before other modules because of RAM use. +import hardware_setup # Create a display instance +import cmath +from gui.core.ugui import Screen, ssd + +from gui.widgets.label import Label +from gui.widgets.buttons import CloseButton +from gui.widgets.adjuster import Adjuster +from gui.widgets.dial import Dial, Pointer +from gui.core.writer import CWriter + +# Font for CWriter +import gui.fonts.font10 as font +from gui.core.colors import * + + +class BaseScreen(Screen): + + def __init__(self): + + super().__init__() + wri = CWriter(ssd, font, GREEN, BLACK) + col = 2 + row = 2 + self.dial = Dial(wri, row, col, bdcolor=None) + self.vec = Pointer(self.dial) + a = Adjuster(wri, row, self.dial.mcol + 2, fgcolor=BLUE, callback=self.phi_cb) + Label(wri, row, a.mcol + 2, "ϕ", fgcolor=BLUE) + a = Adjuster(wri, row + 20, self.dial.mcol + 2, value=1, fgcolor=MAGENTA, callback=self.r_cb) + Label(wri, row + 20, a.mcol + 2, "r", fgcolor=MAGENTA) + CloseButton(wri) # Quit the application + + def phi_cb(self, adj): + v = adj.value() + r, phi = cmath.polar(self.vec.value()) + self.vec.value(cmath.rect(r, v * 2 * cmath.pi)) + + def r_cb(self, adj): + v = adj.value() + r, phi = cmath.polar(self.vec.value()) + self.vec.value(cmath.rect(v, phi)) + +def test(): + print('Alter a vector using Adjuster control.') + Screen.change(BaseScreen) # A class is passed here, not an instance. + +test() diff --git a/gui/demos/adjuster.py b/gui/demos/adjuster.py new file mode 100644 index 0000000..2c71fd9 --- /dev/null +++ b/gui/demos/adjuster.py @@ -0,0 +1,58 @@ +# adjuster.py Demo of Adjusters linked to Labels + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + +# 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.label import Label +from gui.widgets.buttons import CloseButton +from gui.widgets.adjuster import Adjuster +from gui.core.writer import CWriter + +# Font for CWriter +import gui.fonts.font10 as font +from gui.core.colors import * + + +class BaseScreen(Screen): + + def __init__(self): + + super().__init__() + wri = CWriter(ssd, font, GREEN, BLACK) + col = 2 + row = 2 + self.lbl1 = Label(wri, row, col, 60, bdcolor=RED) + a = Adjuster(wri, row, self.lbl1.mcol + 2, fgcolor=RED, callback=self.adj1_callback) + Label(wri, row, a.mcol + 2, "Simple") + row = self.lbl1.mrow + 5 + self.lbl2 = Label(wri, row, col, 60, bdcolor=RED) + a =Adjuster(wri, row, self.lbl2.mcol + 2, fgcolor=RED, value=0.5, callback=self.adj2_callback) + Label(wri, row, a.mcol + 2, "Scale") + row = self.lbl2.mrow + 5 + self.lbl3 = Label(wri, row, col, 60, bdcolor=YELLOW) + a = Adjuster(wri, row, self.lbl3.mcol + 2, fgcolor=YELLOW, callback=self.adj3_callback) + Label(wri, row, a.mcol + 2, "Log") + CloseButton(wri) # Quit the application + + def adj1_callback(self, adj): + v = adj.value() # Typically do mapping here + self.lbl1.value(f'{v:4.2f}') + + def adj2_callback(self, adj): + v = (adj.value() - 0.5) * 10 # Scale and offset + self.lbl2.value(f'{v:4.2f}') + + def adj3_callback(self, adj): + v = 10 ** (3 * adj.value()) # Log 3 decades + self.lbl3.value(f'{v:4.2f}') + + +def test(): + print('Demo of Adjuster control.') + Screen.change(BaseScreen) # A class is passed here, not an instance. + +test() diff --git a/gui/widgets/adjuster.py b/gui/widgets/adjuster.py new file mode 100644 index 0000000..97edbc2 --- /dev/null +++ b/gui/widgets/adjuster.py @@ -0,0 +1,49 @@ +# adjuster.py Tiny control knob (rotary potentiometer) widget + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + +from gui.core.ugui import LinearIO, display +import math + +TWOPI = 2 * math.pi +# Null function +dolittle = lambda *_ : None + +# *********** CONTROL KNOB CLASS *********** + +class Adjuster(LinearIO): + def __init__(self, writer, row, col, *, value=0.0, + fgcolor=None, bgcolor=None, color=None, prcolor=None, + callback=dolittle, args=[]): + height = writer.height # Match a user-linked Label + super().__init__(writer, row, col, height, height, fgcolor, + bgcolor, False, value, True, prcolor) + super()._set_callbacks(callback, args) + radius = height / 2 + self.arc = 1.5 * math.pi # Usable angle of control + self.radius = radius + self.xorigin = col + radius + self.yorigin = row + radius + self.color = color + self.draw = True # Ensure a redraw on next refresh + # Run callback (e.g. to set dynamic colors) + self.callback(self, *self.args) + + def show(self): + if super().show(False): # Honour bgcolor + arc = self.arc + radius = self.radius + if self.color is not None: + display.fillcircle(self.xorigin, self.yorigin, radius, self.color) + display.circle(self.xorigin, self.yorigin, radius, self.fgcolor) + display.circle(self.xorigin, self.yorigin, radius, self.fgcolor) + self._drawpointer(self._value, self.fgcolor) # draw new + + def _drawpointer(self, value, color): + arc = self.arc + length = self.radius - 1 + angle = value * arc - arc / 2 + x_end = int(self.xorigin + length * math.sin(angle)) + y_end = int(self.yorigin - length * math.cos(angle)) + display.line(int(self.xorigin), int(self.yorigin), x_end, y_end, color) diff --git a/hardware_setup.py b/hardware_setup.py index df02c1b..3049d80 100644 --- a/hardware_setup.py +++ b/hardware_setup.py @@ -51,4 +51,4 @@ sel = Pin(16, Pin.IN, Pin.PULL_UP) # Operate current control prev = Pin(18, Pin.IN, Pin.PULL_UP) # Move to previous control increase = Pin(20, Pin.IN, Pin.PULL_UP) # Increase control's value decrease = Pin(17, Pin.IN, Pin.PULL_UP) # Decrease control's value -display = Display(ssd, nxt, sel, prev, increase, decrease) +display = Display(ssd, nxt, sel, prev, increase, decrease, 5)