Learning Goals 5 min
Cluster H's capstone: take the "blink without delay" pattern (L02-35) — which you've now repeated in > 10 sketches — and bottle it into a class that's a single line to instantiate. By the end of this lesson you will:
- Write a
Blinkerclass that wraps pin + on-time + off-time + last-toggle state, with oneupdate()method to call fromloop(). - Use multiple
Blinkerinstances in one sketch — three LEDs at three different rates, no interference. - Extend the class with extra behaviours:
pause(),resume(),setRate(on, off),burst(n)— and feel how a small, well-designed class becomes a powerful Lego piece.
Warm-Up 10 min
Wire three LEDs with 220 Ω resistors on three GPIO pins (any pins). We'll blink them at different rates simultaneously.
The old way (still works)
const int L1 = 9, L2 = 10, L3 = 11;
unsigned long t1 = 0, t2 = 0, t3 = 0;
bool s1 = false, s2 = false, s3 = false;
void setup() {
pinMode(L1, OUTPUT);
pinMode(L2, OUTPUT);
pinMode(L3, OUTPUT);
}
void loop() {
unsigned long now = millis();
if (now - t1 >= 200) { t1 = now;
s1 = !s1;
digitalWrite(L1, s1);
}
if (now - t2 >= 500) { t2 = now;
s2 = !s2;
digitalWrite(L2, s2);
}
if (now - t3 >= 1000) { t3 = now;
s3 = !s3;
digitalWrite(L3, s3);
}
}Works. Ugly. Three copies of the same pattern with subscript-1/2/3 sprinkled everywhere. Adding a 4th LED = 4 more global variables + 1 more if line. Cluster H promise: there's a cleaner way.
New Concept · The Blinker class 20 min
What it needs to remember
- Pin number.
- Currently HIGH or LOW.
- How long to stay HIGH (on time).
- How long to stay LOW (off time).
- When the last state change happened.
- Whether it's currently paused.
Methods
Blinker(pin, onMs, offMs)— constructor.begin()— call fromsetup()topinModeand reset state.update()— call every loop iteration; toggles the pin if it's time.setRate(onMs, offMs)— change blink timing live.pause()/resume()— freeze / unfreeze without changing state.burst(n)— quickly toggle n times then return to normal pattern. (Stretch goal.)
The header
// Blinker.h
#ifndef BLINKER_H
#define BLINKER_H
#include <Arduino.h>
class Blinker {
public:
Blinker(int pin, unsigned long onMs, unsigned long offMs);
void begin();
void update();
void setRate(unsigned long onMs, unsigned long offMs);
void pause();
void resume();
bool isOn() const;
private:
int pin_;
unsigned long onMs_, offMs_;
unsigned long lastToggle_;
bool state_;
bool paused_;
};
#endifThe implementation
// Blinker.cpp
#include "Blinker.h"
Blinker::Blinker(int pin, unsigned long onMs, unsigned long offMs)
: pin_(pin), onMs_(onMs), offMs_(offMs),
lastToggle_(0), state_(false), paused_(false) {}
void Blinker::begin() {
pinMode(pin_, OUTPUT);
digitalWrite(pin_, LOW);
state_ = false;
lastToggle_ = millis();
}
void Blinker::update() {
if (paused_) return;
unsigned long now = millis();
unsigned long wait = state_ ? onMs_ : offMs_;
if (now - lastToggle_ >= wait) {
lastToggle_ = now;
state_ = !state_;
digitalWrite(pin_, state_ ? HIGH : LOW);
}
}
void Blinker::setRate(unsigned long onMs, unsigned long offMs) {
onMs_ = onMs;
offMs_ = offMs;
}
void Blinker::pause() { paused_ = true;
}
void Blinker::resume() { paused_ = false;
lastToggle_ = millis();
}
bool Blinker::isOn() const { return state_;
}Using it
#include "Blinker.h"
Blinker fast(9, 100, 100);
Blinker mid (10, 250, 250);
Blinker slow(11, 1000, 1000);
void setup() {
fast.begin();
mid.begin();
slow.begin();
}
void loop() {
fast.update();
mid.update();
slow.update();
}Compare with the 3-blinker example from §2. Same behaviour. The duplication is gone. Adding a 4th: one more Blinker x(12, 50, 950); one more x.begin(); one more x.update(). That's it.
Worked Example · Three LEDs + control + bursts 25 min
Step 1 — wire 3 LEDs to D9, D10, D11
millis() means three sets of the same four variables; three Blinker objects means one set of four, written once.Step 2 — the sketch from §3
Upload. Confirm three LEDs blink at three rates. The fast one ticks 5 Hz, mid 2 Hz, slow 1 Hz. None drift.
Step 3 — add a serial control layer
// Serial commands: 1, 2, 3 pause the matching blinker; 4, 5, 6 resume them;
// 7, 8, 9 toggle a burst of 5 quick flashes
void loop() {
fast.update();
mid.update();
slow.update();
if (Serial.available()) {
char c = Serial.read();
switch (c) {
case '1': fast.pause();
Serial.println("fast paused");
break;
case '2': mid.pause();
Serial.println("mid paused");
break;
case '3': slow.pause();
Serial.println("slow paused");
break;
case '4': fast.resume();
Serial.println("fast resumed");
break;
case '5': mid.resume();
Serial.println("mid resumed");
break;
case '6': slow.resume();
Serial.println("slow resumed");
break;
}
}
}Each instance has its own pause() / resume(). The other two keep blinking.
Step 4 — add a burst() method
For the "quick 5 flashes" behaviour, add to Blinker:
// in Blinker.h, add:
public:
void burst(int times, unsigned long fastMs = 80);
private:
int burstRemaining_;
unsigned long savedOn_, savedOff_;// in Blinker.cpp:
Blinker::Blinker(int pin, unsigned long onMs, unsigned long offMs)
: pin_(pin), onMs_(onMs), offMs_(offMs),
lastToggle_(0), state_(false), paused_(false),
burstRemaining_(0), savedOn_(onMs), savedOff_(offMs) {}
void Blinker::burst(int times, unsigned long fastMs) {
savedOn_ = onMs_;
savedOff_ = offMs_;
onMs_ = fastMs;
offMs_ = fastMs;
burstRemaining_ = times * 2; // count both on AND off transitions
paused_ = false;
}
void Blinker::update() {
if (paused_) return;
unsigned long now = millis();
unsigned long wait = state_ ? onMs_ : offMs_;
if (now - lastToggle_ >= wait) {
lastToggle_ = now;
state_ = !state_;
digitalWrite(pin_, state_ ? HIGH : LOW);
if (burstRemaining_ > 0) {
burstRemaining_--;
if (burstRemaining_ == 0) {
onMs_ = savedOn_;
offMs_ = savedOff_;
}
}
}
}Add to setup(): case '7': fast.burst(5); break; etc. Press 7: the fast LED suddenly does 5 quick flashes then returns to its normal 5 Hz.
Step 5 — see the encapsulation pay off
You changed the class — saved onMs_ / offMs_, added burst tracking. The loop() code didn't change at all. Every instance gets the new ability for free. That's the whole point of OOP.
Step 6 — array of blinkers
Blinker blinkers[] = {
Blinker(9, 100, 100),
Blinker(10, 250, 250),
Blinker(11, 1000, 1000),
};
const int N = sizeof(blinkers) / sizeof(blinkers[0]);
void setup() {
for (int i = 0; i < N; i++) blinkers[i].begin();
}
void loop() {
for (int i = 0; i < N; i++) blinkers[i].update();
}4-LED Knight Rider in 3 lines of loop() body. The class is now a building block.
Basic 5 min
Goal: Add a setPin(int pin) method that moves the blinker to a different pin at runtime (calls pinMode, sets the new pin LOW, updates pin_).
Challenge 1 5 min
Goal: Add a Morse-letter mode: blinker.morse('A') queues up dots and dashes to play, then returns to normal blinking.
Challenge 2 5 min
Goal: Create a FadingBlinker class that inherits from Blinker (your first taste of inheritance). Override update() to fade with analogWrite instead of toggling.
Challenge 3 · Ship a personal helper library 10 min
You now have several reusable classes scattered across your sketches: Motor, SmoothedSensor, Blinker, ShiftReg, etc. Time to consolidate.
- Create a folder
~/Documents/Arduino/libraries/AdvasUtil/(the standard Arduino library location). - Inside, put
AdvasUtil.hthat#includes each class header. - Add a
library.propertiesfile (we'll cover the format in L04-41). - Restart Arduino IDE; your library appears in Sketch → Include Library → AdvasUtil.
Now any sketch on your machine can #include <AdvasUtil.h> and get all your reusable pieces. This is exactly how Adafruit, SparkFun and Arduino ship their libraries.
Cluster H is done. Cluster I (Build, Reflect, Recap) starts tomorrow with the three big projects of L3 — chassis car v2, plant monitor, pan-tilt camera — each combining many of the patterns from L03-01 to L03-42.
Recap 5 min
The Blinker class bottles the "blink without delay" pattern. One line per LED to instantiate, one line per LED in setup, one line per LED in loop. Class state lives inside the instance — no globals. Adding a feature (pause, burst, fade) is changing one file; every existing user gets the new ability. This is the moment OOP starts to feel like a superpower instead of a chore. From here on, every reusable behaviour is a candidate for a class. Cluster I next: three weeks of big builds that exercise every Cluster A–H pattern.
- Blinker pattern
- Non-blocking LED toggle based on millis() comparison. The L02-35 idea, now bottled into a class.
- begin() method
- Separate from the constructor; called in
setup()when the runtime is ready. Standard Arduino library convention (Wire.begin(),Serial.begin()). - update() method
- The "heartbeat" method called every
loop()iteration. Does whatever incremental work is needed. - Asymmetric blink
- Different on-time and off-time.
Blinker(pin, 50, 1950)= 50ms flash, 1950ms gap = classic heartbeat. - Burst
- A short temporary override of the normal pattern. Returns to default once done.
- Inheritance
- A derived class that extends a base class's behaviour. Powerful; easy to overuse.
class B : public A. - Personal library
- A folder of your own headers shared across all your sketches. Lives in
~/Documents/Arduino/libraries/<name>/. - Single-line user experience
- The goal of a well-designed class: the user writes
Thing t;+t.update()and gets all the functionality.Servo,Stepper,Blinkerall hit this bar.
Extra Mission 5 min
Part 1 — Design a light language for a gadget
Many gadgets talk with one or two blinking lights: a router, a charger, a smoke alarm. Pick a gadget you have built in Level 3 — the car, the lamp, the doorbell. On paper, design a light language for it.
Your design must include:
- At least four states the gadget can be in, such as "ready", "busy", "error" or "low battery".
- For each state: which LED, its on and off times in ms, or a burst count.
- A check that no two states look the same to someone watching.
- Which state wins if two happen at once.
Part 2 — Make it
Build your light language with the Blinker class, using setRate(), pause(), resume() and burst(). Use Serial keys to switch between states, so you can show every one. loop() must contain no delay().
Bring back next class: your design, your uploaded sketch, and a short video showing every state. Bring the L03-28 chassis ready for L03-43.