kopia lustrzana https://github.com/peterhinch/micropython-micro-gui
Changes to Tstat logic. README corrections.
rodzic
b75f3558ad
commit
a75eda8e85
46
README.md
46
README.md
|
@ -438,6 +438,7 @@ minimal and aim to demonstrate a single technique.
|
||||||
* `aclock.py` An analog clock using the `Dial` vector display. Also shows
|
* `aclock.py` An analog clock using the `Dial` vector display. Also shows
|
||||||
screen layout using widget metrics. Has a simple `uasyncio` task.
|
screen layout using widget metrics. Has a simple `uasyncio` task.
|
||||||
* `tbox.py` Text boxes and user-controlled scrolling.
|
* `tbox.py` Text boxes and user-controlled scrolling.
|
||||||
|
* `tstat.py` A demo of the `Tstat` class.
|
||||||
|
|
||||||
### 1.11.2 Test scripts
|
### 1.11.2 Test scripts
|
||||||
|
|
||||||
|
@ -893,7 +894,7 @@ Constructor mandatory positional args:
|
||||||
|
|
||||||
Keyword only args:
|
Keyword only args:
|
||||||
|
|
||||||
* `height=12` Height of LED.
|
* `height=30` Height of LED.
|
||||||
* `fgcolor=None` Color of foreground (the control itself). If `None` the
|
* `fgcolor=None` Color of foreground (the control itself). If `None` the
|
||||||
`Writer` foreground default is used.
|
`Writer` foreground default is used.
|
||||||
* `bgcolor=None` Background color of object. If `None` the `Writer` background
|
* `bgcolor=None` Background color of object. If `None` the `Writer` background
|
||||||
|
@ -901,21 +902,16 @@ Keyword only args:
|
||||||
* `bdcolor=False` Color of border. If `False` no border will be drawn. If a
|
* `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.
|
color is provided, a border line will be drawn around the control.
|
||||||
shown in the foreground color. If a color is passed, it is used.
|
shown in the foreground color. If a color is passed, it is used.
|
||||||
* `label=None` A text string will cause a `Label` to be drawn below the
|
* `color=RED` Color when illuminated (i.e. if `value` is `True`.
|
||||||
LED. An integer will create a `Label` of that width for later use.
|
|
||||||
|
|
||||||
Methods:
|
Methods:
|
||||||
1. `value` arg `val=None` If `True` is passed, lights the `LED` in its current
|
1. `value` arg `val=None` If `True` is passed, lights the `LED` in its current
|
||||||
color. `False` extinguishes it. `None` has no effect. Returns current value.
|
color. `False` extinguishes it. `None` has no effect. Returns current value.
|
||||||
2. `color` arg `c=None` Change the LED color to `c`. If `c` is `None` the LED
|
2. `color` arg `c=None` Change the LED color to `c`. If `c` is `None` the LED
|
||||||
is turned off (rendered in the background color).
|
is turned off (rendered in the background color).
|
||||||
3. `text` Updates the label if present (otherwise throws a `ValueError`). Args:
|
|
||||||
* `text=None` The text to display. If `None` displays last value.
|
Note that `__call__` is a synonym for `value`. An `LED` instance can be
|
||||||
* ` invert=False` If true, show inverse text.
|
controlled with `led(True)` or `led(False)`.
|
||||||
* `fgcolor=None` Foreground color: if `None` the `Writer` default is used.
|
|
||||||
* `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.
|
|
||||||
|
|
||||||
###### [Contents](./README.md#0-contents)
|
###### [Contents](./README.md#0-contents)
|
||||||
|
|
||||||
|
@ -983,8 +979,9 @@ Constructor mandatory positional args:
|
||||||
|
|
||||||
Optional keyword only arguments:
|
Optional keyword only arguments:
|
||||||
* `shape=RECTANGLE` Must be `CIRCLE`, `RECTANGLE` or `CLIPPED_RECT`.
|
* `shape=RECTANGLE` Must be `CIRCLE`, `RECTANGLE` or `CLIPPED_RECT`.
|
||||||
* `height=20` Height of the bounding box.
|
* `width=50` Width of button. If `text` is supplied and `width` is too low to
|
||||||
* `width=50` Width of the bounding box.
|
accommodate the text, it will be increased to enable the text to fit.
|
||||||
|
* `height=20` Height. In `CIRCLE` case any passed value is ignored.
|
||||||
* `fgcolor=None` Color of foreground (the control itself). If `None` the
|
* `fgcolor=None` Color of foreground (the control itself). If `None` the
|
||||||
`Writer` foreground default is used.
|
`Writer` foreground default is used.
|
||||||
* `bgcolor=None` Background color of object. If `None` the `Writer` background
|
* `bgcolor=None` Background color of object. If `None` the `Writer` background
|
||||||
|
@ -1451,19 +1448,24 @@ and below the `Meter` to display the top and bottom legends.
|
||||||
|
|
||||||
## 16.1 Tstat widget
|
## 16.1 Tstat widget
|
||||||
|
|
||||||
This subclass of `Meter` supports one or more `Region` instances. Visually
|
This subclass of `Meter` is also a `passive` widget but provides for callbacks
|
||||||
these appear as colored bands on the scale. If the meter's value enters, leaves
|
which run in response to specific changes in the object's value.
|
||||||
or crosses one of these bands a callback is triggered which receives an arg
|
|
||||||
indicating the nature of the change which caused the trigger. For example an
|
The class supports one or more `Region` instances. Visually these appear as
|
||||||
alarm might be triggered on entry or traverse through a region from below, and
|
colored bands on the scale. If the meter's value enters, leaves or crosses one
|
||||||
cleared by an exit or traverse from above. Hysteresis as used in thermostats is
|
of these bands a callback is triggered. This receives an arg indicating the
|
||||||
simple to implement.
|
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.
|
Regions may be modified, added or removed programmatically.
|
||||||
|
|
||||||
Constructor args and methods are as per `Meter`. The `Tstat` class adds the
|
Constructor args and methods are as per `Meter`. The `Tstat` class adds the
|
||||||
following method:
|
following method:
|
||||||
1. `del_region` Arg: a `Region` instance. Deletes the region.
|
1. `del_region` Arg: a `Region` instance. Deletes the region. No callback will
|
||||||
|
run.
|
||||||
|
|
||||||
### 16.1.1 Region class
|
### 16.1.1 Region class
|
||||||
|
|
||||||
|
@ -1515,6 +1517,10 @@ operator if you prefer that coding style:
|
||||||
```python
|
```python
|
||||||
if reason & (reg.EX_WA_IB | reg.T_IB): # Leaving region heading down
|
if reason & (reg.EX_WA_IB | reg.T_IB): # Leaving region heading down
|
||||||
```
|
```
|
||||||
|
On instantiation of a `Region` callbacks do not run. The desirability of this
|
||||||
|
is application dependent. If the user `Screen` is provided with an `after_open`
|
||||||
|
method, this can be used to assign a value to the `Tstat` to cause region
|
||||||
|
callbacks to run as appropriate.
|
||||||
|
|
||||||
###### [Contents](./README.md#0-contents)
|
###### [Contents](./README.md#0-contents)
|
||||||
|
|
||||||
|
|
|
@ -572,6 +572,9 @@ class Widget:
|
||||||
self.callback(self, *self.args)
|
self.callback(self, *self.args)
|
||||||
return self._value
|
return self._value
|
||||||
|
|
||||||
|
def __call__(self, val=None):
|
||||||
|
return self.value(val)
|
||||||
|
|
||||||
# Some widgets (e.g. Dial) have an associated Label
|
# Some widgets (e.g. Dial) have an associated Label
|
||||||
def text(self, text=None, invert=False, fgcolor=None, bgcolor=None, bdcolor=None):
|
def text(self, text=None, invert=False, fgcolor=None, bgcolor=None, bdcolor=None):
|
||||||
if hasattr(self, 'label'):
|
if hasattr(self, 'label'):
|
||||||
|
|
|
@ -27,6 +27,8 @@ class BaseScreen(Screen):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
def btncb(btn, reg, low, high):
|
def btncb(btn, reg, low, high):
|
||||||
reg.adjust(low, high)
|
reg.adjust(low, high)
|
||||||
|
def rats(btn, ts, reg):
|
||||||
|
ts.del_region(reg)
|
||||||
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False)
|
wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False)
|
||||||
|
@ -37,40 +39,53 @@ class BaseScreen(Screen):
|
||||||
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 = Tstat(wri, row, sl.mcol + 5, divisions = 4, ptcolor=YELLOW, height=100, width=15,
|
||||||
style=Tstat.BAR, legends=('0.0', '0.5', '1.0'))
|
style=Tstat.BAR, legends=('0.0', '0.5', '1.0'))
|
||||||
reg = Region(self.ts, 0.4, 0.6, LIGHTRED, self.ts_cb)
|
reg = Region(self.ts, 0.4, 0.6, 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)
|
||||||
self.lbl = Label(wri, row, self.ts.mcol + 5, 35, bdcolor=RED, bgcolor=BLACK)
|
col = self.ts.mcol + 5
|
||||||
self.led = LED(wri, row + 30, self.ts.mcol + 5, color=YELLOW, bdcolor=BLACK)
|
self.lbl = Label(wri, row, col, 35, bdcolor=RED, bgcolor=BLACK)
|
||||||
btn = Button(wri, row, self.lbl.mcol + 5,
|
self.alm = LED(wri, self.lbl.mrow + 5, col, height=20, color=RED, bdcolor=BLACK)
|
||||||
|
self.led = LED(wri, self.alm.mrow + 5, col, height=20, color=YELLOW, bdcolor=BLACK)
|
||||||
|
self.grn = LED(wri, self.led.mrow + 5, col, height=20, color=GREEN, bdcolor=BLACK)
|
||||||
|
col = self.lbl.mcol + 5
|
||||||
|
btn = Button(wri, row + 30, col, width=0,
|
||||||
text='down', litcolor=RED, bgcolor=DARKGREEN,
|
text='down', litcolor=RED, bgcolor=DARKGREEN,
|
||||||
callback=btncb, args=(reg, 0.2, 0.3))
|
callback=btncb, args=(reg, 0.2, 0.3))
|
||||||
Button(wri, btn.mrow + 5, self.lbl.mcol + 5,
|
btn1 = Button(wri, btn.mrow + 5, col, width=btn.width,
|
||||||
text='up', litcolor=RED, bgcolor=DARKGREEN,
|
text='up', litcolor=RED, bgcolor=DARKGREEN,
|
||||||
callback=btncb, args=(reg, 0.5, 0.6))
|
callback=btncb, args=(reg, 0.5, 0.6))
|
||||||
|
Button(wri, btn1.mrow + 5, col, width=btn.width,
|
||||||
|
text='del', litcolor=RED, bgcolor=DARKGREEN,
|
||||||
|
callback=rats, args=(self.ts, al))
|
||||||
CloseButton(wri)
|
CloseButton(wri)
|
||||||
|
|
||||||
|
def after_open(self):
|
||||||
|
self.ts.value(0) # Trigger callback
|
||||||
|
|
||||||
def slider_cb(self, s):
|
def slider_cb(self, s):
|
||||||
if hasattr(self, 'lbl'):
|
if hasattr(self, 'lbl'):
|
||||||
v = s.value()
|
v = s()
|
||||||
self.lbl.value('{:5.3f}'.format(v))
|
self.lbl('{:5.3f}'.format(v))
|
||||||
self.ts.value(v)
|
self.ts(v)
|
||||||
|
|
||||||
def ts_cb(self, reg, reason):
|
def ts_cb(self, reg, reason):
|
||||||
# Turn on if T drops below low threshold when it had been above high threshold. Or
|
# Hysteresis
|
||||||
# in the case of a low going drop so fast it never registered as being within bounds
|
|
||||||
if reason == reg.EX_WA_IB or reason == reg.T_IB:
|
if reason == reg.EX_WA_IB or reason == reg.T_IB:
|
||||||
print('Turning on')
|
self.led(False)
|
||||||
self.led.value(True)
|
self.grn(True)
|
||||||
elif reason == reg.EX_WB_IA or reason == reg.T_IA:
|
elif reason == reg.EX_WB_IA or reason == reg.T_IA:
|
||||||
print('Turning off')
|
self.led(True)
|
||||||
self.led.value(False)
|
self.grn(False)
|
||||||
|
|
||||||
def al_cb(self, reg, reason):
|
def al_cb(self, reg, reason):
|
||||||
if reason == reg.EN_WB or reason == reg.T_IA:
|
if reason == reg.EN_WB or reason == reg.T_IA: # Logical OR
|
||||||
print('Alarm')
|
self.alm(True)
|
||||||
|
elif reason & (reg.EX_WB_IB | reg.EX_WA_IB | reg.T_IB): # Bitwise OR alternative
|
||||||
|
self.alm(False)
|
||||||
|
|
||||||
def test():
|
def test():
|
||||||
|
if ssd.height < 128 or ssd.width < 128:
|
||||||
|
print(' This test requires a display of at least 128x128 pixels.')
|
||||||
|
else:
|
||||||
print('Tstat demo.')
|
print('Tstat demo.')
|
||||||
Screen.change(BaseScreen)
|
Screen.change(BaseScreen)
|
||||||
|
|
||||||
|
|
|
@ -26,78 +26,80 @@ class Region:
|
||||||
tstat.draw = True
|
tstat.draw = True
|
||||||
self.tstat = tstat
|
self.tstat = tstat
|
||||||
if vlo >= vhi:
|
if vlo >= vhi:
|
||||||
raise ValueError('TStat Region: vlo must be <= vhi')
|
raise ValueError('TStat Region: vlo must be < vhi')
|
||||||
self.vlo = vlo
|
self.vlo = vlo
|
||||||
self.vhi = vhi
|
self.vhi = vhi
|
||||||
self.color = color
|
self.color = color
|
||||||
self.cb = callback
|
self.cb = callback
|
||||||
self.args = args
|
self.args = args
|
||||||
self.is_in = False # Value is in region
|
v = self.tstat.value() # Get current value
|
||||||
self.fa = False # Entered from above
|
self.is_in = vlo <= v <= vhi # Is initial value in region
|
||||||
self.vprev = self.tstat.value()
|
# .wa: was above. Value prior to any entry to region.
|
||||||
|
self.wa = None # None indicates unknown
|
||||||
|
|
||||||
|
# Where prior state is unknown because instantiation occurred with a value
|
||||||
|
# in the region (.wa is None) we make the assumption that, on exit, it is
|
||||||
|
# leaving from the opposite side from purported entry.
|
||||||
def do_check(self, v):
|
def do_check(self, v):
|
||||||
cb = self.cb
|
cb = self.cb
|
||||||
args = self.args
|
args = self.args
|
||||||
if v < self.vlo:
|
if v < self.vlo:
|
||||||
if not self.is_in:
|
if not self.is_in:
|
||||||
if self.vprev > self.vhi: # Low going transit
|
if self.wa is None or self.wa: # Low going transit
|
||||||
cb(self, self.T_IB, *args)
|
cb(self, self.T_IB, *args)
|
||||||
return # Was and is outside: no action.
|
return # Was and is outside: no action.
|
||||||
# Was in the region, find direction of exit
|
# Was in the region, find direction of exit
|
||||||
self.is_in = False
|
reason = self.EX_WA_IB if (self.wa is None or self.wa) else self.EX_WB_IB
|
||||||
reason = self.EX_WA_IB if self.fa else self.EX_WB_IB
|
|
||||||
cb(self, reason, *args)
|
cb(self, reason, *args)
|
||||||
elif v > self.vhi:
|
elif v > self.vhi:
|
||||||
if not self.is_in:
|
if not self.is_in:
|
||||||
if self.vprev < self.vlo:
|
if self.wa is None or not self.wa:
|
||||||
cb(self, self.T_IA, *args)
|
cb(self, self.T_IA, *args)
|
||||||
return
|
return
|
||||||
# Was already in region
|
# Was already in region
|
||||||
self.is_in = False
|
reason = self.EX_WB_IA if (self.wa is None or not self.wa) else self.EX_WA_IA
|
||||||
reason = self.EX_WA_IA if self.fa else self.EX_WB_IA
|
|
||||||
cb(self, reason, *args)
|
cb(self, reason, *args)
|
||||||
else: # v is in range
|
else: # v is in range
|
||||||
if self.is_in:
|
if self.is_in:
|
||||||
return # Nothing to do
|
return # Nothing to do
|
||||||
self.is_in = True
|
if self.wa is None or self.wa:
|
||||||
if self.vprev > self.vhi:
|
|
||||||
self.fa = True # Save entry direction
|
|
||||||
cb(self, self.EN_WA, *args)
|
cb(self, self.EN_WA, *args)
|
||||||
elif self.vprev < self.vlo:
|
else:
|
||||||
self.fa = False
|
|
||||||
cb(self, self.EN_WB, *args)
|
cb(self, self.EN_WB, *args)
|
||||||
|
|
||||||
def check(self, v):
|
def check(self, v):
|
||||||
self.do_check(v)
|
self.do_check(v)
|
||||||
self.vprev = v # do_check gets value at prior check
|
self.is_in = self.vlo <= v <= self.vhi
|
||||||
|
if not self.is_in: # Current value is outside
|
||||||
|
self.wa = v > self.vhi # Save state for next check
|
||||||
|
|
||||||
def adjust(self, vlo, vhi):
|
def adjust(self, vlo, vhi):
|
||||||
|
if vlo >= vhi:
|
||||||
|
raise ValueError('TStat Region: vlo must be < vhi')
|
||||||
old_vlo = self.vlo
|
old_vlo = self.vlo
|
||||||
old_vhi = self.vhi
|
old_vhi = self.vhi
|
||||||
self.vlo = vlo
|
self.vlo = vlo
|
||||||
self.vhi = vhi
|
self.vhi = vhi
|
||||||
vc = self.tstat.value()
|
v = self.tstat.value()
|
||||||
self.tstat.draw = True
|
self.tstat.draw = True
|
||||||
# Despatch cases where there is nothing to do.
|
# Despatch cases where there is nothing to do.
|
||||||
# Outside both regions on same side
|
# Outside both regions on same side
|
||||||
if vc < vlo and vc < old_vlo:
|
if v < vlo and v < old_vlo:
|
||||||
return
|
return
|
||||||
if vc > vhi and vc > old_vhi:
|
if v > vhi and v > old_vhi:
|
||||||
return
|
return
|
||||||
is_in = vlo <= vc <= vhi # Currently inside
|
is_in = vlo <= v <= vhi # Currently inside
|
||||||
if is_in and self.is_in: # Regions overlapped
|
if is_in and self.is_in: # Regions overlapped
|
||||||
return # Still inside so no action
|
return # Still inside so no action
|
||||||
|
|
||||||
if is_in: # Inside new region but not in old
|
if is_in: # Inside new region but not in old
|
||||||
self.check(vc) # Treat as if entering new region from previous value
|
self.check(v) # Treat as if entering new region from previous value
|
||||||
else: # Outside new region
|
else: # Outside new region
|
||||||
if not self.is_in: # Also outside old region
|
if not self.is_in: # Also outside old region. Hence it lay
|
||||||
# Lay between old and new regions. Force
|
# between old and new regions. Force a traverse of new.
|
||||||
# a traverse of new
|
self.wa = v < vlo
|
||||||
self.vprev = vlo - 0.1 if vc > vhi else vhi + 0.1
|
|
||||||
# If it was in old region treat as if leaving it
|
# If it was in old region treat as if leaving it
|
||||||
self.check(vc)
|
self.check(v)
|
||||||
|
|
||||||
|
|
||||||
class Tstat(Meter):
|
class Tstat(Meter):
|
||||||
|
@ -107,6 +109,7 @@ class Tstat(Meter):
|
||||||
|
|
||||||
def del_region(self, reg):
|
def del_region(self, reg):
|
||||||
self.regions.discard(reg)
|
self.regions.discard(reg)
|
||||||
|
self.draw = True
|
||||||
|
|
||||||
# Called by subclass prior to drawing scale and data
|
# Called by subclass prior to drawing scale and data
|
||||||
def preshow(self, x, y, width, height):
|
def preshow(self, x, y, width, height):
|
||||||
|
|
Ładowanie…
Reference in New Issue