From 0507a33203770885b9abb49683123463c9a9aa21 Mon Sep 17 00:00:00 2001 From: Cameron Prince Date: Wed, 15 Jan 2025 01:04:20 -0600 Subject: [PATCH] Stubs out support for I2CEncoder. --- gui/core/ugui.py | 68 +++++++++++++++++++++++++++-- gui/primitives/__init__.py | 1 + gui/primitives/i2cencoder.py | 61 ++++++++++++++++++++++++++ setup_examples/st7789_i2cencoder.py | 61 ++++++++++++++++++++++++++ 4 files changed, 188 insertions(+), 3 deletions(-) create mode 100644 gui/primitives/i2cencoder.py create mode 100644 setup_examples/st7789_i2cencoder.py diff --git a/gui/core/ugui.py b/gui/core/ugui.py index 1104d79..7d41393 100644 --- a/gui/core/ugui.py +++ b/gui/core/ugui.py @@ -177,6 +177,64 @@ class InputEnc: 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. @@ -286,9 +344,13 @@ class Display(DisplayIP): 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 diff --git a/gui/primitives/__init__.py b/gui/primitives/__init__.py index ef1604f..cccc366 100644 --- a/gui/primitives/__init__.py +++ b/gui/primitives/__init__.py @@ -28,6 +28,7 @@ _attrs = { "Pushbutton": "pushbutton", "ESP32Touch": "pushbutton", "Switch": "switch", + "I2CEncoder": "i2cencoder", } # Copied from uasyncio.__init__.py diff --git a/gui/primitives/i2cencoder.py b/gui/primitives/i2cencoder.py new file mode 100644 index 0000000..23d7891 --- /dev/null +++ b/gui/primitives/i2cencoder.py @@ -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 diff --git a/setup_examples/st7789_i2cencoder.py b/setup_examples/st7789_i2cencoder.py new file mode 100644 index 0000000..e030ddf --- /dev/null +++ b/setup_examples/st7789_i2cencoder.py @@ -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 \ No newline at end of file