diff --git a/pj_base/CLAUDE.md b/pj_base/CLAUDE.md index 1b25bd7..c9ece42 100644 --- a/pj_base/CLAUDE.md +++ b/pj_base/CLAUDE.md @@ -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`, `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/`). diff --git a/pj_base/CMakeLists.txt b/pj_base/CMakeLists.txt index 9c94b72..d733fbe 100644 --- a/pj_base/CMakeLists.txt +++ b/pj_base/CMakeLists.txt @@ -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 ) diff --git a/pj_base/include/pj_base/time.hpp b/pj_base/include/pj_base/time.hpp new file mode 100644 index 0000000..99a01b8 --- /dev/null +++ b/pj_base/include/pj_base/time.hpp @@ -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, 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 + +#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; + +/// 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 fromRawRange(const Range& r) noexcept { + return {fromRaw(r.min), fromRaw(r.max)}; +} + +} // namespace PJ diff --git a/pj_base/tests/time_spine_test.cpp b/pj_base/tests/time_spine_test.cpp new file mode 100644 index 0000000..b6edf09 --- /dev/null +++ b/pj_base/tests/time_spine_test.cpp @@ -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 + +#include + +#include "pj_base/time.hpp" + +namespace { + +template +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, "Timepoint + Timepoint must be ill-formed"); +// ...but instant - instant (a span) and instant + duration (a shifted instant) do. +static_assert(Addable, "Timepoint + Duration must compile"); + +// A raw int64 timestamp must go through fromRaw(), never implicitly become a +// Timepoint. +static_assert(!std::is_convertible_v); + +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 raw{1'000'000'000LL, 5'000'000'000LL}; + const PJ::Range 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