Built-in lobes (identity, wheeled, tracked, legged_gait, hovering, limb_placement, trim, blink_led) cover most common kinematics, but custom robots often need bespoke control logic. This guide walks through authoring a new FixoLobeType — pick a placement (board-only / native-only / both), declare output ports with semantic tags, and write the C++ template that does the work.
Three big choices
- Placement — does this lobe run on the MCU, on the node, or either? Decides what HAL primitives you can use.
- Output ports — which named, tagged values does the lobe drive? These are what stanza templates auto-connect to actuators via Jaccard similarity.
- Input parameters — what runtime-tunable values does the user / control widget feed in?
You need:
In the Fixo Studio, navigate to Control → Lobe Type Manager. Built-ins are greyed out; custom types are editable.
Either:
wheeled for a steered-wheel robot, legged_gait for a multi-leg variant), rename, edit;omni-drive), and start from a blank.Double-click the new entry to open the lobe-type editor.
Same as actuator types — id, title, description, main header. The Studio writes to <personality>/fixo/lobe-types/<id>/.
The dropdown offers three options:
| Placement | When to use | What you give up |
|---|---|---|
board-only |
Real-time loops with hard timing requirements (e.g. quadrature decoding); needs hardware register access | Can't use Qt, threads, libstdc++ heap; AVR-only ABI |
native-only |
Compute-heavy logic (gait planning, IK, optimisation, neural nets); benefits from full host CPU | Adds round-trip latency over numex; can't run when node is offline |
both |
Lobes whose code is portable C++ with no hardware tricks (mixers, filters, kinematics) | Your code has to actually compile both ways — see Pitfalls below |
bothis the default for "math-only" lobesIf your lobe is pure arithmetic that takes inputs and produces outputs, choose
bothso the runtime can decide per-stanza where to run it. Phase 5 introducedsetStanzaPlacement()precisely so users can dial this without rewriting the lobe.
Each output is a FixoPort:
"outputPorts": [
{"name": "left_wheel", "tags": ["wheel", "left", "motor"], "min": -1.0, "max": 1.0, "direction": "Output"},
{"name": "right_wheel", "tags": ["wheel", "right", "motor"], "min": -1.0, "max": 1.0, "direction": "Output"}
]
Tag conventions (see Fixo Data Model for the full vocabulary):
leg, arm, joint, coxa, femur, tibia, shoulder, …wheel, track, motor, throttle, steering, brake, …left, right, front, rear, top, bottom, centerleg_0, joint_2Stanza templates that wrap this lobe will auto-connect to actuators whose semanticTags overlap with these. The more specific your tags, the better the match.
Input parameters are runtime-tunable values the user (or a control widget) drives:
"inputParameters": ["speed", "steering"]
The corresponding FixoInput records (with name, min, max, default) are filled in per-stanza when the lobe is used. The control widget on Remote (SliderBankWidget by default, or a namedWidget adapter) generates one slider per input.
The minimal shape for a placement: "both" lobe:
#pragma once
#include <fixo/lobes/LobeBase.hpp>
#include <cstdint>
namespace fixo {
/// Differential-drive mixer.
/// Inputs: throttle [-1, 1], steering [-1, 1]
/// Outputs: left_wheel, right_wheel each in [-1, 1]
class WheeledLobe {
public:
static constexpr int inputCount = 2;
static constexpr int outputCount = 2;
void tick(const double *inputs, double *outputs) {
const double throttle = inputs[0];
const double steering = inputs[1];
double left = throttle + steering;
double right = throttle - steering;
if (left > 1.0) left = 1.0;
if (left < -1.0) left = -1.0;
if (right > 1.0) right = 1.0;
if (right < -1.0) right = -1.0;
outputs[0] = left;
outputs[1] = right;
}
};
} // namespace fixo
Conventions:
placement: "both", no Qt, no std::vector, no <string> — only freestanding C++. The AVR target lacks the heap-using portions of libstdc++.tick(const double*, double*) is the canonical signature; the runtime passes inputCount doubles in and reads outputCount doubles out.For placement: "native-only" you can use the full standard library, threads, Qt — but the lobe will only ever run on the node side.
The type editor compiles your source after each pause with the LLVM diagnostic engine running against a small synthetic preset. Errors and warnings appear in the panel under the editor; click a row to jump to the offending line. Lines with diagnostics get a coloured gutter mark in the source editor.
The synthetic preset is the smallest valid firmware that uses your lobe
— one board (uno), one stanza, and one actuator/sensor of the type
you're editing. You don't need an open preset to get diagnostics; the
generated source mirrors what would land in a real firmware build.
For placement: "board-only" lobes the synthetic preset compiles for
the AVR target so you'll catch the same template-instantiation errors
the real build would. For placement: "native-only" it compiles for
the host so libstdc++ is available.
If a diagnostic doesn't match anything in your source, the most common
causes are a missing input-port name or an output-port count that
doesn't match what your tick() reads/writes. Both are visible in the
synthesized preset.
.hpp in whichever pass(es) the placement allows.StanzaControlActivity.| Symptom | Likely cause |
|---|---|
Linker error mentioning std::vector (board pass only) |
You used STL containers; for both placement, stick to C arrays / std::array / fixed-size buffers |
unresolved reference to vtable (board pass only) |
You used virtual functions; AVR build doesn't support RTTI cleanly — use templates instead |
| Auto-connect doesn't match anything | Your output port tags don't overlap with any actuator's semanticTags. Tag both sides consistently |
| Inputs don't appear in the Remote slider bank | You declared inputParameters but the stanza didn't list them in inputs; cross-check the preset |
| Lobe runs but does nothing | Forgot to write outputs; make sure tick() writes every index in outputs[0..outputCount-1] |
.hpp