From fdb75ef4d7864e89d58376e853c1efdf49d0a279 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 6 Jul 2021 19:12:42 +0100 Subject: [PATCH] Add tstat class and demo. --- README.md | 6 +- gui/demos/slider_label.py | 2 +- gui/demos/tstat.py | 77 +++++++++++++++ gui/widgets/meter.py | 4 + gui/widgets/tstat.py | 124 +++++++++++++++++++++++++ hardware_setup.py | 2 +- setup_examples/ili9341_pico_encoder.py | 54 +++++++++++ 7 files changed, 265 insertions(+), 4 deletions(-) create mode 100644 gui/demos/tstat.py create mode 100644 gui/widgets/tstat.py create mode 100644 setup_examples/ili9341_pico_encoder.py diff --git a/README.md b/README.md index 5d582e4..ab599a7 100644 --- a/README.md +++ b/README.md @@ -903,9 +903,11 @@ Keyword only args: LED. An integer will create a `Label` of that width for later use. Methods: - 1. `color` arg `c=None` Change the LED color to `c`. If `c` is `None` the LED + 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). - 2. `text` Updates the label if present (otherwise throws a `ValueError`). Args: + 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. diff --git a/gui/demos/slider_label.py b/gui/demos/slider_label.py index 425ea56..16b65b6 100644 --- a/gui/demos/slider_label.py +++ b/gui/demos/slider_label.py @@ -26,7 +26,7 @@ class BaseScreen(Screen): col = 2 row = 2 self.lbl = Label(wri, row + 45, col + 50, 35, bdcolor=RED, bgcolor=DARKGREEN) - # Instntiate Label first, because Slider callback will run now. + # Instantiate Label first, because Slider callback will run now. # See linked_sliders.py for another approach. Slider(wri, row, col, callback=self.slider_cb, bdcolor=RED, slotcolor=BLUE, diff --git a/gui/demos/tstat.py b/gui/demos/tstat.py new file mode 100644 index 0000000..0a7364f --- /dev/null +++ b/gui/demos/tstat.py @@ -0,0 +1,77 @@ +# tstat.py nanogui demo for the Tstat class + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + +# Usage: +# import gui.demos.tstat + +# hardware_setup must be imported before other modules because of RAM use. +import hardware_setup # Create a display instance +from gui.core.ugui import Screen, ssd + +from gui.widgets.buttons import Button, CloseButton +from gui.widgets.sliders import Slider +from gui.widgets.label import Label +from gui.widgets.tstat import Tstat +from gui.widgets.led import LED +from gui.core.writer import CWriter + +# Font for CWriter +import gui.fonts.arial10 as arial10 +from gui.core.colors import * + + +class BaseScreen(Screen): + + def __init__(self): + def btncb(btn, reg, low, high): + reg.adjust(low, high) + + super().__init__() + wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False) + col = 2 + row = 10 + sl = Slider(wri, row, col, callback=self.slider_cb, + bdcolor=RED, slotcolor=BLUE, + 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 = self.ts.add_region(0.4, 0.6, LIGHTRED, self.ts_cb) + al = self.ts.add_region(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, + text='down', litcolor=RED, bgcolor=DARKGREEN, + callback=btncb, args=(reg, 0.2, 0.3)) + Button(wri, btn.mrow + 5, self.lbl.mcol + 5, + text='up', litcolor=RED, bgcolor=DARKGREEN, + callback=btncb, args=(reg, 0.5, 0.6)) + + CloseButton(wri) + + def slider_cb(self, s): + if hasattr(self, 'lbl'): + v = s.value() + self.lbl.value('{:5.3f}'.format(v)) + self.ts.value(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 + if reason == reg.EX_WA_IB or reason == reg.T_IB: + print('Turning on') + self.led.value(True) + elif reason == reg.EX_WB_IA or reason == reg.T_IA: + print('Turning off') + self.led.value(False) + + def al_cb(self, reg, reason): + if reason == reg.EN_WB or reason == reg.T_IA: + print('Alarm') + +def test(): + print('Tstat demo.') + Screen.change(BaseScreen) + +test() diff --git a/gui/widgets/meter.py b/gui/widgets/meter.py index 428d521..6b7fc2e 100644 --- a/gui/widgets/meter.py +++ b/gui/widgets/meter.py @@ -54,6 +54,7 @@ class Meter(Widget): x1 = self.col + width y0 = self.row y1 = self.row + height + self.preshow(x0, y1, width, height) # Subclass draws regions if self.divisions > 0: dy = height / (self.divisions) # Tick marks for tick in range(self.divisions + 1): @@ -66,3 +67,6 @@ class Meter(Widget): else: w = width / 2 display.fill_rect(int(x0 + w - 2), y, 4, y1 - y, self.ptcolor) + + def preshow(self, x, y, width, height): + pass # For subclass diff --git a/gui/widgets/tstat.py b/gui/widgets/tstat.py new file mode 100644 index 0000000..d03a41a --- /dev/null +++ b/gui/widgets/tstat.py @@ -0,0 +1,124 @@ +# tstat.py Extension to nanogui providing the Tstat class + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + +# Usage: +# from gui.widgets.tstat import Tstat + +from gui.core.ugui import display +from gui.core.colors import * +from gui.widgets.meter import Meter + +class Region: + # Callback reasons + EX_WB_IA = 1 # Exit region. Was below. Is above. + EX_WB_IB = 2 # Exit, was below, is below + EX_WA_IA = 4 # Exit, was above, is above. + EX_WA_IB = 8 # Exit, was above, is below + T_IA = 16 # Transit, is above + T_IB = 32 # Transit, is below + EN_WA = 64 # Entry, was above + EN_WB = 128 # Entry, was below + + def __init__(self, tstat, vlo, vhi, color, callback, args): + self.tstat = tstat + if vlo >= 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() + + 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 + 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 + cb(self, reason, *args) + elif v > self.vhi: + if not self.is_in: + if self.vprev < self.vlo: + 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 + 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 + cb(self, self.EN_WA, *args) + elif self.vprev < self.vlo: + self.fa = False + cb(self, self.EN_WB, *args) + + def check(self, v): + self.do_check(v) + self.vprev = v # do_check gets value at prior check + + def adjust(self, vlo, vhi): + old_vlo = self.vlo + old_vhi = self.vhi + self.vlo = vlo + self.vhi = vhi + vc = 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: + return + if vc > vhi and vc > old_vhi: + return + is_in = vlo <= vc <= 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 + 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 it was in old region treat as if leaving it + self.check(vc) + + +class Tstat(Meter): + def __init__(self, *args, **kwargs): + self.regions = set() + super().__init__(*args, **kwargs) + + def add_region(self, vlo, vhi, color, callback, args=()): + reg = Region(self, vlo, vhi, color, callback, args) + self.regions.add(reg) + return reg + + # 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 diff --git a/hardware_setup.py b/hardware_setup.py index 5f9e15c..df02c1b 100644 --- a/hardware_setup.py +++ b/hardware_setup.py @@ -51,4 +51,4 @@ sel = Pin(16, Pin.IN, Pin.PULL_UP) # Operate current control prev = Pin(18, Pin.IN, Pin.PULL_UP) # Move to previous control increase = Pin(20, Pin.IN, Pin.PULL_UP) # Increase control's value decrease = Pin(17, Pin.IN, Pin.PULL_UP) # Decrease control's value -display = Display(ssd, nxt, sel, prev, increase, decrease, 5) # Encoder +display = Display(ssd, nxt, sel, prev, increase, decrease) diff --git a/setup_examples/ili9341_pico_encoder.py b/setup_examples/ili9341_pico_encoder.py new file mode 100644 index 0000000..5f9e15c --- /dev/null +++ b/setup_examples/ili9341_pico_encoder.py @@ -0,0 +1,54 @@ +# ili9341_pico.py Customise for your hardware config + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + +# As written, supports: +# ili9341 240x320 displays on Pi Pico +# Edit the driver import for other displays. + +# Demo of initialisation procedure designed to minimise risk of memory fail +# when instantiating the frame buffer. The aim is to do this as early as +# possible before importing other modules. + +# WIRING +# Pico Display +# GPIO Pin +# 3v3 36 Vin +# IO6 9 CLK Hardware SPI0 +# IO7 10 DATA (AKA SI MOSI) +# IO8 11 DC +# IO9 12 Rst +# Gnd 13 Gnd +# IO10 14 CS + +# Pushbuttons are wired between the pin and Gnd +# Pico pin Meaning +# 16 Operate current control +# 17 Decrease value of current control +# 18 Select previous control +# 19 Select next control +# 20 Increase value of current control + +from machine import Pin, SPI, freq +import gc + +from drivers.ili93xx.ili9341 import ILI9341 as SSD +freq(250_000_000) # RP2 overclock +# Create and export an SSD instance +pdc = Pin(8, Pin.OUT, value=0) # Arbitrary pins +prst = Pin(9, Pin.OUT, value=1) +pcs = Pin(10, Pin.OUT, value=1) +spi = SPI(0, baudrate=30_000_000) +gc.collect() # Precaution before instantiating framebuf +ssd = SSD(spi, pcs, pdc, prst, usd=True) + +from gui.core.ugui import Display +# Create and export a Display instance +# Define control buttons +nxt = Pin(19, Pin.IN, Pin.PULL_UP) # Move to next control +sel = Pin(16, Pin.IN, Pin.PULL_UP) # Operate current control +prev = Pin(18, Pin.IN, Pin.PULL_UP) # Move to previous control +increase = Pin(20, Pin.IN, Pin.PULL_UP) # Increase control's value +decrease = Pin(17, Pin.IN, Pin.PULL_UP) # Decrease control's value +display = Display(ssd, nxt, sel, prev, increase, decrease, 5) # Encoder