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.
Try It Yourself 15 min
Goal: Apply the same audit to your Motor or SmoothedSensor class. Add defaults, const-correctness, doxygen.
Goal: Add inheritance: a FadingBlinker subclass that uses PWM. Reuse the parent's state. Note which members need protected (visible to subclass).
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.
Mini-Challenge · Public surface review 10 min
- List every public method on your Blinker.
- For each: would a user call this directly? If no, mark it for moving to private.
- Trim. Smaller public surface = easier to use + easier to maintain.
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.
Homework 5 min
- Polish your Blinker header with doxygen comments + defaults + const-correctness.
- Verify it still compiles + works.
- Read ahead to ARD-L04-43 (Publishing to Library Manager).