#pragma once #include #include #include #include #include #include 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(now.time_since_epoch()) % 1000; std::tm tm{}; gmtime_r(&t, &tm); std::array 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(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(-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(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