Computer Sciencefor Theatre & Technical Direction
✕

Class 5 · From Code to Effect: Arduino and Raspberry Pi

4 h · 2 at the keyboard
00:00

The gap between "it works on my desk" and "it works on the eightieth performance" is where this whole trade lives. Everything in this class is about crossing it on purpose.

Logic gym#

Fifteen minutes, on paper, in pairs. Every answer in this one is a number or a decision. No adjectives.

1. Write the deadline down as a number#

For each, say how late is too late, in milliseconds, and say how you would know:

  • A gunshot sound against a flash.
  • A hazer starting before a beam cue.
  • A trap door opening under an actor who has already started moving.
  • A caption appearing against a line of dialogue.

Two of those are in the tens of milliseconds and two are in the hundreds. Say which, and say what that means for which board holds each one.

2. Does it fit in the pass#

Your loop runs at 30 Hz. In one pass you must: read two buttons, read a sensor over I²C, update 60 pixels, and check the network.

Say the budget for one pass in milliseconds. Then say which of those four you would suspect first when it starts stuttering, and why. The answer is not the one that does the most work.

3. Which board#

Say Arduino, ESP32, Pi, or "two of them", and say the one reason:

  • A prop letter that catches fire on a microswitch.
  • Forty wireless pixel batons carried by performers.
  • A touchscreen for the DSM showing the current cue.
  • A stepper that must arrive at a position and not lose a step.
  • An installation running unattended for three years.
  • Anything that can crush somebody.

The last one has an answer that is not on the list. Say it.

4. What does it do when it is alone#

Power is on, control is gone. For each output, say hold, stop, off or fade, and say who is protected by your choice:

  • A wash covering a stage with people on it.
  • A truck moving upstage.
  • A hazer.
  • A practical in an actor's hand.
  • A gerb.

Then say which one you found hardest, and why. Almost everybody says the practical, and the reason is interesting.

5. The number nobody counts#

A pixel draws 60 mA at full white.

  • 60 pixels, all white: how many amps?
  • 170 pixels?
  • Is that a USB cable?
  • What is the first symptom, before anything fails?

The first symptom is not a failure, it is a colour, and knowing that is the difference between an afternoon and a fortnight.

What the gym is for#

This class is where code stops being an argument and starts being amps, milliseconds and a thing that moves. Every item here is a number you must be able to say out loud in a production meeting, and the ones you cannot say are the ones you have not designed yet.

Two kinds of computer, and choosing between them#

You will build show hardware out of two things. They look similar in a catalogue and they are not remotely the same, and picking wrong costs a week.

A microcontroller (Arduino, ESP32, RP2040, Teensy) runs one program from power-on to power-off. No operating system, no scheduler, nothing competing. Its response time is measured in microseconds and it is the same every time. It boots in milliseconds. Cut the power mid-operation and it starts again cleanly. It cannot do networking, video or files without help, and it has kilobytes of memory.

A single board computer (Raspberry Pi and its relatives) runs a full operating system. It does networking, files, video, Python and any library you like. It boots in twenty to forty seconds. Its response time is usually good and occasionally terrible, because the kernel decided something else mattered for 30 ms. Cut the power mid-write and you can corrupt the card.

Two kinds of computer, and choosing between them#

MicrocontrollerSingle board computer
Response timeMicroseconds, consistentMilliseconds, usually. Not guaranteed
Boot timeMilliseconds20 to 40 seconds
Power lossRestarts cleanlyCan corrupt the filesystem
NetworkingAdd-on, limitedNative, full
Video, audio, filesNoYes
MemoryKilobytesGigabytes
LanguageC++, MicroPythonAnything
Good atTiming, GPIO, one job foreverTalking to things, media, logic

The rule that decides it:

If it must happen at an exact time, or it moves something, put it on a microcontroller. If it must talk to a network, play media, or make a decision that takes more than arithmetic, put it on a computer. When you need both, use both, and be deliberate about which one holds the safety.

Two kinds of computer, and choosing between them#

That last clause is the professional one. The common architecture for a good show prop is a Pi handling the network and the show logic, and a microcontroller handling the pins and the timing, with a serial link between them. Each does what it is actually good at, and the thing that must not stop is not sharing a processor with a package update.


Three families, not two#

The comparison above is the old one, and it is still the one that decides most designs. The board that changed the middle of it is the ESP32: two cores at 240 MHz, Wi-Fi and Bluetooth on the die, for less than an Arduino costs. It is why a prop with no cable to it stopped being a project and became a Tuesday.

The rule that comes with it, and it is the whole craft of using one. Wi-Fi lives on core 0 and is not deterministic: the radio will occasionally take milliseconds and give you no warning. Pin your timing-critical work to core 1 and it stays microsecond-clean. Ignore this and you get a board that works in the office and stutters in a room with four hundred phones in it.

The boards page is the full comparison with prices, three worked effects with parts lists, and the four questions to ask in the order that ends the argument fastest.


The loop, and what a deadline actually means#

The loop, and what a deadline actually means#

A microcontroller's program is a loop. Everything about its behaviour comes from how long one pass takes and what happens inside it.

void loop() {
  readSensors();      // 0.4 ms
  decide();           // 0.1 ms
  writeOutputs();     // 0.2 ms
  sendStatus();       // 3.5 ms  <-- this one is the problem
}

That loop runs about 240 times a second. Fine. Now consider what sendStatus() is doing: writing 40 characters to a serial port at 9600 baud, which takes 3.5 ms whatever else you would like to be doing. Fourteen times as long as the actual work.

Three numbers you need for any effect:

  • How often must it check? A button pressed by a person: 10 ms is imperceptible. A rotating encoder on a moving prop: 1 ms or you lose counts. A DMX frame: 22 ms arrives whether you are ready or not.
  • How long does one pass take? Measure it. Do not estimate. Toggle a spare pin high at the top of the loop and low at the bottom, and read it with a scope. This is the standard technique and it takes two minutes.
  • What is the worst case, not the average? The pass where the sensor timed out, the buffer was full and the message was long. Show hardware is judged on its worst pass, because that is the one that lands on the cue.

The blocking bug#

This is the single most common fault in show firmware and everybody writes it once.

// The version that misses the cue
void loop() {
  if (cueReceived()) {
    digitalWrite(FOG, HIGH);
    delay(3000);              // three seconds of doing NOTHING ELSE
    digitalWrite(FOG, LOW);
  }
  checkEmergencyStop();       // not called for three seconds
}

delay(3000) does not schedule anything. The processor sits in a tight loop counting, for three whole seconds, during which no button is read, no cue arrives, no message is answered, and the emergency stop check does not run. On a machine with a moving part, that last line is not a performance problem. It is the fault.

The blocking bug#

The fix is to stop thinking "wait" and start thinking "what time is it now":

// The version that does not
unsigned long fogStarted = 0;
bool fogging = false;

void loop() {
  if (cueReceived() && !fogging) {
    digitalWrite(FOG, HIGH);
    fogStarted = millis();
    fogging = true;
  }
  if (fogging && millis() - fogStarted >= 3000) {
    digitalWrite(FOG, LOW);
    fogging = false;
  }
  checkEmergencyStop();       // runs every pass, all the way through
}

The blocking bug#

Longer, and it is a state machine, which is what Class 1 said all show code turns out to be. The loop never stops turning, so everything else in the program keeps working while the fog runs.

Write millis() timing from the start. Converting a program full of delay() into a non-blocking one later is a rewrite, not an edit, and it always happens the week of the show.

One detail worth knowing before it bites: write millis() - started >= 3000, never millis() >= started + 3000. millis() is an unsigned counter that wraps back to zero after about 49 days. The subtraction form keeps working across the wrap; the addition form does not. An installation that fails once every seven weeks is a genuinely horrible fault to be handed, and this is where it comes from.


Do it now: feel the blocking bug, then fix it#

Build it with a wait block first. Watch STOP do nothing, for six seconds, while the hazer keeps running. The workspace will tell you how many events were never delivered, because its runtime is single threaded exactly like the loop on the board in front of you.

Then build it again with a polling loop and watch STOP work. That is the entire bug, felt in about three minutes, with no hardware and nothing to install.

Then build the chase that has to be stoppable:

Two blocks, the same shape, one word of difference: wait and wait and listen. On a board that difference is delay() against a loop that checks millis(). Build it wrong first, watch the page count the events that were never delivered, then fix it.

Now write the same fix as code:

Six cases, and every branch of your answer must return immediately. The retrigger case is the one that separates a correct answer from a thoughtful one: an operator holding the button means "keep going", so a trigger while running extends from now rather than being ignored.


Getting the cue in#

Your effect needs to know when to go. Five ways, in rising order of how much of the building has to agree.

A contact closure. Two wires; the circuit is either made or not. Every desk in the world can do it, it works over a long cable, and it cannot be misinterpreted. You know why it bounces and you have watched it on a scope; the software side is one line, and it is to ignore further changes for 20 to 50 ms after the first one.

DMX512. Your prop appears as a fixture. The lighting operator has it on a fader and nobody has to learn anything new, which is the real reason to choose it. 512 slots per universe, refreshed up to 44 times a second, one-way, with no acknowledgement whatsoever. Arrives at 250 kbit/s on an RS-485 receiver into a UART.

MIDI or MSC. Old, universal, well understood by sound and stage management.

OSC over the network. Text-ish messages over UDP: /prop/fog 1. Flexible, human readable, routable, and increasingly the default because everything already has an Ethernet port. UDP is fire-and-forget, so nothing tells you the message did not arrive. Design as if some will not.

Getting the cue in#

HTTP, WebSocket or MQTT. When the effect is talking to a system rather than a console, or when somebody wants a web page to press it.

The trade you are making, every time:

Getting the cue in#

MethodLatencyTells you it arrivedWho has to agree
Contact closureUnder 1 msThe circuit itselfTwo people and a cable
DMX512Up to 23 msNoThe lighting department
MIDI / MSC1 ms per byteNoSound and stage management
OSC over UDPSub-millisecond on a quiet networkNoNetworking, and a documented address
HTTP / WebSocket5 to 50 msYesNetworking, and somebody's server

The full treatment of every one of these is Show Networking and Control Systems. What this class adds is the code at the far end, and one rule from that course worth repeating here:

Ask of any protocol: what happens when the message does not arrive? If your effect only knows how to start, and the "stop" is a message that got lost, you have built something that stays on.


Pixels, and the point where a nice idea becomes a data problem#

Addressable LED tape is the effect everybody wants to build, and it is where "just a bit bigger" stops working suddenly rather than gradually.

Each pixel is three bytes, one per colour, and the whole strip is rewritten every frame.

Pixels, and the point where a nice idea becomes a data problem#

PixelsBytes per frameAt 40 fpsWhat that means
60 (1 m of tape)1807.2 kB/sAny microcontroller, easily
600 (10 m)1 80072 kB/sComfortable, watch the refresh rate
5 00015 000600 kB/sYou need a proper controller and a network
50 000150 0006 MB/sA media server and a designed network

Two numbers that decide the design:

The protocol has a fixed cost per pixel. WS2812 tape is 30 microseconds per pixel, and while it is being written interrupts are usually off. A thousand pixels is 30 ms of writing, during which nothing else happens, which caps you at about 33 fps and starves everything else on that chip. This is a hardware fact, not a coding style, and it is why large rigs use controllers that push pixels with dedicated hardware.

Pixels, and the point where a nice idea becomes a data problem#

Over a network, pixels become universes. 512 DMX slots is 170 RGB pixels. 5 000 pixels is 30 universes, which is a real network design question with multicast and switch capability in it, handled properly in Computer Systems and Networking Class 3.

The pixel calculator does this arithmetic, and it is the arithmetic worth doing in the design meeting rather than in the load-in.


Do it now: the message that waits for you to finish#

The message goes out at two seconds and arrives at about four, because this runtime runs one script at a time and so does the board on your bench. That gap is why a "simultaneous" cue arrives 20 to 60 milliseconds apart on a real show, and why the sound department has an opinion about it.

Do it now: a fade that lands exactly on the number#

Seven cases. The tempting version adds a small increment every frame, accumulates floating point error, and sits at 99.97 percent forever with the operator staring at a desk that says full. Compute from elapsed time and there is nothing to accumulate.

The last case is the one that catches people: a zero second fade is a snap, and dividing by zero there puts NaN on a desk, which lights nothing.


The Raspberry Pi as show hardware#

A Pi is a computer, and a computer in a show is a computer with things switched off. Seven changes turn a hobby board into something you can leave in a wall.

1. Fix the address. DHCP is fine until the router reboots at 19:20 and your prop is somewhere else. Static, or a DHCP reservation, documented on the drawing.

2. Start on boot, not on login. A systemd service, enabled, with Restart=always. If nobody logs in, it must still run, and if it dies at 20:15 it must come back by itself.

3. Turn off what you are not using. Automatic updates, Bluetooth, the desktop, screen blanking, Wi-Fi if it is on Ethernet. Every background service is something that can decide to be busy at the wrong moment. An unattended upgrade during a performance is a real, documented way to lose a show.

4. Make the filesystem read-only if you can. Most corrupted cards were being written when the power went. If it writes nothing, pulling the plug is safe, which means the venue's cleaner is no longer a risk to your installation.

5. Log somewhere bounded. Logs that grow without limit fill the card and the failure looks like something else entirely. journald with a size limit, or a rotating file.

The Raspberry Pi as show hardware#

6. Handle the network being gone. It will be. Reconnect, retry with a delay, and carry on. Do not exit. Do not stop retrying after five attempts because a tutorial did it that way.

7. Know your boot time and write it on the case. Forty seconds is normal. That is forty seconds between "power on" and "responds to cues", and stage management needs to know that number rather than discover it.

# /etc/systemd/system/showprop.service
[Unit]
Description=Show prop control
After=network-online.target

[Service]
ExecStart=/usr/bin/python3 /opt/showprop/main.py
Restart=always
RestartSec=2
User=show

[Install]
WantedBy=multi-user.target

Nine lines. It is the difference between a prototype and an installation, and it is the single highest-value thing in this class to copy down.


Safe states, and the two moments nobody plans for#

Every effect has two moments that are never in the cue sheet and always in the incident report.

Power-up. The instant the board has power and your code has not yet decided anything. Pins are in their default state, which on most microcontrollers is "input, floating". If your fog machine's relay is wired so that floating means on, your fog machine turns on when somebody restores power at 08:00 with nobody in the building.

The discipline: arrange the hardware so that the de-energised state is the safe state, then set every pin explicitly in the first lines of setup(), before anything else. Do not rely on defaults that a different board revision may not share.

Loss of signal. The cue stopped arriving. The network went. The console crashed. What does your effect do?

That question has no universal answer, and the industry's answers differ by department because their worst cases differ:

Safe states, and the two moments nobody plans for#

DepartmentOn loss of signalBecause
Lighting fixtureHolds its last lookA blackout mid-scene is worse than a stale state
Audio amplifierMutes after a few secondsA burst of noise through a big PA is worse than silence
Automation hoistStops and holdsA moving load with no supervision is the worst case in the building
Your effectYou decide, in writing, before you build itNobody else is going to

Implementing it is a timer and a threshold:

Safe states, and the two moments nobody plans for#

unsigned long lastCue = 0;
const unsigned long CUE_TIMEOUT = 2000;   // decided in the design meeting

void loop() {
  if (cueReceived()) lastCue = millis();

  if (millis() - lastCue > CUE_TIMEOUT) {
    goSafe();      // whatever your five sentences said this should be
  }
  checkEmergencyStop();
}

Six lines, one constant, and it is the difference between a prop and a hazard. The constant belongs in your documentation with a sentence explaining it, because the person who has to change it in three years will not be you.

Write the failure behaviour into the design before you write the working behaviour. If you cannot say what it does when it is alone and confused, you have not designed it yet.


Do it now: what it does when it is alone#

Five kinds of output, five different right answers, and the reason for each is about people rather than about code. A light holds because dark is more dangerous than the wrong colour when there are bodies on stage. A motor stops because a moving thing with no controller is the one that hurts somebody. Sound fades because the audience reads a cut as a fault.

The unknown kind gets stop, and a default you did not choose is a default that chose you.


Break all of this at 1:25 first#

Everything in this class can be built on a desk, in a white card model, for less than the price of one seat at a decent opera.

A 1:25 model box with 40 pixels on card booms, an ESP32 taking Art-Net from a free desk running on your laptop, a servo moving a truck against a limit switch, and a Pi playing sound on an OSC message. Every protocol is the real one. Every failure mode is the real one. The far end of the pixel string really does brown out, the servo really does hunt on arrival, and the Wi-Fi really does drop when the room fills.

In a simulator, everything works, which is why a simulator teaches you nothing about the night the string browned out. The model box is the five stage build, the shopping list, and five shows to run on it.


Numbers from this class#

ThingValue
Microcontroller response timeMicroseconds, consistent
Raspberry Pi boot time20 to 40 seconds
A Pi's response timeUsually milliseconds. Not guaranteed
Button debounceIgnore changes for 20 to 50 ms
Human-imperceptible check interval for a button10 ms
DMX512 data rate250 kbit/s
DMX512 full frameUp to 23 ms, so 44 refreshes per second maximum
DMX slots in one universe512
RGB pixels in one universe170
Bytes per RGB pixel per frame3
WS2812 write time per pixelAbout 30 microseconds
1000 WS2812 pixels, one frameAbout 30 ms of blocking write
5000 pixels at 40 fps600 kB/s, and 30 universes
millis() wrap periodAbout 49 days
Correct timing comparisonmillis() - started >= interval
A microcontroller pin's current limitAbout 20 mA, and less across the whole chip
Cue timeout before going safeDecided in the design meeting, written down