Reference for the state machine that takes a FixoPreset and drives it through compile → flash → load → run, owning the toolchain, flasher, dlopen wrapper, serial link, and tick timer along the way.
One sentence summary
FixoRuntimeis the orchestrator: you give it a preset and it transitions through Compiling, Flashing, Loading, Running, taking care of every subsystem call along the way and emitting a singlestatusChangedsignal so the UI can keep up.
Idle
├─ build() → Compiling ─┬─ CompileSucceeded ─┬─ inject() → Flashing ─┬─ FlashSucceeded ─┐
│ └─ CompileFailed ↩ │ └─ FlashFailed ↩ │
│ │ │
│ (start may be │ start() → │
│ called from │ Loading ─┬─ Running
│ CompileSucceeded │ └─ Error ↩
│ when running │
│ native-only) │
│ │
└─ stop() ← Running ←─────────────────────── │
↓
Stopping → Idle
| State | Meaning |
|---|---|
Idle |
No preset, or preset set but nothing started |
Compiling |
LLVM build in progress |
CompileSucceeded |
Build finished; .elf and .so ready |
CompileFailed |
Build failed; see lastError() and buildLog() |
Flashing |
Writing .elf to board via stk500v2 |
FlashSucceeded |
Flash verified |
FlashFailed |
Flash failed (serial error, verification mismatch) |
Loading |
dlopen of .so in progress |
Running |
.so loaded, numex active, firmware ticking |
Stopping |
Shutting down |
Error |
Unrecoverable; see lastError() |
statusChanged(FixoRuntimeStatus) fires on every transition.
class FixoRuntime : public QObject {
public:
bool setPreset(const FixoPreset &preset);
public slots:
void build(); // Idle / Failed → Compiling → Succeeded / Failed
void inject(); // CompileSucceeded → Flashing → Succeeded / Failed
void start(); // CompileSucceeded / FlashSucceeded → Loading → Running
void stop(); // Running → Stopping → Idle
void tick(); // public for testing; normally driven by QTimer
};
setPreset() is allowed only in Idle, CompileFailed, FlashFailed, or Error (i.e. not while running). Each lifecycle method validates the current state and sets lastError() if rejected.
FixoRuntime doesn't reimplement the heavy lifting; it composes Phase-6-and-earlier classes:
| Subsystem | Role |
|---|---|
FirmwareGenerator |
Preset → C++ source |
FixoBuildManager |
Embedded LLVM compile (background thread, debounced); buildNow() bypasses debounce for explicit build() calls |
FirmwareFlasher |
stk500v2 over serial (background thread) |
FirmwareRuntime |
dlopen wrapper for the native .so |
QTimer (internal) |
Ticks FirmwareRuntime::tick() at the configured interval |
The runtime never owns the user; it just owns these. Caller doesn't need to manage subsystem lifetimes.
enum class FixoRuntimeMode { Debug, Live };
runtime.setMode(FixoRuntimeMode::Debug); // default
| Aspect | Debug | Live |
|---|---|---|
| Logging | Verbose serial + numex | Minimal |
| Error handling | Pause on error, inspect | Auto-restart on recoverable failure |
| Auto-restart | Off | CompileFailed / FlashFailed re-enter build() via QTimer::singleShot(0, …) |
| Tick logging | Per-tick state logged | Counters only |
Use Debug mode while iterating on a preset; switch to Live for unattended deployment.
runtime.setTickInterval(20); // ms; default 50 Hz
int interval = runtime.tickInterval();
FirmwareRuntime::tick() is called from a dedicated QTimer. The tick thread is the sole caller of firmware functions; control inputs (setInput, setActiveStanza) are queued and applied at the start of each tick to keep ordering deterministic.
Phase 5 lobes can declare placement: "both", in which case the runtime can route the lobe to either the firmware (board-only) or the native .so (native-only). The override is applied per-stanza:
bool ok = runtime.setStanzaPlacement(
"walk", // stanza id
"native-only", // override
"both" // lobe type's placement capability
);
Validation:
board-only lobe overridden to native-only) → rejected, error in lastError()setStanzaPlacement() must be called before start()stanzaPlacements() returns the full map for code generation and wire-protocol routing.
.so loadingruntime.setNativeSoPath("/path/to/firmware.so");
When set, start() will dlopen the .so into FirmwareRuntime as it transitions Loading → Running. Left empty, start() enters Running with no .so loaded — introspection returns empty, but setInput / setActiveStanza / tick are safe no-ops.
The native compilation pipeline that produces the
.sois currently work-in-progress (construct::native::NativeCompilerin libnative compiles C++ to a loadable.sovia the same vendored libllvm/clang+lld toolchain that the AVR path uses; its API is being generalised to accept libfixo include paths). Once that lands,FixoRuntimewill produce both targets in one build.
While Running, the runtime mirrors FirmwareRuntime's introspection so callers don't have to reach through:
quint16 stanzaSetCount();
QString presetName();
QString cachedConfigHash(); // BLAKE3 hash of last successful build
QString lastError();
QString buildLog();
Control passthroughs (no-op when not Running):
void setInput(quint16 stanzaSet, quint16 inputIndex, double value);
void setActiveStanza(quint16 stanzaSet, quint32 stanzaId);
FixoLibrary::createRuntime(presetName) returns a FixoRuntime * preconfigured with the named preset, or nullptr when the preset doesn't exist. The library tracks created runtimes in runtimes() and emits runtimeCreated(FixoRuntime*) on each creation, which is the hook the Phase 7 FixoRuntimeCourier uses to start publishing state to ConfigSync.
void statusChanged(FixoRuntimeStatus newStatus);
void modeChanged(FixoRuntimeMode newMode);
void buildProgress(int percent, const QString &message);
void buildDiagnostic(int severity, const QString &message);
void flashProgress(int bytesWritten, int totalBytes);
void error(const QString &message);
void presetChanged();
| Class | Header |
|---|---|
FixoRuntime |
libfixo/fixo/runtime/FixoRuntime.hpp |
FirmwareRuntime |
libfixo/fixo/runtime/FirmwareRuntime.hpp |
FixoBuildManager |
libfixo/fixo/controller/FixoBuildManager.hpp |
FirmwareFlasher |
libfixo/firmware/FirmwareFlasher.hpp |
FirmwareGenerator |
libfixo/fixo/controller/FirmwareGenerator.hpp |
FixoRuntimeCourier |
libfixo/fixo/runtime/FixoRuntimeCourier.hpp |
CompilingFixoRuntime end-to-end