Skip to content

Commit 8f485e5

Browse files
facontidavideclaudeCopilot
authored
feat(pj_base): add absolute time spine (Timepoint/Duration + fromRaw/toRaw) (#123)
Introduce pj_base/time.hpp: the absolute time vocabulary one lossless step above the int64-ns PJ::Timestamp — Timepoint (sys_time<ns>), Duration, and the fromRaw/toRaw/fromRawRange seams between the int64 spine and the chrono world. These types previously lived in the application (pj_runtime/Time.h), above pj_base, so lower layers could not name absolute time without depending on the app: pj_scene3d_core re-declared structurally-identical shadow typedefs and pj_datastore could not reach the seams at all. Hosting the spine in the Level-0 SDK lets every layer share one vocabulary. Display-relative time (the Qwt-axis / playback coordinate) is an app-presentation policy and stays in the application. Header content originates from the app's MPL-2.0 pj_runtime/Time.h and is relicensed Apache-2.0 here by the copyright holder to match the SDK. Header-only (constexpr/using), depends only on pj_base/types.hpp; additive with no ABI change (abidiff baseline unaffected). Covered by time_spine_test. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 62a3b12 commit 8f485e5

4 files changed

Lines changed: 93 additions & 1 deletion

File tree

pj_base/CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
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`.
44

55
## Layout
6-
- `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`.
6+
- `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`.
77
- `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.
88
- `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/`.
99
- `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/`).

pj_base/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ if(PJ_BUILD_TESTS)
102102
tests/video_frame_codec_test.cpp
103103
tests/scene_entities_codec_test.cpp
104104
tests/asset_video_codec_test.cpp
105+
tests/time_spine_test.cpp
105106
tests/poses_in_frame_codec_test.cpp
106107
)
107108

pj_base/include/pj_base/time.hpp

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#pragma once
2+
// Copyright 2026 Davide Faconti
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
// The absolute time spine: a chrono vocabulary that sits one lossless step above
6+
// the int64-ns PJ::Timestamp. Timestamp stays the storage/ABI/wire currency;
7+
// these types are how layered code names absolute time without re-deriving the
8+
// epoch or hand-rolling 1e9 conversions. Two concepts, kept un-mixable by the
9+
// type system:
10+
// * Timepoint — an absolute instant (sys_time<nanoseconds>, Unix epoch).
11+
// * Duration — a span (nanoseconds); Timepoint + Timepoint won't compile.
12+
//
13+
// fromRaw()/toRaw() are the only sanctioned crossing between the int64 spine and
14+
// the chrono world: lift a Timestamp into a Timepoint to compute with it, lower
15+
// it back immediately before a storage/ABI/wire boundary. Display-relative time
16+
// (the Qwt-axis / PlaybackEngine coordinate) is a separate, app-level vocabulary
17+
// that builds on this one — it lives in pj_runtime, not here.
18+
19+
#include <chrono>
20+
21+
#include "pj_base/types.hpp" // PJ::Timestamp, PJ::Range
22+
23+
namespace PJ {
24+
25+
/// An ABSOLUTE wall-clock instant, nanosecond precision, Unix epoch. Lossless
26+
/// mirror of PJ::Timestamp (C++20 guarantees system_clock's epoch == Unix epoch).
27+
using Timepoint = std::chrono::sys_time<std::chrono::nanoseconds>;
28+
29+
/// A length of time (a span, not a point): retention windows, lifetimes, deltas.
30+
using Duration = std::chrono::nanoseconds;
31+
32+
/// Lift an int64-ns PJ::Timestamp out of the spine into a Timepoint.
33+
[[nodiscard]] constexpr Timepoint fromRaw(Timestamp ns) noexcept {
34+
return Timepoint{Duration{ns}};
35+
}
36+
37+
/// Lower a Timepoint back to the int64-ns spine, immediately before crossing a
38+
/// storage/ABI boundary (DataWriter, the C-ABI trampolines, the codecs).
39+
[[nodiscard]] constexpr Timestamp toRaw(Timepoint t) noexcept {
40+
return t.time_since_epoch().count();
41+
}
42+
43+
/// Lift an int64 interval into an absolute Timepoint interval (reuses PJ::Range,
44+
/// never std::pair).
45+
[[nodiscard]] constexpr Range<Timepoint> fromRawRange(const Range<Timestamp>& r) noexcept {
46+
return {fromRaw(r.min), fromRaw(r.max)};
47+
}
48+
49+
} // namespace PJ

pj_base/tests/time_spine_test.cpp

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright 2026 Davide Faconti
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
// Compile-fence + behavior tests for the absolute time spine (pj_base/time.hpp).
5+
// The static_asserts are the real point: they prove the type system rejects the
6+
// instant-vs-duration and raw-vs-Timepoint mistakes the vocabulary prevents.
7+
// Display-relative time lives in pj_runtime and is tested there.
8+
9+
#include <gtest/gtest.h>
10+
11+
#include <type_traits>
12+
13+
#include "pj_base/time.hpp"
14+
15+
namespace {
16+
17+
template <class A, class B>
18+
concept Addable = requires(A a, B b) { a + b; };
19+
20+
// An absolute instant plus another absolute instant is meaningless and must not
21+
// compile — this is the instant-vs-duration guarantee std::chrono gives us.
22+
static_assert(!Addable<PJ::Timepoint, PJ::Timepoint>, "Timepoint + Timepoint must be ill-formed");
23+
// ...but instant - instant (a span) and instant + duration (a shifted instant) do.
24+
static_assert(Addable<PJ::Timepoint, PJ::Duration>, "Timepoint + Duration must compile");
25+
26+
// A raw int64 timestamp must go through fromRaw(), never implicitly become a
27+
// Timepoint.
28+
static_assert(!std::is_convertible_v<PJ::Timestamp, PJ::Timepoint>);
29+
30+
TEST(TimeSpine, RawRoundTrip) {
31+
const PJ::Timestamp ts = 1'717'500'000'123'456'789LL;
32+
EXPECT_EQ(PJ::toRaw(PJ::fromRaw(ts)), ts);
33+
}
34+
35+
TEST(TimeSpine, FromRawRangeLiftsBothEnds) {
36+
const PJ::Range<PJ::Timestamp> raw{1'000'000'000LL, 5'000'000'000LL};
37+
const PJ::Range<PJ::Timepoint> lifted = PJ::fromRawRange(raw);
38+
EXPECT_EQ(PJ::toRaw(lifted.min), 1'000'000'000LL);
39+
EXPECT_EQ(PJ::toRaw(lifted.max), 5'000'000'000LL);
40+
}
41+
42+
} // namespace

0 commit comments

Comments
 (0)