Class 5 of 6 Unit 3 · Making it real 4 hours · 2 at the keyboard
From Code to Effect: Arduino and Raspberry Pi
Microcontroller against computer, the loop deadline, the blocking bug, getting a cue in, and what your effect does when it is alone.
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.
| Microcontroller | Single board computer | |
|---|---|---|
| Response time | Microseconds, consistent | Milliseconds, usually. Not guaranteed |
| Boot time | Milliseconds | 20 to 40 seconds |
| Power loss | Restarts cleanly | Can corrupt the filesystem |
| Networking | Add-on, limited | Native, full |
| Video, audio, files | No | Yes |
| Memory | Kilobytes | Gigabytes |
| Language | C++, MicroPython | Anything |
| Good at | Timing, GPIO, one job forever | Talking 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.
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#
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 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
}
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.
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:
| Method | Latency | Tells you it arrived | Who has to agree |
|---|---|---|---|
| Contact closure | Under 1 ms | The circuit itself | Two people and a cable |
| DMX512 | Up to 23 ms | No | The lighting department |
| MIDI / MSC | 1 ms per byte | No | Sound and stage management |
| OSC over UDP | Sub-millisecond on a quiet network | No | Networking, and a documented address |
| HTTP / WebSocket | 5 to 50 ms | Yes | Networking, 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 | Bytes per frame | At 40 fps | What that means |
|---|---|---|---|
| 60 (1 m of tape) | 180 | 7.2 kB/s | Any microcontroller, easily |
| 600 (10 m) | 1 800 | 72 kB/s | Comfortable, watch the refresh rate |
| 5 000 | 15 000 | 600 kB/s | You need a proper controller and a network |
| 50 000 | 150 000 | 6 MB/s | A 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.
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.
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:
| Department | On loss of signal | Because |
|---|---|---|
| Lighting fixture | Holds its last look | A blackout mid-scene is worse than a stale state |
| Audio amplifier | Mutes after a few seconds | A burst of noise through a big PA is worse than silence |
| Automation hoist | Stops and holds | A moving load with no supervision is the worst case in the building |
| Your effect | You decide, in writing, before you build it | Nobody else is going to |
Implementing it is a timer and a threshold:
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#
| Thing | Value |
|---|---|
| Microcontroller response time | Microseconds, consistent |
| Raspberry Pi boot time | 20 to 40 seconds |
| A Pi's response time | Usually milliseconds. Not guaranteed |
| Button debounce | Ignore changes for 20 to 50 ms |
| Human-imperceptible check interval for a button | 10 ms |
| DMX512 data rate | 250 kbit/s |
| DMX512 full frame | Up to 23 ms, so 44 refreshes per second maximum |
| DMX slots in one universe | 512 |
| RGB pixels in one universe | 170 |
| Bytes per RGB pixel per frame | 3 |
| WS2812 write time per pixel | About 30 microseconds |
| 1000 WS2812 pixels, one frame | About 30 ms of blocking write |
| 5000 pixels at 40 fps | 600 kB/s, and 30 universes |
millis() wrap period | About 49 days |
| Correct timing comparison | millis() - started >= interval |
| A microcontroller pin's current limit | About 20 mA, and less across the whole chip |
| Cue timeout before going safe | Decided in the design meeting, written down |
Self test with model answers
Answer it yourself first, out loud or on paper, then open the answer. Reading the answer without attempting the question teaches you almost nothing.
-
It runs an operating system that will occasionally be busy for tens of milliseconds doing something it considers more important, and nobody promised you otherwise. That is fine for cue logic and unacceptable for motion. The standard good architecture is both: a Pi for the network, the media and the decisions, a microcontroller for the pins and the timing, a serial link between them, and the thing that must not stop is not sharing a processor with a package update.
-
It sits in a tight loop counting for three seconds while the processor does nothing else: no cue is received, no button read, no status sent, no safety check run. A shorter delay makes the window smaller and does not remove it. The fix is to record when the thing started and check the elapsed time on each pass, which turns the program into a state machine whose loop never stops turning. Written that way from the start it is a few extra lines; converted later it is a rewrite, in show week.
-
millis() is an unsigned counter that wraps to zero after about 49 days. The subtraction form is still correct across the wrap because unsigned arithmetic wraps consistently; the addition form overflows and the condition never becomes true. The symptom is an installation that fails once every seven weeks, which is a genuinely horrible thing to be handed and almost impossible to reproduce on a bench.
-
WS2812 protocol takes about 30 microseconds per pixel, and interrupts are usually disabled while writing. A thousand pixels is about 30 ms of blocking write per frame, which caps the frame rate at roughly 33 per second and leaves almost no time for anything else on that chip. It is a property of the protocol, not of your code, which is why large rigs use controllers that push pixels with dedicated hardware and why past a few thousand pixels this becomes a network design question with universes in it.
-
Power-up: pins set explicitly in setup(), hardware arranged so the de-energised state is off, so restoring power at 08:00 with nobody in the building does not start the fog. Loss of signal: a timeout, a number chosen in the design meeting, after which it goes to its safe state, which for fog is off. Repeat trigger while running: decided explicitly, restart or ignore. It matters because OSC over UDP is unacknowledged, so a lost stop message is a normal event rather than an exotic one, and an effect that only knows how to start is one that stays on.
Note. These are model answers, not the only correct ones. If yours differs and you can defend it with the arithmetic and a measurement, that is worth more than matching the wording.
The four hours, accounted for. This is the lecturer's view of the session, and it is here rather than hidden in a pack because a student who can see where the time goes can see what the class is actually for.
Run of the session #
240 minutes · 125 at the keyboard · 80 on the idea
- 10min Open The five sentence effects, and which sentence everybody found hardest
- 25min The idea Microcontroller, ESP32 and single board computer, and choosing on purpose
- 15min Lab · gym Logic gym 5, on paper. The deadline, written as a number
- 20min The idea The loop deadline: what fits in one pass, and what blocking costs you
- 15min Break —
- 35min Lab A Measure your own loop: how fast is it really, and what makes it stutter
- 35min Lab B The blocking bug, built in blocks, felt, then fixed in code
- 15min The idea Getting a cue in: contact, serial, OSC, DMX, and the network in between
- 40min Lab C Pi to Arduino: an OSC message becomes a physical thing moving
- 20min The idea Safe states, power-up, and the model box as the place to break all of it
- 10min Close The failure behaviour you specified, said out loud
Blocks are an order of work rather than a timetable: a class that runs long at the keyboard is a class that is going well. The minutes are here so you know what you are trading against when it does.
Four hours goes fast, and most of it is at the keyboard. What you do before the class, and in particular whether the software is installed and working, decides whether you spend those hours learning or catching up.
What you must already be able to do#
Write and run a Python script. Read a function signature. Answer the four questions about code you did not write. Use git.
And everything you did in Electronics last year, which this class assumes outright rather than revisiting: that a pin sources about 20 mA and no more; that a floating input is undefined rather than off, and what a pull-up is for; that an ADC's accuracy is its reference's accuracy; what PWM is actually doing; why a switch bounces; and why nothing inductive is ever connected directly to a chip. You spent four hours on each of those with a meter in your hand, so this class spends none.
If any of that has gone, half an hour with Electronics Class 11 brings it back. Do that before you arrive rather than during the first lab.
The division between the two years is worth stating plainly, because it is what makes both classes short enough to be good. Electronics taught you the hardware, and used firmware as the thing that drove it. This class teaches the software, and treats the hardware as settled. Neither repeats the other, and if you find somewhere they do, that is a defect and I want to know.
Three things to do#
- Blink an LED on a board before you arrive. Any Arduino-compatible board. Install the toolchain, load the blink example, change the interval to 200 ms, watch it change. If the toolchain fights you it will fight you for the first hour of class instead, and the toolchain is not what we are here to learn.
- Get a shell on a Raspberry Pi, over SSH if you can, on a keyboard and monitor if you cannot. Run
uname -aandip addr. Write down the IP address it gave itself and how it got it. - Describe one effect you want to build, in five sentences: what the audience sees, what starts it, what happens if it starts twice, what happens if it loses power halfway, and who is standing near it. Bring it. We will build one of them, and the third and fourth sentences are the ones we will actually spend the time on.
What to bring#
Laptop with the Arduino toolchain and Python. A board, if you own one. A USB cable that carries data rather than only charge, which wastes the first ten minutes of every class of this kind. Your five sentence effect.
The calculations this class asks for, with the working shown. Use them to check your arithmetic, never to replace it: the assessment asks for the method.
Loop budget and deadline
Serial, DMX and time on the wire
PWM duty, frequency and resolution
ADC resolution and sensor scaling
Pixels, universes and data rate
End to end latency budget
The part of this you can only learn by doing it. Everything here runs in this page, keeps your work in this browser, and is marked by tests rather than by anybody. A failed run costs nothing, which is the only condition under which people take the risks that teach them something. 6 more sit inside the class itself, at the point the idea is taught.
Retrieval, not rereading. Everything here is self-graded, stored in this browser only, and reported to nobody.