Add support for I2CEncoder V2.1 rotary encoders
Hello,
I'd like to extend micropython-micro-gui to include support for the I2CEncoder V2.1 encoders (https://www.tindie.com/products/saimon/i2cencoder-v21-connect-rotary-encoder-on-i2c-bus/).
Could you please take a look at https://github.com/peterhinch/micropython-micro-gui/pull/55 and let me know if I'm on the right track?
I'm instantiating the i2cEncoderLibV2 object for the encoder in hardware_setup.py and passing the object to Display as display = Display(ssd, None, None, None, False, None, encoder)
I didn't want to directly couple the i2cEncoderLibV2 library to ugui.py, so an isinstance check isn't possible in the Display method. I went with this for now, but not sure if there's a better way:
if incr is False: # Special encoder-only mode
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)
It looks like we'll possibly need a new version of the Pushbutton primitive, too, or just handle the button completely in InputI2CEnc.
Ugh, I see that I neglected to provide a way to pass the I2C interrupt pin to display. This interrupt fires with any change of the encoder and is needed. Maybe we pass an array with the encoder and the interrupt pin as elements as the encoder argument to Display's init function. Thoughts or guidance would be appreciated.
Here's a working script which shows how to interact with the I2CEncodes using the i2cEncoderLibV2 library.
import time
import struct
from machine import Pin, I2C
import i2cEncoderLibV2
# Setup the Interrupt Pin from the encoder.
INT_pin = Pin(22, Pin.IN, Pin.PULL_UP)
# Initialize the device.
i2c = I2C(1, scl=Pin(19), sda=Pin(18))
encoder = i2cEncoderLibV2.i2cEncoderLibV2(i2c, 0x44)
def EncoderChange():
encoder.writeLEDG(100)
valBytes = struct.unpack('>i', encoder.readCounter32())
print('Changed: %d' % valBytes[0])
encoder.writeLEDG(0)
def EncoderPush():
encoder.writeLEDB(100)
print('Encoder Pushed!')
encoder.writeLEDB(0)
def EncoderDoublePush():
encoder.writeLEDB(100)
encoder.writeLEDG(100)
print('Encoder Double Push!')
encoder.writeLEDB(0)
encoder.writeLEDG(0)
def EncoderMax():
encoder.writeLEDR(100)
print('Encoder max!')
encoder.writeLEDR(0)
def EncoderMin():
encoder.writeLEDR(100)
print('Encoder min!')
encoder.writeLEDR(0)
def Encoder_INT(pin):
encoder.updateStatus()
# Initialize encoder
encoder.reset()
print("Encoder reset")
time.sleep(0.1)
encconfig = (i2cEncoderLibV2.INT_DATA | i2cEncoderLibV2.WRAP_ENABLE
| i2cEncoderLibV2.DIRE_RIGHT | i2cEncoderLibV2.IPUP_ENABLE
| i2cEncoderLibV2.RMOD_X1 | i2cEncoderLibV2.RGB_ENCODER)
encoder.begin(encconfig)
print("Encoder begin with config:", encconfig)
encoder.writeCounter(0)
encoder.writeMax(35)
encoder.writeMin(-20)
encoder.writeStep(1)
encoder.writeAntibouncingPeriod(8)
encoder.writeDoublePushPeriod(50)
encoder.writeGammaRLED(i2cEncoderLibV2.GAMMA_2)
encoder.writeGammaGLED(i2cEncoderLibV2.GAMMA_2)
encoder.writeGammaBLED(i2cEncoderLibV2.GAMMA_2)
encoder.onChange = EncoderChange
encoder.onButtonPush = EncoderPush
encoder.onButtonDoublePush = EncoderDoublePush
encoder.onMax = EncoderMax
encoder.onMin = EncoderMin
encoder.autoconfigInterrupt()
print('Board ID code: 0x%X' % encoder.readIDCode())
print('Board Version: 0x%X' % encoder.readVersion())
encoder.writeRGBCode(0x640000)
time.sleep(0.3)
encoder.writeRGBCode(0x006400)
time.sleep(0.3)
encoder.writeRGBCode(0x000064)
time.sleep(0.3)
encoder.writeRGBCode(0x00)
# Setup an interrupt handler
INT_pin.irq(trigger=Pin.IRQ_FALLING, handler=Encoder_INT)
while True:
# For debugging, we can poll status
encoder.updateStatus()
time.sleep(0.1)
I have a number of problems with this.
- What is its purpose? A simple switch works fine. You are the first person to request this - I need a well-defined use case.
- I would not support a device based on a TIndie post with an unspecified chip. I would require a well supported and documented device from a known supplier.
- From a quick look at your driver I suspect the approach is wrong. The chip maintains an up-down counter so there is no need to replicate the code in my driver which uses IRQ's to maintain an up-down counter.
- If I were to support it I would write a MicroPython driver using the chip datasheet. It would be designed quite differently from the existing switch-based driver.
To do this properly would involve me in a significant amount of work. In the absence of a compelling use-case, I think the best approach may be for you to maintain your own fork.
Thanks for the reply.
For a little background on the I2CEncoder, it was a Kickstarter campaign here: https://www.kickstarter.com/projects/1351830006/i2c-encoder-v2
The library and datasheet are here: https://github.com/Fattoresaimon/I2CEncoderV2.1
The library from the author doesn't include MicroPython, but it does include a Python. I ported this to MicroPython here: https://github.com/cameronprince/i2cEncoderLibV2
The entire I2CEncoder project is open source, which includes the PCB and firmware for its microcontroller. As best I can tell, the supplier is not documented.
I'll keep working on the PR using the advice you've provided and will update you shortly.
Thanks, Cameron
Oh, I forgot to mention a nice project with a lot of good info on the I2CEncoders is here: https://www.vinthewrench.com/p/rings-and-knobs
If I were to support an I2C encoder it would probably be this one from Sparkfun. I have a strong preference for products from established companies on the basis that they are likely to be supported for some time into the future.
But I fail to see the use case. I spent some effort writing code to support cheap, readily available, switches. I can't see any benefit in complicating the design to add support for I2C encoders. I would need to see a substantial benefit to do this.
I understand... The benefits are; reduction in GPIO usage, wiring, consolidation of code and they're pretty cool visual status indicators. The good thing is, if you do add support for one I2C encoder, it should be pretty straightforward to add it for the other. I do understand these benefits aren't directly applicable to your project because it currently only requires a single encoder.
@cameronprince - I've got a few of these nice encoder boards a few years ago (V2.0). I expect you may have considered using your own code to read buttons and encoders etc. but then to use nano-gui for the display. I have taken this approach, though not with these particular encoder boards, in the past. Are there particular widgets in micro-gui that are not in nano-gui leading you to try to adopt the micro-gui library?
I think the code for using the likes of the SparkFun i2c encoder would be rather different to the DUPPA encoder board that uses an interrupt to initiate an i2c communication to read the PIC microcontrollers registers so I'm not sure that support for one i2c encoder would easily translate to another. But that's just a rather quick surmise.
Thanks for your V2.1 MicroPython library though, and after a check on how it needs to be amended for my V2 encoder boards, I think it will prove most useful.
I expect you may have considered using your own code to read buttons and encoders etc. but then to use nano-gui for the display.
This is the approach I have recommended. This is because @cameronprince intends to use multiple encoders, each controlling its own widget. The concept behind micro-gui is that only one widget has the focus at a given time, and all control is directed to that widget. This is quite deeply embedded in the design, which is why I think a "start from scratch" approach with neno-gui is best.
Back-porting widgets to nano-gui should be easy as all you're keeping is the visual appearance.