feat(sdk): dataset-qualified names + pj.playback/viewport/plot_tabs host services (0.28.0) - #184
Open
Alvvalencia wants to merge 9 commits into
Open
feat(sdk): dataset-qualified names + pj.playback/viewport/plot_tabs host services (0.28.0)#184Alvvalencia wants to merge 9 commits into
Alvvalencia wants to merge 9 commits into
Conversation
Alvvalencia
force-pushed
the
feat/playback-viewport-services
branch
from
September 1, 2026 12:49
661bc9f to
a437921
Compare
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
force-pushed
the
feat/playback-viewport-services
branch
from
September 5, 2026 07:55
8433f39 to
e1a8fa9
Compare
facontidavide
marked this pull request as ready for review
September 5, 2026 08:11
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.v1addressed input series by baretopic/fieldnames 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 thePJ_data_processors_host_vtable_tdoc block (DATASET-QUALIFIED NAMES):dataset_source:topic/field— the same form hosts print as a series identity, so displayed names round-trip as inputs.:, so stream-style names like[stream] UDP Serverneed no escaping.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.v1pj.playback.v1(PJ_playback_host_vtable_t,sdk::PlaybackHostView/PlaybackHostService):play/pause/seek/set_playback_rate/get_state(ABI-frozenPJ_playback_state_tsnapshot) /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) andzoom_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 (thepj.data_processors.v1discipline) 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_configreads back what a tab actually holds, so a caller can report what was drawn instead of what it asked for.deserializePlotMarkersdecodes 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.PJ_ERROR_CODE_REJECTED/PJ_ERROR_CODE_INTERNALnamed in the ABI header — aggregators (a kind router probing several backends) rely on the distinction, which was previously folklore.pj_enable_whenand the newpj_visible_when(host behaviour: conditional visibility and;-joined clauses, documented here for plugin authors);playback_viewport_api_testandplot_tabs_api_testcovering the ABI structs, view wrappers and traits.Versioning
VERSIONremains 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
./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.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 onrun_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).Follow-ups (separate changes, once 0.28.0 is picked up)