Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 65 additions & 3 deletions gui/core/ugui.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,64 @@ def is_adjust(self):
return self._adj


# Special mode where an encoder with a "press" pushbutton is the only control.
# nxt and prev are Pin instances corresponding to encoder X and Y.
# sel is a Pin for the encoder's pushbutton.
# encoder is the division ratio.
# Note that using a single click for adjust mode failed because the mode changed when
# activating pushbuttons, checkboxes etc.
class InputI2CEnc:
def __init__(self, encoder):
from gui.primitives import I2CEncoder

self._encoder = encoder # Encoder in use
self._enc = I2CEncoder(encoder=encoder, callback=self.enc_cb)
self._precision = False # Precision mode
self._adj = False # Adjustment mode
self._sel = Pushbutton(sel, suppress=True)
self._sel.release_func(self.release) # Widgets are selected on release.
self._sel.long_func(self.precision, (True,)) # Long press -> precision mode
self._sel.double_func(self.adj_mode, (True,)) # Double press -> adjust mode

# Screen.adjust: adjust the value of a widget. In this case 1st button arg
# is an int (discarded), val is the delta. (With button interface 1st arg
# is the button, delta is +1 or -1).
def enc_cb(self, position, delta): # Eencoder callback
if self._adj:
Screen.adjust(0, delta)
else:
Screen.ctrl_move(_NEXT if delta > 0 else _PREV)

def release(self):
self.adj_mode(False) # Cancel adjust and precision
Screen.sel_ctrl()

def precision(self, val): # Also called by Screen.ctrl_move to cancel mode
if val:
if not self._adj:
self.adj_mode()
self._precision = True
else:
self._precision = False
Screen.redraw_co()

# If v is None, toggle adjustment mode. Bool sets or clears
def adj_mode(self, v=None): # Set, clear or toggle adjustment mode
self._adj = not self._adj if v is None else v
if not self._adj:
self._precision = False
Screen.redraw_co() # Redraw curret object

def encoder(self):
return self._encoder

def is_precision(self):
return self._precision

def is_adjust(self):
return self._adj


# Wrapper for global ssd object providing framebuf compatible methods.
# Must be subclassed: subclass provides input device and populates globals
# display and ssd.
Expand Down Expand Up @@ -286,9 +344,13 @@ def __init__(
global display, ssd
ssd = objssd
if incr is False: # Special encoder-only mode
ev = isinstance(encoder, int)
assert ev and touch is False and decr is None and prev is not None, "Invalid args"
ipdev = InputEnc(nxt, sel, prev, encoder)
if not isinstance(encoder, (int, bool)):
assert touch is False and nxt is None and sel is None and prev is None and decr is None, "Invalid args"
ipdev = InputI2CEnc(encoder)
else:
ev = isinstance(encoder, int)
assert ev and touch is False and decr is None and prev is not None, "Invalid args"
ipdev = InputEnc(nxt, sel, prev, encoder)
else:
if touch:
from gui.primitives import ESP32Touch
Expand Down
1 change: 1 addition & 0 deletions gui/primitives/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def launch(func, tup_args):
"Pushbutton": "pushbutton",
"ESP32Touch": "pushbutton",
"Switch": "switch",
"I2CEncoder": "i2cencoder",
}

# Copied from uasyncio.__init__.py
Expand Down
61 changes: 61 additions & 0 deletions gui/primitives/i2cencoder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# encoder.py Asynchronous driver for incremental quadrature encoder.
# This is minimised for micro-gui. Derived from
# https://github.com/peterhinch/micropython-async/blob/master/v3/primitives/encoder.py

# Copyright (c) 2021-2024 Peter Hinch
# Released under the MIT License (MIT) - see LICENSE file

# Thanks are due to @ilium007 for identifying the issue of tracking detents,
# https://github.com/peterhinch/micropython-async/issues/82.
# Also to Mike Teachman (@miketeachman) for design discussions and testing
# against a state table design
# https://github.com/miketeachman/micropython-rotary/blob/master/rotary.py

# Now uses ThreadSafeFlag.clear()

import asyncio
from machine import Pin


class I2CEncoder:
delay = 100 # Debounce/detent delay (ms)

def __init__(self, encoder, callback):
self._v = 0 # Encoder value set by ISR
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(div, callback))

def _x_cb(self, pin_x):
if (x := pin_x()) != self._x:
self._x = x
self._v += 1 if x ^ self._pin_y() else -1
self._tsf.set()

def _y_cb(self, pin_y):
if (y := pin_y()) != self._y:
self._y = y
self._v -= 1 if y ^ self._pin_x() else -1
self._tsf.set()

async def _run(self, div, cb):
pv = 0 # Prior hardware value
pcv = 0 # Prior divided value passed to callback
while True:
self._tsf.clear()
await self._tsf.wait() # Wait for an edge
await asyncio.sleep_ms(Encoder.delay) # Wait for motion/bounce to stop.
hv = self._v # Sample hardware (atomic read).
if hv == pv: # A change happened but was negated before
continue # this got scheduled. Nothing to do.
pv = hv
cv = round(hv / div) # cv is divided value.
if (cv - pcv) != 0: # dv is change in divided value.
cb(cv, cv - pcv) # Run user CB in uasyncio context
pcv = cv
61 changes: 61 additions & 0 deletions setup_examples/st7789_i2cencoder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# hardware_setup.py Customise for your hardware config

# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2021 Peter Hinch, Ihor Nehrutsa

# Supports:
# Waveshare Pico LCD 1.14" 135*240(Pixel) based on ST7789V
# https://www.waveshare.com/wiki/Pico-LCD-1.14
# https://www.waveshare.com/pico-lcd-1.14.htm

from machine import Pin, SPI, I2C
import i2cEncoderLibV2
import gc
from drivers.st7789.st7789_4bit import *
SSD = ST7789

SPI_CHANNEL = 0
SPI_SCK = 2
SPI_MOSI = 3
SPI_CS = 5
SPI_DC = 4
SPI_RST = 0
SPI_BAUD = 30_000_000
DISPLAY_BACKLIGHT = 1
I2C_CHANNEL = 1
I2C_SDA = 18
I2C_SCL = 19
I2C_INTERRUPT = 22
I2C_ENCODER_ADDRESS = 0x50

mode = LANDSCAPE

gc.collect() # Precaution before instantiating framebuf
# Conservative low baudrate. Can go to 62.5MHz.
spi = SPI(SPI_CHANNEL, SPI_BAUD, sck=Pin(SPI_SCK), mosi=Pin(SPI_MOSI), miso=None)
pcs = Pin(SPI_CS, Pin.OUT, value=1)
prst = Pin(SPI_RST, Pin.OUT, value=1)
pbl = Pin(DISPLAY_BACKLIGHT, Pin.OUT, value=1)
pdc = Pin(SPI_DC, Pin.OUT, value=0)

portrait = mode & PORTRAIT
ssd = SSD(spi, height=240, width=320, dc=pdc, cs=pcs, rst=prst, disp_mode=mode, display=PI_PICO_LCD_2)

# Setup the Interrupt Pin from the encoder.
INT_pin = Pin(I2C_INTERRUPT, Pin.IN, Pin.PULL_UP)

# Initialize the device.
i2c = I2C(I2C_CHANNEL, scl=Pin(I2C_SCL), sda=Pin(I2C_SDA))

encconfig = (i2cEncoderLibV2.INT_DATA | i2cEncoderLibV2.WRAP_ENABLE
| i2cEncoderLibV2.DIRE_RIGHT | i2cEncoderLibV2.IPUP_ENABLE
| i2cEncoderLibV2.RMOD_X1 | i2cEncoderLibV2.RGB_ENCODER)

encoder = i2cEncoderLibV2.i2cEncoderLibV2(i2c, I2C_ENCODER_ADDRESS)
encoder.reset()

# Create and export a Display instance
from gui.core.ugui import Display

# I2cEncoder Rotary w/ Button only
display = Display(ssd, None, None, None, False, None, encoder) # Encoder