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
17 changes: 10 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ two modules below have no own CLAUDE.md.
### Modules

- **pj_base** — vocabulary types (`Timestamp`, `DatasetId`, `Expected<T>`, `Span<T>`, type trees),
the canonical builtin object vocabulary (`pj_base/builtin/`: 16 struct headers — Image, DepthImage,
the canonical builtin object vocabulary (`pj_base/builtin/`: 17 struct headers — Image, DepthImage,
PointCloud, CompressedPointCloud, OccupancyGrid(+Update), Mesh3D, VideoFrame, AssetVideo,
SceneEntities, RobotDescription, CameraInfo, Log, ImageAnnotations, FrameTransforms, PosesInFrame) and their 15
SceneEntities, RobotDescription, CameraInfo, Log, ImageAnnotations, FrameTransforms, PosesInFrame, VoxelGrid) and their 16
wire codecs (RobotDescription carries source text as-is — no codec), the C-ABI protocol headers for
DataSource/MessageParser/Toolbox + the C++ SDK base classes / host-view helpers built on them.
- **pj_plugins** — host-side loaders + RAII handles + plugin discovery/catalog for four plugin
Expand Down Expand Up @@ -102,11 +102,14 @@ changes within `0.x`** — the next ABI/API break ships as `1.0.0`. So a plugin
`[>=0.Y.Z <1.0.0]`. (Deliberately stricter than the usual "0.x may break" convention, because
plugins pin against this SDK.)

**Mechanics.** The version lives in two places that must stay in sync — `version` in `conanfile.py`
and `PJ_PACKAGE_VERSION` in the root `CMakeLists.txt` (also update the example tag in the
`conanfile.py` docstring). A non-MAJOR PR must not alter `abi/baseline.abi` beyond additions (verify
with `abidiff`). Tagging and pushing a release is a separate, explicitly-authorized step — never tag
or push a release without the user's go-ahead.
**Mechanics.** The version lives in **three** places that must stay in sync — `version` in
`conanfile.py`, `PJ_PACKAGE_VERSION` in the root `CMakeLists.txt`, and `context.version` in
`recipe.yaml` (the conda/pixi package version `rattler-build` embeds; `pixi.toml` itself carries
**no** version) — plus the example tag in the `conanfile.py` docstring. The `conda-release.yml`
release job hard-fails ("Verify tag matches all version sources") if the `v*` tag and any of the
three disagree, so a bump that misses `recipe.yaml` cannot be released. A non-MAJOR PR must not
alter `abi/baseline.abi` beyond additions (verify with `abidiff`). Tagging and pushing a release is
a separate, explicitly-authorized step — never tag or push a release without the user's go-ahead.

## Coding Conventions

Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ endif()
if(PJ_INSTALL_SDK)
include(CMakePackageConfigHelpers)

set(PJ_PACKAGE_VERSION "0.9.0")
set(PJ_PACKAGE_VERSION "0.10.0")
set(PJ_PACKAGE_CMAKE_DIR ${CMAKE_INSTALL_LIBDIR}/cmake/plotjuggler_sdk)

install(EXPORT plotjuggler_sdkTargets
Expand Down
4 changes: 2 additions & 2 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.9.0` and then:
A consuming Conan recipe declares e.g. `plotjuggler_sdk/0.10.0` and then:

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

class PlotjugglerSdkConan(ConanFile):
name = "plotjuggler_sdk"
version = "0.9.0"
version = "0.10.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
38 changes: 37 additions & 1 deletion docs/builtin_type.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ Builtin objects fall into two serialization families:

| Family | Current types | Storage model | Codec policy |
|--------|---------------|---------------|--------------|
| Byte-backed views | `Image`, `DepthImage`, `PointCloud`, `CompressedPointCloud`, `OccupancyGrid`, `OccupancyGridUpdate`, `Mesh3D`, `VideoFrame` | Header fields live in the SDK struct; payload bytes live behind `Span<const uint8_t>` plus `BufferAnchor`. | No mandatory canonical codec; preserve zero-copy views over ROS, MCAP, compressed image, point-cloud, or plugin-owned payloads. If conversion is unavoidable, allocate a new payload and anchor it. |
| Byte-backed views | `Image`, `DepthImage`, `PointCloud`, `CompressedPointCloud`, `OccupancyGrid`, `OccupancyGridUpdate`, `VoxelGrid`, `Mesh3D`, `VideoFrame` | Header fields live in the SDK struct; payload bytes live behind `Span<const uint8_t>` plus `BufferAnchor`. | No mandatory canonical codec; preserve zero-copy views over ROS, MCAP, compressed image, point-cloud, or plugin-owned payloads. If conversion is unavoidable, allocate a new payload and anchor it. |
| Owned values | `ImageAnnotations`, `FrameTransforms`, `SceneEntities`, `AssetVideo`, `RobotDescription`, `CameraInfo`, `PosesInFrame`; future marker types | SDK structs own their vectors/strings/scalars directly. | Add explicit codecs when canonical bytes are needed. Codecs serialize the owned value to the protobuf-wire payload described by the `.proto` contract, using shared private wire primitives. `RobotDescription` carries source-format text as-is (no canonical codec) — the format hint distinguishes URDF / SDF / MJCF. |

Canonical `.proto` files live under `pj_base/proto/pj` and act as the wire
Expand Down Expand Up @@ -126,6 +126,7 @@ annotations, frame transforms, or no builtin object.
| `kOccupancyGridUpdate` | `PJ::sdk::OccupancyGridUpdate` | Incremental sub-rectangle patch for a previously-published `OccupancyGrid`. |
| `kLog` | `PJ::sdk::Log` | Textual log message (severity level + text + originating name). |
| `kPosesInFrame` | `PJ::sdk::PosesInFrame` | Array of poses in one frame (PoseArray / particle clouds); styling is viewer-side. |
| `kVoxelGrid` | `PJ::sdk::VoxelGrid` | Dense 3D voxel grid (occupancy/cost/ESDF/semantic); the volumetric sibling of `OccupancyGrid`. |

`BuiltinObject` is `std::any`. Producers store a concrete builtin value in it;
consumers recover the concrete type with `std::any_cast<T>(&object)` or ask
Expand Down Expand Up @@ -593,6 +594,40 @@ so no `BufferAnchor` is needed.
`pj_base/builtin/poses_in_frame_codec.hpp` serializes and deserializes this
type using the canonical `PJ.PosesInFrame` protobuf wire format.

## VoxelGrid

`VoxelGrid` is a dense 3D voxel grid placed in world coordinates — the
volumetric sibling of `OccupancyGrid` — for 3D occupancy maps, costmaps,
ESDFs, and semantic grids (e.g. `foxglove.VoxelGrid`, `costmap_2d/VoxelGrid`).

It is a byte-backed view: the lattice of `column_count * row_count *
slice_count` voxels lives in `data` (a `Span<const uint8_t>` + `BufferAnchor`)
in depth-major, row-major **Z-Y-X** order (x fastest), and the per-voxel layout
is described by `fields` — the same `PointField` channel model used by
`PointCloud`. The byte layout mirrors `foxglove.VoxelGrid` (three strides +
Z-Y-X order) so a parser can hand out `data` zero-copy rather than transcoding
millions of cells; expanding occupied voxels into renderable primitives is a
viewer-side concern.

Unlike the 2D `OccupancyGrid` (which fixes `-1`/`0..100` cell semantics), the
per-voxel **value is generic**: occupancy byte, RGBA, a float cost/intensity, a
class id, etc. The draw predicate (which voxels are visible) and any colormap are
viewer-side, so one type serves occupancy/cost/ESDF/semantic grids.

| Field | Type | Notes |
|-------|------|-------|
| `timestamp_ns` | `Timestamp` | Time of the grid. `0` when the source had none. |
| `frame_id` | `std::string` | Source coordinate frame; 3D consumers TF-transform from it. |
| `origin` | `Pose` | Lower-front-left `(0,0,0)` corner of the grid in `frame_id`. |
| `cell_size` | `Vector3` | Metric voxel size along x/y/z (meters); need not be cubic. |
| `column_count` / `row_count` / `slice_count` | `uint32_t` | Voxels along x / y / z. |
| `cell_stride` / `row_stride` / `slice_stride` | `uint32_t` | Byte spacing of a voxel / row / z-slice (padding allowed). |
| `fields` | `std::vector<PointField>` | Per-voxel channel layout. |
| `data` | `Span<const uint8_t>` + `BufferAnchor` | Packed voxel bytes in Z-Y-X order. |

`pj_base/builtin/voxel_grid_codec.hpp` serializes and deserializes this type
using the canonical `PJ.VoxelGrid` protobuf wire format.

## Conversion Examples

| Source type | Canonical builtin type | Conversion intent |
Expand All @@ -612,6 +647,7 @@ type using the canonical `PJ.PosesInFrame` protobuf wire format.
| ROS `std_msgs/String` on `/robot_description` (or matching name) carrying URDF XML | `RobotDescription` | Validate root element matches `format`, then carry the raw text + format hint. No mesh resolution at parse time. |
| ROS `sensor_msgs/CameraInfo` | `CameraInfo` | Map K / D / R / P plus dimensions; correlate to the image topic by name. Sub-window (binning / ROI) is dropped. |
| ROS `map_msgs/OccupancyGridUpdate` | `OccupancyGridUpdate` | Forward the cell-space patch (`x`/`y`/`width`/`height` + bytes); the consumer pairs it with the base grid and supplies origin/resolution. |
| `foxglove.VoxelGrid` / `costmap_2d/VoxelGrid` | `VoxelGrid` | Map counts/strides/`cell_size`/`origin` into the struct; keep voxel bytes zero-copy in Z-Y-X order. The draw predicate is viewer-side. |

The builtin type is the boundary object. After conversion, consumers should not
need to know which third-party schema produced it.
4 changes: 2 additions & 2 deletions pj_base/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# pj_base — SDK vocabulary, builtin object schemas, and the C plugin ABI

pj_base is the **Level 0** foundation and the **SDK boundary** for plugin authors. It owns: the zero-dependency vocabulary types (`Timestamp`, `DatasetId`, `Range`, `Expected<T>`, `Span`, `TypeTree`); the canonical *builtin object* schemas (`sdk::Image`, `PointCloud`, `DepthImage`, `OccupancyGrid`, `FrameTransforms`, … — 16 types) **and 15 of their wire codecs** (RobotDescription has none); and the **C ABI** primitives every plugin family speaks (`plugin_data_api.h` + the service registry) plus the C-ABI protocol headers for **three** families — `data_source_protocol.h`, `message_parser_protocol.h`, `toolbox_protocol.h`. The **Dialog** protocol header is the exception: it lives in `pj_plugins/dialog_protocol/`, not here. It also ships the C++ SDK base classes for DataSource and Toolbox; the MessageParser and Dialog base classes live in `pj_plugins`. Builds as a STATIC lib with **zero public deps** — `fast_float` is a `BUILD_INTERFACE` private impl detail of `parseNumber`. Must NOT depend on `pj_datastore`, `pj_plugins`, Qt, or any Conan runtime lib. This is a read-only submodule subtree: change it only when explicitly working in `plotjuggler_sdk`.
pj_base is the **Level 0** foundation and the **SDK boundary** for plugin authors. It owns: the zero-dependency vocabulary types (`Timestamp`, `DatasetId`, `Range`, `Expected<T>`, `Span`, `TypeTree`); the canonical *builtin object* schemas (`sdk::Image`, `PointCloud`, `DepthImage`, `OccupancyGrid`, `VoxelGrid`, `FrameTransforms`, … — 17 types) **and 16 of their wire codecs** (RobotDescription has none); and the **C ABI** primitives every plugin family speaks (`plugin_data_api.h` + the service registry) plus the C-ABI protocol headers for **three** families — `data_source_protocol.h`, `message_parser_protocol.h`, `toolbox_protocol.h`. The **Dialog** protocol header is the exception: it lives in `pj_plugins/dialog_protocol/`, not here. It also ships the C++ SDK base classes for DataSource and Toolbox; the MessageParser and Dialog base classes live in `pj_plugins`. Builds as a STATIC lib with **zero public deps** — `fast_float` is a `BUILD_INTERFACE` private impl detail of `parseNumber`. Must NOT depend on `pj_datastore`, `pj_plugins`, Qt, or any Conan runtime lib. This is a read-only submodule subtree: change it only when explicitly working in `plotjuggler_sdk`.

## Layout
- `include/pj_base/` — vocabulary primitives: `types.hpp`, `time.hpp` (absolute time spine: `Timepoint`/`Duration` + `fromRaw`/`toRaw`), `type_tree.hpp`, `dataset.hpp`, `expected.hpp`, `span.hpp`, `number_parse.hpp`, `assert.hpp`, `diagnostic_sink.hpp`, `buffer_anchor.hpp`.
- `include/pj_base/builtin/` — the 16 builtin object struct headers (`*.hpp`; 17 enum values in `BuiltinObjectType`, value 2 reserved) + their 15 wire codecs (`*_codec.hpp`; RobotDescription has none) + the `BuiltinObject` (`std::any`) type-erased holder.
- `include/pj_base/builtin/` — the 17 builtin object struct headers (`*.hpp`; 18 enum values in `BuiltinObjectType`, value 2 reserved) + their 16 wire codecs (`*_codec.hpp`; RobotDescription has none) + the `BuiltinObject` (`std::any`) type-erased holder.
- `include/pj_base/sdk/` — C++ SDK over the ABI: DataSource + Toolbox `*_plugin_base.hpp`, `service_registry.hpp`/`service_traits.hpp`, host views, Arrow RAII holders, `testing/`.
- `include/pj_base/*_protocol.h`, `plugin_data_api.h`, `builtin_object_abi.h`, `plugin_abi_export.hpp` — the stable C-ABI surface for DataSource/MessageParser/Toolbox (the Dialog protocol header lives in `pj_plugins/dialog_protocol/`).
- `proto/pj/` — canonical `.proto` wire contracts for the builtin types (see its README).
Expand Down
2 changes: 2 additions & 0 deletions pj_base/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ add_library(pj_base STATIC
src/builtin/poses_in_frame_codec.cpp
src/builtin/scene_entities_codec.cpp
src/builtin/video_frame_codec.cpp
src/builtin/voxel_grid_codec.cpp
src/number_parse.cpp
src/type_tree.cpp
)
Expand Down Expand Up @@ -105,6 +106,7 @@ if(PJ_BUILD_TESTS)
tests/asset_video_codec_test.cpp
tests/time_spine_test.cpp
tests/poses_in_frame_codec_test.cpp
tests/voxel_grid_codec_test.cpp
)

foreach(test_src ${PJ_BASE_TESTS})
Expand Down
10 changes: 10 additions & 0 deletions pj_base/include/pj_base/builtin/builtin_object.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
#include "pj_base/builtin/robot_description.hpp"
#include "pj_base/builtin/scene_entities.hpp"
#include "pj_base/builtin/video_frame.hpp"
#include "pj_base/builtin/voxel_grid.hpp"

namespace PJ {
namespace sdk {
Expand All @@ -64,6 +65,7 @@ enum class BuiltinObjectType : uint16_t {
kOccupancyGridUpdate = 15, ///< sdk::OccupancyGridUpdate — incremental sub-rectangle patch for an OccupancyGrid.
kLog = 16, ///< sdk::Log — textual log message (level + text + name).
kPosesInFrame = 17, ///< sdk::PosesInFrame — array of poses in one reference frame.
kVoxelGrid = 18, ///< sdk::VoxelGrid — dense 3D voxel grid (occupancy/cost/ESDF/semantic).
};

/// A-priori classification of a schema. Currently carries only the type;
Expand Down Expand Up @@ -110,6 +112,8 @@ struct SchemaClassification {
return "kLog";
case BuiltinObjectType::kPosesInFrame:
return "kPosesInFrame";
case BuiltinObjectType::kVoxelGrid:
return "kVoxelGrid";
}
return "kNone";
}
Expand Down Expand Up @@ -168,6 +172,9 @@ struct SchemaClassification {
if (s == "kPosesInFrame") {
return BuiltinObjectType::kPosesInFrame;
}
if (s == "kVoxelGrid") {
return BuiltinObjectType::kVoxelGrid;
}
return std::nullopt;
}

Expand Down Expand Up @@ -229,6 +236,9 @@ using BuiltinObject = std::any;
if (t == typeid(PosesInFrame)) {
return BuiltinObjectType::kPosesInFrame;
}
if (t == typeid(VoxelGrid)) {
return BuiltinObjectType::kVoxelGrid;
}
return BuiltinObjectType::kNone;
}

Expand Down
70 changes: 70 additions & 0 deletions pj_base/include/pj_base/builtin/voxel_grid.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* @file voxel_grid.hpp
* @brief Dense 3D voxel grid placed in world coordinates.
*/
// Copyright 2026 Davide Faconti
// SPDX-License-Identifier: Apache-2.0

#pragma once

#include <cstdint>
#include <string>
#include <vector>

#include "pj_base/buffer_anchor.hpp"
#include "pj_base/builtin/frame_transforms.hpp" // for Pose, Vector3
#include "pj_base/builtin/point_cloud.hpp" // for PointField (shared channel descriptor)
#include "pj_base/span.hpp"
#include "pj_base/types.hpp"

namespace PJ {
namespace sdk {

/// Dense 3D voxel grid placed in world coordinates — the volumetric sibling of
/// OccupancyGrid, reusing PointCloud's per-channel field model (`PointField`).
///
/// The grid is a regular lattice of `column_count * row_count * slice_count`
/// voxels. Each voxel occupies `cell_stride` bytes laid out per `fields`; cells
/// are stored densely in depth-major, row-major **Z-Y-X** order (x varies
/// fastest), matching the foxglove.VoxelGrid wire layout so a parser can expose
/// `data` as a zero-copy view rather than transcoding millions of cells.
///
/// Strides (in bytes) describe the packing and allow trailing padding:
/// cell_stride : one voxel (>= sum of field element sizes)
/// row_stride : one row of x (>= column_count * cell_stride)
/// slice_stride : one z-slice (>= row_count * row_stride)
/// so the byte offset of voxel (cx, ry, sz) is
/// sz*slice_stride + ry*row_stride + cx*cell_stride
/// and `data.size()` must be at least `slice_count * slice_stride`.
///
/// Voxel (column cx, row ry, slice sz) has its **center** at, in `frame_id`:
/// origin ∘ ((cx + .5)*cell_size.x, (ry + .5)*cell_size.y, (sz + .5)*cell_size.z)
/// where `origin` is the grid's lower-front-left corner (the (0,0,0) corner).
///
/// Unlike the 2D OccupancyGrid (which fixes -1/0..100 occupancy semantics), the
/// per-voxel value is **generic**: `fields` declares the layout (occupancy byte,
/// RGBA, a float cost/intensity, class id, …) and the renderer decides which
/// voxels are drawn (the draw predicate / colormap is viewer-side), so one type
/// serves occupancy maps, costmaps, ESDFs, and semantic grids.
///
/// `anchor` keeps the underlying buffer alive — the producer may have made
/// `data` a view into the source payload (zero-copy) or into a freshly
/// allocated vector; consumers don't need to know which.
struct VoxelGrid {
Timestamp timestamp_ns = 0;
std::string frame_id; ///< Source coordinate frame; required for 3D TF resolution.
Pose origin; ///< Lower-front-left (0,0,0) corner of the grid in `frame_id`.
Vector3 cell_size; ///< Metric voxel size along x, y, z (meters); need not be cubic.
uint32_t column_count = 0; ///< Voxels along x (fastest-varying).
uint32_t row_count = 0; ///< Voxels along y.
uint32_t slice_count = 0; ///< Voxels along z (depth).
uint32_t cell_stride = 0; ///< Bytes per voxel.
uint32_t row_stride = 0; ///< Bytes per row of x.
uint32_t slice_stride = 0; ///< Bytes per z-slice.
std::vector<PointField> fields;
Span<const uint8_t> data;
BufferAnchor anchor;
};

} // namespace sdk
} // namespace PJ
25 changes: 25 additions & 0 deletions pj_base/include/pj_base/builtin/voxel_grid_codec.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#pragma once
// Copyright 2026 Davide Faconti
// SPDX-License-Identifier: Apache-2.0

#include <cstddef>
#include <cstdint>
#include <string_view>
#include <vector>

#include "pj_base/builtin/voxel_grid.hpp"
#include "pj_base/expected.hpp"

namespace PJ {

inline constexpr std::string_view kSchemaVoxelGrid = "PJ.VoxelGrid";

/// Serializes sdk::VoxelGrid to canonical PJ.VoxelGrid wire bytes
/// (see pj_base/proto/pj/VoxelGrid.proto).
[[nodiscard]] std::vector<uint8_t> serializeVoxelGrid(const sdk::VoxelGrid& grid);

/// Decodes canonical PJ.VoxelGrid wire bytes. The returned object owns its
/// voxel bytes via `anchor`.
[[nodiscard]] Expected<sdk::VoxelGrid> deserializeVoxelGrid(const uint8_t* data, size_t size);

} // namespace PJ
1 change: 1 addition & 0 deletions pj_base/include/pj_base/builtin_object_abi.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ typedef enum PJ_builtin_object_type_t {
PJ_BUILTIN_OBJECT_TYPE_OCCUPANCY_GRID_UPDATE = 15,
PJ_BUILTIN_OBJECT_TYPE_LOG = 16,
PJ_BUILTIN_OBJECT_TYPE_POSES_IN_FRAME = 17,
PJ_BUILTIN_OBJECT_TYPE_VOXEL_GRID = 18,
/* Reserve future types; appended at the tail. Numeric values are stable
* across releases — never renumber. Each new value here must match the
* matching kFoo entry in BuiltinObjectType (builtin_object.hpp). */
Expand Down
2 changes: 2 additions & 0 deletions pj_base/proto/pj/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ rationale.
- `Mesh3D`
- **`PosesInFrame.proto`** — array of poses in a single reference frame at one instant (`geometry_msgs/PoseArray`, particle clouds); mirrors `foxglove.PosesInFrame` field-for-field and carries no styling — rendering is viewer-side.
- `PosesInFrame`
- **`VoxelGrid.proto`** — dense 3D voxel grid (the volumetric sibling of `OccupancyGrid`); reuses `PointField` for the per-voxel channel layout and mirrors `foxglove.VoxelGrid`'s Z-Y-X byte layout so `data` stays a zero-copy view. Serves occupancy/cost/ESDF/semantic grids; the draw predicate is viewer-side.
- `VoxelGrid`

### 2D image annotations (vector overlays)

Expand Down
Loading
Loading