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
2 changes: 1 addition & 1 deletion pj_base/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
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`.

## Layout
- `include/pj_base/` — vocabulary primitives: `types.hpp`, `type_tree.hpp`, `dataset.hpp`, `expected.hpp`, `span.hpp`, `number_parse.hpp`, `assert.hpp`, `diagnostic_sink.hpp`, `buffer_anchor.hpp`.
- `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/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/`).
Expand Down
1 change: 1 addition & 0 deletions pj_base/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ if(PJ_BUILD_TESTS)
tests/video_frame_codec_test.cpp
tests/scene_entities_codec_test.cpp
tests/asset_video_codec_test.cpp
tests/time_spine_test.cpp
tests/poses_in_frame_codec_test.cpp
)

Expand Down
49 changes: 49 additions & 0 deletions pj_base/include/pj_base/time.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#pragma once
// Copyright 2026 Davide Faconti
// SPDX-License-Identifier: Apache-2.0

// The absolute time spine: a chrono vocabulary that sits one lossless step above
// the int64-ns PJ::Timestamp. Timestamp stays the storage/ABI/wire currency;
// these types are how layered code names absolute time without re-deriving the
// epoch or hand-rolling 1e9 conversions. Two concepts, kept un-mixable by the
// type system:
// * Timepoint — an absolute instant (sys_time<nanoseconds>, Unix epoch).
// * Duration — a span (nanoseconds); Timepoint + Timepoint won't compile.
//
// fromRaw()/toRaw() are the only sanctioned crossing between the int64 spine and
// the chrono world: lift a Timestamp into a Timepoint to compute with it, lower
// it back immediately before a storage/ABI/wire boundary. Display-relative time
// (the Qwt-axis / PlaybackEngine coordinate) is a separate, app-level vocabulary
// that builds on this one — it lives in pj_runtime, not here.

#include <chrono>

#include "pj_base/types.hpp" // PJ::Timestamp, PJ::Range

namespace PJ {

/// An ABSOLUTE wall-clock instant, nanosecond precision, Unix epoch. Lossless
/// mirror of PJ::Timestamp (C++20 guarantees system_clock's epoch == Unix epoch).
using Timepoint = std::chrono::sys_time<std::chrono::nanoseconds>;

/// A length of time (a span, not a point): retention windows, lifetimes, deltas.
using Duration = std::chrono::nanoseconds;

/// Lift an int64-ns PJ::Timestamp out of the spine into a Timepoint.
[[nodiscard]] constexpr Timepoint fromRaw(Timestamp ns) noexcept {
return Timepoint{Duration{ns}};
}

/// Lower a Timepoint back to the int64-ns spine, immediately before crossing a
/// storage/ABI boundary (DataWriter, the C-ABI trampolines, the codecs).
[[nodiscard]] constexpr Timestamp toRaw(Timepoint t) noexcept {
return t.time_since_epoch().count();
}

/// Lift an int64 interval into an absolute Timepoint interval (reuses PJ::Range,
/// never std::pair).
[[nodiscard]] constexpr Range<Timepoint> fromRawRange(const Range<Timestamp>& r) noexcept {
return {fromRaw(r.min), fromRaw(r.max)};
}

} // namespace PJ
42 changes: 42 additions & 0 deletions pj_base/tests/time_spine_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright 2026 Davide Faconti
// SPDX-License-Identifier: Apache-2.0

// Compile-fence + behavior tests for the absolute time spine (pj_base/time.hpp).
// The static_asserts are the real point: they prove the type system rejects the
// instant-vs-duration and raw-vs-Timepoint mistakes the vocabulary prevents.
// Display-relative time lives in pj_runtime and is tested there.

#include <gtest/gtest.h>

#include <type_traits>

#include "pj_base/time.hpp"

namespace {

template <class A, class B>
concept Addable = requires(A a, B b) { a + b; };

// An absolute instant plus another absolute instant is meaningless and must not
// compile — this is the instant-vs-duration guarantee std::chrono gives us.
static_assert(!Addable<PJ::Timepoint, PJ::Timepoint>, "Timepoint + Timepoint must be ill-formed");
// ...but instant - instant (a span) and instant + duration (a shifted instant) do.
static_assert(Addable<PJ::Timepoint, PJ::Duration>, "Timepoint + Duration must compile");

// A raw int64 timestamp must go through fromRaw(), never implicitly become a
// Timepoint.
static_assert(!std::is_convertible_v<PJ::Timestamp, PJ::Timepoint>);

TEST(TimeSpine, RawRoundTrip) {
const PJ::Timestamp ts = 1'717'500'000'123'456'789LL;
EXPECT_EQ(PJ::toRaw(PJ::fromRaw(ts)), ts);
}

TEST(TimeSpine, FromRawRangeLiftsBothEnds) {
const PJ::Range<PJ::Timestamp> raw{1'000'000'000LL, 5'000'000'000LL};
const PJ::Range<PJ::Timepoint> lifted = PJ::fromRawRange(raw);
EXPECT_EQ(PJ::toRaw(lifted.min), 1'000'000'000LL);
EXPECT_EQ(PJ::toRaw(lifted.max), 5'000'000'000LL);
}

} // namespace
Loading