Changes to Tstat logic. README corrections.

pull/8/head
Peter Hinch 2021-07-07 12:54:05 +01:00
rodzic b75f3558ad
commit a75eda8e85
4 zmienionych plików z 92 dodań i 65 usunięć

Wyświetl plik

@ -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)

Wyświetl plik

@ -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'):

Wyświetl plik

@ -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,41 +39,54 @@ 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():
print('Tstat demo.') if ssd.height < 128 or ssd.width < 128:
Screen.change(BaseScreen) print(' This test requires a display of at least 128x128 pixels.')
else:
print('Tstat demo.')
Screen.change(BaseScreen)
test() test()

Wyświetl plik

@ -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):