Skip to content

Commit 22b05ab

Browse files
GNERSISclaudeDavide Faconti
authored
feat(dialog-protocol): widget channels + Image codec for the Mosaico toolbox (#93)
* feat(dialog-protocol): widget channels + Image codec for the Mosaico toolbox Adds the RangeSlider, SequencePicker date-range, MetadataQueryBar, QDateTimeEdit and QTableWidget filter/cell-style channels, the requestClose panel command, the canonical Image byte codec, and the toolbox object-write API the Mosaico plugin builds on. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: update tests after rebase onto v5 ABI main Adapt the rebased Mosaico-deps work to main's current state: - ABI sentinel: PJ_toolbox_host_vtable_t grew 72->88 bytes when this branch appended register_object_topic / push_owned_object; pin the two new tail-slot offsets and bump the size assertion. - arrow_stream_round_trip_test: a test case added on main still used the pre-rebase 1-arg DatastoreToolboxHost(engine); supply the ObjectStore the constructor now requires. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: document toolbox object-write API and new dialog widgets Bring SDK docs in line with the Mosaico-deps surface added on this branch: - dialog-plugin-guide: add RangeSlider, SequencePicker, MetadataQueryBar, QDateTimeEdit and the QTableWidget filter/style setters to the widget table; document onHeaderClicked / onQuerySelector / requestClose. - toolbox-guide: document registerObjectTopic / pushOwnedObject with a serializeImage example and the older-host degradation caveat. - ARCHITECTURE: note the toolbox host's object-topic write slots are tail-appended under ABI v5 (struct_size-gated, no version bump) and now require an ObjectStore& at construction. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(release): bump version to 0.3.1 Backward-compatible release: adds the toolbox object-write API and dialog widget channels (tail-appended ABI slots, additive SDK), so a PATCH bump over 0.3.0 per the project's 0.MINOR.PATCH convention. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(claude): require a Conan release decision in every PR Document the release policy: each PR should propose a version bump — MINOR for API/ABI breaks, PATCH for backward-compatible changes — with conanfile.py and CMakeLists.txt kept in sync, and tagging/pushing gated on explicit approval. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Davide Faconti <dfaconti@aurynrobotics.com>
1 parent a98f458 commit 22b05ab

22 files changed

Lines changed: 801 additions & 19 deletions

CLAUDE.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,24 @@ Before committing, always run:
8989

9090
Code formatting and linting are enforced via pre-commit hooks (clang-format v17).
9191

92+
## Release Versioning
93+
94+
In **every PR**, proactively raise whether it warrants a new Conan release, and
95+
propose the version bump rather than waiting to be asked. Pre-1.0 versioning
96+
convention (`0.MINOR.PATCH`):
97+
98+
- **MINOR** bump (`0.X.0`) — any API or ABI **break**: removing/reordering ABI
99+
vtable slots, changing existing struct layouts or function signatures, or any
100+
source-incompatible SDK change.
101+
- **PATCH** bump (`0.x.Y`) — **backward-compatible** changes: tail-appended ABI
102+
slots (gated by `struct_size`), additive SDK helpers, bug fixes, docs.
103+
104+
The version is declared in two places that **must stay in sync**: `version` in
105+
`conanfile.py` and `PJ_PACKAGE_VERSION` in the root `CMakeLists.txt` (also update
106+
the example tag in the `conanfile.py` docstring). Tagging and pushing the release
107+
is a separate, explicitly-authorized step — never tag or push a release without
108+
the user's go-ahead.
109+
92110
## Instructions Glossary
93111

94112
- **"Read all documentation"** means: find and read every `.md` file in the entire project tree (all subdirectories). Use `find . -name "*.md"` or equivalent. This includes docs in `docs/`, `pj_datastore/docs/`, `pj_plugins/docs/`, and any other location.

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ endif()
183183
if(PJ_INSTALL_SDK)
184184
include(CMakePackageConfigHelpers)
185185

186-
set(PJ_PACKAGE_VERSION "0.3.0")
186+
set(PJ_PACKAGE_VERSION "0.3.1")
187187
set(PJ_PACKAGE_CMAKE_DIR ${CMAKE_INSTALL_LIBDIR}/cmake/plotjuggler_core)
188188

189189
install(EXPORT plotjuggler_coreTargets

conanfile.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
plugin_sdk — umbrella for plugin authors (base + dialog SDK + parser SDK)
88
plugin_host — umbrella for host loaders (data_source/parser/toolbox/dialog)
99
10-
A consuming Conan recipe declares e.g. `plotjuggler_core/0.3.0` and then:
10+
A consuming Conan recipe declares e.g. `plotjuggler_core/0.3.1` and then:
1111
1212
find_package(plotjuggler_core REQUIRED COMPONENTS plugin_sdk)
1313
target_link_libraries(my_plugin PRIVATE plotjuggler_core::plugin_sdk)
@@ -27,7 +27,7 @@
2727

2828
class PlotjugglerCoreConan(ConanFile):
2929
name = "plotjuggler_core"
30-
version = "0.3.0"
30+
version = "0.3.1"
3131
license = "MIT"
3232
url = "https://github.com/PlotJuggler/plotjuggler_core"
3333
description = "C++20 foundation libraries for PlotJuggler: storage engine, plugin SDK, plugin host loaders."

pj_base/include/pj_base/plugin_data_api.h

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,15 @@ typedef struct {
183183
uint32_t id;
184184
} PJ_field_handle_t;
185185

186+
/* ABI-FROZEN: layout permanent; changes = v5 break.
187+
*
188+
* Declared up here (rather than next to the object-store host vtables below)
189+
* because PJ_toolbox_host_vtable_t now references it: toolbox plugins can
190+
* register/push canonical media topics directly through the toolbox host. */
191+
typedef struct {
192+
uint32_t id; /* 0 == invalid handle */
193+
} PJ_object_topic_handle_t;
194+
186195
/* ==========================================================================
187196
* Protocol v4 core types
188197
*
@@ -489,6 +498,26 @@ typedef struct PJ_toolbox_host_vtable_t {
489498
bool (*read_series_arrow)(
490499
void* ctx, PJ_field_handle_t field, struct ArrowSchema* out_schema, struct ArrowArray* out_array,
491500
PJ_error_t* out_error) PJ_NOEXCEPT;
501+
502+
/* [main-thread] Register an object topic for media payloads (images, point
503+
* clouds, annotations). The topic is namespaced under the data source
504+
* previously created via create_data_source. `metadata_json` is opaque to
505+
* the host — viewers and parsers read it to pick a renderer (e.g.
506+
* {"object_type":"image"} for the MediaViewerWidget). Returns false (with
507+
* out_error populated) if the source handle is unknown or a topic with
508+
* this name already exists for the data source. */
509+
bool (*register_object_topic)(
510+
void* ctx, PJ_data_source_handle_t source, PJ_string_view_t topic_name, PJ_string_view_t metadata_json,
511+
PJ_object_topic_handle_t* out_handle, PJ_error_t* out_error) PJ_NOEXCEPT;
512+
513+
/* [main-thread] Eager push of an object payload — host copies the bytes
514+
* into ObjectStore. Appropriate for both small messages and the
515+
* one-shot media writes that toolbox plugins typically perform; lazy
516+
* push is not offered on the toolbox surface (plugin holds the bytes
517+
* already by the time it calls). */
518+
bool (*push_owned_object)(
519+
void* ctx, PJ_object_topic_handle_t topic, int64_t timestamp_ns, const uint8_t* data, size_t size,
520+
PJ_error_t* out_error) PJ_NOEXCEPT;
492521
} PJ_toolbox_host_vtable_t;
493522

494523
typedef struct {
@@ -511,10 +540,8 @@ typedef struct {
511540
* `pj.parser_object_write.v1` in addition to `pj.parser_write.v1`.
512541
* ========================================================================== */
513542

514-
/* ABI-FROZEN: layout permanent; changes = ABI break. */
515-
typedef struct {
516-
uint32_t id; /* 0 == invalid handle */
517-
} PJ_object_topic_handle_t;
543+
/* PJ_object_topic_handle_t is declared earlier (next to PJ_field_handle_t)
544+
* because the toolbox host vtable references it as well. */
518545

519546
/* Lazy-fetch callback type. Invoked by the host on-demand when a consumer
520547
* reads an entry stored via push_lazy. On success the plugin populates

pj_base/include/pj_base/sdk/plugin_data_api.hpp

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,12 +1178,63 @@ class ToolboxHostView {
11781178
return MaterializedSeriesView(std::move(schema), std::move(array));
11791179
}
11801180

1181+
/// Register an object topic for media payloads (images, point clouds,
1182+
/// annotations) under a previously created data source. `metadata_json`
1183+
/// is opaque to the store and retained verbatim; viewers and parsers
1184+
/// read it to pick a renderer.
1185+
///
1186+
/// Returns `unexpected` if the host predates this ABI slot — older hosts
1187+
/// can be detected via the ABI's `PJ_HAS_TAIL_SLOT` macro; in this
1188+
/// view's terms, the function pointer will be null.
1189+
[[nodiscard]] Expected<ObjectTopicHandle> registerObjectTopic(
1190+
DataSourceHandle source, std::string_view name, std::string_view metadata_json) const {
1191+
if (!valid()) {
1192+
return unexpected("toolbox host is not bound");
1193+
}
1194+
if (!hasTailSlot(offsetof(PJ_toolbox_host_vtable_t, register_object_topic), host_.vtable->register_object_topic)) {
1195+
return unexpected("toolbox host does not support object topics (older host)");
1196+
}
1197+
ObjectTopicHandle handle{};
1198+
PJ_error_t err{};
1199+
if (!host_.vtable->register_object_topic(
1200+
host_.ctx, source, toAbiString(name), toAbiString(metadata_json), &handle, &err)) {
1201+
return unexpected(errorToString(err));
1202+
}
1203+
return handle;
1204+
}
1205+
1206+
/// Eager push of an object payload — host copies the bytes into its own
1207+
/// storage. Returns `unexpected` on older hosts that don't expose the
1208+
/// slot (see registerObjectTopic).
1209+
[[nodiscard]] Status pushOwnedObject(ObjectTopicHandle topic, Timestamp ts, Span<const uint8_t> payload) const {
1210+
if (!valid()) {
1211+
return unexpected("toolbox host is not bound");
1212+
}
1213+
if (!hasTailSlot(offsetof(PJ_toolbox_host_vtable_t, push_owned_object), host_.vtable->push_owned_object)) {
1214+
return unexpected("toolbox host does not support object payloads (older host)");
1215+
}
1216+
PJ_error_t err{};
1217+
if (!host_.vtable->push_owned_object(host_.ctx, topic, ts, payload.data(), payload.size(), &err)) {
1218+
return unexpected(errorToString(err));
1219+
}
1220+
return okStatus();
1221+
}
1222+
11811223
[[nodiscard]] const PJ_toolbox_host_t& raw() const noexcept {
11821224
return host_;
11831225
}
11841226

11851227
private:
11861228
PJ_toolbox_host_t host_{};
1229+
1230+
/// Tail-slot guard mirroring PJ_HAS_TAIL_SLOT from the C ABI: the host's
1231+
/// struct_size must be large enough to cover the field, AND the slot
1232+
/// must be non-null. Templated on the function-pointer type so the
1233+
/// sizeof check stays accurate without naming the typedef.
1234+
template <class Fn>
1235+
[[nodiscard]] bool hasTailSlot(std::size_t field_offset, Fn fn) const noexcept {
1236+
return host_.vtable != nullptr && host_.vtable->struct_size >= field_offset + sizeof(Fn) && fn != nullptr;
1237+
}
11871238
};
11881239

11891240
// ---------------------------------------------------------------------------

pj_base/tests/abi_layout_sentinels_test.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,9 @@ static_assert(offsetof(PJ_toolbox_host_vtable_t, struct_size) == 4, "toolbox hos
172172
static_assert(offsetof(PJ_toolbox_host_vtable_t, append_bound_record) == 40, "toolbox host baseline pinned");
173173
static_assert(offsetof(PJ_toolbox_host_vtable_t, append_arrow_stream) == 48, "toolbox host bulk slot pinned");
174174
static_assert(offsetof(PJ_toolbox_host_vtable_t, read_series_arrow) == 64, "toolbox host read slot pinned");
175-
static_assert(sizeof(PJ_toolbox_host_vtable_t) == 72, "Toolbox host size");
175+
static_assert(offsetof(PJ_toolbox_host_vtable_t, register_object_topic) == 72, "toolbox host object-topic slot pinned");
176+
static_assert(offsetof(PJ_toolbox_host_vtable_t, push_owned_object) == 80, "toolbox host object-push tail slot pinned");
177+
static_assert(sizeof(PJ_toolbox_host_vtable_t) == 88, "Toolbox host size (update deliberately on append)");
176178

177179
// --- ABI version symbol ------------------------------------------------------
178180
static_assert(PJ_ABI_VERSION == 5, "v5 ABI version");

pj_datastore/include/pj_datastore/plugin_data_host.hpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,11 @@ class DatastoreParserObjectWriteHost {
117117

118118
class DatastoreToolboxHost {
119119
public:
120-
explicit DatastoreToolboxHost(DataEngine& engine);
120+
/// Construct with both an engine (scalar/Arrow column writes) and an
121+
/// object store (canonical media payloads — images, point clouds,
122+
/// annotations). The two are independent storage backends; toolbox
123+
/// plugins write into one or both via the same host fat pointer.
124+
DatastoreToolboxHost(DataEngine& engine, ObjectStore& object_store);
121125
~DatastoreToolboxHost();
122126

123127
DatastoreToolboxHost(const DatastoreToolboxHost&) = delete;

pj_datastore/src/plugin_data_host.cpp

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -921,8 +921,17 @@ struct DatastoreParserWriteHostState {
921921
};
922922

923923
struct DatastoreToolboxHostState {
924-
explicit DatastoreToolboxHostState(DataEngine& engine) : core(engine) {}
924+
DatastoreToolboxHostState(DataEngine& engine, ObjectStore& store) : core(engine), object_store(store) {}
925925
ToolboxCore core;
926+
// Toolbox plugins share the session's object store; the host holds a
927+
// reference so register_object_topic + push_owned_object can forward
928+
// without going back through the engine.
929+
ObjectStore& object_store;
930+
std::string object_last_error;
931+
932+
void setObjectError(std::string msg) {
933+
object_last_error = std::move(msg);
934+
}
926935
};
927936

928937
struct DatastoreSourceObjectWriteHostState {
@@ -1199,6 +1208,75 @@ bool toolboxReadSeriesArrow(
11991208
});
12001209
}
12011210

1211+
bool toolboxRegisterObjectTopic(
1212+
void* ctx, DataSourceHandle source, PJ_string_view_t topic_name, PJ_string_view_t metadata_json,
1213+
PJ_object_topic_handle_t* out_handle, PJ_error_t* out_error) noexcept {
1214+
auto* impl = static_cast<DatastoreToolboxHostState*>(ctx);
1215+
if (out_handle == nullptr) {
1216+
propagateError(out_error, "out_handle must not be null");
1217+
return false;
1218+
}
1219+
// Validate the source handle against the engine — same check used by
1220+
// scalar ensureTopic so the toolbox can't register a topic against a
1221+
// dataset that doesn't exist.
1222+
if (impl->core.engine_.getDataset(source.id) == nullptr) {
1223+
impl->setObjectError(fmt::format("data source {} not found", source.id));
1224+
propagateError(out_error, impl->object_last_error.c_str());
1225+
return false;
1226+
}
1227+
try {
1228+
ObjectTopicDescriptor desc{};
1229+
desc.dataset_id = source.id;
1230+
desc.topic_name = std::string(toStringView(topic_name));
1231+
desc.metadata_json = std::string(toStringView(metadata_json));
1232+
auto result = impl->object_store.registerTopic(desc);
1233+
if (!result) {
1234+
impl->setObjectError(result.error());
1235+
propagateError(out_error, impl->object_last_error.c_str());
1236+
return false;
1237+
}
1238+
out_handle->id = result->id;
1239+
impl->object_last_error.clear();
1240+
return true;
1241+
} catch (const std::exception& e) {
1242+
impl->setObjectError(e.what());
1243+
propagateError(out_error, impl->object_last_error.c_str());
1244+
return false;
1245+
} catch (...) {
1246+
impl->setObjectError("registerObjectTopic: unknown exception");
1247+
propagateError(out_error, impl->object_last_error.c_str());
1248+
return false;
1249+
}
1250+
}
1251+
1252+
bool toolboxPushOwnedObject(
1253+
void* ctx, PJ_object_topic_handle_t topic, int64_t timestamp_ns, const uint8_t* data, std::size_t size,
1254+
PJ_error_t* out_error) noexcept {
1255+
auto* impl = static_cast<DatastoreToolboxHostState*>(ctx);
1256+
try {
1257+
std::vector<uint8_t> bytes;
1258+
if (data != nullptr && size > 0) {
1259+
bytes.assign(data, data + size);
1260+
}
1261+
auto result = impl->object_store.pushOwned(ObjectTopicId{topic.id}, timestamp_ns, std::move(bytes));
1262+
if (!result) {
1263+
impl->setObjectError(result.error());
1264+
propagateError(out_error, impl->object_last_error.c_str());
1265+
return false;
1266+
}
1267+
impl->object_last_error.clear();
1268+
return true;
1269+
} catch (const std::exception& e) {
1270+
impl->setObjectError(e.what());
1271+
propagateError(out_error, impl->object_last_error.c_str());
1272+
return false;
1273+
} catch (...) {
1274+
impl->setObjectError("pushOwnedObject: unknown exception");
1275+
propagateError(out_error, impl->object_last_error.c_str());
1276+
return false;
1277+
}
1278+
}
1279+
12021280
/// RAII holder for the plugin-owned `fetch_ctx` passed to push_lazy. Stores
12031281
/// the destroy callback pointer and the ctx value; destroys both on drop.
12041282
/// Wrapped in a shared_ptr so the lambda that ObjectStore stores remains
@@ -1597,6 +1675,8 @@ const PJ_toolbox_host_vtable_t kToolboxVTable = {
15971675
toolboxAppendArrowStream,
15981676
toolboxAcquireCatalogSnapshot,
15991677
toolboxReadSeriesArrow,
1678+
toolboxRegisterObjectTopic,
1679+
toolboxPushOwnedObject,
16001680
};
16011681

16021682
const PJ_object_write_host_vtable_t kSourceObjectWriteVTable = {
@@ -1647,8 +1727,8 @@ void DatastoreParserWriteHost::flushPending() {
16471727
state_->core.flushPending();
16481728
}
16491729

1650-
DatastoreToolboxHost::DatastoreToolboxHost(DataEngine& engine)
1651-
: state_(std::make_unique<DatastoreToolboxHostState>(engine)) {}
1730+
DatastoreToolboxHost::DatastoreToolboxHost(DataEngine& engine, ObjectStore& object_store)
1731+
: state_(std::make_unique<DatastoreToolboxHostState>(engine, object_store)) {}
16521732
DatastoreToolboxHost::~DatastoreToolboxHost() = default;
16531733
DatastoreToolboxHost::DatastoreToolboxHost(DatastoreToolboxHost&&) noexcept = default;
16541734
DatastoreToolboxHost& DatastoreToolboxHost::operator=(DatastoreToolboxHost&&) noexcept = default;

pj_datastore/tests/arrow_stream_round_trip_test.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include "pj_base/type_tree.hpp"
2727
#include "pj_base/types.hpp"
2828
#include "pj_datastore/engine.hpp"
29+
#include "pj_datastore/object_store.hpp"
2930
#include "pj_datastore/plugin_data_host.hpp"
3031

3132
namespace PJ {
@@ -152,7 +153,8 @@ TEST(ArrowStreamRoundTripTest, WriteViaAppendArrowStreamReadViaReadSeriesArrow)
152153
write_host.flushPending();
153154

154155
// Catalog snapshot — look up the field handle for "value".
155-
DatastoreToolboxHost tb_host(engine);
156+
ObjectStore object_store;
157+
DatastoreToolboxHost tb_host(engine, object_store);
156158
auto tb_vtable = tb_host.raw();
157159

158160
PJ_catalog_snapshot_t snapshot{};
@@ -239,7 +241,8 @@ TEST(ArrowStreamRoundTripTest, ParserWriteHostAppendArrowStreamWritesBoundTopic)
239241

240242
parser_write_host.flushPending();
241243

242-
DatastoreToolboxHost tb_host(engine);
244+
ObjectStore object_store;
245+
DatastoreToolboxHost tb_host(engine, object_store);
243246
auto tb_vtable = tb_host.raw();
244247

245248
PJ_catalog_snapshot_t snapshot{};

pj_datastore/tests/plugin_host_read_test.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include "pj_base/sdk/plugin_data_api.hpp"
1414
#include "pj_base/type_tree.hpp"
1515
#include "pj_datastore/engine.hpp"
16+
#include "pj_datastore/object_store.hpp"
1617
#include "pj_datastore/plugin_data_host.hpp"
1718
#include "pj_datastore/writer.hpp"
1819

@@ -23,7 +24,8 @@ using namespace PJ::sdk;
2324

2425
struct Fixture {
2526
DataEngine engine;
26-
DatastoreToolboxHost toolbox_impl{engine};
27+
ObjectStore object_store;
28+
DatastoreToolboxHost toolbox_impl{engine, object_store};
2729
ToolboxHostView toolbox{toolbox_impl.raw()};
2830
};
2931

0 commit comments

Comments
 (0)