Making a "CheerDot" with MicroPython

Andy Piper - Feb 6 '22 - - Dev Community

The next step in my exploration of the "bedazzled" ESP32-C3 RGB board was always going to involve the fact that the chip provides network capabilities with a wifi stack (it offers Bluetooth as well, but this time, we're using wifi).

Reference card showing annotated images of both sides of the ESP32-C3 RGB board

We have 25 WS2812B LEDs, on a small device that we can power via USB and connect to the interwebs... so clearly, I had to do something with that. Using the Cheerlights project, I also got to drive the whole thing using both two more of my favourite technologies: Twitter, and MQTT.

In the previous post, I got everything working pretty nicely with the basics on the board - we had flashed a working MicroPython build, and had a simple piece of code that exercised the NeoPixel LEDs. Since writing that post, I also created a reference card (image above - there's a PDF, and the source GoodNotes file, on GitHub as well); and updated the code a little to add a test for the onboard status LED and programmable button.

My approach to building the Cheerlights support was to use the MicroPython REPL to run and test things bit-by-bit, before combining into a program:

  • get wifi up and connected to the network
  • install MQTT client module
  • subscribe to the Cheerlights MQTT server, output the colour values to the console
  • convert the colour values to something that can be fed to the NeoPixels
  • finally, put it all together: when powered on, connect to network, subscribe to Cheerlights, change the LED colour as new values come in.

Connect to wifi

The wifi piece is straightforward, and is pretty much the same as you will read in the ESP32 MicroPython reference documentation. We import the network module, and also configure a pin, so that when we get an IP address, the small blue status LED next to the USB-C port can be switched on. Here's the connection code, similar to the boilerplate from the docs, apart from the status LED piece. If we are connected to the console when this runs, we will see the IP address printed out.

def do_connect():
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    if not wlan.isconnected():
        status_led.off()
        print("connecting to network...")
        wlan.connect(my_ssid, my_pass)
        while not wlan.isconnected():
            pass
    status_led.on()  # status LED shows connected
    print("network config:", wlan.ifconfig())
Enter fullscreen mode Exit fullscreen mode

MQTT Client library

Once we're all set, we need to set up an MQTT subscription on the Cheerlights broker.

In order to do this, we first need an MQTT client library. I'm relatively new to MicroPython, and in many cases there's no way to easily drag/drop code to the board as you can in CircuitPython, so there was some learning for me, here. There are a couple of options - get the library locally onto the development machine and then copy it to the board using a tool like ampy; or, get the board online, and use upip to install to the filesystem.

Initially I tried using rshell to access the board and REPL, but for some reason, I seemed to keep getting disconnected on the serial port. In the end, I went back into the terminal in Thonny, which seemed to be slightly more reliable. I'll look into this some more in the future.

After running the do_connect() function to get online, I was then able to run:

import upip
upip.install('micropython-umqtt.robust')
upip.install('micropython-umqtt.simple')

This installed the umqtt client library into the /lib folder on the ESP32 filesystem.

MQTT subscription

The code is set to connect directly to mqtt.cheerlights.com, but if you are running your own local broker (for example if want to do something using Node-RED or another tool locally), you could change the address to connect locally. The other thing is that we are using the umqtt.robust client, which should mean that we automatically get reconnected to the broker if the network connection drops. Here are the key functions and calls involved (the whole program has it all in the right sequence).

def cl_callback(topic, payload):
    cl_rgb = hex_to_rgb(payload)
    print("message received: " + str(cl_rgb))
    light_up(cl_rgb)


def client_name():
    m_id = machine.unique_id()
    client_id = (
        "{:02x}{:02x}{:02x}{:02x}".format(m_id[0], m_id[1], m_id[2], m_id[3])
        + "-cl-dot"
    )
    return client_id


# this makes the MQTT connection 
# and subscribes to the colour change topic
mqtt = MQTTClient(client_name(), cl_broker)
mqtt.connect()
mqtt.set_callback(cl_callback)
mqtt.subscribe("cheerlightsRGB")

while True:
    mqtt.check_msg()
Enter fullscreen mode Exit fullscreen mode

Dropping down to the bottom of the code snippet, we create an instance of MQTTClient, drawn from the umqtt.robust library. In the client_name() function, we stringify the unique ID of the board, append a bit of extra text, and use that as a client name for the MQTT connection. We set up a callback function that will receive the messages, and subscribe to the "cheerlightsRGB" topic (which delivers the colours in a web-friendly / hexadecimal RGB format). Then, we loop, checking for new messages.

Convert colour, make bright light!

As new MQTT messages arrive, we drop into the cl_callback function, which calls a function hex_to_rgb to convert the value into a decimal RGB tuple. We print the colour to the console for informational purposes (all of the console output could easily be taken out, but can be handy for testing), and then call our function to light the NeoPixels, passing in the RGB value. If things are working as expected, we should receive a message as soon as we subscribe, since the last colour information is published as a retained message.

def light_up(rgb):
    # Set all Neopixels to colour
    np.fill(rgb)
    np.write()
Enter fullscreen mode Exit fullscreen mode

That is it! I'm choosing to call this, a "CheerDot" - a small standalone bright[*] IoT device that follows the global Cheerlights feed colour. We can go much smaller, of course, down to the individual pixels on the board, and I have some ideas around that to follow up in the future...

GitHub logo andypiper / fivebyfive

Polyglot examples for the 01Space ESP32-C3FH4-RGB board

[ for something similar, check out Andy Stanford-Clark's CheerOrbs (fka GlowOrbs) - I've got a couple of these around the house, as well... ]

[*] aside: these LEDs are very bright... one option would be to programmatically reduce the RGB value "intensity" before lighting them, another would be to find a manufacturer who could provide diffusing 2cm x 2cm acrylic squares or boxes... but then I guess I would have created "micro CheerOrbs"... 🤣 at the moment, several pieces of tracing paper in layers over the light, make it just about bearable.

This may also have been part of the reason I made a Shortcut to check the current Cheerlights colour...

Next steps

One thing to do is to look at a crash I saw while building this code - after a few messages arrived, there was a restart with the output W (24) boot.esp32c3: PRO CPU has been reset by WDT - that is interesting enough to investigate! It might have been poor coding / memory related, or it could have been something else. I'm curious to learn more.

I've also got more project ideas. I've still not done anything with several of the features of the board (either in MicroPython, or in Arduino/C). There's a Qwiic JST-SH connector available, and I'd like to see whether that is usable from MicroPython. There's the Bluetooth side of things, as well as a number of GPIO pins to play with, and potentially some low power mode experimentation.

Thoughts and questions are welcome! What would YOU do with a board like this?

You can also follow me on Twitter for more. If you enjoyed this post, you could help to fund my gadget obsession via a small tip on Ko-Fi 😀

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .