Lobes
Locomotion controllers and kinematics
Reference for OctoMY™ Lobes — pluggable controllers that translate high-level commands into actuator control.
Heads up — type names and placement
Lobe types are described by
FixoLobeTyperecords with two notable fields:outputPorts(aQVector<FixoPort>declaring named, semantically-tagged outputs) andplacement(one ofboard-only,native-only,both). The placement field is what decides whether the lobe runs in the AVR firmware ELF, in the node-side.so, or either. Stanza templates use the output-port semantic tags to auto-wire to actuators via Jaccard similarity. The ground-truth references are Fixo Data Model and Stanza Templates; the conceptual model below applies with these extra knobs.
Did You Know?
Lobes are named after brain regions because they encapsulate decision-making that can run either on the OctoMY™ Agent or directly on the controller hardware. When running on-board the Arduino, lobes provide lower latency, reduced power draw, and resilience — your robot can keep walking even if it briefly loses connection to the Agent. The
placementfield onFixoLobeTypeis how each lobe declares which environment it can run in.
What is a Lobe?
Lobes are named after the distinct regions in our brain dedicated to specific tasks. In OctoMY™, Lobes represent real-time processing units that take input from sensors and user controls and generate output to actuators.
A Lobe encapsulates decision-making into a unit that can run in different environments:
- If your controller doesn't support Lobes, the processing runs in software on the OctoMY™ Agent
- If the controller supports Lobes, processing moves on-board the controller itself
Since the lobe API unification, both environments run the same C++
template from src/libs/libfixo/fixo/lobes/ — the host-side .so
and the AVR firmware are compiled from one header, and the parity
harness (test/testLobeParity/) asserts byte-identical actuator
output between them.
Benefits of on-board processing
When Lobes run on the controller hardware:
- The Agent is freed up to focus on other tasks
- Less error-prone operation as signals don't travel as far
- Lower power draw
- More responsive operation
- More resilient operation (works even if Agent connection is lost)
Since Lobes are well-defined, they can be tested in simulation environments before deployment to real hardware.
Example: car steering lobe
Basic version
A simple car steering lobe (steering_throttle):
- Input: Throttle position (from remote control)
- Input: Steering angle (from remote control)
- Output: Motor throttle (brushless motor ESC)
- Output: Steering servo (RC servo)
In this basic example, the Lobe simply passes throttle input to the motor and steering angle to the steering servo. The key is that it sets up expectations - this is made to control any car-like robot, allowing a fancy UI with steering wheel and foot pedal to be created.
Advanced version with sensors
Adding IMU and GPS enables advanced processing:
- Input: Throttle position (from remote control)
- Input: Steering angle (from remote control)
- Input: IMU (inertial measurement unit)
- Input: GPS (global positioning system)
- Output: Motor throttle (brushless motor ESC)
- Output: Steering servo (RC servo)
Now the Lobe can perform:
- Traction control - Burst throttle if vehicle movement doesn't match wheel speed
- Adaptive throttle sensitivity - Less throttle at lower speeds
- Adaptive steering sensitivity - Less steering angle at higher speeds
- Adaptive steering correction - Rapid adjustments based on vehicle direction vs travel direction to avoid spinning out
- Drift mode - Detect slip and adjust steering to favor a drifting angle
This takes a basic toy to a full race-car with just firmware changes.
Lobe overview
Lobes provide abstraction between movement intent and actuator control:

Built-in lobe types
These are the shipping lobe templates. The lobeType string is what a
preset stanza names; the header is the single source compiled into
both targets.
| lobeType | Header | Inputs | Outputs |
|---|---|---|---|
identity |
IdentityLobe.hpp |
one per actuator | passthrough to each actuator |
trim |
TrimLobe.hpp |
one per actuator + trim pot sensor | passthrough with wire-space trim offset on one axis |
idle |
IdleLobe.hpp |
none | none (explicit no-op for each channel's idle stanza) |
steering_throttle |
SteeringThrottleLobe.hpp |
forward, yaw | throttle servo, steering servo |
differential_drive |
DifferentialDriveLobe.hpp |
forward, yaw, parking_brake, max_step_rate | two accumulating stepper step targets |
legged_gait |
LeggedGaitLobe.hpp |
phase_offset, velocity, turn, body_pitch, body_roll, step_height | 3 x legCount servo joints (coxa/femur/tibia per leg), trot pattern |
legged_gait_2d |
LeggedGaitLobeIntent2D.hpp |
forward, yaw | same as legged_gait (intent-face wrapper) |
Configuration constants (e.g. legCount, maxStepRate) live on the
generated Config; the Phase-18 parameter-binding machinery decides per
target whether each is compile-time baked or runtime-adjustable.
The template interface
A lobe is a header-only struct with a nested Instance<Config>
template — no QObject, no virtuals, no allocation:
struct MyLobe {
static constexpr LobeId id = LobeId::IDENTITY;
template<typename Config>
struct Instance {
using Inputs = typename Config::LobeInputs;
using Outputs = typename Config::LobeOutputs;
void init();
void update(const Inputs &in, Outputs &out, uint16_t deltaMs);
// Phase-7 transition hooks (usually forwarded to
// DefaultLobeTransition<Outputs>):
void readCurrentState(const Outputs ¤t);
void interpolate(const Outputs &target, double t, Outputs &out) const;
};
};
Key facts:
- I/O is byte buffers, not doubles.
Inputs/OutputsareLobeOutputBufferinstantiations whose slots each carry aValueRep(WORD servo pulse, DWORD step count, …). Lobes read and write through the semantic accessors —semantic<I>(),setSemantic<I>(v), and the runtime-indexedsemanticAt(i)for uniform buffers — which route every value through the sharedValueCodec, the single definition of the semantic-to-wire mapping. - Math is float32 with no libm. Shared helpers live in
fixo/LobeMath.hpp. This keeps host and AVR output byte-identical. - Parameters are
ParamInforecords in the header — the host UI reads them via native introspection; there is no duplicate parameter list anywhere else. - Logical inputs can be declared per stanza. A preset input may
declare its wire representation (
"rep": "sword"etc.); when every input of a stanza declares one, codegen builds the lobe's input buffer from the preset. Otherwise the lobe's contract default applies.
Adding a lobe type
Three steps (see the Custom Lobes tutorial for the full walk-through):
- Write the header in
src/libs/libfixo/fixo/lobes/. - Register it: one row in
kUnifiedLobeSpecs(fixo/controller/FirmwareGenerator.cpp) and aFixoLobeTyperecord in the registry. An unregistered lobe type in a preset is a loud build failure — there is no silent fallback. - Add golden parity vectors to
test/testLobeParity/.
Using lobes
Lobes are selected per stanza in a preset — the stanza names the
lobeType, maps its outputs onto actuators, and declares the logical
inputs the remote drives. Switching stanzas at runtime switches lobes,
optionally blending outputs over a transition duration. See
Stanza Templates for the preset side and the
Fixo Data Model for the full record shapes.
Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
| Build fails with "unknown lobe type" | Preset names a type missing from kUnifiedLobeSpecs |
Register the type or fix the preset |
| Robot goes wrong direction | Motor wiring | Set invert flags |
| Robot curves when going straight | Motor speed difference | Calibrate motors |
| Walking robot falls | Wrong gait timing | Tune step timing / step_height |
| Host and board disagree | A lobe bypassing the semantic accessors or using double/libm math | Fix the lobe; testLobeParity pins the contract |