Create a custom lobe type

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

  1. Placement — does this lobe run on the MCU, on the node, or either? Decides what HAL primitives you can use.
  2. Output ports — which named, tagged values does the lobe drive? These are what stanza templates auto-connect to actuators via Jaccard similarity.
  3. Input parameters — what runtime-tunable values does the user / control widget feed in?

Before you start

You need:


Step 1: Open the Lobe Type Manager

In the Fixo Studio, navigate to Control → Lobe Type Manager. Built-ins are greyed out; custom types are editable.


Step 2: Duplicate or start fresh

Either:

Double-click the new entry to open the lobe-type editor.


Step 3: Fill in metadata

Same as actuator types — id, title, description, main header. The Studio writes to <personality>/fixo/lobe-types/<id>/.


Step 4: Choose a placement

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

both is the default for "math-only" lobes

If your lobe is pure arithmetic that takes inputs and produces outputs, choose both so the runtime can decide per-stanza where to run it. Phase 5 introduced setStanzaPlacement() precisely so users can dial this without rewriting the lobe.


Step 5: Declare output ports

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

Stanza templates that wrap this lobe will auto-connect to actuators whose semanticTags overlap with these. The more specific your tags, the better the match.


Step 6: Declare input parameters

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.


Step 7: Write the C++ template

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:

For placement: "native-only" you can use the full standard library, threads, Qt — but the lobe will only ever run on the node side.


Live errors

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.


Step 8: Save and try it out

  1. Click Save. The Studio writes the type and reloads the registry.
  2. Open a preset, add a stanza, pick your lobe type from the Lobe dropdown.
  3. Build — the firmware generator includes your .hpp in whichever pass(es) the placement allows.
  4. Inject and run; drive the lobe inputs from Remote's StanzaControlActivity.

Pitfalls

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]

See also