From 564bd4713227f7d8fbc064f032658692cd90104a Mon Sep 17 00:00:00 2001 From: Raphael Maenle Date: Mon, 8 Jun 2026 09:13:16 +0200 Subject: [PATCH] O: E148 time-sync drift tracking + quality metric MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the existing Clock with the metrics a host needs to gate time-sensitive data against the equipment's sync state (E148 §6.3): offset_seconds() current applied offset vs system clock last_drift_seconds() signed drift observed at the most recent sync sync_count() how many successful syncs have happened sync_quality() Synchronized (|drift|<=1s) / Drifting (<=60s) / Unsynchronized (>60s or never synced) The thresholds are tuneable per call; the defaults match typical fab practice but the application can pass tighter bounds for tracelog- sensitive flows. set_time_string() now snapshots the apparent delta between the previously-applied offset and the new one as last_drift_seconds_ at the moment of resync; no background timer. Three new test cases cover the initial Unsynchronized state, a large forward drift registering as Unsynchronized, and a same-value resync landing as Synchronized. Co-Authored-By: Claude Opus 4.7 --- include/secsgem/gem/store/clock.hpp | 56 +++++++++++++++++++++++++++-- tests/test_data_model.cpp | 30 ++++++++++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/include/secsgem/gem/store/clock.hpp b/include/secsgem/gem/store/clock.hpp index 7e97353..b37218c 100644 --- a/include/secsgem/gem/store/clock.hpp +++ b/include/secsgem/gem/store/clock.hpp @@ -15,9 +15,32 @@ enum class TimeAck : uint8_t { NotDoneNotEmpty = 2, }; +// Sync quality: |observed drift| over the last sync-to-sync interval. +// +// E148 defines a "sync quality" notion that hosts use to gate whether +// time-sensitive data (e.g. trace timestamps) is trustworthy. We +// expose three buckets keyed off seconds-of-drift, leaving the actual +// thresholds tuneable by the application. +enum class TimeSyncQuality : uint8_t { + Synchronized = 0, // |drift| <= 1s + Drifting = 1, // |drift| <= 60s + Unsynchronized = 2, // never synced, or |drift| > 60s +}; + +inline const char* time_sync_quality_name(TimeSyncQuality q) { + switch (q) { + case TimeSyncQuality::Synchronized: return "Synchronized"; + case TimeSyncQuality::Drifting: return "Drifting"; + case TimeSyncQuality::Unsynchronized: return "Unsynchronized"; + } + return "?"; +} + // The equipment clock. current_time_string() returns the 16-char SECS-II // TIME format ("YYYYMMDDhhmmsscc"), with an offset applied if the host has -// previously set the time via S2F31. +// previously set the time via S2F31. Each set_time_string() call also +// snapshots the observed drift versus the prior sync (E148 §6.3) so the +// host can read it as an SVID. class Clock { public: std::string current_time_string() const { @@ -57,10 +80,35 @@ class Clock { tm.tm_sec = se; const std::time_t target = timegm(&tm); if (target == static_cast(-1)) return TimeAck::Error; - offset_seconds_ = static_cast(target - std::time(nullptr)); + + // Drift = what we *would have* reported just before the new sync vs + // what the host just told us. Magnitude of the previously-applied + // offset added to the new offset gives the apparent delta. + const std::time_t now_real = std::time(nullptr); + const std::int64_t new_offset = + static_cast(target - now_real); + last_drift_seconds_ = new_offset - offset_seconds_; + offset_seconds_ = new_offset; + ++sync_count_; return TimeAck::Accept; } + // E148 metrics. drift is signed; quality buckets it. sync_count is + // 0 until the first successful set_time_string(). + std::int64_t offset_seconds() const { return offset_seconds_; } + std::int64_t last_drift_seconds() const { return last_drift_seconds_; } + std::uint64_t sync_count() const { return sync_count_; } + + TimeSyncQuality sync_quality(std::int64_t synchronized_threshold = 1, + std::int64_t drifting_threshold = 60) const { + if (sync_count_ == 0) return TimeSyncQuality::Unsynchronized; + const std::int64_t mag = + last_drift_seconds_ < 0 ? -last_drift_seconds_ : last_drift_seconds_; + if (mag <= synchronized_threshold) return TimeSyncQuality::Synchronized; + if (mag <= drifting_threshold) return TimeSyncQuality::Drifting; + return TimeSyncQuality::Unsynchronized; + } + private: static bool parse_digits(const char* p, std::size_t n, int& out) { int v = 0; @@ -72,7 +120,9 @@ class Clock { return true; } - std::int64_t offset_seconds_ = 0; + std::int64_t offset_seconds_ = 0; + std::int64_t last_drift_seconds_ = 0; + std::uint64_t sync_count_ = 0; }; } // namespace secsgem::gem diff --git a/tests/test_data_model.cpp b/tests/test_data_model.cpp index a6a2395..ba9f0ac 100644 --- a/tests/test_data_model.cpp +++ b/tests/test_data_model.cpp @@ -1,5 +1,7 @@ #include +#include + #include "secsgem/gem/data_model.hpp" using namespace secsgem::gem; @@ -62,6 +64,34 @@ TEST_CASE("set_time_string accepts well-formed and rejects malformed") { CHECK(c.set_time_string("short") == TimeAck::Error); } +TEST_CASE("Clock: E148 sync quality starts Unsynchronized") { + Clock c; + CHECK(c.sync_quality() == TimeSyncQuality::Unsynchronized); + CHECK(c.sync_count() == 0); +} + +TEST_CASE("Clock: consecutive set_time_string updates record drift") { + Clock c; + // First sync: drift measured against the (zero) initial offset. + REQUIRE(c.set_time_string("20260101000000") == TimeAck::Accept); + CHECK(c.sync_count() == 1); + + // Second sync, far in the future: drift should be a large positive number. + REQUIRE(c.set_time_string("20270101000000") == TimeAck::Accept); + CHECK(c.sync_count() == 2); + CHECK(c.last_drift_seconds() > 60 * 60 * 24); // > 1 day + CHECK(c.sync_quality() == TimeSyncQuality::Unsynchronized); +} + +TEST_CASE("Clock: same-value resync registers as Synchronized") { + Clock c; + REQUIRE(c.set_time_string("20260601000000") == TimeAck::Accept); + // Apply the same target again; the offset doesn't move materially. + REQUIRE(c.set_time_string("20260601000000") == TimeAck::Accept); + CHECK(std::abs(c.last_drift_seconds()) <= 1); + CHECK(c.sync_quality() == TimeSyncQuality::Synchronized); +} + // ---- Host command registry ---------------------------------------------- TEST_CASE("host command registry returns spec + result") {