adafruit / Adafruit_CircuitPython_NeoPixel

CircuitPython drivers for neopixels.
MIT License
302 stars 98 forks source link

NeoPixel list slicing seems unintuitive to use #155

Closed Sarioah closed 1 year ago

Sarioah commented 1 year ago

There are a couple of examples out there in the wild that suggest that list slicing can be a shortcut to setting many NeoPixel-compatible LEDs at once (one such example).

The gist seems to be that one can assign a colour to a sliced list, i.e. NeoPixel[::2] = (255, 0, 0) should set every second pixel to red.

In practice it seems like the slicing exposes a mostly plain tuple instead. Attempts to assign to it appear to invoke sequence unpacking on the colour, usually throwing a ValueError since the LHS and RHS have different lengths.

I've been able to assign a tuple of colours to a sliced NeoPixel list, but this seems pretty cumbersome to do every time...

Adafruit CircuitPython 8.1.0 on 2023-05-22; Seeed XIAO nRF52840 Sense with nRF52840
>>> import neopixel
>>> import board
>>> pix = neopixel.NeoPixel(board.D10, 32)
>>> pix.fill(0xFFFFFF)  # all 32 lights turn white
>>> pix[::2] = 0xFF0000
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object of type 'int' has no len()
>>> pix[::2] = (255, 0, 0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: Unmatched number of items on RHS (expected 16, got 3).
>>> pix[::2].fill
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'tuple' object has no attribute 'fill'
>>> pix[::2] = (255, 0, 0) * len(pix[::2])  # every second light turns red
>>>

The final statement works, but doesn't feel like this is the way the slicing implementation was supposed to be used. The examples I can find seem suggest I should be able to have a bare colour on the RHS.

dhalbert commented 1 year ago

A slice on the left side of the = designates multiple pixels. It will not expand a single pixel value (an RGB tuple) on the right. You have to do that yourself, as in pix[::2] = [(255, 0, 0)] * len(pix[::2]).

This is true of any Python slicing operation. It's not just neopixels. E.g.:

>>> a = [2,4,6]
>>> a[::2]
[2, 6]
>>> a[::2] = 3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: must assign iterable to extended slice
>>> a[::2] = (3, 3)
>>> a
[3, 4, 3] 

If you are seeing examples that are doing implicit expanding, they are wrong and we need to fix them, so please give us links to those if you see them.

Neradoc commented 1 year ago

Note that you can loop on indices and assign each color using auto_write=False to avoid updating until you are finished. len(pix) // 2 would be a better choice too (len(pix[::2]) will allocate a temporary slice).

There's also helpers that can abstract a group of pixels that you use regularly like adafruit_pixelmap's PixelMap, which has a fill() method and is accelerated in C. If your board's build doesn't support the low level _pixelmap, you can use the PixelMap class from the adafruit_led_animation library helpers module.

Sarioah commented 1 year ago

A slice on the left side of the = designates multiple pixels. It will not expand a single pixel value (an RGB tuple) on the right... ...This is true of any Python slicing operation. It's not just neopixels

That's what I figured, but then the example I found suggested custom behaviour with the single colour on the RHS which ended up confusing me so thanks for clearing that up.

In my case iterating over the temporary slice with a delayed .show() is going to be the cleanest way I think (since I'm already instantiating with auto_write=False anyway). The PixelMap looks like an interesting library though, when I get a chance this weekend I'll probably have a play with it, thanks :)

I found the main example suggesting the custom behaviour here. I do remember seeing it in a couple other places, though this one is all I can find right at the moment. I suspect perhaps the others might reference this same "CheatSheet" like this forum post does.