Class 12 · Program a Prop
4 h · 3 at the bench 00:00The first performance is easy. Today is about the eightieth, and about the one where somebody unplugs it in the interval.
What makes prop firmware different#
Bench code has one job: demonstrate that the idea works. Show firmware has four:
- Do the effect.
- Do it identically every time, for the whole run.
- Fail safely and visibly when something goes wrong.
- Be recoverable in thirty seconds by somebody who did not write it, in the dark, during an interval.
Points 2 to 4 are almost all of the work, and they are what separates a prop that goes on stage from one that stays in the workshop.
State machines: the shape all of this wants to be#
State machines: the shape all of this wants to be#
A prop is nearly always a state machine: it is in exactly one state, it does something appropriate to that state, and defined events move it to another state.
Take a lantern that flickers, is triggered to flare, then settles:
IDLE --cue received--> FLARE --after 800 ms--> SETTLE --after 2 s--> BURN
BURN --cue received--> FLARE
any --fault detected--> SAFE
SAFE --reset held 2 s--> IDLEWriting this out before you write code changes what you build, because it forces three questions that are otherwise never asked:
- What happens if the cue arrives during FLARE? Ignore it, restart it, or queue it. All three are defensible and you must choose deliberately, because the answer is what the operator will experience on the night they double-tap.
- What state is it in at power-up? Whatever you initialise it to, and if you did not think about it, that is whatever the compiler put there.
- What state does it go to on a fault? If you have no answer, it has no fault behaviour.
In code#
enum State { IDLE, FLARE, SETTLE, BURN, SAFE };
State state = SAFE; // WHY: power-up lands in SAFE, never in an
// energised state. A prop that comes up
// burning when the power blips mid-scene is
// a prop that has surprised somebody on stage.
unsigned long stateEntered = 0;
void enter(State s) {
state = s;
stateEntered = millis();
}
void loop() {
unsigned long now = millis();
bool cue = readCue(); // debounced, every pass
if (!healthy()) enter(SAFE); // checked before anything else acts
switch (state) {
case IDLE:
setLamp(0);
if (cue) enter(FLARE);
break;
case FLARE:
setLamp(255);
if (now - stateEntered >= 800) enter(SETTLE);
break;
case SETTLE:
setLamp(map(now - stateEntered, 0, 2000, 255, 90));
if (now - stateEntered >= 2000) enter(BURN);
break;
case BURN:
setLamp(90 + flicker());
if (cue) enter(FLARE);
break;
case SAFE:
setLamp(0);
if (resetHeld(2000)) enter(IDLE);
break;
}
}
Every state sets its outputs explicitly on every pass. That is deliberate: an output that is only set on a transition will be wrong if anything ever reaches that state by another route, and "anything" includes the noise spike from Class 5.
Failing safely#
Failing safely#
Four questions to answer, in writing, for every prop you build. In the capstone they are marked.
1. What happens on power-up? Everything off, and stay off until something positively says otherwise. A prop that resumes its previous state on power-up is a prop that fires in the interval when the power blips.
2. What happens when control is lost? DMX stops, the cue cable is unplugged, the network drops. Options: hold the last state, fade to a safe state over some seconds, or go dark immediately.
All three are correct in different contexts. A practical lamp holding its last state is fine. A moving element holding its last state may be fine or may be dangerous. What is not acceptable is not having decided, and the decision must be on the documentation sheet where the production manager can read it.
3. What happens when something is out of range? A sensor reading that is impossible — a distance of −4 mm, a temperature of 300 °C — means the sensor or its wiring has failed, not that the world has become strange. Detect it and go to SAFE. A prop that acts on an impossible reading is a prop that does something violent because a wire came loose.
4. How does somebody reset it? One action, documented, ideally physical: hold a button for two seconds. Not a power cycle, because a power cycle in a rack means finding which of forty plugs it is. And not automatic, because a fault that clears itself will recur on stage.
The watchdog#
Most microcontrollers have a hardware timer that resets the chip unless the software regularly tells it not to. If your code hangs, the watchdog resets it within a second or two.
Use it, and understand what it does and does not give you. It saves you from a software hang. It does not save you from wrong logic, and a prop that resets into a bad state every two seconds forever is worse than one that simply stopped. The watchdog and question 1 have to be answered together.
Receiving DMX#
Receiving DMX#
Making a prop respond to the lighting desk is often the right architecture, because it puts the prop into a cue stack that already exists and is already operated by somebody trained.
Three practical points:
You need an RS-485 receiver chip, a MAX485 or similar. The DMX line is differential at RS-485 levels; a microcontroller pin cannot read it directly, and connecting one to it is how you damage a pin.
Isolate it. A DMX line runs all over the building and touches equipment on other power circuits. An isolated receiver protects your prop, and more importantly protects the rest of the rig from a fault in your prop. This is the Class 5 argument in its most concrete form.
Watch for signal loss. Note the time each valid packet arrives. If more than a second passes with none, control has been lost — apply the answer to question 2 above. This is a few lines of code and it is the difference between a prop that behaves predictably when a cable is kicked out and one that does something nobody can explain.
Making it maintainable#
You will not be in the building on the night it fails. The person who is has never seen your code.
A status indicator that says something. Not "powered". An LED that blinks a pattern per state: one blink idle, two blinks running, rapid blinking for a fault. Somebody with the documentation sheet can then diagnose from six metres away without a laptop.
Constants at the top, named. Every timing, every threshold, every pin, named and in one place. On the night somebody needs the flare 200 ms longer, the change should be one obvious line.
Comment the why, not the what. // set pin 9 high is noise. // Lamp off before the relay switches: the inrush from a cold filament welds the contacts over a run. is worth its space and will still be true in three years.
The documentation sheet. The same discipline as the Class 6 board: what it is, its ratings, its pinout, its behaviour on power-up and on loss of control, its reset procedure, its indicator codes, and who to call. One side of A4, laminated, in the flight case.
Making it maintainable#
Keep the source somewhere that is not one laptop. A sketch that exists only on the machine of a student who has graduated is a liability with a countdown on it. Version control is four commands, and it is the difference between a prop somebody can maintain and a prop somebody has to rebuild. Computer Science for Theatre Class 3 is where that is actually taught, and its Class 4 covers the thing this course does not touch at all: what to do when a model writes the first draft of your firmware, and how to verify it before it drives anything that moves.
Debugging without a debugger#
You have no breakpoints and no step button. What you have is a serial port and an LED, and used properly they are enough.
Print state changes, not values. A loop printing a sensor reading thousands of times a second tells you nothing and slows the loop enough to change the behaviour you were investigating. Print when the state machine moves: IDLE -> FLARE at 41230 ms. Twenty lines from a whole show is readable; twenty thousand is not.
Print the timestamp with everything. millis() at the front of every line. Half of all prop faults are ordering and timing questions, and a log with no times cannot answer either.
Watch out for what printing costs. At 9600 baud a forty-character line takes about 42 milliseconds, and if that is inside your loop then your loop now takes 42 milliseconds. Use 115200, print sparingly, and remember that a fault which disappears when you add printing is usually a timing fault you have just changed.
The indicator LED is the field version. On a production nobody is connecting a laptop. A pattern per state — one blink idle, two running, rapid for a fault — is diagnosable from six metres with the documentation sheet, and it works when the serial port is buried in a set.
Debugging without a debugger#
The two questions to ask before adding any instrumentation: what would I expect to see if my theory is right, and what would I see if it is wrong? If those two are the same, the measurement is not worth taking. That is the Class 2 prediction discipline, in software.
Remembering things across a power cut#
Remembering things across a power cut#
RAM is gone the moment power is. If a prop must remember something — a calibration, a homing offset, a fixture address, a cycle count — it has to be written to flash or EEPROM deliberately.
Two rules, and the first one has ended a lot of installations:
Do not write on every loop. EEPROM endures roughly a hundred thousand writes per cell. A loop running a thousand times a second reaches that in under two minutes. Write when something has actually changed, and never in a code path that can repeat.
Assume the first read is garbage. A brand new chip, or one whose stored data was written by an earlier version of your firmware, holds whatever it holds. Store a version byte and a simple checksum alongside the value, and if either is wrong, fall back to a sane default rather than trusting it. A prop that homes to a garbage offset moves somewhere nobody expected.
And decide what is worth remembering. A prop that remembers it was mid-flare, and resumes, has used non-volatile storage to defeat the power-up rule from earlier in this class. Configuration is worth keeping. State is not.
At the bench#
Three hours and twenty minutes. You are building a real prop and it must work at the end.
Block A — Draw the state machine (25 min)#
On paper, before any code. States, transitions, events, and the answers to the four failure questions. Get it signed off by another pair.
Most of the bugs found later today will be in state machines that were not drawn.
Block B — Build the skeleton (45 min)#
States, transitions, timing with millis(), debounced input, and the status LED. No effect yet, no outputs beyond the indicator. Prove every transition happens when it should by watching the indicator.
Getting this working before adding the effect is the whole method: the skeleton is the part that has to be correct, and it is much easier to debug with nothing else moving.
Block C — Add the effect (50 min)#
Drive the real load through your Class 6 board. PWM for the lamp, with a dimming curve. Flicker that looks like fire rather than like a random number, which takes a low-frequency wander plus a small fast component and is worth the twenty minutes it takes to tune.
Measure the PWM frequency on the scope and confirm it is where you intended.
Block D — Break it deliberately (40 min)#
Test every failure mode you wrote down:
- Cut power mid-effect and restore it. Where does it come up? Is that where you said?
- Disconnect the trigger cable. What happens?
- Short the sensor input to ground, then to the supply. Both should be detected as out of range.
- Hold the trigger permanently. Does it fire once or forever? Which did you intend?
- Send a cue during FLARE. What happens? Is it what you documented?
Log each one. The behaviours you did not predict are the interesting ones.
Block E — Documentation and handover (30 min)#
Write the sheet. Then swap props with another pair and operate theirs from their sheet alone, without talking to them.
Anything you cannot work out from the sheet is a defect in the sheet. That exchange is the assessed part, and it is the closest thing in this course to what actually happens on a production.
Numbers from this class#
| Quantity | Value |
|---|---|
| Power-up state, always | the safe one, outputs off |
| DMX loss detection window | about 1 second with no valid packet |
| Typical debounce time | 20–50 ms |
| Watchdog timeout, typical | 1–2 seconds |
| Reset action | one deliberate physical action, 2 second hold |
| Status indicator, idle | one blink |
| Status indicator, fault | rapid blinking |
| RS-485 receiver for DMX | MAX485 or equivalent, isolated |
| PWM frequency for a stage lamp | above 20 kHz if cameras are present |
| Sensor reading outside its physical range | treat as a fault, go to SAFE |
| Loop pass time, target | under 1 ms |
| Documentation sheet | one side of A4, in the case |