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
11 changes: 10 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ PlotJuggler Core — C++20 foundation libraries for PlotJuggler storage, plugin
- **pj_base** — vocabulary types, plugin ABI headers, plugin SDK headers (zero external deps)
- **pj_datastore** — columnar storage engine + `ObjectStore` (for media blobs) + `DerivedEngine` (fmt, tsl::robin_map, nanoarrow)
- **pj_plugins** — C-ABI plugin protocol, C++ SDK base classes, plugin discovery, host-side loaders, config envelope helpers, and dialog protocol primitives; four plugin families: DataSource, MessageParser, Dialog, Toolbox
- **pj_scene_protocol** — canonical schema + Foxglove `ImageAnnotations` Protobuf codec (writer + reader); SDK boundary for plugin authors producing or consuming 2D markers / scene primitives. `pj_base`-only deps.

### Dependency graph

- `pj_datastore` → `pj_base`
- `pj_plugins` → `pj_base`
- `pj_scene_protocol` → `pj_base`

## Key Documentation

Expand Down Expand Up @@ -44,6 +46,13 @@ PlotJuggler Core — C++20 foundation libraries for PlotJuggler storage, plugin
| `dialog-plugin-guide.md` | SDK tutorial: WidgetData, typed events, EmbedUi, requestAccept, onTick |
| `toolbox-guide.md` | SDK tutorial: read+write access, catalog, notifyDataChanged |

**Scene protocol** (`pj_scene_protocol/docs/`):

| Document | Content |
|----------|---------|
| `ARCHITECTURE.md` | Wire format spec (`foxglove.ImageAnnotations` Protobuf), type catalog, encoding rules, design rationale (single canonical decoder, loader-side conversion) |
| `USER_GUIDE.md` | Producer recipe (loader writing markers) and consumer recipe (sink/viewer decoding), common pitfalls, pointer to PJ4 reference adapters |

## Build & Test

```bash
Expand All @@ -64,7 +73,7 @@ Before committing, always run:

## Instructions Glossary

- **"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 `pj_base/`, `pj_datastore/docs/`, `pj_plugins/docs/`, and any other location.
- **"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 `pj_base/`, `pj_datastore/docs/`, `pj_plugins/docs/`, `pj_scene_protocol/docs/`, and any other location.

- **"Update the documentation"** means: based on what you learned during this session, correct any documentation that is outdated or inaccurate, and clarify any ambiguity that caused confusion or errors. If a doc says one thing but the code does another, fix the doc to match reality. If missing information led to a bug, add it.

Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ if(PJ_BUILD_DATASTORE)
add_subdirectory(pj_datastore)
endif()
add_subdirectory(pj_plugins)
add_subdirectory(pj_scene_protocol)

if(PJ_BUILD_PORTED_PLUGINS AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/pj_ported_plugins/CMakeLists.txt")
set(PJ_HAS_PORTED_PLUGINS TRUE)
Expand Down
3 changes: 1 addition & 2 deletions pj_base/include/pj_base/plugin_abi_export.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@
#if defined(_MSC_VER)
#define PJ_PLUGIN_ABI_LINK __declspec(dllexport) __declspec(selectany)
#else
#define PJ_PLUGIN_ABI_LINK \
__attribute__((visibility("default"))) __attribute__((weak)) __attribute__((used))
#define PJ_PLUGIN_ABI_LINK __attribute__((visibility("default"))) __attribute__((weak)) __attribute__((used))
#endif

extern "C" {
Expand Down
28 changes: 14 additions & 14 deletions pj_base/include/pj_base/sdk/platform.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@
#include <string>

#if defined(__linux__) || defined(__APPLE__)
# include <dlfcn.h>
#include <dlfcn.h>
#elif defined(_WIN32)
# ifndef WIN32_LEAN_AND_MEAN
# define WIN32_LEAN_AND_MEAN
# endif
# ifndef NOMINMAX
# define NOMINMAX
# endif
# include <windows.h>
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#endif

namespace PJ::sdk {
Expand All @@ -39,12 +39,12 @@ namespace PJ::sdk {
/// without forcing _CRT_SECURE_NO_WARNINGS project-wide.
inline std::optional<std::string> getEnv(const char* name) {
#if defined(_MSC_VER)
# pragma warning(push)
# pragma warning(disable : 4996)
#pragma warning(push)
#pragma warning(disable : 4996)
#endif
const char* value = std::getenv(name);
#if defined(_MSC_VER)
# pragma warning(pop)
#pragma warning(pop)
#endif
if (value == nullptr || *value == '\0') {
return std::nullopt;
Expand Down Expand Up @@ -109,9 +109,9 @@ inline std::filesystem::path getSharedLibDir(const void* fn_addr) {
#elif defined(_WIN32)
wchar_t buf[MAX_PATH] = {};
HMODULE hm = nullptr;
if (::GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCWSTR>(fn_addr), &hm)) {
if (::GetModuleHandleExW(
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCWSTR>(fn_addr), &hm)) {
::GetModuleFileNameW(hm, buf, MAX_PATH);
return fs::path(buf).parent_path();
}
Expand Down
6 changes: 2 additions & 4 deletions pj_base/tests/platform_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,7 @@ TEST(UserDataDirTest, PrefersLocalAppDataOnWindows) {
TEST(UserDataDirTest, UsesApplicationSupportOnMac) {
ScopedEnv guard("HOME", "/tmp/pj_test_home");
auto dir = userDataDir();
EXPECT_EQ(dir,
std::filesystem::path("/tmp/pj_test_home") / "Library" / "Application Support" / "plotjuggler");
EXPECT_EQ(dir, std::filesystem::path("/tmp/pj_test_home") / "Library" / "Application Support" / "plotjuggler");
}
#else
TEST(UserDataDirTest, PrefersXdgDataHomeOnLinux) {
Expand All @@ -107,8 +106,7 @@ TEST(UserDataDirTest, FallsBackToHomeLocalShareOnLinux) {
::unsetenv("XDG_DATA_HOME");
ScopedEnv guard("HOME", "/tmp/pj_test_home");
auto dir = userDataDir();
EXPECT_EQ(dir,
std::filesystem::path("/tmp/pj_test_home") / ".local" / "share" / "plotjuggler");
EXPECT_EQ(dir, std::filesystem::path("/tmp/pj_test_home") / ".local" / "share" / "plotjuggler");
}
#endif

Expand Down
4 changes: 3 additions & 1 deletion pj_datastore/include/pj_datastore/colormap_registry.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ class ColorMapRegistry {
[[nodiscard]] bool hasActive() const;

/// Name of the currently active colormap, or empty string when none.
[[nodiscard]] const std::string& activeName() const { return active_; }
[[nodiscard]] const std::string& activeName() const {
return active_;
}

private:
struct Entry {
Expand Down
3 changes: 1 addition & 2 deletions pj_datastore/include/pj_datastore/query.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,7 @@ class RangeCursor {
class SeriesCursor {
public:
/// Construct cursor over [time_range.min, time_range.max] from committed chunks.
SeriesCursor(
const std::deque<TopicChunk>& chunks, std::size_t column_index, PJ::Range<PJ::Timestamp> time_range);
SeriesCursor(const std::deque<TopicChunk>& chunks, std::size_t column_index, PJ::Range<PJ::Timestamp> time_range);

[[nodiscard]] bool valid() const noexcept;

Expand Down
9 changes: 4 additions & 5 deletions pj_datastore/src/query.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ namespace {
chunk.columns[column_index].descriptor->logical_type == PrimitiveType::kBool;
}

[[nodiscard]] std::optional<double> readSeriesValue(const TopicChunk& chunk, std::size_t column_index, std::size_t row) {
[[nodiscard]] std::optional<double> readSeriesValue(
const TopicChunk& chunk, std::size_t column_index, std::size_t row) {
if (column_index >= chunk.columns.size() || row >= chunk.stats.row_count || chunk.isNull(column_index, row)) {
return std::nullopt;
}
Expand Down Expand Up @@ -205,8 +206,7 @@ RangeCursor rangeQuery(const std::deque<TopicChunk>& chunks, Timestamp t_min, Ti
// SeriesCursor
// ===========================================================================

SeriesCursor::SeriesCursor(
const std::deque<TopicChunk>& chunks, std::size_t column_index, Range<Timestamp> time_range)
SeriesCursor::SeriesCursor(const std::deque<TopicChunk>& chunks, std::size_t column_index, Range<Timestamp> time_range)
: chunks_(&chunks), column_index_(column_index), time_range_(normalized(time_range)) {
skipToSample();
}
Expand Down Expand Up @@ -237,8 +237,7 @@ void SeriesCursor::skipToSample() {
while (chunk_index_ < chunks_->size()) {
const auto& chunk = (*chunks_)[chunk_index_];

if (chunk.stats.row_count == 0 || chunk.stats.t_max < time_range_.min ||
column_index_ >= chunk.columns.size()) {
if (chunk.stats.row_count == 0 || chunk.stats.t_max < time_range_.min || column_index_ >= chunk.columns.size()) {
++chunk_index_;
row_index_ = 0;
continue;
Expand Down
21 changes: 9 additions & 12 deletions pj_datastore/tests/series_reader_test.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
#include "pj_datastore/reader.hpp"

#include <gtest/gtest.h>

#include <memory>
Expand All @@ -8,6 +6,7 @@

#include "pj_base/type_tree.hpp"
#include "pj_datastore/engine.hpp"
#include "pj_datastore/reader.hpp"
#include "pj_datastore/writer.hpp"

namespace PJ {
Expand All @@ -22,16 +21,14 @@ class SeriesReaderTest : public ::testing::Test {

DataWriter writer = engine_.createWriter();
auto schema_or = writer.registerSchema(
"row",
makeStruct(
"row",
{
makePrimitive("dense", PrimitiveType::kFloat64),
makePrimitive("sparse", PrimitiveType::kFloat64),
makePrimitive("text", PrimitiveType::kString),
makePrimitive("flag", PrimitiveType::kBool),
makePrimitive("all_null", PrimitiveType::kFloat64),
}));
"row", makeStruct(
"row", {
makePrimitive("dense", PrimitiveType::kFloat64),
makePrimitive("sparse", PrimitiveType::kFloat64),
makePrimitive("text", PrimitiveType::kString),
makePrimitive("flag", PrimitiveType::kBool),
makePrimitive("all_null", PrimitiveType::kFloat64),
}));
ASSERT_TRUE(schema_or.has_value()) << schema_or.error();

TopicDescriptor descriptor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ class WidgetDataView {
struct ChartSeriesView {
std::string label;
std::vector<std::pair<double, double>> points; // {x, y}
std::string color; // optional hex "#rrggbb"; empty means use chart theme default
std::string color; // optional hex "#rrggbb"; empty means use chart theme default
};

[[nodiscard]] std::optional<std::vector<ChartSeriesView>> chartSeries(std::string_view name) const {
Expand Down Expand Up @@ -269,7 +269,9 @@ class WidgetDataView {
/// Return all widget names that declare drop_target: true.
[[nodiscard]] std::vector<std::string> dropTargets() const {
std::vector<std::string> result;
if (!data_.is_object()) return result;
if (!data_.is_object()) {
return result;
}
for (const auto& [key, val] : data_.items()) {
if (val.is_object()) {
auto it = val.find("drop_target");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ class DialogPluginTyped : public DialogPluginBase {

/// ChartPreviewWidget: zoom or pan changed the visible range.
/// Only called when the plugin has declared setChartZoomEnabled for this widget.
virtual bool onChartViewChanged(std::string_view /*widget_name*/, double /*x_min*/, double /*x_max*/,
double /*y_min*/, double /*y_max*/) {
virtual bool onChartViewChanged(
std::string_view /*widget_name*/, double /*x_min*/, double /*x_max*/, double /*y_min*/, double /*y_max*/) {
return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
#pragma once

#include <nlohmann/json.hpp>

#include <string>
#include <string_view>
#include <vector>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ class WidgetEvent {
auto xmax = data_.find("chart_x_max");
auto ymin = data_.find("chart_y_min");
auto ymax = data_.find("chart_y_max");
if (xmin == data_.end() || !xmin->is_number() || xmax == data_.end() || !xmax->is_number() ||
ymin == data_.end() || !ymin->is_number() || ymax == data_.end() || !ymax->is_number()) {
if (xmin == data_.end() || !xmin->is_number() || xmax == data_.end() || !xmax->is_number() || ymin == data_.end() ||
!ymin->is_number() || ymax == data_.end() || !ymax->is_number()) {
return std::nullopt;
}
return ChartViewState{xmin->get<double>(), xmax->get<double>(), ymin->get<double>(), ymax->get<double>()};
Expand Down
3 changes: 1 addition & 2 deletions pj_plugins/examples/mock_json_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,4 @@ class MockJsonParser : public PJ::MessageParserPluginBase {
} // namespace

PJ_MESSAGE_PARSER_PLUGIN(
MockJsonParser,
R"({"id":"mock-json-parser","name":"Mock JSON Parser","version":"1.0.0","encoding":["json"]})")
MockJsonParser, R"({"id":"mock-json-parser","name":"Mock JSON Parser","version":"1.0.0","encoding":["json"]})")
12 changes: 9 additions & 3 deletions pj_plugins/include/pj_plugins/host/plugin_runtime_catalog.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,19 @@ class PluginRuntimeCatalog {
[[nodiscard]] bool reload();

// Returns loaded DataSource plugins.
[[nodiscard]] const std::vector<RuntimeDataSourcePlugin>& dataSources() const { return data_sources_; }
[[nodiscard]] const std::vector<RuntimeDataSourcePlugin>& dataSources() const {
return data_sources_;
}

// Returns loaded MessageParser plugins.
[[nodiscard]] const std::vector<RuntimeMessageParserPlugin>& messageParsers() const { return message_parsers_; }
[[nodiscard]] const std::vector<RuntimeMessageParserPlugin>& messageParsers() const {
return message_parsers_;
}

// Returns loaded Toolbox plugins.
[[nodiscard]] const std::vector<RuntimeToolboxPlugin>& toolboxes() const { return toolbox_plugins_; }
[[nodiscard]] const std::vector<RuntimeToolboxPlugin>& toolboxes() const {
return toolbox_plugins_;
}

// Returns file-import capable DataSource plugins.
[[nodiscard]] std::vector<RuntimeDataSourcePlugin*> fileImportSources();
Expand Down
13 changes: 4 additions & 9 deletions pj_plugins/src/plugin_runtime_catalog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,7 @@ std::vector<const PluginT*> constPtrs(const std::vector<PluginT>& plugins, uint6

PluginRuntimeCatalog::PluginRuntimeCatalog(
std::filesystem::path plugin_dir, DiagnosticSink sink, std::string diagnostic_source)
: plugin_dir_(std::move(plugin_dir)),
sink_(std::move(sink)),
diagnostic_source_(std::move(diagnostic_source)) {}
: plugin_dir_(std::move(plugin_dir)), sink_(std::move(sink)), diagnostic_source_(std::move(diagnostic_source)) {}

void PluginRuntimeCatalog::setPluginDir(std::filesystem::path plugin_dir) {
plugin_dir_ = std::move(plugin_dir);
Expand All @@ -85,8 +83,7 @@ void PluginRuntimeCatalog::scanDirectory() {
if (!loadAndRegister(descriptor)) {
report(
DiagnosticLevel::kError, descriptor.id,
descriptor.dso_path.string() + ": failed to load " + std::string(toString(descriptor.family)) +
" plugin");
descriptor.dso_path.string() + ": failed to load " + std::string(toString(descriptor.family)) + " plugin");
}
}
}
Expand Down Expand Up @@ -143,8 +140,7 @@ bool PluginRuntimeCatalog::reload() {
} else {
report(
DiagnosticLevel::kError, descriptor.id,
descriptor.dso_path.string() + ": failed to load " + std::string(toString(descriptor.family)) +
" plugin");
descriptor.dso_path.string() + ": failed to load " + std::string(toString(descriptor.family)) + " plugin");
}
}

Expand Down Expand Up @@ -357,8 +353,7 @@ std::string PluginRuntimeCatalog::buildFileFilter() const {
std::string all_exts;
std::string per_plugin;
for (const auto& source : data_sources_) {
if ((source.capabilities & PJ_DATA_SOURCE_CAPABILITY_FINITE_IMPORT) == 0 ||
source.file_extensions.empty()) {
if ((source.capabilities & PJ_DATA_SOURCE_CAPABILITY_FINITE_IMPORT) == 0 || source.file_extensions.empty()) {
continue;
}

Expand Down
51 changes: 51 additions & 0 deletions pj_scene_protocol/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# ---------------------------------------------------------------------------
# pj_scene_protocol — schema + canonical wire codec (writer + reader) for
# foxglove.ImageAnnotations and forthcoming scene primitive types. SDK
# boundary for plugin authors that produce or consume markers / scene data.
# Depends on pj_base only.
# ---------------------------------------------------------------------------

add_library(pj_scene_protocol STATIC
src/image_annotation_codec.cpp
src/scene_decoder.cpp
src/scene_decoder_protobuf.cpp
)
target_include_directories(pj_scene_protocol PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
target_compile_options(pj_scene_protocol PRIVATE ${PJ_WARNING_FLAGS})
target_link_libraries(pj_scene_protocol PUBLIC pj_base)
set_target_properties(pj_scene_protocol PROPERTIES
POSITION_INDEPENDENT_CODE ON
)

# ---------------------------------------------------------------------------
# Install (guarded by PJ_INSTALL_SDK in root CMakeLists.txt)
# ---------------------------------------------------------------------------

if(PJ_INSTALL_SDK)
install(TARGETS pj_scene_protocol EXPORT PlotJugglerSDKTargets
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
)
install(DIRECTORY include/ DESTINATION include)
endif()

# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------

if(PJ_BUILD_TESTS)
set(PJ_SCENE_PROTOCOL_TESTS
tests/image_annotation_codec_test.cpp
tests/scene_decoder_test.cpp
)

foreach(test_src ${PJ_SCENE_PROTOCOL_TESTS})
get_filename_component(test_name ${test_src} NAME_WE)
add_executable(${test_name} ${test_src})
target_link_libraries(${test_name} PRIVATE pj_scene_protocol GTest::gtest_main)
add_test(NAME ${test_name} COMMAND ${test_name})
endforeach()
endif()
Loading
Loading