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 FixoLobeType records with two notable fields: outputPorts (a QVector<FixoPort> declaring named, semantically-tagged outputs) and placement (one of board-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&trade; 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 placement field on FixoLobeType is 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&trade;, 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&trade; 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:

Lobe Architecture
Lobe Architecture

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 &current);
		void interpolate(const Outputs &target, double t, Outputs &out) const;
	};
};

Key facts:

  • I/O is byte buffers, not doubles. Inputs/Outputs are LobeOutputBuffer instantiations whose slots each carry a ValueRep (WORD servo pulse, DWORD step count, …). Lobes read and write through the semantic accessors — semantic<I>(), setSemantic<I>(v), and the runtime-indexed semanticAt(i) for uniform buffers — which route every value through the shared ValueCodec, 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 ParamInfo records 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):

  1. Write the header in src/libs/libfixo/fixo/lobes/.
  2. Register it: one row in kUnifiedLobeSpecs (fixo/controller/FirmwareGenerator.cpp) and a FixoLobeType record in the registry. An unregistered lobe type in a preset is a loud build failure — there is no silent fallback.
  3. 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

In this section
Topics
reference hardware lobes kinematics locomotion
See also