diff --git a/README.md b/README.md index b052a01..25d20d5 100644 --- a/README.md +++ b/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 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) diff --git a/gui/core/ugui.py b/gui/core/ugui.py index 28a21d4..514f2bd 100644 --- a/gui/core/ugui.py +++ b/gui/core/ugui.py @@ -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'): diff --git a/gui/demos/tstat.py b/gui/demos/tstat.py index 307e3c3..3f03dee 100644 --- a/gui/demos/tstat.py +++ b/gui/demos/tstat.py @@ -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() diff --git a/gui/widgets/tstat.py b/gui/widgets/tstat.py index 3635984..9d9c6d7 100644 --- a/gui/widgets/tstat.py +++ b/gui/widgets/tstat.py @@ -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):