Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 31 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,35 @@
All notable changes to `plotjuggler_sdk` are recorded here. Versioning policy is in
[`CLAUDE.md`](./CLAUDE.md) → "Release Versioning".

## [Unreleased] — on branch `feature/plot-markers`, not yet publicly tagged
## [Unreleased] — not yet publicly tagged

### Host service: markers + transforms unified into `pj.data_processors.v1` (UNRELEASED BREAK)
### DataSource: per-topic pause/resume — advertise-without-subscribe + demand-driven control (ADDITIVE)

Lets a streaming source expose *all* its topics cheaply while only transmitting data
for topics the host is actually displaying. Strictly additive (tail slot + plugin
extension, both `struct_size`/`PJ_HAS_TAIL_SLOT`-gated); every existing plugin keeps
working with no recompile, and a new plugin degrades gracefully on an old host.

- **New capability** `PJ_DATA_SOURCE_CAPABILITY_PER_TOPIC_PAUSE = 1 << 6` (C++ mirror
`kCapabilityPerTopicPause`).
- **Plugin → host advertise**: new runtime-host tail slot
`notify_available_topics(topics, count)` carrying `PJ_available_topic_t`
(`topic_name`/`parser_encoding`/`type_name`/`schema`, mirroring
`PJ_parser_binding_request_t`), so the host can list and a-priori classify topics
(via `classify_schema`) before any data flows. C++ helper
`DataSourceRuntimeHostView::notifyAvailableTopics(Span<const AvailableTopic>)`.
- **Host → plugin control**: new `get_plugin_extension("pj.topic_subscription.v1")`
extension `PJ_topic_subscription_v1_t::set_active_topics(names, count)` — declarative
full active-set; the plugin diffs and subscribes/unsubscribes. Host wrapper
`DataSourceHandle::setActiveTopics(...)` (no-op when the extension is absent).
- Runtime-host vtable size grows 96 → 104 (`notify_available_topics` at offset 96);
`PJ_ABI_VERSION` (5), `PJ_DATA_SOURCE_PROTOCOL_VERSION` (4), and
`PJ_DATA_SOURCE_MIN_VTABLE_SIZE` (128) unchanged. `abi/baseline.abi` unchanged
(additions only).

## [0.14.0]

### Host service: markers + transforms unified into `pj.data_processors.v1`

The two whole-series host-driven services were collapsed into ONE contract — Pablo's
`pj.data_processors.v1` — with a string `kind` discriminator, so a plugin chooses
Expand All @@ -31,7 +57,6 @@ so a host that IMPLEMENTS the service (e.g. `DataProcessorsRuntimeHost`) must up
vtable fill; callers using `DataProcessorsHostView::createTransform` keep working via the
shim, while `validateScript` call-sites gain a leading `kind` argument (`"transform"`).

**Versioning note.** This is an ABI/API change to a service that merged to `main`. It
ships as `0.13.0` because no PUBLIC tag has carried `pj.data_processors.v1` yet, so no
released plugin is broken. **The first public release that carries the unified
`pj.data_processors.v1` must be tagged `1.0.0`** per the pre-1.0 break rule in `CLAUDE.md`.
**Versioning note.** This was an ABI/API break to a service that had merged to `main`.
It shipped as `0.14.0` rather than `1.0.0` because no public tag had ever carried
`pj.data_processors.v1`, so no released plugin was broken by the change.
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ endif()
if(PJ_INSTALL_SDK)
include(CMakePackageConfigHelpers)

set(PJ_PACKAGE_VERSION "0.14.0")
set(PJ_PACKAGE_VERSION "0.15.0")
set(PJ_PACKAGE_CMAKE_DIR ${CMAKE_INSTALL_LIBDIR}/cmake/plotjuggler_sdk)

install(EXPORT plotjuggler_sdkTargets
Expand Down
20 changes: 11 additions & 9 deletions conanfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
plugin_sdk — umbrella for plugin authors (base + dialog SDK + parser SDK)
plugin_host — umbrella for host loaders (data_source/parser/toolbox/dialog)

A consuming Conan recipe declares e.g. `plotjuggler_sdk/0.14.0` and then:
A consuming Conan recipe declares e.g. `plotjuggler_sdk/0.15.0` and then:

find_package(plotjuggler_sdk REQUIRED COMPONENTS plugin_sdk)
target_link_libraries(my_plugin PRIVATE plotjuggler_sdk::plugin_sdk)
Expand All @@ -30,15 +30,17 @@

class PlotjugglerSdkConan(ConanFile):
name = "plotjuggler_sdk"
# UNRELEASED BREAK: 0.13.0 unified markers + transforms into the single host
# service `pj.data_processors.v1` via a `kind` discriminator (removed the old
# 0.14.0 unified markers + transforms into the single host service
# `pj.data_processors.v1` via a `kind` discriminator (removed the old
# `pj.markers.v1` and the interim `pj.generators.v1`; generalized
# `create_data_processor`/`validate_data_processor_script` with kind/language/flags).
# 0.14.0 then adds the dialog-protocol additions (radio column + interactive
# sub-panel). All pre-1.0 unreleased — no PUBLIC tag ever shipped
# `pj.data_processors.v1`, so no released plugin breaks. The FIRST public release
# that carries the unified `pj.data_processors.v1` MUST be tagged 1.0.0. See CHANGELOG.md.
version = "0.14.0"
# `create_data_processor`/`validate_data_processor_script` with
# kind/language/flags) and added the dialog-protocol additions (radio
# column + interactive sub-panel). Shipped as a MINOR bump rather than
# 1.0.0 because no public tag had ever carried `pj.data_processors.v1`,
# so no released plugin broke.
# 0.15.0 adds DataSource per-topic pause (advertise + demand-driven
# subscription) — strictly additive. See CHANGELOG.md.
version = "0.15.0"
# Apache-2.0 covers the whole SDK (pj_base + pj_plugins). See LICENSE.
license = "Apache-2.0"
url = "https://github.com/PlotJuggler/plotjuggler_sdk"
Expand Down
2 changes: 1 addition & 1 deletion pj_base/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pj_base is the **Level 0** foundation and the **SDK boundary** for plugin author
- ABI numbering is **frozen**: `BuiltinObjectType` (builtin_object.hpp) and `PJ_builtin_object_type_t` (builtin_object_abi.h) share stable numeric values — never renumber; types 2 and 12 are permanently reserved. Append-only.
- Every vtable slot is `PJ_NOEXCEPT`; a throw across the ABI boundary calls `std::terminate`. See the header block in `plugin_data_api.h`.
- `BuiltinObject` is `std::any`, deliberately not `std::variant`, for forward-compat — recover via `std::any_cast<T>` / `sdk::typeOf`. See `builtin/builtin_object.hpp`.
- ABI/struct-layout or signature changes require a Conan **MINOR** bump and a refreshed `abi/baseline.abi`; tail-appended slots are a PATCH (see submodule CLAUDE.md → Release Versioning).
- Versioning follows the authoritative submodule CLAUDE.md → Release Versioning: a **tail-appended slot / new capability** (backward-compatible ABI *addition*, `abidiff` shows additions-only) is a **MINOR** and does **not** touch `abi/baseline.abi`; a **struct-layout reorder or signature change** (an ABI *break*) is a **MAJOR** — that is what refreshes `abi/baseline.abi` and bumps `PJ_*_PROTOCOL_VERSION`/`PJ_ABI_VERSION`.

## Read deeper
| For | Read |
Expand Down
7 changes: 7 additions & 0 deletions pj_base/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ add_library(pj_base STATIC
src/builtin/voxel_grid_codec.cpp
src/number_parse.cpp
src/type_tree.cpp
src/data_source_host_views.cpp
src/data_source_patterns.cpp
src/data_source_plugin_base.cpp
src/data_source_trampolines.cpp
src/toolbox_plugin_base.cpp
src/toolbox_trampolines.cpp
)
target_include_directories(pj_base PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
Expand Down Expand Up @@ -92,6 +98,7 @@ if(PJ_BUILD_TESTS)
tests/image_annotations_decoder_test.cpp
tests/media_metadata_test.cpp
tests/push_message_test.cpp
tests/notify_available_topics_test.cpp
tests/image_codec_test.cpp
tests/depth_image_codec_test.cpp
tests/point_cloud_codec_test.cpp
Expand Down
69 changes: 67 additions & 2 deletions pj_base/include/pj_base/data_source_protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ enum {
PJ_DATA_SOURCE_CAPABILITY_DELEGATED_INGEST = 1ull << 3, /**< Plugin pushes raw bytes for host-side parsing. */
PJ_DATA_SOURCE_CAPABILITY_SUPPORTS_PAUSE = 1ull << 4, /**< pause()/resume() are implemented. */
PJ_DATA_SOURCE_CAPABILITY_HAS_DIALOG = 1ull << 5, /**< Plugin provides a configuration dialog. */
PJ_DATA_SOURCE_CAPABILITY_PER_TOPIC_PAUSE =
1ull << 6, /**< Host may pause/resume individual topics via the "pj.topic_subscription.v1" extension. */
};

/** Opaque handle returned by ensure_parser_binding, used with push_message. */
Expand Down Expand Up @@ -200,15 +202,31 @@ typedef struct {
PJ_string_view_t parser_config_json; /**< Optional JSON config for the parser. */
} PJ_parser_binding_request_t;

/**
* A topic the source knows it can stream but has not (necessarily)
* subscribed. Handed to the host via notify_available_topics so the host can
* list it and classify it a priori (via the parser's classify_schema) with no
* data flowing. Carries the same parser inputs as PJ_parser_binding_request_t
* so the host can classify and, later, bind identically.
*/
typedef struct {
PJ_string_view_t topic_name; /**< Canonical topic name (the host/engine key). */
PJ_string_view_t parser_encoding; /**< Encoding name, e.g. "protobuf", "ros2msg". */
PJ_string_view_t type_name; /**< Message type name (encoding-specific). */
PJ_bytes_view_t schema; /**< Optional schema bytes for a-priori classify_schema. */
} PJ_available_topic_t;

/**
* DataSource runtime host vtable — control-plane callbacks provided by the
* host and delivered to the plugin via the service registry under the name
* `"pj.runtime.v1"`.
*
* The plugin calls these to report progress, send diagnostic messages,
* notify state changes, and (for delegated ingest) bind parsers and push
* raw message payloads. All calls are made on the thread that called
* start().
* raw message payloads. The per-slot thread tag governs: the host may drive
* start()/stop() on one thread and poll() (with its [stream-thread]
* callbacks) on another, so a host must treat [stream-thread] callbacks as
* arriving off its UI thread and marshal accordingly.
*
* Fallible calls take a `PJ_error_t* out_error` which the callee populates
* on failure. Callers may pass NULL if they don't need the detail.
Expand Down Expand Up @@ -327,6 +345,19 @@ typedef struct PJ_data_source_runtime_host_vtable_t {
bool (*push_message)(
void* ctx, PJ_parser_binding_handle_t handle, int64_t host_timestamp_ns,
PJ_message_data_fetcher_t fetch_message_data, PJ_error_t* out_error) PJ_NOEXCEPT;

/**
* [stream-thread] Advertise the set of topics this source can stream but has
* not (necessarily) subscribed. The host lists them (e.g. in the curve tree)
* and classifies each a priori via classify_schema, WITHOUT any data
* flowing; subscription happens separately (see the "pj.topic_subscription.v1"
* plugin extension). Call again to update as topics appear/vanish; each call
* is the full current set for this source. The plugin calls this on the
* poll/stream thread; the host is responsible for marshaling to its own
* thread. Tail slot — readers MUST gate access with PJ_HAS_TAIL_SLOT.
*/
bool (*notify_available_topics)(void* ctx, const PJ_available_topic_t* topics, uint64_t count, PJ_error_t* out_error)
PJ_NOEXCEPT;
} PJ_data_source_runtime_host_vtable_t;

/** Fat pointer pairing a runtime host context with its vtable. */
Expand Down Expand Up @@ -454,6 +485,40 @@ typedef struct PJ_data_source_vtable_t {
/* The vtable above is ABI-APPENDABLE: new slots may be added at the tail;
* host reads guard with PJ_HAS_TAIL_SLOT. See PJ_DATA_SOURCE_MIN_VTABLE_SIZE. */

/**
* Reverse-DNS id of the per-topic subscription control extension, queried via
* get_plugin_extension. A source advertising PJ_DATA_SOURCE_CAPABILITY_PER_TOPIC_PAUSE
* returns a pointer to a PJ_topic_subscription_v1_t for this id; every other
* source returns NULL.
*/
#define PJ_TOPIC_SUBSCRIPTION_EXTENSION_V1 "pj.topic_subscription.v1"

/**
* Host→plugin per-topic subscription control ("pj.topic_subscription.v1").
*
* The host passes the desired ACTIVE topic set — declarative and complete —
* and the plugin diffs it against its current subscriptions, subscribing to
* newly-active topics and unsubscribing (pausing) the rest, so no data flows
* for a topic that is not being displayed. This is a strict superset control:
* an empty set pauses everything.
*
* The plugin owns this struct; it must stay valid for the plugin instance
* lifetime. IMPORTANT: the `ctx` passed to set_active_topics is the plugin
* INSTANCE (the host's DataSourceHandle::context()), NOT the extension pointer
* returned by get_plugin_extension.
*/
typedef struct {
uint32_t struct_size; /**< sizeof(PJ_topic_subscription_v1_t) — for forward-compatible growth. */
/**
* [main-thread] Set the full active-topic set. Entered on the host thread;
* the plugin marshals to its own stream thread. Returns false + populates
* out_error on failure. Names the source does not recognize are ignored
* (not an error).
*/
bool (*set_active_topics)(void* ctx, const PJ_string_view_t* topic_names, uint64_t count, PJ_error_t* out_error)
PJ_NOEXCEPT;
} PJ_topic_subscription_v1_t;

/** Signature of the exported entry point: `PJ_get_data_source_vtable`. */
typedef const PJ_data_source_vtable_t* (*PJ_get_data_source_vtable_fn)(void);

Expand Down
82 changes: 24 additions & 58 deletions pj_base/include/pj_base/sdk/data_source_host_views.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ constexpr uint64_t kCapabilityDirectIngest = PJ_DATA_SOURCE_CAPABILITY_DIRECT_IN
constexpr uint64_t kCapabilityDelegatedIngest = PJ_DATA_SOURCE_CAPABILITY_DELEGATED_INGEST;
constexpr uint64_t kCapabilitySupportsPause = PJ_DATA_SOURCE_CAPABILITY_SUPPORTS_PAUSE;
constexpr uint64_t kCapabilityHasDialog = PJ_DATA_SOURCE_CAPABILITY_HAS_DIALOG;
constexpr uint64_t kCapabilityPerTopicPause = PJ_DATA_SOURCE_CAPABILITY_PER_TOPIC_PAUSE;

using ParserBindingHandle = PJ_parser_binding_handle_t;

Expand All @@ -95,22 +96,21 @@ struct ParserBindingRequest {
std::string_view parser_config_json;
};

/// A topic the source can stream but has not (necessarily) subscribed. Handed
/// to the host via DataSourceRuntimeHostView::notifyAvailableTopics so the host
/// lists and a-priori classifies it (via classify_schema) with no data flowing.
/// Mirrors ParserBindingRequest's parser inputs so the host can classify and,
/// later, bind identically. `schema` may be empty when the encoding needs none.
struct AvailableTopic {
std::string_view topic_name;
std::string_view parser_encoding;
std::string_view type_name;
Span<const uint8_t> schema;
};

/// Convert a PJ_error_t populated by the ABI into a descriptive std::string.
/// Safe to call on a zero-initialized error (returns "unspecified error").
[[nodiscard]] inline std::string errorToString(const PJ_error_t& err) {
std::string out;
if (err.domain[0] != '\0') {
out.append(err.domain);
out.append(": ");
}
if (err.message[0] != '\0') {
out.append(err.message);
}
if (out.empty()) {
out = "unspecified error";
}
return out;
}
[[nodiscard]] std::string errorToString(const PJ_error_t& err);

class ParserIngestHostView;

Expand Down Expand Up @@ -142,16 +142,7 @@ class DataSourceRuntimeHostView {
}

/// Begin a progress bar. Returns an error if the host refused to start it.
[[nodiscard]] Status progressStart(std::string_view label, uint64_t total_steps, bool cancellable) const {
if (!valid() || host_.vtable->progress_start == nullptr) {
return unexpected("runtime host is not bound");
}
PJ_error_t err{};
if (!host_.vtable->progress_start(host_.ctx, sdk::toAbiString(label), total_steps, cancellable, &err)) {
return unexpected(errorToString(err));
}
return okStatus();
}
[[nodiscard]] Status progressStart(std::string_view label, uint64_t total_steps, bool cancellable) const;

/// Advance progress. Returns true to continue, false if the user cancelled.
[[nodiscard]] bool progressUpdate(uint64_t current_step) const {
Expand Down Expand Up @@ -192,26 +183,15 @@ class DataSourceRuntimeHostView {
}

/// Bind (or look up) a parser for delegated ingest.
[[nodiscard]] Expected<ParserBindingHandle> ensureParserBinding(const ParserBindingRequest& request) const {
if (!valid() || host_.vtable->ensure_parser_binding == nullptr) {
return unexpected("runtime host is not bound");
}
[[nodiscard]] Expected<ParserBindingHandle> ensureParserBinding(const ParserBindingRequest& request) const;

PJ_parser_binding_request_t raw{
.topic_name = sdk::toAbiString(request.topic_name),
.parser_encoding = sdk::toAbiString(request.parser_encoding),
.type_name = sdk::toAbiString(request.type_name),
.schema = sdk::toAbiBytes(request.schema),
.parser_config_json = sdk::toAbiString(request.parser_config_json),
};

ParserBindingHandle handle{};
PJ_error_t err{};
if (!host_.vtable->ensure_parser_binding(host_.ctx, &raw, &handle, &err)) {
return unexpected(errorToString(err));
}
return handle;
}
/// Advertise the full set of topics this source can stream but has not
/// (necessarily) subscribed, so the host can list and a-priori classify them
/// with no data flowing. Each call replaces the previously-advertised set for
/// this source. Returns an error if the host does not expose the tail slot
/// (old host) — callers may treat that as "advertisement unsupported" and
/// fall back to legacy behavior. Call on the poll/stream thread.
[[nodiscard]] Status notifyAvailableTopics(Span<const AvailableTopic> topics) const;

/// Push a message via a deferred FetchMessageData callable. The DataSource
/// hands the host a callable that produces the payload bytes when invoked.
Expand Down Expand Up @@ -309,21 +289,7 @@ class DataSourceRuntimeHostView {
* @return The button clicked, or kOk if the host does not support dialogs.
*/
[[nodiscard]] MessageBoxButton showMessageBox(
MessageBoxType type, std::string_view title, std::string_view message, int buttons = 0) const {
if (!valid() || host_.vtable->show_message_box == nullptr) {
if (buttons & static_cast<int>(MessageBoxButton::kContinue)) {
return MessageBoxButton::kContinue;
}
if (buttons & static_cast<int>(MessageBoxButton::kYes)) {
return MessageBoxButton::kYes;
}
return MessageBoxButton::kOk;
}
int result = host_.vtable->show_message_box(
host_.ctx, static_cast<PJ_message_box_type_t>(type), sdk::toAbiString(title), sdk::toAbiString(message),
buttons == 0 ? PJ_MSG_BTN_OK : buttons);
return static_cast<MessageBoxButton>(result);
}
MessageBoxType type, std::string_view title, std::string_view message, int buttons = 0) const;

void showInfo(std::string_view title, std::string_view message) const {
(void)showMessageBox(MessageBoxType::kInfo, title, message, static_cast<int>(MessageBoxButton::kOk));
Expand Down
Loading
Loading