3

Question Summary

I am writing a program that will run on a Raspberry Pi 4b+ designed to manage hundreds of LEDs, as well as a few other devices (such as small motors). This is for a project I am a part of- a LEGO City designed to simply and accessibly demonstrate cybersecurity issues in a model city with the modern trend towards the 'internet of things'.

I will preface this by saying I am the programmer on our team, but the team is all college students, so I am pretty unexperienced when it comes to programming. Our program so far is in python on the aforementioned Raspberry Pi, and we are utilizing Adafruit TLC59711 chips to manage many LEDs chained together while avoiding safety and other issues associated with wiring hundreds of LEDs in series.

Essentially, our city will have to light up properly, continuing to loop specific patterns (for example, traffic light patterns or sirens on an ambulance). Sometimes these patterns will be changed or disrupted by button presses that indicate the presenter is showing what happens when something is compromised on the security side, so our light patterns will change to reflect things going wrong in the city.

We have access to a maximum of two Raspberry Pis that will be responsible for executing this program(s). The light managing chips we are using make accessing everything along the line fairly easily, and come with the upside of an issue with an LED doesn't cause issues with the rest of the circuit. However, I have a few questions.

Do you have any suggestions for how I should structure the program to manage a high volume of LEDs simultaneously? Importantly, each structure in the city has different patterns it displays that may not have the same timing restrictions. For example, one light might blink every second, another every five seconds. It is also important to note that if we make multiple programs they all must be executed simultaneously and continuously on a single Raspberry Pi. I would like the program to be as readable and extendable as possible. Currently, our city is in four parts (had to be to be portable) that we are building and testing independently of each other, so a good programming framework should make setting up the future sections much easier. Additionally, given that I am intern hired just for this project, if future people want to take the work to make additional LEGO cities as has already been discussed, I'd like our code to be as usable as possible, at least for inspiration and instruction.

For our test programs when we were physically wiring the LEGO buildings and trying patterns, we were using time.sleep for delays, which is obviously not an option when chaining chips together unless every object has its own program.

My first draft has seemed possible, but difficult to read and cumbersome, which would involve running a loop that contained individual instructions for each part of the city on the array of chips we have access to, utilizing variables to ensure that we could wire it dynamically, and individual checks for each time increment we want to check things on. Additionally, rewriting the code with slight alterations for each individual combination of button presses (as one button press can disrupt multiple structures) would not be efficient.

Needless to say, combining 20 odd building patterns into one program was going to be a behemoth. I was hoping people here would have some tips on how to cleanly structure such a structure at a high level, or perhaps had some techniques they had used in their own projects using a Raspberry Pi to manage a high number of LEDs.

Technical Details

I'm not sure how relevant all of this will be, but here are some code snippets to demonstrate how the Adafruit chips run in Python.

import board
import busio
import digitalio
import adafruit_tlc59711
import RPi.GPIO as GPIO
import time

spi = busio.SPI(clock=board.SCK, MOSI=board.MOSI)
chip = adafruit_tlc59711(spi, pixel_count=12)

while 1 == 1:
    chip[0] = (0,0,0)
    chip[1] = (0,0,0)
    chip[4] = (0,0,0)
    chip[5] = (0,0,0)
    chip[8] = (0,0,0)
    chip[9] = (0,0,0)
    
    chip.show()

This program turns off every LED in our testing rig. Each chip has four pixels that sustain either one RGB LED, or for our purposes, 3 single LEDs, meaning each chip is responsible for 12 LEDs. Our testing rig has 3 chips, hence the pixel_count being set to 12. Some of the indexes are missing because we are only wired to half the chip, since the testing rig needed to be simple and space efficient. Index 0 will always be on the chip furthest from the Raspberry Pi itself. If you want to light up an LED, all we would do is change one of the numbers in the array at a chip spot to a number between 0 and 65535. For example:

chip[0] = (0, 65535, 30000)

chip.show()

would leave r at chip 0 off, turn g for chip 0 to max, and put b at chip 0 to half brightness.

import board
import busio
import digitalio
import adafruit_tlc59711
import RPi.GPIO as GPIO
import time

spi = busio.SPI(clock=board.SCK, MOSI=board.MOSI)
chip = adafruit_tlc59711(spi, pixel_count=12)
i = 0
scale = 150
up = True
on = False
timeI = time.time()
while 1 == 1:
    if up == True:
        if (i + scale) <= 65535:
            i = i + scale
        else:
            up = False
    if up == False:
        if (i - scale) >= 0:
            i = i - scale
        else:
            up = True
    chip[8] = (65535,i,i)
    chip.show()
    now = time.time()
    
    if now >= (timeI + 1):
        if on == False:
            chip[9] = (0,0,65535)
            chip.show()
            on = True
            timeI = now
        else:
            chip[9] = (0,0,0)
            chip.show()
            on = False
            timeI = now

This is a program that controls a plane with four lights: one that simply shines with no variation, one that slowly fades in and out at a rate we set (scale) without surpassing the boundaries of 65535 and 0, and one that flickers on a time scale. You can see just from this relatively simple pattern, moving multiple into the same program to run constantly becomes quickly difficult.

I'd be happy to provide any additional information you need or answer any questions, though I work part time, so my answers might not be fast if I'm not at work. I appreciate any input or advice from those who have experience in this sort of thing!

2
  • You may want to look at twistedpython as a way of running the various patterns as independent tasks, that then send event to a master communication task that maintains what the current state of all the LEDs is for each SPI channel and then outputs that state to all LEDs on a regular interval. Oct 18, 2023 at 0:46
  • 3
    What a fun project.
    – Jan Doggen
    Oct 18, 2023 at 9:51

2 Answers 2

5

You asked for a design. Here you go.

This is all about scheduling. And we're especially concerned about servicing the high-frequency tasks with low jitter, so no irritating artifacts will be picked up by the human visual cortex.

pqueue

A single unified scheduler loop will control all LEDs.

The central datastructure will be a priority queue. Each entry will contain a deadline timestamp, such as time() returns. It's cheap to insert an entry, it costs log(N) if there's N entries in there already. Extracting "next item to run" has O(1) constant cost.

Each entry will also have a chip address (one of your examples was chip[5]), an (R, G, B) illumination setting, and a boolean indicating if we should call .show(). (Be sure to add a tiny ϵ to each deadline associated with a .show() call, to ensure that other settings have already been processed.) Finally, an entry will contain a function reference, perhaps along with an arg vector for it if you choose not to use partial(). After setting an LED to a new illumination value, the scheduler loop calls that function, which is responsible for re-scheduling the LED. That is, it determines the next deadline + illumination, constructs and returns a corresponding entry, which will be inserted into the priority queue. In this way each LED always has exactly one entry in the queue.

scheduler

This while True: loop is pretty simple. Pop the next item off the queue. Consult time() to figure out if that deadline is in the future. If so, sleep() briefly. Now the deadline has arrived.

Use the chip address + illumination value to alter what an LED is doing. If the boolean is set, call .show() to push it out.

Call that LED's scheduling function, obtain that LED's next_deadline as return value, and insert it into the priority queue.

Lather, rinse, go back to the top looking for more events.

non LEDs

Once the "LED blinking" is implemented, you will no doubt want to schedule functions which read push buttons and so on. Their deadlines can be put in the same queue. With a little bit of implementation experience under your belt, you'll be in a better position to figure out what that part of the design should look like.

unit tests

Do not "wing it". Write a test suite that exercises the scheduler loop, and each of the scheduling functions it calls.

It should be possible to run these tests on any computer that has python, even if it's not connected to any LEDs. So you will need the concept of a "fake LED" which doesn't interact with SPI hardware in any way.

You will also want automated integration tests and end-to-end system tests. These will only succeed on a Raspberry Pi that has suitable hardware attached to it.

3

First and foremost, you need an addressing scheme to be able to select a subset of LEDs by some simple, semantic means. This maps each LED to its real world place or object.

For example traffic-light/down-town/east or traffic-light/down-town/* or traffic-light/* or even * to light up the entire city !

This would massively simplify the main code:

if some.logic.going.on.here() {
   signals.emit('LED_CONTROL',{ select: 'traffic-light/down-town/*', command: 'OFF' });
}

You can do this from anywhere in your general city logic (business logic).

On the other end, decoupled from city logic , you need a "receiver" & "dispatcher":

signals.on('LED_CONTROL', payload => {
   leds = addressing.select(payload.select);
   for (led of leds){
     // update LED as per payload.command
   }
})

Now, onto the addressing scheme (a.k.a the "router" or "selector"). The mapping can maintained in any structured format that can be queried and is easy to be edited later:

# leaf node: one or more 'chip,pixel'
{
  city: {
    gate: ['1,1','1,2']
  },
  traffic-light: {
     down-town: {
        east: ['2,1'],
        west: ['2,2']
     }
  }
}

You can later edit this when you re-structure the city and move the lights around, Without much affecting rest of the program.

The addressing.select part would now map a path like traffic-light/down-town/* to a number of LEDs ( ['2,1', '2,2'] in this case). Once you have the exact address of LED, it is trivial to send a command to it or control it in some other way. There no shortage of python libraries out there that will let you query structured data files.

Finally, the emit() and on() pattern used above is from event-oriented design. There are many such python frameworks that allow to emit and consume a large number of asynchronous events using a single thread or process.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.