peterhinch / micropython-micro-gui

A lightweight MicroPython GUI library for display drivers based on framebuf, allows input via pushbuttons. See also micropython-touch.
MIT License
270 stars 40 forks source link

encoder-only setup questions #38

Open erichiggins opened 1 year ago

erichiggins commented 1 year ago

Hello! Love this project, thank you for all the work you have done so far!

Trying to use a Pico, an SSD1309, and encoder-only mode on my own project, hitting a few obstacles that I could use some help with. The display portion all works, but I can't seem to get the encoder and encoder push button to work as expected.

  1. Can you clarify why the Encoder class uses x and y input arguments? Most rotary encoders seem to have CLK and DT pins -- it's not clear to me how this translates.
  2. Looking through the various 'setup_examples' that are supplied, a couple of them claim to use an encoder (judging by the filename), but do they? Looking through the code for each, it's not clear to me that any of them use anything other than buttons.

For context, here's my hardware_setup.py

from machine import Pin, SPI, freq
import gc

from drivers.ssd1309.ssd1309 import SSD1309_SPI as SSD
freq(250_000_000)  # RP2 overclock
# Create and export an SSD instance
spi = SPI(0, baudrate=10_000_000, sck=Pin(18), mosi=Pin(19))
gc.collect()  # Precaution before instantiating framebuf
ssd = SSD(128, 64, spi, dc=Pin(16), cs=Pin(17), res=Pin(20))

import sys
print(sys.implementation.version)
from gui.primitives.encoder import Encoder
from gui.core.ugui import Display

enc_clk = Pin(12, Pin.IN, Pin.PULL_UP)
enc_dt = Pin(13, Pin.IN, Pin.PULL_UP)
enc_btn = Pin(10, Pin.IN, Pin.PULL_UP)

display = Display(ssd, nxt=enc_clk, sel=enc_btn, prev=enc_dt, incr=False, encoder=4)

Happy to supply the driver I wrote for the SSD1309 if needed. I can confirm that works as expected. I'll be happy to contribute it for others to use, as well as my own hardware setup example.

Thanks!

peterhinch commented 1 year ago

That setup example looks correct for encoder only mode. The two encoder pins are connected to nxt and prev with the encoder button connected to sel. The naming of encoder signals is vague. I use X and Y but others use CLK and DT. In my opinion X/Y is preferable as it emphasises the symmetry of the device.

The final encoder arg tells the Display to treat the inputs as coming from an encoder rather than buttons.

What is the actual problem you're getting? Can you post a link to the encoder device?

By all means submit a PR for the driver with setup example. Please also submit a PR for the docs which are part of nano-gui. I can copy the driver to nano-gui (all drivers are common to both GUI's).

erichiggins commented 1 year ago

Here is the encoder that I'm using: https://www.amazon.com/dp/B07T3672VK

What is the actual problem you're getting?

When I run the simple.py demo, the display works as intended, but turning and pressing the rotary encoder has no effect on changing the selection of the virtual buttons displayed.

The naming of encoder signals is vague. I use X and Y but others use CLK and DT. In my opinion X/Y is preferable as it emphasises the symmetry of the device.

Totally agree that the naming is vague (at best). Maybe I'm misunderstanding -- are you saying that some encoders treat a clockwise vs couner-clockwise turn as signals sent to two different pins (X/Y)?

peterhinch commented 1 year ago

It's evidently a standard encoder. Going on one of the comments the 5V pin is connected to 10KΩ pullups and the switches are connected to gnd. With a Pico you should connect the 5V pin on the encoder to the 3V3 output on the Pico. This is because the Pico inputs are not 5V-compatible. Gnd on the encoder should be connected to one of the Pico's gnd pins.

The 5V issue doesn't explain your problem and I'm baffled. I think there is an electrical issue. Are you sure you're not confusing GPIO numbers with pin numbers? For example GPIO10 is on Pin14. If the pins are correct I can only suggest double checking the integrity of the wiring.

A mechanical encoder such as this one comprises two switches arranged to produce a quadrature signal as described here. The naming of these switches is immaterial: X/Y A/B or CLK/DT. Clockwise/anticlockwise discrimination is done by detecting the relative phase of the two signals, as described in the doc. If the direction is wrong you can swap the wires or swap the pins in hardware_setup.py.

erichiggins commented 1 year ago

I'm certain the wiring is correct. I can successfully use rotary_irq_rp2.py with the following code to confirm the encoder works as expected.

import time
from rotary_irq_rp2 import RotaryIRQ

r = RotaryIRQ(pin_num_clk=12,
              pin_num_dt=13,
              min_val=0,
              max_val=19,
              reverse=False,
              range_mode=RotaryIRQ.RANGE_WRAP)

val_old = r.value()
try:
    while True:
        val_new = r.value()

        if val_old != val_new:
            val_old = val_new
            print('result =', val_new)
        time.sleep_ms(1)
except KeyboardInterrupt:
    pass

The naming of these switches is immaterial: X/Y A/B or CLK/DT.

Do you recall which pairs your code treats as equal?

X = A = CLK
Y = B = DT

or the reverse?

erichiggins commented 1 year ago

Update: I just built and installed it again -- not certain what I changed but now the encoder seems to work!

erichiggins commented 1 year ago

Update 2: Looks like the issue is with stopping the simple demo in vscode doesn't exit cleanly. Trying to start it again exits early without an exception (main loop doesn't persist?). Have to reset the Pico, then run the script again and it works fine.

I was confused because the screen still showed the simple menu demo but encoder input wasn't registering since the script wasn't running.

erichiggins commented 1 year ago

Update 3: The encoder was working, though using it was finicky. Adjusting the input division via the encoder= int argument helped a little. The real issue was the default delay of 100ms. This isn't easy to override -- I had to do the following:

display = Display(ssd, nxt=enc_clk, sel=enc_btn, prev=enc_dt, incr=False, encoder=4)
display.ipdev._enc.delay = 10

Ideally, all arguments for the Encoder constructor could be accessible via the Display constructor, or alternatively, accessible via public methods.

peterhinch commented 1 year ago

In general with asyncio applications it's best to reset the target between runs.

I'll give some thought to making the delay configurable but I'm puzzled why you found it necessary. I have experimented with delays from 0 to 1000ms. At 1000ms the delay in response was very evident but behaviour was predictable. I don't know if your encoder has mechanical detents: if it does, it's worth ensuring that the division ratio (encoder=4) matches the number of encoder pulses per detent. Then one click on the knob corresponds to exactly one screen movement between objects. That relationship persisted in my test even with the ridiculous 1000ms delay.

The purpose of the delay is to limit the frequency at which the encoder callback is run. However this callback runs in an asyncio context so there is no risk of re-entrancy. Nothing broke when I reduced the delay to zero but I do think that some delay is advisable. Contact bounce can extend for tens of ms.

peterhinch commented 1 year ago

I have pushed an experimental encoder driver that may help with your problem. Go to the branch encoder_driver and copy the file encoder.py to `gui/primitives/' on your target. You'll need to remove the code you added that changes the timeout.

If you're in doubt about the right value of the encoder arg, paste the following script at the REPL and follow the instructions (you'll need to change the pin numbers):

from gui.primitives import Encoder
import asyncio
from machine import Pin
x = Pin(20, Pin.IN, Pin.PULL_UP)
y = Pin(17, Pin.IN, Pin.PULL_UP)
v = 0
n = 0

def cb(a, b):
    global v, n
    print(b)
    v += abs(b)
    n += 1

async def main():
    enc = Encoder(x, y, callback=cb, delay=500)
    await asyncio.sleep(60)
    print(f"encoder value is {round(v/n)}")
    enc.deinit()

s = """
This script determines the value of the encoder arg for encoders with mechanical
detents. It runs for 60 seconds. After starting, rotate the encoder one click
and wait for a printed result. Repeat with one click each time a result is output.
"""
print(s)
try:
    asyncio.run(main())
finally:
    _ = asyncio.new_event_loop()

Please report your findings. This is quite hard for me to diagnose as the original code works here!

peterhinch commented 1 year ago

I have updated main with an improved encoder driver. If there is still a need to change the default time delay, please raise a new issue with a clear description of the setup, encoder value and the problem.