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

  1. Add one line to DASHBOARD_WIDGET_TABLE in widget_table.h: X(my_widget, MyWidget). The first column is the YAML type: name and the enumerator; the second is the class.
  2. Include the widget’s public header in widget_registry.h, next to the others.
  3. 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.


This site uses Just the Docs, a documentation theme for Jekyll.