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
screen layout using widget metrics. Has a simple `uasyncio` task.
* `tbox.py` Text boxes and user-controlled scrolling.
* `tstat.py` A demo of the `Tstat` class.
### 1.11.2 Test scripts
@ -893,7 +894,7 @@ Constructor mandatory positional 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
`Writer` foreground default is used.
* `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
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.
* `label=None` A text string will cause a `Label` to be drawn below the
LED. An integer will create a `Label` of that width for later use.
* `color=RED` Color when illuminated (i.e. if `value` is `True`.
Methods:
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.
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).
3. `text` Updates the label if present (otherwise throws a `ValueError`). Args:
* `text=None` The text to display. If `None` displays last value.
* ` invert=False` If true, show inverse text.
* `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.
Note that `__call__` is a synonym for `value`. An `LED` instance can be
controlled with `led(True)` or `led(False)`.
###### [Contents](./README.md#0-contents)
@ -983,8 +979,9 @@ Constructor mandatory positional args:
Optional keyword only arguments:
* `shape=RECTANGLE` Must be `CIRCLE`, `RECTANGLE` or `CLIPPED_RECT`.
* `height=20` Height of the bounding box.
* `width=50` Width of the bounding box.
* `width=50` Width of button. If `text` is supplied and `width` is too low to
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
`Writer` foreground default is used.
* `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
This subclass of `Meter` 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 which receives an arg
indicating the nature of the change which caused the trigger. For example an
alarm might be triggered on entry or traverse through a region from below, and
cleared by an exit or traverse from above. Hysteresis as used in thermostats is
simple to implement.
This subclass of `Meter` is also a `passive` widget but provides for callbacks
which run in response to specific changes in the object's value.
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 args and methods are as per `Meter`. The `Tstat` class adds the
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
@ -1515,6 +1517,10 @@ operator if you prefer that coding style:
```python
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)

Wyświetl plik

@ -572,6 +572,9 @@ class Widget:
self.callback(self, *self.args)
return self._value
def __call__(self, val=None):
return self.value(val)
# Some widgets (e.g. Dial) have an associated Label
def text(self, text=None, invert=False, fgcolor=None, bgcolor=None, bdcolor=None):
if hasattr(self, 'label'):

Wyświetl plik

@ -27,6 +27,8 @@ class BaseScreen(Screen):
def __init__(self):
def btncb(btn, reg, low, high):
reg.adjust(low, high)
def rats(btn, ts, reg):
ts.del_region(reg)
super().__init__()
wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False)
@ -37,41 +39,54 @@ class BaseScreen(Screen):
legends=('0.0', '0.5', '1.0'))
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'))
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)
self.lbl = Label(wri, row, self.ts.mcol + 5, 35, bdcolor=RED, bgcolor=BLACK)
self.led = LED(wri, row + 30, self.ts.mcol + 5, color=YELLOW, bdcolor=BLACK)
btn = Button(wri, row, self.lbl.mcol + 5,
col = self.ts.mcol + 5
self.lbl = Label(wri, row, col, 35, bdcolor=RED, bgcolor=BLACK)
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,
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,
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)
def after_open(self):
self.ts.value(0) # Trigger callback
def slider_cb(self, s):
if hasattr(self, 'lbl'):
v = s.value()
self.lbl.value('{:5.3f}'.format(v))
self.ts.value(v)
v = s()
self.lbl('{:5.3f}'.format(v))
self.ts(v)
def ts_cb(self, reg, reason):
# Turn on if T drops below low threshold when it had been above high threshold. Or
# in the case of a low going drop so fast it never registered as being within bounds
# Hysteresis
if reason == reg.EX_WA_IB or reason == reg.T_IB:
print('Turning on')
self.led.value(True)
self.led(False)
self.grn(True)
elif reason == reg.EX_WB_IA or reason == reg.T_IA:
print('Turning off')
self.led.value(False)
self.led(True)
self.grn(False)
def al_cb(self, reg, reason):
if reason == reg.EN_WB or reason == reg.T_IA:
print('Alarm')
if reason == reg.EN_WB or reason == reg.T_IA: # Logical OR
self.alm(True)
elif reason & (reg.EX_WB_IB | reg.EX_WA_IB | reg.T_IB): # Bitwise OR alternative
self.alm(False)
def test():
print('Tstat demo.')
Screen.change(BaseScreen)
if ssd.height < 128 or ssd.width < 128:
print(' This test requires a display of at least 128x128 pixels.')
else:
print('Tstat demo.')
Screen.change(BaseScreen)
test()

Wyświetl plik

@ -26,78 +26,80 @@ class Region:
tstat.draw = True
self.tstat = tstat
if vlo >= vhi:
raise ValueError('TStat Region: vlo must be <= vhi')
raise ValueError('TStat Region: vlo must be < vhi')
self.vlo = vlo
self.vhi = vhi
self.color = color
self.cb = callback
self.args = args
self.is_in = False # Value is in region
self.fa = False # Entered from above
self.vprev = self.tstat.value()
v = self.tstat.value() # Get current value
self.is_in = vlo <= v <= vhi # Is initial value in region
# .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):
cb = self.cb
args = self.args
if v < self.vlo:
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)
return # Was and is outside: no action.
# Was in the region, find direction of exit
self.is_in = False
reason = self.EX_WA_IB if self.fa else self.EX_WB_IB
reason = self.EX_WA_IB if (self.wa is None or self.wa) else self.EX_WB_IB
cb(self, reason, *args)
elif v > self.vhi:
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)
return
# Was already in region
self.is_in = False
reason = self.EX_WA_IA if self.fa else self.EX_WB_IA
reason = self.EX_WB_IA if (self.wa is None or not self.wa) else self.EX_WA_IA
cb(self, reason, *args)
else: # v is in range
if self.is_in:
return # Nothing to do
self.is_in = True
if self.vprev > self.vhi:
self.fa = True # Save entry direction
if self.wa is None or self.wa:
cb(self, self.EN_WA, *args)
elif self.vprev < self.vlo:
self.fa = False
else:
cb(self, self.EN_WB, *args)
def check(self, 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):
if vlo >= vhi:
raise ValueError('TStat Region: vlo must be < vhi')
old_vlo = self.vlo
old_vhi = self.vhi
self.vlo = vlo
self.vhi = vhi
vc = self.tstat.value()
v = self.tstat.value()
self.tstat.draw = True
# Despatch cases where there is nothing to do.
# Outside both regions on same side
if vc < vlo and vc < old_vlo:
if v < vlo and v < old_vlo:
return
if vc > vhi and vc > old_vhi:
if v > vhi and v > old_vhi:
return
is_in = vlo <= vc <= vhi # Currently inside
is_in = vlo <= v <= vhi # Currently inside
if is_in and self.is_in: # Regions overlapped
return # Still inside so no action
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
if not self.is_in: # Also outside old region
# Lay between old and new regions. Force
# a traverse of new
self.vprev = vlo - 0.1 if vc > vhi else vhi + 0.1
if not self.is_in: # Also outside old region. Hence it lay
# between old and new regions. Force a traverse of new.
self.wa = v < vlo
# If it was in old region treat as if leaving it
self.check(vc)
self.check(v)
class Tstat(Meter):
@ -107,6 +109,7 @@ class Tstat(Meter):
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):