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
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.
Try It Yourself 15 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_).
Hint
void Blinker::setPin(int pin) {
digitalWrite(pin_, LOW);
pin_ = pin;
pinMode(pin_, OUTPUT);
digitalWrite(pin_, state_ ? HIGH : LOW);
}Niche but possible. Mostly an exercise in clean state mutation.
Goal: Add a Morse-letter mode: blinker.morse('A') queues up dots and dashes to play, then returns to normal blinking.
Hint
Store an internal queue of (duration, gap) pairs. update() pops from the queue if non-empty; when empty, falls back to normal blinking. Use the Morse alphabet table from L03-39.
Goal: Create a FadingBlinker class that inherits from Blinker (your first taste of inheritance). Override update() to fade with analogWrite instead of toggling.
Hint
class FadingBlinker : public Blinker {
public:
using Blinker::Blinker;
void update(); // override
private:
int level_ = 0;
int dir_ = +1;
};
void FadingBlinker::update() {
// ignore parent's state; do PWM ramp
level_ += dir_;
if (level_ >= 255) { level_ = 255; dir_ = -1; }
if (level_ <= 0) { level_ = 0; dir_ = +1; }
analogWrite(pinForInheritance, level_); // requires accessor
}You'll need to expose pin_ via a getter (or change private → protected in Blinker). Inheritance is rich enough for its own lesson; we use it lightly here.
Mini-Challenge · 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.
Homework 5 min
- Build the
Blinkerclass as a header + cpp pair. Verify it works with 3 LEDs. - Move it into your personal library folder so it's available everywhere.
- Take one of your earlier sketches with manual non-blocking blink code; refactor to use Blinker. Note line-count delta.
- Read ahead to ARD-L03-43 (Bluetooth Robot Car v2 — Build). Tomorrow we revisit the chassis car and add PWM speed, polished app UI, and obstacle stop.
Bring back next class:
- Blinker library + 3-LED demo.
- The L03-28 / L03-10 chassis still wired (or ready to re-wire).