Fixo Control Widgets

FixoControlWidget interface and built-in widgets

Fixo Control Widgets

Reference for the FixoControlWidget interface and the widgets that ship with libfixo to drive a running stanza from Remote.

Where these widgets live

StanzaControlActivity on the Remote reads the fixo-runtime ConfigSync section, picks a widget per stanza based on the namedWidget hint, and forwards port-value changes to the agent via ActuatorCourier::setStanzaValue(). The widgets here are the building blocks that activity uses.


FixoControlWidget interface

class FixoControlWidget {
public:
    virtual ~FixoControlWidget() = default;

    /// Ports this widget produces. Stable order; setPortValue(index) refers
    /// to this same ordering. Each port carries minValue/maxValue bounds.
    virtual QVector<FixoPort> outputPorts() const = 0;

    /// Programmatic setter — update the widget's visible state. Widgets
    /// clamp to [port.minValue, port.maxValue]. Out-of-range index is
    /// a silent no-op.
    virtual void setPortValue(int index, double value) = 0;

    /// Current value on the port. Widgets without meaningful read-back
    /// may return the port's default.
    virtual double portValue(int index) const = 0;

    /// Convenience: QWidget* for layout insertion. Subclasses return
    /// `this` via a trivial override.
    virtual QWidget *asWidget() = 0;
};

The interface is not a QObject. Concrete widgets multiply-inherit from QWidget and FixoControlWidget, and emit a Qt signal for value changes. The signal name is subclass-defined to keep moc happy without diamond inheritance into a common Q_OBJECT base. By convention the signal is portValueChangedSignal(int index, double value).

Echo-loop guard

All built-in widgets implement signal-blocking on setPortValue: programmatic state updates do not echo back as portValueChangedSignal. This matters when ConfigSync deltas arrive from the Agent — without the guard, the Remote would re-send the same value back, the Agent would apply it again, and you'd have a loop.


Built-in widgets

SliderBankWidget — generated fallback

When a stanza has no namedWidget hint, StanzaControlActivity instantiates a SliderBankWidget and seeds it with the stanza's input ports. Each port becomes a labelled slider:

SliderBankWidget *bank = new SliderBankWidget;
bank->setPorts({
    portFor("throttle", -1.0, 1.0),
    portFor("steering", -1.0, 1.0),
});
connect(bank, &SliderBankWidget::portValueChangedSignal,
        this, &MyHandler::onChanged);

Implementation notes:

  • Sliders use 1 000 integer steps, mapped linearly onto [port.minValue, port.maxValue].
  • Each slider has a labelled name and a live numeric value display.
  • setPorts() rebuilds the layout; previous signal connections to old sliders are dropped.
  • Programmatic setPortValue clamps to range and is signal-blocked.

CarSteeringControlAdapter — CarSteeringWidget wrapper

For stanzas that declare namedWidget: "CarSteeringWidget", the activity instantiates a CarSteeringControlAdapter. The adapter wraps CarSteeringWidget (in libwidgets) by composition — multi-inheritance from a moc'd widget would cause diamond issues — and exposes two ports:

Port index Name Tags Range
0 (PortThrottle) throttle ["throttle"] -1.0 to 1.0
1 (PortSteering) steering ["steering"] -1.0 to 1.0

The inner widget's steeringChanged(throttle, steering) signal is forwarded as two portValueChangedSignal emissions. Programmatic setPortValue updates internal shadow state without driving the wheel widget visibly (matching the contract: the physical UI is mouse-driven, not programmatically driven).

SensorReadbackWidget — read-only display

Not a FixoControlWidget (it has no output ports), but lives alongside in the same activity. Renders one labelled name + value (+ unit) row per sensor declared by the active preset:

SensorReadbackWidget *sr = new SensorReadbackWidget;
sr->setSensors({{"trim-pot", "V", 0.0}, {"throttle-fb", "%", 0.0}});
sr->setValue(0, 3.14);  // updates only the named row

StanzaControlActivity wires it to the fixo-runtime/sensors/<i>/value deltas so it refreshes live. Out-of-range index in setValue is a silent no-op (transient courier-delta races are harmless).


How StanzaControlActivity picks a widget

  1. Read the active stanza's namedWidget field from fixo-runtime/sets/<i>/stanzas/<j>/namedWidget.
  2. Look it up in a small factory map. Built-in mappings:
    • "CarSteeringWidget" → CarSteeringControlAdapter
    • (more adapters land as they ship)
  3. If no mapping matches, fall back to SliderBankWidget seeded with the stanza's input ports.
  4. Connect the widget's portValueChangedSignal to onPortValueChanged, which forwards through ActuatorCourier::setStanzaValue() (legacy wire format kept stable across the migration).

Authoring a new control widget

Recipe:

  1. Subclass QWidget and FixoControlWidget. Use composition if your base is already moc'd (the CarSteeringControlAdapter pattern).
  2. Implement outputPorts(), setPortValue(), portValue(), asWidget().
  3. Emit a portValueChangedSignal(int index, double value) whenever the user changes a port's value.
  4. Block the emission inside setPortValue so programmatic updates don't echo back.
  5. Register with StanzaControlActivity's factory map (currently a small hard-coded switch; a registration API will land alongside Phase 8 custom components).

Tests for built-in widgets in test/testFixoLibrary (look for testSliderBankWidget*, testCarSteeringAdapter*, testSensorReadback*) demonstrate the signal-blocking and range-mapping invariants.


Source code

Class Header
FixoControlWidget libfixo/fixo/widget/FixoControlWidget.hpp
SliderBankWidget libfixo/fixo/widget/SliderBankWidget.hpp
CarSteeringControlAdapter libfixo/fixo/widget/CarSteeringControlAdapter.hpp
SensorReadbackWidget libfixo/fixo/widget/SensorReadbackWidget.hpp
StanzaControlActivity libremote/remote/StanzaControlActivity.hpp
FixoRuntimeCourier libfixo/fixo/runtime/FixoRuntimeCourier.hpp

See also

In this section
Topics
reference widgets fixo remote control port
See also