Early in this project, firmware variants were not a design goal. They were a necessity forced by two constraints that appeared almost immediately: limited memory on small MCUs and the need for a system that could be administered by non-programmers.
The techniques described here are not new. Compile-time guards, firmware variants, and simplified installation flows all exist independently in other projects. What is less common is treating them as a coherent system pattern, shaped deliberately by constraints like memory limits, administrative usability, and long-term growth. The value here is not invention, but integration: documenting how these decisions came together in a real, evolving system, and why they proved sustainable over time.
Trying to solve both problems with a single, feature-rich firmware image quickly proved unrealistic. Not everything fit, and not everything should.
What began as a pragmatic response to constraint later became a key mechanism for scaling the system without fragmenting it.
The Initial Problem: Memory, Administration, and Growth
Each device in the system performs a narrow role. Attempting to support every possible capability in every firmware image led to predictable issues:
-
Flash and RAM pressure on constrained MCUs
-
Features present but unused on most devices
-
Increased complexity for configuration and testing
-
Administrative ambiguity about what a device was meant to do
At the same time, the system could not assume that the person installing or maintaining devices would be comfortable building firmware. Requiring end users to manage sketches, libraries, and toolchains was not acceptable outside of tightly supervised environments.
The problem was not flexibility — it was control, both technical and administrative.
Why the “Example Sketch” Model Didn’t Scale
Many open-source embedded projects handle variation by providing separate example sketches:
-
one sketch per sensor
-
one sketch per feature
-
one sketch per use case
This model is effective for learning and experimentation. It scales poorly as a system.
As example collections grow, they tend to:
-
diverge from one another
-
duplicate logic
-
lag behind core fixes
-
encode assumptions in multiple places
Over time, it becomes unclear which example represents the “real” system. Fixes must be applied repeatedly, and knowledge fragments along with the code.
For a system expected to evolve over years, this approach created more problems than it solved.
The Shift: One Codebase, Many Variants
Instead of multiplying sketches, the system moved toward a single common source with guarded firmware variants.
// Capability guard: the AP exists only in variants that include it.
#if defined(USE_WIFI_AP)
wifiAp_.begin();
webUi_.begin(); // captive portal / setup pages live here
#else
// No AP, no web UI, no associated dependencies.
#endif
In this model:
-
the core system exists once
-
capabilities are compiled in or out using guards
-
variants exist to manage memory and hardware differences
-
behavior is configured at runtime, not baked into sketches
This approach directly leveraged C++ characteristics:
-
shared abstractions and interfaces
-
compile-time elimination of unused code
-
strong separation between capability and behavior
Development effort was concentrated in the core, not spread across example implementations.
Variants as an Enabler, Not a Constraint
Initially, variants existed to solve immediate problems:
-
fitting within memory limits
-
simplifying administration
-
avoiding user-side compilation
Over time, they enabled something more important.
As understanding of electronics improved and new devices were introduced—new sensors, new interfaces, new capabilities—the system did not need to be restructured. Support was added to the core, and a variant exposed that capability where appropriate.
Expansion followed a predictable pattern:
-
extend the core once
-
enable it selectively via a variant
-
preserve existing deployments unchanged
The system could grow without forcing every device to grow with it.
Firmware Delivery Became Predictable
The documentation site does more than describe firmware—it delivers it. Each supported device/variant has a documentation entry that presents an install action, so firmware is loaded over USB directly from the documentation page. This removed the Arduino-IDE workflow from the end-user path and made deployment repeatable across devices and updates.
Once variants were deliberate artifacts produced from a single source, firmware delivery could be simplified.

The Arduino IDE and CLI remain in use, but only as developer tools to produce known firmware images. End users never interact with source code, libraries, or build systems.
Instead:
-
documentation links point to specific firmware variants
-
devices are connected via USB
-
firmware is loaded as a single, repeatable action
This eliminated many common error classes in example-driven workflows and shifted responsibility to where it could be managed once.
Design Pattern Summary (Reusable)
Problem:
Scaling firmware across constrained devices while supporting non-programmer users.
Anti-Pattern:
Multiple example sketches that diverge over time.
Pattern:
One shared codebase with capability-based, guarded firmware variants.
Key Properties:
-
centralized fixes
-
predictable memory usage
-
runtime configuration
-
scalable feature growth
-
simple, repeatable deployment
What This Enables Next
This foundation made several later system decisions possible, including:
-
documentation-driven firmware delivery
-
runtime configuration without recompilation
-
treating documentation and tooling as part of the system
-
adding new hardware support without destabilizing existing nodes
Those topics are explored in future logs.
Transferable Takeaway
Firmware variants are often viewed as a maintenance burden. In constrained, long-lived systems, they can instead act as a scaling boundary — separating capability from behavior, and development from deployment.
The most important shift was not technical, but conceptual: treating growth as expected, and designing the firmware architecture accordingly.
Pat Fleming
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.