Fixo's defining trick is that the Agent itself ships a copy of LLVM/Clang and can compile firmware in-process, with no external toolchain. This page explains how that happens from end to end so the rest of the docs read sensibly.
One sentence
A
FixoPresetis fed throughFirmwareGeneratorto produce a tree of C++ sources, the embedded LLVM/Clang compiles those into both an AVR ELF (board-side) and a native.so(node-side), the artefacts are content-addressed by BLAKE3 so unchanged presets don't recompile, then the ELF gets flashed and the.sogetsdlopen'd into the running Agent.
FixoPreset
│
▼
FirmwareGenerator.generateFromPreset()
│
│ ├─ Config.hpp (constants from preset)
│ ├─ PinTraits.hpp (board-specific pin map)
│ ├─ main.cpp (board entry point)
│ ├─ Node API wrappers (C ABI for the .so)
│ ├─ #include <fixo/lobes/...> ← from FixoLobeType.mainHeader
│ └─ #include <fixo/actuators/...> ← from FixoActuatorType.mainHeader
│
▼
FixoBuildManager (background thread, debounced)
│
├─▶ libllvm/clang/lld ─▶ AVR ELF ─▶ FirmwareFlasher (stk500v2)
│
└─▶ libllvm/clang/lld ─▶ Native .so ─▶ FirmwareRuntime (dlopen)
│
└─ Implements fixo_firmware_* C API
The two outputs are produced from the same sources but different target triples and HAL specialisations. Lobe placement (per-lobe placement field) decides which target a given lobe's code lands in.
Earlier OctoMY™ releases shipped a runtime-configurable firmware (Fluxo / ArduMY). The motivation for that approach was that compiling C++ for AVR was a multi-tool toolchain dance — installing avr-gcc, AVR libc, dragging in Arduino's build system — and asking users to do it before flashing was a friction wall. So the firmware was made generic and the user just sent configuration over serial.
Once we vendored libllvm/clang+lld into the Agent's own binary, that wall disappeared. The Agent can compile firmware itself, with no external dependencies, in seconds. With the friction gone, the runtime-config drawbacks (heap allocations on 2 KB SRAM, parser overhead, runtime type dispatch, lobes that couldn't run because everything had to be data-driven) became reasons to stop using it. The unified path — one preset, two compiled artefacts, both refreshed automatically — replaces both the runtime-config controller and the manual-flash workflow.
The libllvm vendoring is non-trivial
The Agent statically links a stripped-down libllvm (~50 MB binary cost) configured for AVR + x86_64 targets only. You do not have to do anything to get it: the build fetches the pinned LLVM sources itself, verifies them against a recorded digest and extracts the slice it needs. After that, the resulting static archive is part of the Agent like any other library.
FirmwareGenerator::generateFromPreset(FixoPreset) produces a tree of C++ files in a temporary build directory. The shape:
build/
├── Config.hpp — preset constants
├── PinTraits.hpp — pin map specialised for the preset's board
├── main.cpp — AVR-side entry point
├── NodeApi.cpp — fixo_firmware_* C API for the .so
├── PresetIntrospect.hpp — name/min/max/default tables
└── (implicit) — #includes pull in actuator / sensor / lobe / board headers from
libfixo's qrc and from custom-type fixo.json sources by relative path
Every preset value lands as a constexpr. The C++ optimiser inlines all template parameters, dead-strips unused branches, and produces the smallest possible binary for the exact configuration. There's no runtime configuration parsing, no heap allocation, no virtual dispatch.
The same source tree is compiled twice:
| Pass | Triple | Linker | Output | Consumer |
|---|---|---|---|---|
| AVR | avr-unknown-unknown |
avr-ld (via lld) |
.elf (board flash) |
FirmwareFlasher writes via stk500v2 |
| Native | host (x86_64-linux-gnu etc.) |
lld |
.so (shared library) |
FirmwareRuntime dlopens into the Agent process |
Lobes with placement: "board-only" are compiled into the AVR pass and stubbed out in the native pass; placement: "native-only" is the opposite; placement: "both" is included in both, and the runtime's per-stanza override decides which copy actually runs at execution time.
FixoFirmwareCache content-addresses the compiled artefacts by a BLAKE3 hash of the canonical preset JSON. Workflow:
preset.contentHash() — BLAKE3 of the canonical JSON serialisation. (BLAKE3 is provided by libllvm's bundled C API; falls back to SHA-256 when OC_USE_FEATURE_LLVM is disabled.)<cache>/<hash>.elf and <cache>/<hash>.so.FixoBuildManager, write the outputs into the cache, and proceed.This is what makes "click Build" feel instant after the first compile of a given preset.
Determinism matters
Canonical JSON ordering, deterministic clang invocations, and deterministic linker output are all required for the cache hits to be reliable. The build script pins LLVM to a specific commit (
7b26069828aaof LLVM 23 main) and theFirmwareGeneratorsorts every collection before serialisation.
FixoBuildManager runs the compile on a background thread. By default it debounces — scheduleBuild() defers for 500 ms so rapid preset edits collapse into one compile. buildNow() bypasses the debounce for explicit user requests.
While compiling, the manager emits:
buildStarted()buildProgress(percent, message)buildDiagnostic(severity, message) — clang warnings and errorsbuildFinished(bool success, quint32 binarySize)FirmwareSizeInfo slots into this stream and exposes a live size bar in the preset editor: a coloured progress widget showing how much of the target board's flash the current build occupies.
After a successful compile:
FirmwareFlasher writes the AVR ELF via stk500v2 over the configured serial port. Background thread, progress signals, retries on protocol errors.FirmwareRuntime::load(soPath) dlopens the native .so, resolves the fixo_firmware_* symbols, and verifies the ABI version matches.FixoRuntime orchestrates both — the FixoRuntime reference is the per-step API.
FirmwareGenerator