Files
raphael 564bd47132 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>
2026-06-08 09:13:16 +02:00

129 lines
4.4 KiB
C++

#pragma once
#include <array>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <ctime>
#include <string>
namespace secsgem::gem {
enum class TimeAck : uint8_t {
Accept = 0,
Error = 1,
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. 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 {
using namespace std::chrono;
const auto now = system_clock::now() + seconds(offset_seconds_);
const auto t = system_clock::to_time_t(now);
const auto ms = duration_cast<milliseconds>(now.time_since_epoch()) % 1000;
std::tm tm{};
gmtime_r(&t, &tm);
std::array<char, 64> buf{};
std::snprintf(buf.data(), buf.size(), "%04d%02d%02d%02d%02d%02d%02d",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec,
static_cast<int>(ms.count() / 10));
return std::string(buf.data());
}
TimeAck set_time_string(const std::string& s) {
if (s.size() != 14 && s.size() != 16) return TimeAck::Error;
int y, mo, d, h, mi, se;
if (!parse_digits(s.data() + 0, 4, y) ||
!parse_digits(s.data() + 4, 2, mo) ||
!parse_digits(s.data() + 6, 2, d) ||
!parse_digits(s.data() + 8, 2, h) ||
!parse_digits(s.data() + 10, 2, mi) ||
!parse_digits(s.data() + 12, 2, se)) {
return TimeAck::Error;
}
std::tm tm{};
tm.tm_year = y - 1900;
tm.tm_mon = mo - 1;
tm.tm_mday = d;
tm.tm_hour = h;
tm.tm_min = mi;
tm.tm_sec = se;
const std::time_t target = timegm(&tm);
if (target == static_cast<std::time_t>(-1)) return TimeAck::Error;
// 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;
}
// 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;
for (std::size_t i = 0; i < n; ++i) {
if (p[i] < '0' || p[i] > '9') return false;
v = v * 10 + (p[i] - '0');
}
out = v;
return true;
}
std::int64_t offset_seconds_ = 0;
std::int64_t last_drift_seconds_ = 0;
std::uint64_t sync_count_ = 0;
};
} // namespace secsgem::gem