In many LED animations, you need to apply an operation to all LEDs as fast as possible. Often this is something like “fadeToBlackBy”, i.e. “dim all LEDs by a given amount”. There are some common effects you get by using “fadeToBlackBy” and one other action. For example: the Cylon effect is “Turn an LED on, Fade all LEDs toward black, Go to next LED, Repeat”. A simple firework simulation is “Turn on random LED, Fade all LEDs toward black, Repeat”.
Since I don’t come from a normal Python background, I’ve not used much Numpy. But I knew it was great for vector math. And I knew CircuitPython had a minimal version of Numpy called ulab it inherited from Micropython. Even though I’d seen the wonderful ulab learn guide that jepler did, the usefulness of numpy/ulab didn’t sink in. It was only when I was hacking on LED animation speedups did I re-stumble upon ulab. And am I glad I did.
As a test with a simple fire animation (as optimized as I could make it in normal Python), the results with ulab are striking:
fire_no_ulab.py: ~40 milliseconds per frame for 256 LEDs on RP2040
The general technique is to create a “working copy” of the LED data in a ulab array, and use ulab functions as much as possible to modify that working copy. Then at the last possible moment, copy the working copy data to the real LEDs object. It only adds a few lines of code to existing solutions and you get access to all these cool ulab array functions for LED effects! For instance, to constrain all RGB values of all LEDs to 0-255, this is a single line in ulab: leds_np = np.clip(leds_np,0,255). And it further cements my belief that any time you’re doing a for-loop on a large list in Python, you’re probably doing it wrong. :-)
Screenshots of functionally identical code, before / after:
The video demo below shows the difference (also available on youtube). Each setup is identical, calculating for 256 LEDs even though only 64 are displayed. The “no ulab” case is perfectly usable if a bit choppy, but you lose ~36 milliseconds where you could be doing something else. (Like, for instance, these LED matrices are wired up in a serpentine pattern, so you need some math to unravel that if you want to draw shapes). The ulab version seems much smoother and more organic, at least to my eyes.
Did you know you can run multiple displays in CircuitPython? One way: wire up two displays in parallel.
There’s also true dual displays, more on that in a bit. For the above, here’s the wiring diagram and the code “gc9a01_hellocircles_compact.py”. Note how the second display is hooked up in parallel and the code just sees one display.
But what if you want each display to be independent?
Almost all CircuitPython boards support a display. CircuitPython makes it really easy to configure a display’s driver and all displays act the same for drawing images and text: monochrome OLEDs, RGB TFT LCDs, even ePaper!
But only a single display is supported for the pre-built versions of CircuitPython (The one exception is the MonsterMask) Multiple displays are supported in the design of CircuitPython, but it’s a compile-time setting. This is for RAM savings reasons. That decision is a few years old and I think for modern chips like ESP32-S2/3 and RP2040, there’s more than enough RAM to allow that default to go up to two.
Compiling CircuitPython to Add Second Display
In the meantime, we can re-compile CircuitPython ourselves to increase that limit. It’s pretty easy if you’re comfortable with the command line. If you’re already know how to build CircuitPython, the short answer is:
Add this line to your board’s mpconfigboard.h: #define CIRCUITPY_DISPLAY_LIMIT (2) recompile and install the resulting UF2 file.
If you’ve never compiled CircuitPython before, there is a very useful Adafruit Learn Guide building CircuitPython. I recommend start there and go through each step. This post is not that. This post is a reminder for myself, with an abbreviated version those steps that I can just copy-n-paste when I want an entirely fresh checkout. Not all of these steps are needed if you already have a checkout.
First: initial git checkout, getting the git submodules, and installing python requirements.
git clone https://github.com/adafruit/circuitpython circuitpython-todbot
cd circuitpython-todbot
make fetch-submodules
git checkout main
pip3 install --upgrade -r requirements-dev.txt
pip3 install --upgrade -r requirements-doc.txt
make -C mpy-cross
Next: do an easy build for QTPy M0 to make sure everything works:
cd ports/atmel-samd
make BOARD=qtpy_m0
# or can do: make -j10 BOARD=qtpy_m0 # since you have lots of cores
ls -l build-qtpy_m0/firmware.uf2
cd ports/espressif
./esp-idf/install.sh
. ./esp-idf/export.sh
make -j10 BOARD=adafruit_qtpy_esp32s2
ls -l build-adafruit_qtpy_esp32s2/firmware.uf2
For any of these, when you get a .UF2 file, you can copy to your board in UF2 bootloader mode, like a normal CircuitPython upgrade. This is a great way to try the latest changes without waiting for the automated builds on Adafruit’s S3 cache.
Now we can make the change to support two displays. Let’s add two displays to CircuitPython for QTPy ESP32-S2:
cd ports/espressif
echo "#define CIRCUITPY_DISPLAY_LIMIT (2)" >> boards/adafruit_qtpy_esp32s2/mpconfigboard.h
make -j10 BOARD=adafruit_qtpy_esp32s2
Copy the resulting “firmware.uf2” file to install your new CircuitPython and start trying out dual displays!
Using Multiple Displays
You can now wire up multiple displays as needed for your application, CircuitPython doesn’t care: two SPI busses with independent CS/control lines, a single SPI bus with independent CS/control lines, a single I2C bus, or multiple I2C busses.
For example, if using two I2C SSD1306 monochrome OLED displays at addresses 0x3C and 0x3D on the same I2C bus, creating two display objects would look like:
import busio, displayio
from adafruit_displayio_ssd1306 import SSD1306
dw,dh = 128,64 # display width,height
i2c = busio.I2C(scl=board.GPIO0, sda=board.GPIO1) # e.g. on a Pico
display_busA = displayio.I2CDisplay(i2c, device_address=0x3C)
display_busB = displayio.I2CDisplay(i2c, device_address=0x3D)
displayA = SSD1306(display_busA, width=dw, height=dh)
displayB = SSD1306(display_busB, width=dw, height=dh)
Notice you need a display_bus object for each display. For two GC9A01 round LCDs using SPI, you can still use a single SPI bus, but you do need separate CS, CD, RST lines, like:
I help support the USB HID projects hidapi and node-hid. One of the aspects of HID that isn’t clear is that HID devices transmit and receive data with “reports”, fixed-size data packets. What reports a device responds to is described by the HID Report Descriptor it hands to the OS. The OS then only permits those reports to be sent and received. This descriptor is different from the USB Config & Device Descriptors Those two are easily discoverable using standard system tools, but for whatever reason, getting HID Report Descriptors is much harder.
To address this, I’ve created two tools: win-hid-dump and mac-hid-dump from code snippets around the Net. They are akin to simplified versions of usbhid-dump for Linux. They print the HID Report Descriptors of the HID devices currently connected and the output can be parsed by things like https://eleccelerator.com/usbdescreqparser/
I’ve found many of the issues I have in supporting node-hid is getting people to understand that you need to know the type and sizes of the reports you want. Hopefully they’ll be useful to others as well. Let me know if you have any issues with them.
If you are like me, you play around with Neopixel / WS2812-style LEDs a lot. Normally I solder my own custom-connectors and don’t use the ones that come with the reels I buy. But since we’ve been playing around with WS2812-compatible Xmas lights, I decided to make a “Neopixel Dev Kit” for easier experimenting with different LEDs and different ways of powering them. Most LED reels come with a 3-pin JST SM socket (female) connector installed with also maybe a 2.1mm DC barrel connector to inject power (see reel picture below). They’ll also throw in a pigtail JST SM plug (male) connector to attach to your controller. So I standardized on this. The kit as shown consists of (from bottom to top):
Known-good USB-powered WS2812 controller from those lights, rewired with JST SM plug
Standard 3-pin JST SM plug to JST SM socket, with 2.1mm DC barrel connector power injector
3-pin JST SM plug (male) to header pins, for breadboarding
Known-good normal WS2812 strip with JST SM socket
5V power supply with 2.1mm DC barrel connector
USB power meter for seeing voltage & amps drawn by any particular setup (see example below)
Arduino Pro Micro clone on tiny breadboard. This is a 5V microcontroller, so no need to worry about 3.3V -> 5V data issues. And it’s cheap in case we blow it up
Overall it’s working out okay. I need to add a few “sacrificial neopixels” to the mix to make using 3.3V-based microcontrollers and maybe some breadboard screw-terminals.
I’ve not done much on our plotter Xmas tree this week because, well… if you follow me on Twitter, it’s likely you’ve seen me ramble on about these crazy new Christmas Tree lights we got for our tree (first tree ever!). I wasn’t able to find appropriate “Christmas Tree” light forms of the WS2812 lights, but Carlyn did! And they were even better than normal Xmas light styles. These were “fairy lights” with small beads of light connected by solid enamel-coated wire that almost disappear visually. The LED string appeared to maybe be WS2812 / Neopixel compatible and yup, they were! But the weird thing was, as I learned was discovered back in November, these LEDs remember their address when cut out of the strand. If you’re familiar with WS2812-style LEDs, this is bonkers. Normally these LEDs have four pins: +5V, Gnd, DataIn, and DataOut. To make an LED strip, the LEDs are wired in a chain, one LED’s DataOut going to next LED’s DataIn. This is great! It means you just have one wire to control hundreds of LEDs!
What’s going on with these LEDs? You cut them up, wire them in parallel (there’s no data “in” and “out”, just “data”) and they still know where they should be in the data stream, only picking the data for them. I’m still researching this with the help of some friends who can navigate Shenzhen better than me, but it’s looking like these LEDs have their address set OTP (one-time programmable) at the factory. No LED part number yet. If you’d like to get some to try yourself, here’s the 100-LED exact Amazon ASIN I bought as well as the 200-LED ASIN Carlyn originally found (both affil links).
Here’s some pics showing the box and how I took it apart to test on an Arduino Micro clone.
Weird new WS2812/Neopixel-like chip wired in parallel but knowing their position in the strip, even when cut away. WAT Turns out the WS2812 Xmas lights I got are cleverer than I thought. More details by original discoverers: https://t.co/2rCL8RRSq2https://t.co/HmhpyZruM5pic.twitter.com/BUxowTumOE
We recently got some inexpensive multicolored 0.3mm technical markers (AmzAffil) for use on the AxiDraw and they are pretty good! For this week’s batch of days for our 31-box AxiDraw calendar, I wanted to play with doing “flood fill” / solid color fills. Normally the “hatch fill” extension part of the AxiDraw Inkscape extensions creates slanted lines to imply a filled color. I knew a true full fill would be touchy because the paper can tear when fully inked, but I wanted to try. It turns out to work great!
There’s not much of a trick to it, just adjust the “hatch fill” algorithm to have closely-spaced lines (“2” for a 0.3mm pen worked well) and “connect nearby ends” to go back-n-forth to minimize pen up/down messiness. Also orienting your hatch direction, optimizing for your object (0º in my case) can make the fill succeed better. Plotting does take a while (especially for multi-colors like the 12 ornaments) and does cause some bleed-through. But I think it looks awesome.