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
StanzaControlActivityon the Remote reads thefixo-runtimeConfigSync section, picks a widget per stanza based on thenamedWidgethint, and forwards port-value changes to the agent viaActuatorCourier::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 asportValueChangedSignal. 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
setPortValueclamps 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
- Read the active stanza's
namedWidgetfield fromfixo-runtime/sets/<i>/stanzas/<j>/namedWidget. - Look it up in a small factory map. Built-in mappings:
"CarSteeringWidget"→CarSteeringControlAdapter- (more adapters land as they ship)
- If no mapping matches, fall back to
SliderBankWidgetseeded with the stanza's input ports. - Connect the widget's
portValueChangedSignaltoonPortValueChanged, which forwards throughActuatorCourier::setStanzaValue()(legacy wire format kept stable across the migration).
Authoring a new control widget
Recipe:
- Subclass
QWidgetandFixoControlWidget. Use composition if your base is already moc'd (theCarSteeringControlAdapterpattern). - Implement
outputPorts(),setPortValue(),portValue(),asWidget(). - Emit a
portValueChangedSignal(int index, double value)whenever the user changes a port's value. - Block the emission inside
setPortValueso programmatic updates don't echo back. - 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
- Fixo Data Model — FixoPort — port shape and tag vocabulary
- FixoRuntime — what publishes the stanza data this activity consumes
- Stanza Templates — declares
namedWidgethints for stanzas