Some doc for encoder mode. Remove unwanted code/comments from demos.

pull/8/head
Peter Hinch 2021-07-04 18:21:37 +01:00
rodzic b8d8bdb0a3
commit 3a64bb5691
9 zmienionych plików z 192 dodań i 186 usunięć

Wyświetl plik

@ -19,6 +19,9 @@ to a wide range of displays. It is also portable between hosts.
![Image](./images/ttgo.JPG) TTGO T-Display. Add a joystick switch and an SIL
resistor for a simple, inexpensive, WiFi capable system.
An alternative interface consists of two pushbuttons and an encoder such as
[this one](https://www.adafruit.com/product/377).
# Rationale
Touch GUI's have many advantages, however they have drawbacks, principally cost
@ -115,7 +118,7 @@ of some display drivers.
     22.3.1 [Class Curve](./README.md#2231-class-curve)
     22.3.2 [Class PolarCurve](./README.md#2232-class-polarcurve)
22.4 [Class TSequence](./README.md#224-class-tsequence) Plotting realtime, time sequential data.
[Appendix 1 Application design](./README.md#appendix-1-application-design) Tab order, button layout, use of graphics primitives
[Appendix 1 Application design](./README.md#appendix-1-application-design) Tab order, button layout, encoder interface, use of graphics primitives
# 1. Basic concepts
@ -385,6 +388,8 @@ files in `gui/core` are:
The `gui/primitives` directory contains the following files:
* `switch.py` Interface to physical pushbuttons.
* `delay_ms.py` A software triggerable timer.
* `encoder.py` Driver for a quadrature encoder. This offers an alternative
interface - see [Appendix 1](./README.md#appendix-1-application-design).
The `gui/demos` directory contains a variety of demos and tests described
below.
@ -2202,6 +2207,16 @@ The apparently obvious solution of designing a vertical `Scale` is tricky owing
to the fact that the length of the internal text can be substantial and
variable.
## Encoder interface
This alternative interface comprises just two buttons `Next` and `Prev`.
Selection and Increase/Decrease is handled by an encoder such as
[this one](https://www.adafruit.com/product/377). Selection occurs when the
knob is pressed, and movement when it is rotated. This is more intuitive,
particularly with horizontally oriented controls.
TODO wiring details.
## Screen layout
Widgets are positioned using absolute `row` and `col` coordinates. These may

Wyświetl plik

@ -38,15 +38,15 @@ _LAST = const(3)
# Wrapper for ssd providing buttons and framebuf compatible methods
class Display:
def __init__(self, objssd, nxt, sel, prev=None, up=None, down=None):
def __init__(self, objssd, nxt, sel, prev=None, up=None, down=None, encoder=False):
global display, ssd
self._next = Switch(nxt)
self._sel = Switch(sel)
self._last = None # Last switch pressed.
# Mandatory buttons
# Call current screen bound method
self._next.close_func(self._closure, (self._next, Screen.next_ctrl))
self._sel.close_func(self._closure, (self._sel, Screen.sel_ctrl))
self._next.close_func(self._closure, (self._next, Screen.ctrl_move, _NEXT))
self._sel.close_func(self._closure, (self._sel, Screen.sel_ctrl, 0))
self._sel.open_func(Screen.unsel)
self.height = objssd.height
@ -56,26 +56,30 @@ class Display:
self._prev = None
if prev is not None:
self._prev = Switch(prev)
self._prev.close_func(self._closure, (self._prev, Screen.prev_ctrl))
# Up and down methods get the button as an arg.
self._up = None
if up is not None:
self._up = Switch(up)
self._up.close_func(self._closure, (self._up, self.do_up))
self._down = None
if down is not None:
self._down = Switch(down)
self._down.close_func(self._closure, (self._down, self.do_down))
self._prev.close_func(self._closure, (self._prev, Screen.ctrl_move, _PREV))
if encoder:
if up is None or down is None:
raise ValueError('Must specify pins for encoder.')
from gui.primitives.encoder import Encoder
self._enc = Encoder(up, down, div=encoder, callback=Screen.adjust)
else:
# Up and down methods get the button as an arg.
if up is not None:
sup = Switch(up)
sup.close_func(self._closure, (sup, Screen.adjust, 1))
if down is not None:
sdown = Switch(down)
sdown.close_func(self._closure, (sdown, Screen.adjust, -1))
self._is_grey = False # Not greyed-out
display = self # Populate globals
ssd = objssd
# Reject button presses where a button is already pressed.
# Execute if initialising, if same switch re-pressed or if last switch released
def _closure(self, switch, func):
def _closure(self, switch, func, arg):
if (self._last is None) or (self._last == switch) or self._last():
self._last = switch
func()
func(switch, arg)
def print_centred(self, writer, x, y, text, fgcolor=None, bgcolor=None, invert=False):
sl = writer.stringlen(text)
@ -94,12 +98,6 @@ class Display:
writer.printstring(txt, invert)
writer.setcolor() # Restore defaults
def do_up(self):
Screen.up_ctrl(self._up)
def do_down(self):
Screen.down_ctrl(self._down)
# Greying out has only one option given limitation of 4-bit display driver
# It would be possible to do better with RGB565 but would need inverse transformation
# to (r, g, b), scale and re-convert to integer.
@ -204,35 +202,27 @@ class Screen:
is_shutdown = Event()
@classmethod
def next_ctrl(cls):
def ctrl_move(cls, _, v):
if cls.current_screen is not None:
cls.current_screen.move(_NEXT)
cls.current_screen.move(v)
@classmethod
def prev_ctrl(cls):
if cls.current_screen is not None:
cls.current_screen.move(_PREV)
@classmethod
def sel_ctrl(cls):
def sel_ctrl(cls, b, _):
if cls.current_screen is not None:
cls.current_screen.do_sel()
@classmethod
def unsel(cls):
if cls.current_screen is not None:
cls.current_screen.unsel_i()
# Adjust the value of a widget. If an encoder is used, button arg
# is an int (discarded), val is the delta. If using buttons, 1st
# arg is the button, delta is +1 or -1
@classmethod
def up_ctrl(cls, button):
def adjust(cls, button, val):
if cls.current_screen is not None:
cls.current_screen.do_up(button)
@classmethod
def down_ctrl(cls, button):
if cls.current_screen is not None:
cls.current_screen.do_down(button)
cls.current_screen.do_adj(button, val)
# Move currency to a specific widget (e.g. ButtonList)
@classmethod
@ -440,19 +430,12 @@ class Screen:
if co is not None:
co.unsel()
def do_up(self, button):
def do_adj(self, button, val):
co = self.get_obj()
if co is not None and hasattr(co, 'do_up'):
co.do_up(button) # Widget handles up/down
if co is not None and hasattr(co, 'do_adj'):
co.do_adj(button, val) # Widget can handle up/down
else:
Screen.current_screen.move(_FIRST)
def do_down(self, button):
co = self.get_obj()
if co is not None and hasattr(co, 'do_down'):
co.do_down(button)
else:
Screen.current_screen.move(_LAST)
Screen.current_screen.move(_FIRST if val < 0 else _LAST)
# Methods optionally implemented in subclass
def on_open(self):
@ -710,21 +693,17 @@ class LinearIO(Widget):
# but subclass can be defeat this with WHITE or another color
self.prcolor = YELLOW if prcolor is None else prcolor
def do_up(self, button):
asyncio.create_task(self.btnhan(button, 1))
def do_down(self, button):
asyncio.create_task(self.btnhan(button, -1))
# Adjust widget's value. Args: button pressed, amount of increment
def do_adj(self, button, val):
encoder = isinstance(button, int)
d = self.min_delta * 0.1 if self.precision else self.min_delta
self.value(self.value() + val * d)
if not encoder:
asyncio.create_task(self.btnhan(button, val, d))
# Handle increase and decrease buttons. Redefined by textbox.py, scale_log.py
async def btnhan(self, button, up):
if self.precision:
d = self.min_delta * 0.1
maxd = self.max_delta
else:
d = self.min_delta
maxd = d * 4 # Why move fast in precision mode?
self.value(self.value() + up * d)
async def btnhan(self, button, up, d):
maxd = self.max_delta if self.precision else d * 4 # Why move fast in precision mode?
t = ticks_ms()
while not button():
await asyncio.sleep_ms(0) # Quit fast on button release

Wyświetl plik

@ -47,13 +47,11 @@ class BaseScreen(Screen):
self.vslider = Slider(wri, 2, 2, callback=self.slider_cb,
bdcolor=RED, slotcolor=BLUE,
legends=('0.0', '0.5', '1.0'), value=0.5)
#Label(wri, 2, self.vslider.mcol, 'FF')
col = 80
row = 15
self.hslider = HorizSlider(wri, row, col, callback=self.slider_cb,
bdcolor=GREEN, slotcolor=BLUE,
legends=('0.0', '0.5', '1.0'), value=0.7)
Label(wri, self.hslider.mrow, self.hslider.mcol, 'FF')
row += 30
self.scale = Scale(wri, row, col, width = 150, tickcb = tickcb,
pointercolor=RED, fontcolor=YELLOW, bdcolor=CYAN,

Wyświetl plik

@ -61,11 +61,8 @@ class FooScreen(Screen):
m0 = Meter(wri, 10, 240, divisions = 4, ptcolor=YELLOW, height=80, width=15,
label='Meter example', style=Meter.BAR, legends=('0.0', '0.5', '1.0'))
#Label(wri, 2, m0.mcol, 'FF')
# Instantiate displayable objects. bgcolor forces complete redraw.
dial = Dial(wri, 2, 2, height = 75, ticks = 12, bgcolor=BLACK, bdcolor=None, label=120) # Border in fg color
#Label(wri, dial.mrow, 2, 'FF')
#Label(wri, dial.mrow, dial.mcol, 'FF')
scale = Scale(wri, 2, 100, width = 124, tickcb = tickcb,
pointercolor=RED, fontcolor=YELLOW, bdcolor=CYAN)

Wyświetl plik

@ -0,0 +1,72 @@
# encoder.py Asynchronous driver for incremental quadrature encoder.
# Copyright (c) 2021 Peter Hinch
# Released under the MIT License (MIT) - see LICENSE file
# This driver is intended for encoder-based control knobs. It is
# unsuitable for NC machine applications. Please see the docs.
import uasyncio as asyncio
from machine import Pin
class Encoder:
delay = 100 # Pause (ms) for motion to stop
def __init__(self, pin_x, pin_y, v=0, vmin=None, vmax=None, div=1,
callback=lambda a, b : None, args=()):
self._pin_x = pin_x
self._pin_y = pin_y
self._v = 0 # Hardware value always starts at 0
self._cv = v # Current (divided) value
if ((vmin is not None) and v < min) or ((vmax is not None) and v > vmax):
raise ValueError('Incompatible args: must have vmin <= v <= vmax')
self._tsf = asyncio.ThreadSafeFlag()
trig = Pin.IRQ_RISING | Pin.IRQ_FALLING
try:
xirq = pin_x.irq(trigger=trig, handler=self._x_cb, hard=True)
yirq = pin_y.irq(trigger=trig, handler=self._y_cb, hard=True)
except TypeError: # hard arg is unsupported on some hosts
xirq = pin_x.irq(trigger=trig, handler=self._x_cb)
yirq = pin_y.irq(trigger=trig, handler=self._y_cb)
asyncio.create_task(self._run(vmin, vmax, div, callback, args))
# Hardware IRQ's
def _x_cb(self, pin):
fwd = pin() ^ self._pin_y()
self._v += 1 if fwd else -1
self._tsf.set()
def _y_cb(self, pin):
fwd = pin() ^ self._pin_x() ^ 1
self._v += 1 if fwd else -1
self._tsf.set()
async def _run(self, vmin, vmax, div, cb, args):
pv = self._v # Prior hardware value
cv = self._cv # Current divided value as passed to callback
pcv = cv # Prior divided value passed to callback
mod = 0
delay = self.delay
while True:
await self._tsf.wait()
await asyncio.sleep_ms(delay) # Wait for motion to stop
new = self._v # Sample hardware (atomic read)
a = new - pv # Hardware change
# Ensure symmetrical bahaviour for + and - values
q, r = divmod(abs(a), div)
if a < 0:
r = -r
q = -q
pv = new - r # Hardware value when local value was updated
cv += q
if vmax is not None:
cv = min(cv, vmax)
if vmin is not None:
cv = max(cv, vmin)
self._cv = cv # For value()
if cv != pcv:
cb(cv, cv - pcv, *args) # User CB in uasyncio context
pcv = cv
def value(self):
return self._cv

Wyświetl plik

@ -78,18 +78,17 @@ class Listbox(Widget):
self.value(v)
return v
def do_up(self, _):
if v := self._value:
self.value(v - 1)
def do_adj(self, _, val):
v = self._value
if val > 0:
if v:
self.value(v - 1)
elif val < 0:
if v < len(self.elements) - 1:
self.value(v + 1)
if (self.also & Listbox.ON_MOVE): # Treat as if select pressed
self.do_sel()
def do_down(self, _):
if (v := self._value) < len(self.elements) - 1:
self.value(v + 1)
if (self.also & Listbox.ON_MOVE):
self.do_sel()
# Callback runs if select is pressed. Also (if ON_LEAVE) if user changes
# list currency and then moves off the control. Otherwise if we have a
# callback that refreshes another control, that second control does not

Wyświetl plik

@ -23,6 +23,7 @@ dolittle = lambda *_ : None
# Start value is 1.0. User applies scaling to value and ticks callback.
class ScaleLog(LinearIO):
encoder_rate = 5
def __init__(self, writer, row, col, *,
decades=5, height=0, width=160,
bdcolor=None, fgcolor=None, bgcolor=None,
@ -141,6 +142,14 @@ class ScaleLog(LinearIO):
return self._value
# Adjust widget's value. Args: button pressed, amount of increment
def do_adj(self, button, val):
if isinstance(button, int): # Using an encoder
delta = self.delta * self.encoder_rate * 0.1 if self.precision else self.delta * self.encoder_rate
self.value(self.value() * (1 + delta)**val)
else:
asyncio.create_task(self.btnhan(button, val, d))
async def btnhan(self, button, up):
up = up == 1
if self.precision:

Wyświetl plik

@ -1,122 +1,54 @@
# color_setup.py Customise for your hardware config
# ili9341_pico.py Customise for your hardware config
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2021 Peter Hinch, Ihor Nehrutsa
# Copyright (c) 2021 Peter Hinch
# Supports:
# TTGO T-Display 1.14" 135*240(Pixel) based on ST7789V
# http://www.lilygo.cn/claprod_view.aspx?TypeId=62&Id=1274
# http://www.lilygo.cn/prod_view.aspx?TypeId=50044&Id=1126
# https://github.com/Xinyuan-LilyGO/TTGO-T-Display
# https://github.com/Xinyuan-LilyGO/TTGO-T-Display/blob/master/image/pinmap.jpg
# https://github.com/Xinyuan-LilyGO/TTGO-T-Display/blob/master/schematic/ESP32-TFT(6-26).pdf
# WIRING (TTGO T-Display pin numbers and names).
# Pinout of TFT Driver
# ST7789 ESP32
# TFT_MISO N/A
TFT_MOSI = 19 # (SDA on schematic pdf) SPI interface output/input pin.
TFT_SCLK = 18 # This pin is used to be serial interface clock.
TFT_CS = 5 # Chip selection pin, low enable, high disable.
TFT_DC = 16 # Display data/command selection pin in 4-line serial interface.
TFT_RST = 23 # This signal will reset the device,Signal is active low.
TFT_BL = 4 # (LEDK on schematic pdf) Display backlight control pin
ADC_IN = 34 # Measuring battery or USB voltage, see comment below
ADC_EN = 14 # (PWR_EN on schematic pdf) is the ADC detection enable port
BUTTON1 = 35 # right of the USB connector
BUTTON2 = 0 # left of the USB connector
# ESP32 pins, free for use in user applications
#I2C_SDA = 21 # hardware ID 0
#I2C_SCL = 22
#UART2TXD = 17
#GPIO2 = 2
#GPIO15 = 15
#GPIO13 = 13
#GPIO12 = 12
#GPIO37 = 37
#GPIO38 = 38
#UART1TXD = 4
#UART1RXD = 5
#GPIO18 = 18
#GPIO19 = 19
#GPIO17 = 17
#DAC1 = 25
#DAC2 = 26
# Input only pins
#GPIO36 = 36 # input only
#GPIO39 = 39 # input only
# 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.
from machine import Pin, SPI, ADC, freq
# 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.st7789.st7789_4bit import *
SSD = ST7789
pdc = Pin(TFT_DC, Pin.OUT, value=0) # Arbitrary pins
pcs = Pin(TFT_CS, Pin.OUT, value=1)
prst = Pin(TFT_RST, Pin.OUT, value=1)
pbl = Pin(TFT_BL, Pin.OUT, value=1)
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
# Conservative low baudrate. Can go to 62.5MHz.
spi = SPI(1, 30_000_000, sck=Pin(TFT_SCLK), mosi=Pin(TFT_MOSI))
freq(160_000_000)
''' TTGO
v +----------------+
40 | | |
^ | +------+ | pin 36
| | | | |
| | | | |
240 | | | | |
| | | | |
| | | | |
v | +------+ |
40 | | | Reset button
^ +----------------+
>----<------>----<
52 135 xx
BUTTON2 BUTTON1
'''
# Right way up landscape: defined as top left adjacent to pin 36
ssd = SSD(spi, height=135, width=240, dc=pdc, cs=pcs, rst=prst, disp_mode=LANDSCAPE, display=TDISPLAY)
# Normal portrait display: consistent with TTGO logo at top
# ssd = SSD(spi, height=240, width=135, dc=pdc, cs=pcs, rst=prst, disp_mode=PORTRAIT, display=TDISPLAY)
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(32, Pin.IN, Pin.PULL_UP) # Move to next control
sel = Pin(36, Pin.IN, Pin.PULL_UP) # Operate current control
prev = Pin(38, Pin.IN, Pin.PULL_UP) # Move to previous control
increase = Pin(37, Pin.IN, Pin.PULL_UP) # Increase control's value
decrease = Pin(39, Pin.IN, Pin.PULL_UP) # Decrease control's value
display = Display(ssd, nxt, sel, prev, increase, decrease)
# optional
# b1 = Pin(BUTTON1, Pin.IN)
# b2 = Pin(BUTTON2, Pin.IN)
# adc_en = Pin(ADC_EN, Pin.OUT, value=1)
# adc_in = ADC(Pin(ADC_IN))
# adc_en.value(0)
'''
Set ADC_EN to "1" and read voltage in BAT_ADC,
if this voltage more than 4.3 V device have powered from USB.
If less then 4.3 V - device have power from battery.
To save battery you can set ADC_EN to "0" and in this case the USB converter
will be power off and do not use your battery.
When you need to measure battery voltage first set ADC_EN to "1",
measure voltage and then set ADC_EN back to "0" for save battery.
'''
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

Wyświetl plik

@ -101,9 +101,14 @@ from gui.core.ugui import Display
nxt = Pin(32, Pin.IN, Pin.PULL_UP) # Move to next control
sel = Pin(36, Pin.IN, Pin.PULL_UP) # Operate current control
prev = Pin(38, Pin.IN, Pin.PULL_UP) # Move to previous control
increase = Pin(37, Pin.IN, Pin.PULL_UP) # Increase control's value
decrease = Pin(39, Pin.IN, Pin.PULL_UP) # Decrease control's value
display = Display(ssd, nxt, sel, prev, increase, decrease)
encoder = 5 # Divide by 5
if encoder:
increase = Pin(25, Pin.IN, Pin.PULL_UP) # Encoder x and y pins
decrease = Pin(33, Pin.IN, Pin.PULL_UP)
else:
increase = Pin(37, Pin.IN, Pin.PULL_UP) # Increase control's value
decrease = Pin(39, Pin.IN, Pin.PULL_UP) # Decrease control's value
display = Display(ssd, nxt, sel, prev, increase, decrease, encoder)
# optional
# b1 = Pin(BUTTON1, Pin.IN)