adafruit / Adafruit_CircuitPython_TCA9548A

CircuitPython driver for the TCA9548A I2C Multiplexer.
MIT License
26 stars 15 forks source link

TypeError: i2c_bus must be of type I2C (with multiples SSD1306 display) #37

Closed remisarrailh closed 2 years ago

remisarrailh commented 2 years ago

I'm trying to plug 8 SSD1306 display using a TCA9548A multiplexer (on a Adafruit NRF Sense board). When I scan them, it correctly detects each displays

Unfortunately when I try to initialize the display, I get an TypeError as the tca channel object is not a I2C object.

import board
import adafruit_tca9548a
import displayio
import terminalio
import adafruit_displayio_ssd1306
from adafruit_display_text import label
# Create I2C bus as normal
displayio.release_displays()

i2c = board.I2C()  # uses board.SCL and board.SDA

# Create the TCA9548A object and give it the I2C bus
tca = adafruit_tca9548a.TCA9548A(i2c)
display_bus = displayio.I2CDisplay(tca[0], device_address=0x3C)
display = adafruit_displayio_ssd1306.SSD1306(display_bus, width=70, height=35)

text = "1"
text_area = label.Label(terminalio.FONT, text=text, scale=5, color=0xFFFF00, x=53, y=15)
display.show(text_area)
while True:
    pass
Traceback (most recent call last):
  File "code.py", line 14, in <module>
TypeError: i2c_bus must be of type I2C
caternuson commented 2 years ago

Hmm...since displayio.I2CDisplay() is a core function, the type checking is happening in the C code. And tca[0] is of type TCAChannel. When tca[0] is more typically passed to other CircuitPython libraries, the strict type checking does not occur in that scenario.

It doesn't look like inheritance could solve this:

Adafruit CircuitPython 7.2.5 on 2022-04-06; Adafruit ItsyBitsy M4 Express with samd51g19
>>> import board
>>> import busio
>>> import displayio
>>> class Foo(busio.I2C):
...     def bar(self):
...         print("hello")
... 
>>> foo = Foo(board.SCL, board.SDA)
>>> display_bus = displayio.I2CDisplay(foo, device_address=0x3C)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: i2c_bus must be of type I2C
>>> type(foo)
<class 'Foo'>
>>> 
dhalbert commented 2 years ago

Right, this is not going to work. The communication with the I2C display is done directly via the busio.I2C() implementation, at the C level, so you can't substitute a duck-typed I2C device.

An emulation of displayio and I2CDisplay could be written in python, but it would probably not run very fast.

You can support multiple displays by using the address selector line on the SSD1306 to make it respond to another address (0x3D instead of 0x3C), but that only works for two displays per I2C bus. You can use another I2C bus, and if you run out of I2C busses, you can start using bitbangio.I2C on yet more pins. At some point you may run out of RAM to support so many displays.

caternuson commented 2 years ago

Another option might be to manually mux the TCA? Essentially, don't use this library. Pass in the main I2C bus displayio.I2CDisplay(). Your application code would then need to take care of switching TCA channels between any displayio calls to the displays attached to the various channels.

remisarrailh commented 2 years ago

@caternuson Thanks this solution works.

Of course my code is more of a dirty hack then a functional one and update is not really fast (I need to release displays each times for it to works and add a sleep or else some display will not update, or the code even crash).

I'm also using tiny oled (72x42) and I have a weird offset error (that's why I used a Width/height different to its size, seems to works fine) but I digress.

I managed to make it works without resetting the display if I manually create each display on the terminal, and restart (CTRL-D)

import board
import displayio
import terminalio
import adafruit_displayio_ssd1306
from adafruit_display_text import label
import time
# Create I2C bus as normal
displayio.release_displays()

def tca_select(channel):
    while not i2c.try_lock():
        pass
    i2c.writeto(0x70, bytearray([1 << channel]))
    i2c.unlock()

i2c = board.I2C()  # uses board.SCL and board.SDA
n = 0
while True:
    for i in range(0,8):
        tca_select(i)
        display_bus = displayio.I2CDisplay(i2c, device_address=0x3C)
        display = adafruit_displayio_ssd1306.SSD1306(display_bus, width=100, height=42)
        print("TCA Channel: " + str(i))
        text = str(i + n)
        text_area = label.Label(terminalio.FONT, text=text, scale=5, color=0xFFFF00, x=40, y=16)
        display.show(text_area)
        time.sleep(0.05)
        displayio.release_displays()
    n = n + 1

I posted my solution on Gists for posterity https://gist.github.com/maditnerd/c0da9b2b81544ecf21220ed2a1321dd7

My goal was to make a midi sequencer with button underneath the screen, (each screen displaying the note on each step) but I'll probably never have a refresh rate good enough for that, even if I use C, I'll figure out something else.

https://user-images.githubusercontent.com/2841495/168129648-78e0cdc7-84ba-4e2b-a495-239f2d45935b.mov

I guess you can close the issue, since I have a solution which doesn't involve using the library.

caternuson commented 2 years ago

What refresh rate do you need? You're looping on initialization code above. Might buy back some performance by removing that from loop. Like here:

        display_bus = displayio.I2CDisplay(i2c, device_address=0x3C)
        display = adafruit_displayio_ssd1306.SSD1306(display_bus, width=100, height=42)

But agree this approach is a bit hack-ish and will result in somewhat messy code. Since the TCA muxing will need to be carefully (and manually) interleaved with all the displayio calls.

Going to close issue since this is essentially something that won't work for reasons danh mentions above. Unfortunately. Sorry though. Looks like a really cool project.