STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
BoardClockSync.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cmath>
5
6namespace fsw {
7namespace time {
8
9namespace {
10constexpr int64_t NS_PER_MS = 1000000LL;
11constexpr double JITTER_EMA_ALPHA = 0.1;
12
13/** Modular uint32 forward step (wrap-safe): how far b advanced past a. */
14inline uint32_t forward_step_ms(uint32_t a, uint32_t b) {
15 return static_cast<uint32_t>(b - a);
16}
17
18/** Board timeline position in ns. All offset math is exact int64 — doubles
19 * cannot represent epoch-ns (53-bit mantissa) and the rounding error would
20 * leak into the future-clamp comparison. */
21inline int64_t board_ns(uint64_t unwrapped_ms) {
22 return static_cast<int64_t>(unwrapped_ms) * NS_PER_MS;
23}
24} // namespace
25
26void BoardClockSync::resync(BoardState& s, uint32_t newest_raw_ms, uint64_t arrival_ns,
27 bool count) {
28 // Re-anchor: the newest chunk is assumed sent "just now" — offset derived
29 // from this packet's arrival alone. Accuracy for THIS packet equals the
30 // legacy arrival-stamping; the window rebuilds from here.
31 s.last_raw_ms = newest_raw_ms;
32 s.last_unwrapped_ms = newest_raw_ms; // restart the 64-bit timeline
33 const int64_t residual = static_cast<int64_t>(arrival_ns) - board_ns(s.last_unwrapped_ms);
34 s.offset_ns = residual;
35 s.window.clear();
36 s.window.emplace_back(arrival_ns, residual);
37 s.locked = true;
38 if (count)
39 s.stats.resyncs++;
40}
41
42std::vector<uint64_t> BoardClockSync::stamp_packet(const std::string& board_key,
43 uint64_t arrival_ns,
44 const std::vector<uint32_t>& chunk_ms) {
45 std::vector<uint64_t> out(chunk_ms.size(), arrival_ns);
46 if (chunk_ms.empty())
47 return out;
48
49 BoardState& s = boards_[board_key];
50 s.stats.packets++;
51
53 // Revert switch: historical behavior, every chunk at arrival time.
54 s.stats.locked = false;
55 return out;
56 }
57
58 const uint64_t max_gap_ms = static_cast<uint64_t>(cfg_.max_plausible_gap_s) * 1000ULL;
59 const uint32_t newest_raw = chunk_ms.back();
60
61 // ── Unwrap the packet's newest chunk against the previous packet ─────────
62 bool reinitialized = false;
63 if (!s.locked) {
64 resync(s, newest_raw, arrival_ns, /*count=*/false); // first packet: not a resync
65 reinitialized = true;
66 } else {
67 const uint32_t step = forward_step_ms(s.last_raw_ms, newest_raw);
68 if (step <= max_gap_ms) {
69 // Continuous (a uint32 wrap lands here too — small modular step).
70 s.last_unwrapped_ms += step;
71 s.last_raw_ms = newest_raw;
72 } else {
73 // Implausible forward jump: reboot (millis reset) or a stall longer
74 // than max_plausible_gap. Either way the old anchor is useless.
75 resync(s, newest_raw, arrival_ns, /*count=*/true);
76 reinitialized = true;
77 }
78 }
79
80 if (!reinitialized) {
81 // ── Offset update: sliding-window minimum of residuals ───────────────
82 const int64_t residual = static_cast<int64_t>(arrival_ns) - board_ns(s.last_unwrapped_ms);
83
84 // Lost lock (clock discontinuity that kept a plausible step size)?
85 const int64_t resync_thr_ns = static_cast<int64_t>(cfg_.resync_threshold_ms) * NS_PER_MS;
86 if (residual - s.offset_ns > resync_thr_ns || s.offset_ns - residual > resync_thr_ns) {
87 resync(s, newest_raw, arrival_ns, /*count=*/true);
88 } else {
89 s.window.emplace_back(arrival_ns, residual);
90 const uint64_t window_ns =
91 static_cast<uint64_t>(cfg_.window_seconds) * 1000ULL * NS_PER_MS;
92 while (!s.window.empty() && s.window.front().first + window_ns < arrival_ns) {
93 s.window.pop_front();
94 }
95 int64_t mn = s.window.front().second;
96 for (const auto& e : s.window)
97 mn = std::min(mn, e.second);
98 // Includes the current packet's residual → offset ≤ residual, which
99 // guarantees the newest chunk never stamps past arrival.
100 s.offset_ns = mn;
101
102 // Jitter diagnostic: this packet's delivery excess above the floor.
103 const double excess_ms =
104 static_cast<double>(residual - s.offset_ns) / static_cast<double>(NS_PER_MS);
105 s.stats.jitter_ms += JITTER_EMA_ALPHA * (excess_ms - s.stats.jitter_ms);
106 }
107 }
108
109 // ── Stamp every chunk: unwrap within the packet, apply offset, clamp ─────
110 // Walk backward from the newest chunk (anchored at s.last_unwrapped_ms);
111 // modular steps make mid-packet wraps exact.
112 const size_t n = chunk_ms.size();
113 std::vector<uint64_t> unwrapped(n, 0);
114 std::vector<char> valid(n, 1);
115 unwrapped[n - 1] = s.last_unwrapped_ms;
116 for (size_t i = n - 1; i-- > 0;) {
117 const uint32_t step = forward_step_ms(chunk_ms[i], chunk_ms[i + 1]);
118 if (step <= max_gap_ms && unwrapped[i + 1] >= step) {
119 unwrapped[i] = unwrapped[i + 1] - step;
120 valid[i] = valid[i + 1];
121 } else {
122 // Garbage intra-packet spacing — safety-clamp this and older chunks.
123 valid[i] = 0;
124 }
125 }
126
127 const int64_t arrival = static_cast<int64_t>(arrival_ns);
128 const int64_t age_ns = static_cast<int64_t>(cfg_.max_batch_age_s) * 1000LL * NS_PER_MS;
129 const int64_t lower = arrival - age_ns;
130
131 for (size_t i = 0; i < n; i++) {
132 uint64_t t_ns;
133 const int64_t t = board_ns(unwrapped[i]) + s.offset_ns;
134 if (!valid[i] || t < lower || t > arrival) {
135 // Safety clamp: flat arrival time — exactly the pre-sync behavior.
136 t_ns = arrival_ns;
138 } else {
139 t_ns = static_cast<uint64_t>(t);
140 }
141 // Per-board monotonic guard: emitted chunk timestamps never decrease.
142 if (t_ns < s.last_emitted_ns)
143 t_ns = s.last_emitted_ns;
144 s.last_emitted_ns = t_ns;
145 out[i] = t_ns;
146 }
147
148 s.stats.offset_ms = static_cast<double>(s.offset_ns) / static_cast<double>(NS_PER_MS);
149 s.stats.locked = s.locked;
150 return out;
151}
152
153BoardClockSync::BoardStats BoardClockSync::stats(const std::string& board_key) const {
154 auto it = boards_.find(board_key);
155 return it != boards_.end() ? it->second.stats : BoardStats{};
156}
157
158std::vector<std::string> BoardClockSync::boards() const {
159 std::vector<std::string> keys;
160 keys.reserve(boards_.size());
161 for (const auto& [k, _] : boards_)
162 keys.push_back(k);
163 return keys;
164}
165
166} // namespace time
167} // namespace fsw
Per-board clock sync for DAQ sample timestamping.
std::vector< uint64_t > stamp_packet(const std::string &board_key, uint64_t arrival_ns, const std::vector< uint32_t > &chunk_ms)
Stamp one packet's chunks for one board.
void resync(BoardState &s, uint32_t newest_raw_ms, uint64_t arrival_ns, bool count)
std::vector< std::string > boards() const
All boards seen (for periodic diagnostics logging).
BoardStats stats(const std::string &board_key) const
Stats for one board (default-constructed if unknown).
std::map< std::string, BoardState > boards_
*brief create JSON pointer *sa reference_tokens const string_t const string_t & b
Definition json.hpp:13878
*brief create JSON pointer *sa reference_tokens const string_t & a
Definition json.hpp:13877
size_type count(const key_type &key) const
Definition json.hpp:19264
uint32_t forward_step_ms(uint32_t a, uint32_t b)
Modular uint32 forward step (wrap-safe): how far b advanced past a.
int64_t board_ns(uint64_t unwrapped_ms)
Board timeline position in ns.
Motorized ball valve position: raw ADC counts -> % open.
*brief user defined string literal for JSON values *sa std::size_t n
Definition json.hpp:24474
std::deque< std::pair< uint64_t, int64_t > > window