FixoRuntime

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

FixoRuntime is 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 single statusChanged signal so the UI can keep up.


State machine

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

Status enum

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.


Lifecycle methods

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.


Owned subsystems

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.


Mode: Debug vs Live

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.


Tick loop

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.


Per-stanza placement override

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:

stanzaPlacements() returns the full map for code generation and wire-protocol routing.


Native .so loading

runtime.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 .so is currently work-in-progress (construct::native::NativeCompiler in libnative compiles C++ to a loadable .so via 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, FixoRuntime will produce both targets in one build.


Introspection passthroughs

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);

Library factory

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.


Signals

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();

Source code

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

See also