Learning Goals 5 min
By the end of this lesson you will be able to:
- Wire six components onto one breadboard — three LEDs, a buzzer and two buttons — all sharing one GND bus through the − rail.
- Use a single mutable global (
alarmActive) as a state variable that the sketch consults at the top of everyloop()pass to decide what to do. - Write a working alarm: idle and quiet by default; flashing and wailing when the trigger button fires; back to idle the moment the disarm button is pressed.
Warm-Up 10 min
Cluster C is over. From L01-16 you learned digital input; today you turn the whole cluster into something that looks like a real product — a burglar alarm with sound, light and two-button control.
Quick-fire puzzle
Kavitha walks into a house with a real burglar alarm panel by the door. The panel has a green light glowing steadily. She turns her key in the keypad and walks inside. As she walks past the hallway sensor, the panel suddenly flashes red and an awful wail fills the house. She types her code on the keypad; the wail stops, the green light returns.
- How many distinct states does this alarm have? Name them.
- What causes the alarm to move from one state to another? Name two events.
- While the alarm is in each state, what does the panel display (lights, sound)?
Reveal the answer
- Two main states: idle (armed, quiet) and alarming (loud, flashing). Real alarms have more (disarmed, arming-countdown, etc.) but two is enough to start.
- The trigger (hallway sensor seeing motion) moves it from idle → alarming. The disarm code (the right keypad press) moves it back from alarming → idle.
- In idle: solid green LED, silence. In alarming: rapid red/yellow flash, loud buzzer.
That's exactly the project we'll build today. The "hallway sensor" becomes button A; the "keypad code" becomes button B.
New Concept — the state machine pattern 20 min
The big idea — a sketch with two personalities
So far every loop() body you've written has done the same kind of work every pass — read sensors, drive outputs, repeat. A state machine is different: it asks "what mode am I in right now?" at the top of every pass, and then runs different code depending on the answer.
For today's alarm there are two modes:
- Idle (alarmActive = 0): show green LED, watch for the trigger button.
- Alarming (alarmActive = 1): flash red/yellow, beep the buzzer, watch for the disarm button.
State diagram
How the state variable works in code
In code, the "current state" is just a mutable global integer (the L01-19 pattern). The loop() body asks "which state am I in?" and runs the matching branch:
int alarmActive = 0; // 0 = IDLE, 1 = ALARMING
void loop() {
if (alarmActive == 0) {
// IDLE behaviour
}
else {
// ALARMING behaviour
}
}Inside each branch, two things happen: do the work for this state, and check whether to leave for the other state. The only way a state changes is by an explicit assignment to alarmActive inside one of the branches.
Wiring map — six components, one bus
Today's hardware is the union of L01-15 (3 LEDs + buzzer) and L01-21 (2 buttons). No new parts — just everything from Cluster B/C wired together.
| Component | Arduino pin | Position | Role in the alarm |
|---|---|---|---|
| Red LED | D9 | cols 1–6 | Alarm flash (with yellow) |
| Yellow LED | D10 | cols 11–16 | Alarm flash (with red) |
| Green LED | D11 | cols 21–26 | "Armed and idle" indicator |
| Piezo buzzer | D8 | cols 28–29 | Alarm wail |
| Button A (trigger) | D7 | cols 7/10 (straddles trough) | Press = "intruder!" → start alarm |
| Button B (disarm) | D6 | cols 17/20 (straddles trough) | Press = "correct code" → stop alarm |
Two helper functions to keep loop() readable
This project's loop() will get long unless we hide the messy bits inside helpers. Two helpers cover most of it:
setLeds(r, y, g)— set all three LEDs in one call (from L01-20).wail()— one cycle of the alarm sound + flash: red on, beep high, switch to yellow, beep low.
Why it matters
A burglar alarm is the simplest member of a huge family of "state-machine devices" — microwaves with cook/standby/error states, traffic lights with red/amber/green, vending machines with idle/coin-inserted/dispensing/change states. Today's project is the template: one int says which mode, an if picks the right behaviour, explicit transitions change the mode.
Worked Example — guided build 20 min
Goal: build the wiring (assembling L01-15 + L01-21 in one place), then write the state-machine sketch.
The wiring
The sketch
// Burglar alarm — Cluster C project
const int RED_PIN = 9;
const int YELLOW_PIN = 10;
const int GREEN_PIN = 11;
const int BUZZER_PIN = 8;
const int TRIGGER_PIN = 7;
const int DISARM_PIN = 6;
int alarmActive = 0; // 0 = IDLE, 1 = ALARMING
void setLeds(int r, int y, int g) {
digitalWrite(RED_PIN, r);
digitalWrite(YELLOW_PIN, y);
digitalWrite(GREEN_PIN, g);
}
void wail() {
setLeds(HIGH, LOW, LOW);
tone(BUZZER_PIN, 880, 100);
delay(100);
setLeds(LOW, HIGH, LOW);
tone(BUZZER_PIN, 660, 100);
delay(100);
}
void setup() {
pinMode(RED_PIN, OUTPUT);
pinMode(YELLOW_PIN, OUTPUT);
pinMode(GREEN_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(TRIGGER_PIN, INPUT_PULLUP);
pinMode(DISARM_PIN, INPUT_PULLUP);
setLeds(LOW, LOW, HIGH); // start IDLE: green on
}
void loop() {
if (alarmActive == 0) {
// IDLE state: watch for trigger
if (digitalRead(TRIGGER_PIN) == LOW) {
alarmActive = 1;
}
}
else {
// ALARMING state: wail, then check disarm
wail();
if (digitalRead(DISARM_PIN) == LOW) {
alarmActive = 0;
noTone(BUZZER_PIN);
setLeds(LOW, LOW, HIGH);
}
}
}Upload and test the state machine
- Plug in the USB. The green LED comes on; everything else is dark and quiet. IDLE.
- Press Button A (trigger). Green LED goes off. Red and yellow start flashing alternately. The buzzer wails. ALARMING.
- Hold Button B (disarm) for about half a second. The wail stops, the LEDs go to green-only. Back to IDLE.
- Press A again. Alarm fires again.
Trace one full cycle on paper
Map the state diagram onto your sketch. For each transition, identify (a) what triggers it, (b) what code-line(s) execute it, (c) what visual change you should see.
| Transition | What triggers it | Code line(s) that fire | Visible change |
|---|---|---|---|
| IDLE → ALARMING | ____ | alarmActive = 1; | ____ |
| ALARMING → IDLE | ____ | alarmActive = 0; noTone(…); setLeds(…); | ____ |
Filling this in is the project's "did I really understand it?" check. If you can't trace these two transitions, re-read the New Concept's state-diagram section.
Try It Yourself — extend the project 20 min
Goal: Change the alarm sound to be more menacing. Try a faster wail (50 ms per note instead of 100 ms), or higher frequencies (e.g. 1200 Hz and 900 Hz instead of 880 and 660), or both.
Edit only the wail() helper. The rest of the sketch — including the state machine — doesn't change.
void wail() {
setLeds(HIGH, LOW, LOW);
tone(BUZZER_PIN, 1200, 50); // higher and faster
delay(50);
setLeds(LOW, HIGH, LOW);
tone(BUZZER_PIN, 900, 50);
delay(50);
}Questions:
- Does the disarm button feel more responsive or less responsive with the faster wail? Why? ____ (Hint: shorter delays per cycle = more frequent disarm checks.)
- If you cut the per-note delay to
10 ms, does the disarm feel almost instant? What's the trade-off? ____
Goal: Add an arming countdown. Real alarm systems let you "arm" the system and give you 5–10 seconds to leave before the trigger becomes active. Use millis() from L01-22 to implement this.
Plan: add a new state — call it ARMING. When you press Button B from IDLE (or from a fresh sketch boot), enter ARMING. While ARMING, blink the green LED quickly for 5 seconds. After 5 seconds, transition to IDLE (armed). Now the trigger button works as before.
Change alarmActive from a 0/1 flag to a three-value integer:
0= IDLE (armed)1= ALARMING2= ARMING (5-second countdown)
Use a three-way if/else if/else from L01-20 in loop(). Add a mutable global unsigned long armingStart = 0; to remember when the countdown began.
Questions:
- The state diagram now has three states and at least four transitions. Sketch the new diagram on paper — circles and arrows. ____
- If the trigger button is pressed during the ARMING countdown, what should happen? Real alarms typically ignore it (you're meant to be leaving). How does your code handle this? ____
Goal: Make the disarm button instant. The Worked Example uses delay(100) inside wail(), which blocks the disarm read for up to 200 ms per cycle. Replace it with a millis()-based non-blocking pattern (previewed in L01-22).
The core idea: don't delay. Instead, store lastFlashAt = millis() and check if (millis() - lastFlashAt >= 100) on every loop pass. When the condition is true, advance the flash state. In between, the loop is free to check the disarm button thousands of times per second.
unsigned long lastFlashAt = 0;
int flashState = 0; // 0 or 1 — alternates
void wailNonBlocking() {
if (millis() - lastFlashAt >= 100) {
lastFlashAt = millis();
flashState = !flashState;
if (flashState) {
setLeds(HIGH, LOW, LOW);
tone(BUZZER_PIN, 880, 100);
}
else {
setLeds(LOW, HIGH, LOW);
tone(BUZZER_PIN, 660, 100);
}
}
}Replace wail() with wailNonBlocking() in the ALARMING branch of loop(). No delay anywhere.
Questions:
- How does the disarm latency change compared to the blocking version? ____
- This non-blocking pattern is the same one used by professional embedded code to drive multiple things at once (e.g., flash an LED and read a sensor and talk to a screen). Why doesn't
delayscale to that? ____
Mini-Challenge — add a feature 15 min
The "panic mode"
Real alarm systems have a panic button — pressing two specific keys at the same time fires the alarm immediately, even from idle. We simulate it: pressing both buttons (A and B) at the same moment from IDLE triggers a louder, faster-flashing alarm that requires holding both buttons again to disarm.
Your task:
- From IDLE: if both buttons are pressed (use
&&from L01-21), enter a new PANIC state —alarmActive = 2. - In PANIC, the wail is faster (50 ms cycles) and higher (1200 Hz / 900 Hz). The LEDs flash all three at once.
- To disarm PANIC, the user must press BOTH buttons together again.
- The normal ALARMING state still works — pressing only the trigger from IDLE goes to ALARMING (single-tone wail). Only the simultaneous press goes to PANIC.
It works if:
- Pressing only A from IDLE → normal ALARMING (red/yellow flash, 880/660 Hz). Disarmed by B alone.
- Pressing only B from IDLE → nothing (B only disarms, doesn't trigger).
- Pressing A AND B together from IDLE → PANIC (all three LEDs flash, 1200/900 Hz). Disarmed only by A AND B together.
Reveal one valid sketch
// Burglar alarm with panic mode
const int RED_PIN = 9;
const int YELLOW_PIN = 10;
const int GREEN_PIN = 11;
const int BUZZER_PIN = 8;
const int TRIGGER_PIN = 7;
const int DISARM_PIN = 6;
int alarmActive = 0; // 0 = IDLE, 1 = ALARMING, 2 = PANIC
void setLeds(int r, int y, int g) {
digitalWrite(RED_PIN, r);
digitalWrite(YELLOW_PIN, y);
digitalWrite(GREEN_PIN, g);
}
void wail(int hi, int lo, int ms) {
tone(BUZZER_PIN, hi, ms);
delay(ms);
tone(BUZZER_PIN, lo, ms);
delay(ms);
}
void setup() {
pinMode(RED_PIN, OUTPUT);
pinMode(YELLOW_PIN, OUTPUT);
pinMode(GREEN_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(TRIGGER_PIN, INPUT_PULLUP);
pinMode(DISARM_PIN, INPUT_PULLUP);
setLeds(LOW, LOW, HIGH);
}
void loop() {
int a = (digitalRead(TRIGGER_PIN) == LOW);
int b = (digitalRead(DISARM_PIN) == LOW);
if (alarmActive == 0) {
if (a && b) {
alarmActive = 2; // → PANIC
}
else if (a) {
alarmActive = 1; // → ALARMING
}
}
else if (alarmActive == 1) {
setLeds(HIGH, LOW, LOW);
wail(880, 660, 100);
if (b) {
alarmActive = 0;
noTone(BUZZER_PIN);
setLeds(LOW, LOW, HIGH);
}
}
else {
// PANIC
setLeds(HIGH, HIGH, HIGH);
wail(1200, 900, 50);
if (a && b) {
alarmActive = 0;
noTone(BUZZER_PIN);
setLeds(LOW, LOW, HIGH);
}
}
}The state machine now has three states and four transitions (idle → alarming, idle → panic, alarming → idle, panic → idle). The order of the IDLE branches matters: we check a && b first (panic), because if we checked a alone first, panic would be impossible — single-A would always win. "First match wins" from L01-20, in action.
Recap 5 min
A state machine is one mutable global int plus an if/else if chain at the top of loop(). Each branch does the work for its state and checks for transitions out. This single pattern — IDLE/ALARMING here, but generalisable to any number of states — is the backbone of every interactive Arduino device. You've also now built your first project that pulls together every component you've met since L01-07: LEDs, resistor, buzzer, two buttons, all on one breadboard.
- State machine
- A program organised around a small set of named modes (states), with explicit transitions between them. Almost every microwave, traffic light, vending machine and burglar alarm is one.
- State variable
- The mutable global that holds the current state's identifier — usually an
intwith values like 0, 1, 2 for each state. - Transition
- The moment a state machine changes from one state to another. In code, always an explicit assignment to the state variable.
- State diagram
- A picture of all states (as circles or boxes) and the transitions between them (as arrows labelled with the triggering event).
- Blocking vs non-blocking
- A blocking pattern (using
delay) freezes the whole sketch while it waits. A non-blocking pattern (usingmillis()snapshots) lets the loop keep checking other things. Real products almost always use non-blocking. - One-bus design
- The pattern from L01-15 onwards of sharing GND through the breadboard's − rail, so that adding components doesn't add wires to the Arduino. Today's six-component circuit uses one shared GND wire to the Arduino.
Homework 5 min
The 30-second auto-rearm. Real alarms re-arm themselves automatically after disarming, so the user doesn't have to remember to rearm. Add this behaviour to your sketch.
- When transitioning from ALARMING to IDLE (or from PANIC to IDLE), record
millis()into a new mutable globalunsigned long disarmedAt = 0;. - Add a new state: DISARMED (alarmActive = 3). In this state, the green LED slowly blinks (toggle every 500 ms).
- After 30 seconds in DISARMED, automatically transition back to IDLE: green LED steady on.
- During DISARMED, ignore the trigger button — the user is meant to be settling in.
Use millis() - disarmedAt >= 30000 as your "have 30 seconds elapsed?" check.
Also: a design reflection on paper.
- Draw the updated state diagram. How many states? How many transitions? ____
- If a transition from DISARMED → IDLE happens, what should the LED do? What about if the trigger button is pressed during DISARMED — should it instantly re-arm, or wait? Justify your choice. ____
- Cluster C is complete. Looking at your alarm sketch, name three Cluster C ideas you used today that you didn't have on day 1 of Cluster C. ____ (Hint:
INPUT_PULLUP, state-change detection,if/else if,&&/||,millis()— pick three.)
Bring back next class:
- The saved
.inofile (call italarm-with-auto-rearm). - A short phone video (20–30 seconds) showing one full cycle: trigger → alarming → disarm → 30-second DISARMED slow blink → return to IDLE.
- Your updated state diagram + reflection answers on a notebook page.
Heads up for next class: Cluster C is done. L01-24 "Serial.begin & println" begins Cluster D — finally the proper introduction to the Serial Monitor you've been previewing since L01-16. From now on, your sketches can talk back to your laptop.