Dr. Neil's Notes
Building a Glowbit Server
The Glowbit product from Core Electronics is an array of LEDs setup for easy programming using a micro-controller development board. In this project I used five Glowbit Matrix 8 x 8 modules and a Raspberry Pi Pico W. Connecting the Glowbit modules in a 5 x 1 array created a rectangular display. Connecting the Raspberry Pi Pico allowed for wireless control.
Connecting the Glowbit Matrix modules is well documented on the Core Electronics web site. The image below is taken from their website.
Originally I expected I was going to need another power supply for the Glowbit array, however the power from the Pico was more than sufficient to illuminate all the LEDs ins the array. This is 320 LEDs being controlled and powered from a $10 micro-controller board. I started testing the setup with a solderless bread-board as shown.
Then using some cheap offcut plyboard (that was packaging from something unrelated), I cut and drilled a board to mount the 5x1 Glowbit matrix array.
After soldering the 5 Glowbit Matrix modules to each other, I mounted them to the board. After testing the LEDs worked, I added the Raspberry Pi Pico W to the board under the LED modules and to one side. To achieve this I removed the modules from the board, soldered the wires to the Pico, and then mounted the Pico first, and then the modules back on the board.
There are two parts to the software project. The first is building the code to control the LEDs in the Glowbit matrix. The second part is the web app on the Pico W, so that it can be connected from another device to control the LEDs.
The LED Actions
In order to speed up the development of the code to control the LEDs I started by building a Glowbit emulator. This allows me to write and test code on my PC without having to upload to the Pico and test on the actual hardware each time. You can find the code for the Glowbit Emulator here.
The image below shows the emulator running in a Debian Terminal through WSL2 on a Windows PC.
With the emulator python module I can then import the emulator on the PC and the actual Glowbit code on the target device.
try: import glowbitEmulator as glowbit except: import glowbit
actions.py file I created an actions python class to encapsulate the different animations to play on the LEDs.
import _thread import time class actions(): def __init__(self): self.matrix = glowbit.matrix8x8(1, 5) self.animate = False self.lock = _thread.allocate_lock()
This code initializes a glowbit matrix8x8 array of 1 row and 5 columns. If I wanted to use less or more of the glowbit modules, this is where I can change the shape and size of the modules.
self.animate flag is used to indicate if an animation is running.
self.lock allows for a thread to lock a resource while using it. This is used to wait for an animation to complete before starting a new animation in the LED array.
def getLock(self): self.animate = False self.lock.acquire()
def showText(self, text, colour = 0xFF0000): self.getLock() self.matrix.blankDisplay() self.matrix.printTextWrap(text,0,0, colour) self.matrix.pixelsShow() self.lock.release()
lockis acquired, then the matrix methods are used to display the text. Finally the
setColour method is similar.
def setColour(self, colour): self.getLock() for i in range(self.matrix.numLEDs): self.matrix.pixelSet(i, colour) self.matrix.pixelsShow() self.lock.release()
setColour methods are fairly simple as they displays static content and then finish by releasing the lock.
showScrollingText method will scroll text across the LEDs until the
animate flag is set to false, by another method calling
def showScrollingText(self, text, colour = 0xFF0000): self.animate = True self.matrix.blankDisplay() while self.animate: self.matrix.addTextScroll(text,0,0,colour) while self.animate and self.matrix.scrollingText: self.matrix.updateTextScroll() self.matrix.pixelsShow() time.sleep(0.2) self.lock.release()
Trueat the start to indicate it is now animating. Then the matrix is cleared with the
whileloop will keep scrolling the text forever while the
cycle method also continues to animate while the
animate flag is set to
cycle code is mostly taken from the Glowbit demo code, and modified to support the
animate flag and
lock.release() when finished.
def cycle(self): self.animate = True self.matrix.blankDisplay() maxX = int(self.matrix.numLEDsX) maxY = int(self.matrix.numLEDsY) ar = self.matrix.ar pixelSetXY = self.matrix.pixelSetXY wheel = self.matrix.wheel show = self.matrix.pixelsShow while self.animate: for colourOffset in range(255): for x in range(maxX): for y in range(maxY): temp1 = (x-((maxX-1) // 2)) temp1 *= temp1 temp2 = (y-((maxY-1) // 2)) temp2 *= temp2 r2 = temp1 + temp2 # Square root estimate r = 5 r = (r + r2//r) // 2 pixelSetXY(x,y,wheel((r*300)//maxX - colourOffset*10)) show() if not self.animate: break time.sleep(0.2) self.lock.release()
To define the actions that can be called on this
actions class, the class exposes a list of actions.
def actionList(self): return ['off', 'red', 'green', 'blue', 'warning', 'cycle']
To call the different actions a
callAction method determines which actions is requested and calls the correct method for that action.
def callAction(self, action, params): if action == 'off': self.getLock() self.matrix.blankDisplay() self.lock.release() elif action == 'red': self.setColour(0xFF0000) elif action == 'green': self.setColour(0x00FF00) elif action == 'blue': self.setColour(0x0000FF) elif action == 'warning': self.getLock() _thread.start_new_thread(self.showScrollingText, ('WARNING!!',)) elif action == 'cycle': self.getLock() _thread.start_new_thread(self.cycle, ()) elif action == 'text': if (len(params)>0): text = params[2:] print(text) colour = 0xFFFFFF if (len(params)>1): colour = int(params[3:], 16) self.showText(text, colour) elif action == 'stext': self.getLock() if (len(params)>0): text = params print(text) colour = 0xFFFFFF if (len(params)>1): colour = int(params, 16) _thread.start_new_thread(self.showScrollingText, (text, colour,))
callActions method uses the name of the action to determine which method to run. For the
stext methods the params contain an array to determine the text and the colour of the text to display on the LEDs.
Note that the
showScrollingText methods are started on another thread. The Raspberry Pi Pico supports two cores, and so this allows the animation to be displayed in a thread while the application can still receive commands to change the display.
In order to test the
actions class in the emulator, I wrote an
actionRunner.py script in another file.
import actions actions = actions.actions() actionList = actions.actionList() while True: actionNumber = 0 for a in actionList: print(str(actionNumber) + ": " + a) actionNumber += 1 routineNumber = input("Enter routine number: ") if routineNumber.isdigit() and int(routineNumber) < len(actionList): routine = actionList[int(routineNumber)] actions.callAction(routine, ) else: actions.callAction("text", ["t="+routineNumber])
Notice that if the number entered is not a digit that matches the action list, then the entered text is displayed on the LEDs.
Running this in the emulator looks like this:
The Web App
To call the actions through a web page, or a REST service, requires that the
actionRunner script shown in the previous step is written as a web application.
Building a web app in micro-python is well documented, however for completeness I will outline the code here.
To display a web page with the actions requires an HTML response is created. This is done in a
def webpage(value): html = f""" <!DOCTYPE html> <html> <body> <H1>Glowbit Server</H1> <h2>Select an action</h2>""" for a in actions.actionList(): html += """ <a href="/"""+a+"""">"""+a+"""</a><br> """ html += """ <form action="./text"> <label>Text:</label><br> <input type="text" name="t" value="Hello"><br> <label>Color:</label><br> <input type="color" name="c" value="#00ff00"><br> <input type="submit" value="Submit"> </form> """ html += """ <p>Request is """+value+""" </p> </body> </html> """ return html
actionsListdefined in the
actions.pyclass discussed previously.
To serve this page on a connection a
serve method is needed.
def serve(connection): while True: client = connection.accept() request = client.recv(1024) request = str(request) try: request = request.split() except IndexError: pass print(request) parts = request.lstrip('/\\').split('?') args = list() if (len(parts)>1): args = urlParse(parts).split('&') action = parts print(action) actions.callAction(action, args) value=action html=webpage(value) client.send(html) client.close()
This method uses a connection to parse the request and call an action in the
actions class created earlier. Then the
html for the web page is sent to the client.
actions.callAction(action, args) is the place this code enables a web page to control the output on the LEDs.
urlParse method converts any text with spaces, or special characters' into the text to display on the LEDs.
def urlParse(url): l = len(url) data = bytearray() i = 0 while i < l: if url[i] != '%': d = ord(url[i]) i += 1 else: d = int(url[i+1:i+3], 16) i += 3 data.append(d) return data.decode('utf8')
To create the
connection that is used in the
serve method a socket is opened on port 80 on the micro controller device, in this case the Pico W.
def openSocket(ip): address = (ip, 80) connection = socket.socket() connection.bind(address) connection.listen(1) print(connection) return(connection)
openSocket method uses the provided
ip address to the create and return a
connection. The same
connection used to
Putting all of this together in a main 'run` method, looks like this.
def run(): actions.showText('boot.', 0xFF0000) wlan = network.WLAN(network.STA_IF) wlan.active(True) wlan.connect(creds.ssid, creds.pwd) # Wait for connect or fail wait = 10 while wait > 0: if wlan.status() < 0 or wlan.status() >= 3: break wait -= 1 print('waiting for connection...') time.sleep(1) # Handle connection error if wlan.status() != 3: actions.showText('fail', 0xFF0000) raise RuntimeError('wifi connection failed') else: print('connected') ip=wlan.ifconfig() print('IP: ', ip) actions.showText(ip[-4:], 0x00FF00) time.sleep(1) try: if ip is not None: connection=openSocket(ip) serve(connection) except KeyboardInterrupt: machine.reset()
run method starts by using the
actions.showText method to display boot. on the screen in red. This notifies the person running the app that the device is booting, or starting up.
To connect to the WIFI requires the SSID and the password for that network. Obviously you want to keep this secret and so it should live in a file that is not part of the main code. Ideally this would be set as an environment variable, or a certificate installed on the micro controller device. However for this example, to keep it simple, I used a
creds.py file like this
ssid = "MySSID" pwd = "ssid_passcode"
NOTE this is not secure and should not be considered best practice
run method waits for the connection to the WIFI network to complete. If it fails then the
actions.showText method is used to notify of the connection 'fail'.
If the connection to the WIFI network succeeds then the last 4 digits of the IP address are displayed on the LED array, in green. This should help with finding the page to connect to the device.
Once the connection is made, the socket is opened with
openSocket, and then the device can
serve the connection.
To start this web app and the actions the code is then simple
actions = actions.actions() run()
Here is the final result running the colour cycle.
Once this is working you can extend out the actions to display a variety of animations, here the Glowbit server is running Conway's Game of Life
With the Glowbit server controlled via a web URL you can hook it up to other software. Here is displaying that I am in a call, which is determined from my status in Microsoft Teams.
Created: February 18, 2023 05:42:17