Adding a widget
A dashboard widget is a QWidget with a reflected configuration struct. Once it is registered, the YAML loader, the editor’s palette and properties panel, the widget.* agent methods and the config tests all pick it up from one table; none of them needs to be told about it separately. The canonical description of the steps is the comment at the top of libs/dashboard_widgets/include/dashboard/widget_registry.h; this page is the same recipe with more room.
Bindings and loss of comm
A widget that reads a topic carries two things per binding: the zenoh_key and expression it subscribes with, and a stale_after_ms field beside them. Build the subscription with the overload that takes the timeout:
_expression_parser = dashboard::makeExpressionSubscription<double>(
_cfg.schema_type, _cfg.value_expression, _cfg.zenoh_key,
this, &MyWidget::setValue,
std::chrono::milliseconds(_cfg.stale_after_ms));
That is the whole wiring. The subscription repaints the widget when the answer changes; the widget asks it where it paints:
bool valueStale() const { return _expression_parser && _expression_parser->isStale(); }
validate() calls config_codec::limits::clampStaleAfter().
Draw something different. A widget that reads isStale() and paints the same picture leaves a dead sensor looking like a steady reading, which is the whole failure this exists to retire. Nothing sweeps the widget table for it any more, so check it by eye when you add a binding.
Overview
Every widget lives in its own directory under libs/dashboard_widgets/widgets/ and builds as its own static library. DASHBOARD_WIDGET_TABLE in libs/dashboard_widgets/include/dashboard/widget_table.h is the one list of widget types. The widget_type_t enum, the config variant, the default-config function, the YAML decoder, the validator, the palette, and the agent methods are all generated by sweeping that table, so the enum and the sweeps cannot drift apart, and a static_assert in the registry fires if someone adds an enumerator by hand.
The widget class
The class needs these members; the sweeps use every one of them:
class MyWidget : public QWidget
{
public:
static constexpr widget_type_t kWidgetType = widget_type_t::my_widget;
static constexpr std::string_view kFriendlyName = "My Widget"; // the palette entry
using config_t = MyWidgetConfig_t;
explicit MyWidget(const config_t& cfg, QWidget* parent = nullptr);
config_t getConfig() const;
};
config_t is a struct declared with REFLECT_STRUCT in the widget’s own include/<name>/config.h. Nested structs and enums declared with REFLECT_STRUCT and REFLECT_ENUM convert to and from YAML on their own; nothing about the config needs registering.
Do not put a uint8_t in a reflected config. yaml-cpp treats unsigned char as a character, so the value 14 is written as the byte 0x0E and read back as a bad conversion that throws out of the decoder and takes the whole layout with it. Use uint16_t.
Data comes from the bus through dashboard::ExpressionSubscription (libs/dashboard_widgets/include/dashboard/expression_subscription.h): the config names an expression, the subscription evaluates it, and the widget gets the value on the GUI thread. Zenoh callbacks arrive on zenoh threads and must not touch Qt; the subscription already hops with a queued invokeMethod, so follow its shape rather than subscribing directly.
Gauges that paint arcs, needles and tick marks share gauge_painting.h, and the layered paint cache in qt_helpers keeps a static background from being repainted on every value change. Fonts and SVGs come from the shared Qt resources under libs/dashboard_widgets/resources/, loaded with qt_helpers::loadResourceFont().
Registering it
- Add one line to
DASHBOARD_WIDGET_TABLEinwidget_table.h:X(my_widget, MyWidget). The first column is the YAMLtype:name and the enumerator; the second is the class. - Include the widget’s public header in
widget_registry.h, next to the others. - In the widget’s
CMakeLists.txt, declare it with the helper:
add_dashboard_widget(my_widget
SOURCES my_widget.cpp include/my_widget/my_widget.h
)
and add its directory in libs/dashboard_widgets/widgets/CMakeLists.txt. The helper (cmake/DashboardWidget.cmake) sets up AUTOMOC, the include paths, the common link set, and registers the target in the global list both apps link. A widget that needs more than the common set passes PUBLIC_LIBS or PRIVATE_LIBS; the map and carplay widgets are the two that write their CMake by hand instead, and they still call dashboard_widget_register().
Build, and the widget is in the editor’s palette, accepted by the YAML loader, and answerable through widget.get_config, widget.set_config and widget.describe_config.
Checking it
The config tests under libs/dashboard_widgets/tests/ sweep the table, so a new widget is round-tripped through YAML and validated without a new test. Then look at it: build, launch the dashboard on a layout that includes the widget (or drop it onto the editor’s canvas), drive it with zenoh_publish, and take a screenshot. Agent control has the loop. A widget change is not done when it builds; it is done when you have looked at it.
Give the widget an id: in any layout you will use for testing. Without one it is addressed as <type>#<index>, which changes when widgets are reordered.