Learning Goals 5 min
Yesterday you wrote a library; today we focus on API design — what to expose, what to hide. A clean header is the difference between a library people use and one they abandon. By the end of this lesson you will:
- Apply the "minimum public surface" principle: expose only what users need.
- Use access modifiers (
public,private,protected) andconstcorrectness. - Write self-documenting headers with doxygen-style comments and sensible defaults.
Warm-Up 10 min
Open three headers you use regularly: Servo.h, Adafruit_SSD1306.h, ArduinoJson.h. Compare them.
Patterns you'll see
- Class declarations with
public+private. - Lots of comments — sometimes Doxygen-style (
/** ... */). - Default arguments in constructors / methods.
- const-correct method declarations.
- Sometimes
#defined constants; sometimesconstexpr.
Today we apply these patterns to our own libraries.
New Concept · API design 25 min
Principle 1 — Minimum public surface
Every public method is a contract you must support forever. Add only what users will actually call.
- Good:
blinker.update(). - Bad:
blinker.internalRefreshIntervalCounter()— internal detail leaked out.
Principle 2 — Sensible defaults
Most users want the common case. Defaults reduce the typing for 80% of uses.
class Blinker {
public:
// pin is required; on/off times default to 500/500 ms (symmetric blink)
Blinker(int pin, unsigned long onMs = 500, unsigned long offMs = 500);
// ...
};
// Users can do any of:
Blinker led(LED_BUILTIN); // default 500/500
Blinker led(LED_BUILTIN, 50); // 50 / 500 (asymmetric)
Blinker led(LED_BUILTIN, 50, 1950); // custom bothPrinciple 3 — const correctness
Methods that don't modify the object should be marked const. Allows users to call them on const references.
class Blinker {
public:
void update(); // changes state -> not const
bool isOn() const; // doesn't change state -> const
int getPin() const;
};Code that takes const Blinker& can call isOn() but not update(). Catches accidental mutations at compile time.
Principle 4 — Hide everything else
Anything users won't call goes private. Implementation details — member variables, helper methods — are private by default.
class Blinker {
public:
Blinker(int pin, unsigned long onMs = 500, unsigned long offMs = 500);
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_;
};Principle 5 — Doxygen comments
/**
* @brief Non-blocking LED blinker.
*
* Wraps the millis() blink-without-delay pattern into a reusable class.
* Call begin() once in setup(), then update() every loop iteration.
*/
class Blinker {
public:
/**
* @brief Construct a Blinker.
* @param pin Digital pin number to drive.
* @param onMs Duration the LED stays HIGH per cycle.
* @param offMs Duration the LED stays LOW per cycle.
*/
Blinker(int pin, unsigned long onMs = 500, unsigned long offMs = 500);
/// Configure the pin as OUTPUT and reset state. Call in setup().
void begin();
/// Advance the state machine if the next transition is due. Call every loop().
void update();
// ...
};Doxygen parses these and generates HTML documentation. The IDE shows the @brief in tooltips when you hover the function name.
Principle 6 — Errors via return value, not exceptions
Arduino doesn't support C++ exceptions efficiently. Use return codes or a special "invalid" value.
bool Blinker::begin() {
if (pin_ < 0 || pin_ > 53) return false; // bad pin
pinMode(pin_, OUTPUT);
return true;
}Principle 7 — Consistent naming
Pick one style and stick with it across the library:
- Methods:
camelCase(Arduino convention). - Members (private): trailing underscore (
pin_) orm_prefix (m_pin). - Constants:
UPPER_SNAKE_CASE.
Worked Example · Improve the Blinker header 25 min
Take yesterday's Blinker.h. Apply the principles.
#ifndef BLINKER_H
#define BLINKER_H
#include <Arduino.h>
/**
* @brief Non-blocking LED blinker for Arduino.
*
* Use multiple instances to blink LEDs at different rates from one loop().
* Supports asymmetric on/off times, live rate changes, and pause/resume.
*/
class Blinker {
public:
/**
* @param pin Digital pin (0–53 on Mega; 0–13 on UNO).
* @param onMs Time the LED stays HIGH per cycle (default 500 ms).
* @param offMs Time the LED stays LOW per cycle (default 500 ms).
*/
Blinker(int pin, unsigned long onMs = 500, unsigned long offMs = 500);
/// Initialise the pin. Returns false if pin is invalid.
bool begin();
/// Advance state if the cycle's next transition is due. Call every loop().
void update();
/// Change the on/off durations live.
void setRate(unsigned long onMs, unsigned long offMs);
/// Stop toggling without changing pin state.
void pause();
/// Resume from where we left off.
void resume();
/// Trigger N quick flashes then return to normal rate.
void burst(int times, unsigned long fastMs = 80);
/// Last commanded state. true = pin currently HIGH.
bool isOn() const { return state_; }
/// Currently paused?
bool isPaused() const { return paused_; }
/// Returns the pin number.
int getPin() const { return pin_; }
private:
int pin_;
unsigned long onMs_, offMs_;
unsigned long savedOn_, savedOff_;
unsigned long lastToggle_;
bool state_;
bool paused_;
int burstRemaining_;
};
#endifCleaner than yesterday's: defaults, doxygen, const-correctness, helper accessor methods, structured into setup-side / control / query.
Basic 5 min
Goal: Apply the same audit to your Motor or SmoothedSensor class. Add defaults, const-correctness, doxygen.
Challenge 1 5 min
Goal: Add inheritance: a FadingBlinker subclass that uses PWM. Reuse the parent's state. Note which members need protected (visible to subclass).
Challenge 2 5 min
Goal: Generate HTML docs from your header with Doxygen. doxygen -g creates a config; edit OUTPUT_DIRECTORY; doxygen generates HTML. Polished, browsable docs for your library.
Challenge 3 · Fix the leaky header 10 min
This Dimmer class breaks several of today's principles. The test sketch below must compile without changes. Right now it gives two compile errors, and the class has three design faults.
Dimmer.h:
#ifndef DIMMER_H
#define DIMMER_H
#include <Arduino.h>
#define MAX_LEVEL 255
class Dimmer {
public:
Dimmer(int pin, int startLevel);
void begin();
void setLevel(int newLevel);
int getLevel();
void writePwmRaw();
int level;
private:
int pin_;
};
#endifDimmer.cpp:
#include "Dimmer.h"
Dimmer::Dimmer(int pin, int startLevel) : level(startLevel), pin_(pin) {}
void Dimmer::begin() {
pinMode(pin_, OUTPUT);
writePwmRaw();
}
void Dimmer::setLevel(int newLevel) {
level = constrain(newLevel, 0, MAX_LEVEL);
writePwmRaw();
}
int Dimmer::getLevel() {
return level;
}
void Dimmer::writePwmRaw() {
analogWrite(pin_, level);
}Test sketch (do not edit):
#include "Dimmer.h"
Dimmer lamp(9);
void report(const Dimmer& d) {
Serial.println(d.getLevel());
}
void setup() {
Serial.begin(9600);
lamp.begin();
lamp.setLevel(300);
report(lamp);
}
void loop() {
}Fix the two compile errors and the three design faults in Dimmer.h and Dimmer.cpp. Name the principle behind each fix.
It works if the test sketch compiles unchanged, prints 255, and an LED on pin 9 lights at full brightness.
Recap 5 min
API design = minimum public surface + sensible defaults + const correctness + doxygen comments + private implementation. Headers are contracts; trim ruthlessly. Tomorrow we publish.
- Public surface
- The set of names + types your library exposes. Smaller = easier to maintain and use.
- Default argument
- A parameter value used when the caller doesn't provide one. Simplifies the common case.
- const method
- A method declared with
constat the end of its signature. Promises not to modify the object. - Doxygen
- Standard tool for generating HTML docs from
/** ... */comments in C++ headers. - protected
- Access modifier between public and private. Visible to subclasses but not to outside callers.
- constexpr / static const
- Compile-time constants with type information. Preferred over
#define. - Backward compatibility
- The promise that new versions don't break code written for old versions. Constrains how aggressively you can change.
Extra Mission 5 min
Part 1 — Design an API before you write it
Good libraries are designed on paper first. Design the public API for a Button class that debounces a push button and reports presses.
Your design must include:
- The constructor, with at least one sensible default.
- Every public method, marked
constwhere it does not change the object. - The private members it needs.
- A three-line example of how a user would use it in
loop().
Part 2 — Make it
Write Button.h and Button.cpp to match your design, with Doxygen comments on every public method. Test it with a sketch that counts presses and passes the button to a function as a const Button&.
Bring back next class: your three files and a Serial Monitor screenshot showing one count per press.