O: E148 time-sync drift tracking + quality metric
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 <noreply@anthropic.com>
This commit is contained in:
@@ -15,9 +15,32 @@ enum class TimeAck : uint8_t {
|
|||||||
NotDoneNotEmpty = 2,
|
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
|
// The equipment clock. current_time_string() returns the 16-char SECS-II
|
||||||
// TIME format ("YYYYMMDDhhmmsscc"), with an offset applied if the host has
|
// 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 {
|
class Clock {
|
||||||
public:
|
public:
|
||||||
std::string current_time_string() const {
|
std::string current_time_string() const {
|
||||||
@@ -57,10 +80,35 @@ class Clock {
|
|||||||
tm.tm_sec = se;
|
tm.tm_sec = se;
|
||||||
const std::time_t target = timegm(&tm);
|
const std::time_t target = timegm(&tm);
|
||||||
if (target == static_cast<std::time_t>(-1)) return TimeAck::Error;
|
if (target == static_cast<std::time_t>(-1)) return TimeAck::Error;
|
||||||
offset_seconds_ = static_cast<std::int64_t>(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<std::int64_t>(target - now_real);
|
||||||
|
last_drift_seconds_ = new_offset - offset_seconds_;
|
||||||
|
offset_seconds_ = new_offset;
|
||||||
|
++sync_count_;
|
||||||
return TimeAck::Accept;
|
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:
|
private:
|
||||||
static bool parse_digits(const char* p, std::size_t n, int& out) {
|
static bool parse_digits(const char* p, std::size_t n, int& out) {
|
||||||
int v = 0;
|
int v = 0;
|
||||||
@@ -73,6 +121,8 @@ class Clock {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
} // namespace secsgem::gem
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include <doctest/doctest.h>
|
#include <doctest/doctest.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
#include "secsgem/gem/data_model.hpp"
|
#include "secsgem/gem/data_model.hpp"
|
||||||
|
|
||||||
using namespace secsgem::gem;
|
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);
|
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 ----------------------------------------------
|
// ---- Host command registry ----------------------------------------------
|
||||||
|
|
||||||
TEST_CASE("host command registry returns spec + result") {
|
TEST_CASE("host command registry returns spec + result") {
|
||||||
|
|||||||
Reference in New Issue
Block a user