Meter class now replaces Tstat.

pull/8/head
Peter Hinch 2021-07-10 18:17:10 +01:00
rodzic 4f42847a99
commit 5fa17bb085
9 zmienionych plików z 79 dodań i 73 usunięć

Wyświetl plik

@ -61,6 +61,8 @@ Code is new and issues are likely: please report any found. The project is
under development so check for updates. I also plan to upgrade the performance under development so check for updates. I also plan to upgrade the performance
of some display drivers. of some display drivers.
The encoder interface is under development and currently is rather erratic.
# 0. Contents # 0. Contents
1. [Basic concepts](./README.md#1-basic-concepts) Including installation and test. 1. [Basic concepts](./README.md#1-basic-concepts) Including installation and test.
@ -104,9 +106,8 @@ of some display drivers.
13. [Dropdown widget](./README.md#13-dropdown-widget) Dropdown lists. 13. [Dropdown widget](./README.md#13-dropdown-widget) Dropdown lists.
14. [DialogBox class](./README.md#14-dialogbox-class) Pop-up modal dialog boxes. 14. [DialogBox class](./README.md#14-dialogbox-class) Pop-up modal dialog boxes.
15. [Textbox widget](./README.md#15-textbox-widget) Scrolling text display. 15. [Textbox widget](./README.md#15-textbox-widget) Scrolling text display.
16. [Meter widget](./README.md#16-meter-widget) Display floats on an analog meter. 16. [Meter widget](./README.md#16-meter-widget) Display floats on an analog meter, with data driven callbacks.
16.1 [Tstat widget](./README.md#161-tstat-widget) Meter subclass enables thermostats, alarms etc. 16.1 [Region class](./README.md#161-region-class)
     16.1.1 [Region class](./README.md#1611-region-class)
17. [Slider and HorizSlider widgets](./README.md#17-slider-and-horizslider-widgets) Linear potentiometer float data entry and display 17. [Slider and HorizSlider widgets](./README.md#17-slider-and-horizslider-widgets) Linear potentiometer float data entry and display
18. [Scale widget](./README.md#18-scale-widget) High precision float entry and display. 18. [Scale widget](./README.md#18-scale-widget) High precision float entry and display.
19. [ScaleLog widget](./README.md#19-scalelog-widget) Wide dynamic range float entry and display. 19. [ScaleLog widget](./README.md#19-scalelog-widget) Wide dynamic range float entry and display.
@ -767,7 +768,10 @@ explicitly in code.
This is a `Screen` subclass providing for modal windows. As such it has This is a `Screen` subclass providing for modal windows. As such it has
positional and dimension information. Usage consists of writing a user class positional and dimension information. Usage consists of writing a user class
subclassed from `Window`. Example code is in `demos/screens.py`. subclassed from `Window`. Example code is in `demos/screens.py`. Code in a
window must not attempt to open another `Window` or `Screen`. Doing so will
raise a `ValueError`. Modal behaviour means that the only valid screen change
is a return to the calling screen.
## 5.1 Constructor ## 5.1 Constructor
@ -1392,13 +1396,32 @@ the oldest (topmost) being discarded as required.
# 16. Meter widget # 16. Meter widget
This `passive` widget displays a single floating point value on a vertical
linear scale. Optionally it can support data dependent callbacks.
```python ```python
from gui.widgets.meter import Meter from gui.widgets.meter import Meter
``` ```
![Image](./images/meter.JPG) ![Image](./images/meter.JPG)
The two styles of `meter`, both showing a value of 0.65. This `passive` widget The two styles of `meter`, both showing a value of 0.65. This `passive` widget
provides a vertical linear meter display of values scaled between 0.0 and 1.0. provides a vertical linear meter display of values scaled between 0.0 and 1.0.
In these examples each meter simply displays a data value.
![Image](./images/tstat.JPG)
This example has two data sensitive regions, a control region with hysteresis
and an alarm region. Callbacks can run in response to specific changes in the
`Meter`'s value emulating data-dependent behaviour including alarms and
controls (like thermostats) having hysteresis.
The class supports one or more `Region` instances. Visually these appear as
colored bands on the scale. If the meter's value enters, leaves or crosses one
of these bands a callback is triggered. This receives an arg indicating the
nature of the change which caused the trigger. For example an alarm might be
triggered when the value, initially below the region, enters it or crosses it.
The alarm might be cleared on exit or if crossed from above. Hysteresis as used
in thermostats is simple to implement. Examples of these techniques may be
found in `gui.demos.tstat.py`.
Regions may be modified, added or removed programmatically.
Constructor mandatory positional args: Constructor mandatory positional args:
1. `writer` The `Writer` instance (defines font) to use. 1. `writer` The `Writer` instance (defines font) to use.
@ -1435,55 +1458,45 @@ Methods:
Returns the current value. Returns the current value.
2. `text` Updates the label if present (otherwise throws a `ValueError`). Args: 2. `text` Updates the label if present (otherwise throws a `ValueError`). Args:
* `text=None` The text to display. If `None` displays last value. * `text=None` The text to display. If `None` displays last value.
* ` invert=False` If true, show inverse text. * `invert=False` If true, show inverse text.
* `fgcolor=None` Foreground color: if `None` the `Writer` default is used. * `fgcolor=None` Foreground color: if `None` the `Writer` default is used.
* `bgcolor=None` Background color, as per foreground. * `bgcolor=None` Background color, as per foreground.
* `bdcolor=None` Border color. As per above except that if `False` is * `bdcolor=None` Border color. As per above except that if `False` is
passed, no border is displayed. This clears a previously drawn border. passed, no border is displayed. This clears a previously drawn border.
3. `del_region` Arg: a `Region` instance. Deletes the region. No callback will
run.
### Legends ### Legends
Depending on the font in use for legends additional space may be required above Depending on the font in use for legends additional space may be required above
and below the `Meter` to display the top and bottom legends. and below the `Meter` to display the top and bottom legends.
###### [Contents](./README.md#0-contents) ### Example of use of Regions
## 16.1 Tstat widget
```python ```python
from gui.widgets.tstat import Tstat # Instantiate Meter
ts = Meter(wri, row, sl.mcol + 5, ptcolor=YELLOW, height=100, width=15,
style=Meter.BAR, legends=('0.0', '0.5', '1.0'))
# Instantiate two Regions and associate with the Meter instance.
reg = Region(ts, 0.4, 0.55, MAGENTA, ts_cb)
al = Region(ts, 0.9, 1.0, RED, al_cb)
``` ```
![Image](./images/tstat.JPG) The callback `ts_cb` will run in response to data values between 0.4 and 0.55:
This example has two data sensitive regions, a control region with hysteresis if the value enters that range having been outside it, if it leaves the range,
and an alarm region. or if successive values are either side of the range. The `al_cb` callback
behaves similarly for data values between 0.9 and 1.0.
The `Tstat` subclass of `Meter` is a `passive` widget with callbacks which run ###### [Contents](./README.md#0-contents)
in response to specific changes in the object's value. Notionally based on a
thermostat, it can implement objects with a variety of data-dependent behaviour
including alarms and controls having hysteresis.
The class supports one or more `Region` instances. Visually these appear as ## 16.1 Region class
colored bands on the scale. If the meter's value enters, leaves or crosses one
of these bands a callback is triggered. This receives an arg indicating the
nature of the change which caused the trigger. For example an alarm might be
triggered when the value, initially below the region, enters it or crosses it.
The alarm might be cleared on exit or if crossed from above. Hysteresis as used
in thermostats is simple to implement. Examples of these techniques may be
found in `gui.demos.tstat.py`.
Regions may be modified, added or removed programmatically. ```python
from gui.widgets.region import Region
```
Instantiating a `Region` associates it with a supporting widget (currently only
a `Meter`). Constructor positional args are as follows:
Constructor args and methods are as per `Meter`. The `Tstat` class adds the * `tstat` The parent instance.
following method:
1. `del_region` Arg: a `Region` instance. Deletes the region. No callback will
run.
### 16.1.1 Region class
Instantiating a `Region` associates it with a `Tstat`. Constructor args are as
follows:
* `tstat` The `Tstat` instance.
* `vlo` Low value (0 <= `vlo` <= 1.0). * `vlo` Low value (0 <= `vlo` <= 1.0).
* `vhi` High value (`vlo` < `vhi` <= 1.0). * `vhi` High value (`vlo` < `vhi` <= 1.0).
* `color` For visible band. * `color` For visible band.

Wyświetl plik

@ -1,7 +1,7 @@
# colors.py Micropython GUI library for TFT displays: colors and shapes # colors.py Micropython GUI library for TFT displays: colors and shapes
# Released under the MIT License (MIT). See LICENSE. # Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2019 Peter Hinch # Copyright (c) 2019-2021 Peter Hinch
from hardware_setup import SSD from hardware_setup import SSD
# Code can be portable between 4-bit and other drivers by calling create_color # Code can be portable between 4-bit and other drivers by calling create_color
@ -49,8 +49,6 @@ else:
# Color used when clearing the screen # Color used when clearing the screen
BGCOLOR = BLACK BGCOLOR = BLACK
# RA8875 defines colors as 3-tuples with greying-out operating on those.
# Should we assign GREY to any color > 0?
CIRCLE = 1 CIRCLE = 1
RECTANGLE = 2 RECTANGLE = 2
CLIPPED_RECT = 3 CLIPPED_RECT = 3

Wyświetl plik

@ -252,6 +252,8 @@ class Screen:
if forward: if forward:
if isinstance(cls_new_screen, type): if isinstance(cls_new_screen, type):
# Instantiate new screen. __init__ must terminate # Instantiate new screen. __init__ must terminate
if cs_old is not None and cs_old.__name__ == 'Window':
raise ValueError('Windows are modal.')
new_screen = cls_new_screen(*args, **kwargs) new_screen = cls_new_screen(*args, **kwargs)
else: else:
raise ValueError('Must pass Screen class or subclass (not instance)') raise ValueError('Must pass Screen class or subclass (not instance)')

Wyświetl plik

@ -11,6 +11,7 @@ from gui.core.writer import CWriter
from gui.widgets.menu import Menu from gui.widgets.menu import Menu
from gui.widgets.buttons import CloseButton from gui.widgets.buttons import CloseButton
from gui.core.colors import * from gui.core.colors import *
from gui.widgets.dialog import DialogBox
class BaseScreen(Screen): class BaseScreen(Screen):
@ -19,7 +20,13 @@ class BaseScreen(Screen):
print('Top level callback', n) print('Top level callback', n)
def cb_sm(lb, n): def cb_sm(lb, n):
kwargs = {'writer' : wri, 'row': 60, 'col' : 2,
'elements' : (('Yes', GREEN), ('No', RED), ('Foo', YELLOW)),
'label' : 'Test dialog',
}
print('Submenu callback', lb.value(), lb.textvalue(), n) print('Submenu callback', lb.value(), lb.textvalue(), n)
if lb.value() == 0:
Screen.change(DialogBox, kwargs = kwargs)
super().__init__() super().__init__()
mnu = (('Gas', cb_sm, (0,), ('Argon','Neon','Xenon','Radon')), mnu = (('Gas', cb_sm, (0,), ('Argon','Neon','Xenon','Radon')),

Wyświetl plik

@ -13,7 +13,8 @@ from gui.core.ugui import Screen, ssd
from gui.widgets.buttons import Button, CloseButton from gui.widgets.buttons import Button, CloseButton
from gui.widgets.sliders import Slider from gui.widgets.sliders import Slider
from gui.widgets.label import Label from gui.widgets.label import Label
from gui.widgets.tstat import Tstat, Region from gui.widgets.meter import Meter
from gui.widgets.region import Region
from gui.widgets.led import LED from gui.widgets.led import LED
from gui.core.writer import CWriter from gui.core.writer import CWriter
@ -38,8 +39,8 @@ class BaseScreen(Screen):
sl = Slider(wri, row, col, callback=self.slider_cb, sl = Slider(wri, row, col, callback=self.slider_cb,
bdcolor=RED, slotcolor=BLUE, bdcolor=RED, slotcolor=BLUE,
legends=('0.0', '0.5', '1.0')) legends=('0.0', '0.5', '1.0'))
self.ts = Tstat(wri, row, sl.mcol + 5, divisions = 4, ptcolor=YELLOW, height=100, width=15, self.ts = Meter(wri, row, sl.mcol + 5, ptcolor=YELLOW, height=100, width=15,
style=Tstat.BAR, legends=('0.0', '0.5', '1.0')) style=Meter.BAR, legends=('0.0', '0.5', '1.0'))
reg = Region(self.ts, 0.4, 0.55, MAGENTA, self.ts_cb) reg = Region(self.ts, 0.4, 0.55, MAGENTA, self.ts_cb)
al = Region(self.ts, 0.9, 1.0, RED, self.al_cb) al = Region(self.ts, 0.9, 1.0, RED, self.al_cb)
col = self.ts.mcol + 5 col = self.ts.mcol + 5

Wyświetl plik

@ -25,6 +25,7 @@ import uasyncio as asyncio
import utime import utime
import gc import gc
defaults['focus'] = YELLOW
class FooScreen(Screen): class FooScreen(Screen):
def __init__(self): def __init__(self):

Wyświetl plik

@ -11,9 +11,9 @@ from gui.widgets.buttons import Button
from gui.widgets.listbox import Listbox from gui.widgets.listbox import Listbox
from gui.core.colors import * from gui.core.colors import *
# A SubMenu is a Window containing a Listbox
# Next and Prev close the listbox without running the callback. This is # Next and Prev close the listbox without running the callback. This is
# handled by Screen .move bound method # handled by Screen .move bound method
# A SubMenu is a Window containing a Listbox
class SubMenu(Window): class SubMenu(Window):
def __init__(self, menu, button, elements, cb, args): # menu is parent Menu def __init__(self, menu, button, elements, cb, args): # menu is parent Menu
@ -39,6 +39,8 @@ class SubMenu(Window):
Screen.back() Screen.back()
self.cb(obj_listbox, *self.args) # CB can access obj_listbox.value() or .textvalue() self.cb(obj_listbox, *self.args) # CB can access obj_listbox.value() or .textvalue()
# A Menu is a set of Button objects at the top of the screen. On press, Buttons either run the
# user callback or instantiate a SubMenu
class Menu: class Menu:
def __init__(self, writer, *, height=25, bgcolor=None, fgcolor=None, textcolor=None, select_color=DARKBLUE, args): # ((text, cb, (args,)),(text, cb, (args,), (elements,)), ...) def __init__(self, writer, *, height=25, bgcolor=None, fgcolor=None, textcolor=None, select_color=DARKBLUE, args): # ((text, cb, (args,)),(text, cb, (args,), (elements,)), ...)

Wyświetl plik

@ -34,6 +34,7 @@ class Meter(Widget):
yl -= dy yl -= dy
mcol = max(mcol, l.mcol) mcol = max(mcol, l.mcol)
self.mcol = mcol - 2 # For metrics. Legends never have border. self.mcol = mcol - 2 # For metrics. Legends never have border.
self.regions = set()
self.value(value) self.value(value)
def value(self, n=None, color=None): def value(self, n=None, color=None):
@ -42,6 +43,8 @@ class Meter(Widget):
n = super().value(min(1, max(0, n))) n = super().value(min(1, max(0, n)))
if color is not None: if color is not None:
self.ptcolor = color self.ptcolor = color
for r in self.regions:
r.check(n)
return n return n
def show(self): def show(self):
@ -54,7 +57,10 @@ class Meter(Widget):
x1 = self.col + width x1 = self.col + width
y0 = self.row y0 = self.row
y1 = self.row + height y1 = self.row + height
self.preshow(x0, y1, width, height) # Subclass draws regions for r in self.regions:
ht = round(height * (r.vhi - r.vlo))
yr = y1 - round(height * r.vhi)
display.fill_rect(x0, yr, width, ht, r.color)
if self.divisions > 0: if self.divisions > 0:
dy = height / (self.divisions) # Tick marks dy = height / (self.divisions) # Tick marks
for tick in range(self.divisions + 1): for tick in range(self.divisions + 1):
@ -68,5 +74,6 @@ class Meter(Widget):
w = width / 2 w = width / 2
display.fill_rect(int(x0 + w - 2), y, 4, y1 - y, self.ptcolor) display.fill_rect(int(x0 + w - 2), y, 4, y1 - y, self.ptcolor)
def preshow(self, x, y, width, height): def del_region(self, reg):
pass # For subclass self.regions.discard(reg)
self.draw = True

Wyświetl plik

@ -1,4 +1,4 @@
# tstat.py Extension to nanogui providing the Tstat class # region.py Extension to nanogui providing the Region class
# Released under the MIT License (MIT). See LICENSE. # Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2021 Peter Hinch # Copyright (c) 2021 Peter Hinch
@ -100,28 +100,3 @@ class Region:
self.wa = v < vlo self.wa = v < vlo
# If it was in old region treat as if leaving it # If it was in old region treat as if leaving it
self.check(v) self.check(v)
class Tstat(Meter):
def __init__(self, *args, **kwargs):
self.regions = set()
super().__init__(*args, **kwargs)
def del_region(self, reg):
self.regions.discard(reg)
self.draw = True
# Called by subclass prior to drawing scale and data
def preshow(self, x, y, width, height):
for r in self.regions:
ht = round(height * (r.vhi - r.vlo))
y1 = y - round(height * r.vhi)
display.fill_rect(x, y1, width, ht, r.color)
def value(self, n=None, color=None):
if n is None:
return super().value()
v = super().value(n, color)
for r in self.regions:
r.check(v)
return v