Skip to content

feat(sdk): dataset-qualified names + pj.playback/viewport/plot_tabs host services (0.28.0) - #184

Open
Alvvalencia wants to merge 9 commits into
mainfrom
feat/playback-viewport-services
Open

feat(sdk): dataset-qualified names + pj.playback/viewport/plot_tabs host services (0.28.0)#184
Alvvalencia wants to merge 9 commits into
mainfrom
feat/playback-viewport-services

Conversation

@Alvvalencia

@Alvvalencia Alvvalencia commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Everything the Assistant Agent toolbox needs from the SDK, in one MINOR: the dataset-qualified naming contract its reads and creates rely on (previously #183), and the host services that let a plugin drive the transport and compose views of its own. Additions only — no existing struct, vtable slot or wire format changes, so every already-built plugin keeps working without a recompile (abidiff: additions only).

Dataset-qualified series names — contract and shared helper

pj.data_processors.v1 addressed input series by bare topic/field names and never said what a name means when several loaded datasets share topic names, so conforming hosts filled the gap incompatibly (PJ4's transform path refused duplicates while its marker path silently bound the first-loaded dataset; fixed by PlotJuggler/PJ4#619). The contract is now explicit in the PJ_data_processors_host_vtable_t doc block (DATASET-QUALIFIED NAMES):

  • A series' full identity is (dataset, topic, field); a bare name is an abbreviation. An input MAY carry the qualifier dataset_source:topic/field — the same form hosts print as a series identity, so displayed names round-trip as inputs.
  • The qualifier is matched against the loaded source names (longest match wins), never split blindly at :, so stream-style names like [stream] UDP Server need no escaping.
  • An unknown qualifier is an error (no fallback to the bare reading); a bare name that exists in several datasets MUST be refused with the qualified candidates, never resolved by load order; qualified inputs of one processor must agree on a single dataset. Marker per-series output keys accept the qualifier the same way.

New installed header pj_base/sdk/dataset_qualified_name.hpp (splitDatasetQualifier / qualifiedSeriesName, header-only, std-only) is the shared parser/composer, replacing the two copies that exist today in PJ4 and in a plugin. Tests: pj_base/tests/dataset_qualified_name_test.cpp.

Host services pj.playback.v1, pj.viewport.v1, pj.plot_tabs.v1

  • pj.playback.v1 (PJ_playback_host_vtable_t, sdk::PlaybackHostView / PlaybackHostService): play / pause / seek / set_playback_rate / get_state (ABI-frozen PJ_playback_state_t snapshot) / to_display_time (absolute ns → display-axis seconds via the owning dataset's offset). Every time in the service is display-axis seconds, the numbers the plot X axes and the playback slider show. Global by nature: one time cursor is shared by every plot.
  • pj.viewport.v1 (PJ_viewport_host_vtable_t, sdk::ViewportHostView / ViewportHostService): zoom_to_time_range (X window in display-axis seconds; per-plot Y preserved; XY/empty plots untouched) and zoom_reset (fit). Both are scoped to the tabs the calling plugin owns; a plugin owning none has nothing to zoom, which is an error rather than a silent no-op.
  • pj.plot_tabs.v1 (PJ_plot_tab_host_vtable_t, sdk::PlotTabHostView / PlotTabHostService): a plugin composes plotting tabs of its own — create_tab / close_tab / list_tab_ids / tab_config / add_curve / remove_curve / clear_tab. Ids are plugin-chosen and namespaced per plugin (the pj.data_processors.v1 discipline) and every slot is scoped to the caller's own tabs, so the user's tabs are neither disclosed nor mutable through it. Curves are addressed by their parts (topic, field, dataset source) rather than a joined path, because field paths contain / and dataset names contain :. tab_config reads back what a tab actually holds, so a caller can report what was drawn instead of what it asked for.
  • Codec fix: deserializePlotMarkers decodes an empty buffer as an empty set (the "clear my markers" tombstone a producer publishes on a replace-only store) instead of erroring; null-with-size, truncated and malformed payloads still error.
  • Error-code convention: PJ_ERROR_CODE_REJECTED / PJ_ERROR_CODE_INTERNAL named in the ABI header — aggregators (a kind router probing several backends) rely on the distinction, which was previously folklore.
  • Dialog-property docs for pj_enable_when and the new pj_visible_when (host behaviour: conditional visibility and ;-joined clauses, documented here for plugin authors); playback_viewport_api_test and plot_tabs_api_test covering the ABI structs, view wrappers and traits.

Versioning

VERSION remains 0.28.0. These additions share the unreleased, untagged 0.28.0 release with source-record attachment (#189). The branch is rebased onto main, including #189 and #190; the existing release history is preserved and all of this PR's entries are under 0.28.0.

Verification

  • Rebased SDK: ./build.sh --debug && ./test.sh, 88/88 tests passed with ASAN, including a regression test for a growing tab list between count and fill. The wrapper returns a retryable error instead of reading beyond its buffer.
  • Contract, host side: PJ4 fix/dataset-qualified-inputs (#619) rewired locally to delegate to the new header — all of its dataset-qualification tests pass unchanged, and a GUI end-to-end with two datasets sharing an identical topic layout (a plugin creating markers on run_b.mcap:/sensor/temperature/data) landed on the correct dataset, region count and covered time matching an independent decode of the file (7 regions, ~12.9 s; the first-loaded dataset would have given ~7.7 s).
  • Services, host side: the consumer tools were E2E-verified in the application (play/pause/seek/rate clamped and reported, zoom/framing pixel-verified, model-owned tabs created, zoomed and closed with the user's tabs untouched). The host-side PJ4 wiring is in #573.

Follow-ups (separate changes, once 0.28.0 is picked up)

  • PJ4 drops its private copy of the splitter and includes the header.
  • Plugin-side callers do the same and start emitting the qualified form on creates.
  • Addressing two datasets that share one source name stays out of contract (ambiguous → error); a typed dataset id would be a tail-appended ABI addition if ever needed.

@Alvvalencia
Alvvalencia force-pushed the feat/playback-viewport-services branch from 661bc9f to a437921 Compare September 1, 2026 12:49
@Alvvalencia Alvvalencia changed the title feat(sdk): pj.playback.v1 + pj.viewport.v1 host services feat(sdk): dataset-qualified names + pj.playback/viewport/plot_tabs host services (0.27.0) Sep 3, 2026
Alvvalencia and others added 9 commits September 5, 2026 09:52
Two new optional host services so a plugin (first consumer: the Assistant
Agent toolbox) can drive the app like a user — transport and zoom:

- pj.playback.v1 (PJ_playback_host_vtable_t, sdk::PlaybackHostView):
  play/pause/seek/set_playback_rate/get_state (ABI-frozen
  PJ_playback_state_t) + to_display_time (absolute int64 ns -> display-axis
  seconds via the topic's dataset offset; current-frame semantics).
- pj.viewport.v1 (PJ_viewport_host_vtable_t, sdk::ViewportHostView):
  zoom_to_time_range (every open time plot's X window, per-plot Y kept) and
  zoom_reset (fit all).

All times are display-axis seconds — the numbers the plot X axes and the
playback slider show. Additive only: no existing struct or slot touched
(abidiff: additions only). Version provisional at 0.16.0 (0.15.0 taken by
the in-flight per-topic subscription work).
… playback state layout

An empty buffer IS the canonical proto encoding of an empty set — the
"clear my markers" tombstone a producer publishes to a replace-only
store — but deserializePlotMarkers rejected size==0 as an error, making
the tombstone unrepresentable on the wire. Now it round-trips; null-with-
size, truncated, and malformed payloads still error. (Logically part of
the plot-markers feature line; riding this branch until PRs are sorted.)

Also pin PJ_playback_state_t (ABI-FROZEN, bool + padding + 4 doubles)
in the layout-sentinels test.
- PJ_ERROR_CODE_REJECTED / PJ_ERROR_CODE_INTERNAL: the two error classes
  every PJ host service distinguishes, now with an authoritative home —
  aggregators (a kind router probing multiple backends) rely on REJECTED
  meaning "not mine, safe to try another backend" vs INTERNAL meaning "a
  real failure that must never be masked by a fallback".
- Document the host-honored "pj_enable_when" dynamic property (combo-
  driven field enabling, incl. inside modal sub-dialogs) in the dialog
  plugin guide + SDK reference.
A plugin can create a plotting tab, place and remove curves in it, read back
what it holds, and close it. The point is a boundary: a tab a plugin made is
its own to compose, and the user's tabs stay the user's.

Scoping is the HOST's duty, enforced where ownership is actually known. A tab
this plugin did not create is rejected exactly as an unknown id is, so the
service never discloses, mutates, or even confirms the existence of anyone
else's tab; ownership comes from the per-binding ctx and never crosses the
wire, the same unspoofable-identity discipline pj.data_processors.v1 already
uses for its per-plugin isolation.

Three choices worth their reasoning:

- Ids are plugin-chosen and host-namespaced, so no handle has to travel back
  as an out-string. The resemblance to pj.data_processors.v1 stops at
  namespacing, and the doc says so: a processor id survives a reload because
  the host replays it from a recipe, whereas a tab is a view and this service
  promises nothing of the kind.
- Curves are addressed by their parts — topic, field, dataset source — not by
  a joined path. Field paths legitimately contain '/' and dataset names contain
  ':', so splitting a joined form would be guesswork the host should not have
  to do. An empty dataset source means the pair must be unique, and an
  ambiguous one is refused with the candidates rather than picked.
- tab_config reads back what a tab actually holds, so a caller reports what was
  drawn instead of what it asked for — the same reason create_markers reads its
  own output back out of the store.

pj.viewport.v1 keeps both signatures and narrows only its documented scope, to
the tabs the calling plugin owns; it has never shipped, so nothing is broken.
That does couple the two services in practice even though the registry treats
them as independently optional, and the doc now says they are registered
together or not at all. The boundary is the view: pj.playback.v1 stays global,
because one time cursor is shared by every plot.

Additions only — no existing struct or vtable slot was touched. 85 tests
(10 new); the one red is the environmental plugin_catalog_test.
…er (0.26.0)

pj.data_processors.v1 addressed input series by bare topic/field names and never
said what a name means when several loaded datasets share topic names, so
conforming hosts filled the gap incompatibly (PJ4's transform path refused
duplicates while its marker path silently bound the first-loaded dataset).

The contract is now explicit in the PJ_data_processors_host_vtable_t doc block
(DATASET-QUALIFIED NAMES), and the new installed header
pj_base/sdk/dataset_qualified_name.hpp ships the shared parser/composer
(splitDatasetQualifier / qualifiedSeriesName) so hosts and plugins use one
implementation instead of the two copies that exist today.

MINOR bump to 0.26.0: new installed public header, no ABI change.
The dataset-qualified naming contract (formerly #183, 0.26.0 → 0.27.0 after
GridMap took 0.26.0) and the playback/viewport/plot-tabs services (formerly
claiming 0.28.0 on top of it) are one deliverable for one consumer, so they
ship as one MINOR: 0.27.0, one CHANGELOG section with both parts.
…menu

A plugin could show a list and delete rows from it, but had no way to hear a
right click. The menu's entries are not protocol: they are declared on the
widget in the .ui, next to pj_enable_when and pj_visible_when, and the host
paints them. All the protocol learns is which action fired on which row,
reported with the plugin-order index the trash and double-click already use.

Additive: no struct changes, no protocol version bump, and a plugin that
declares nothing keeps today's behaviour.
@facontidavide facontidavide changed the title feat(sdk): dataset-qualified names + pj.playback/viewport/plot_tabs host services (0.27.0) feat(sdk): dataset-qualified names + pj.playback/viewport/plot_tabs host services (0.28.0) Sep 5, 2026
@facontidavide
facontidavide force-pushed the feat/playback-viewport-services branch from 8433f39 to e1a8fa9 Compare September 5, 2026 07:55
@facontidavide
facontidavide marked this pull request as ready for review September 5, 2026 08:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants