STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
BoardClockSync.hpp
Go to the documentation of this file.
1#ifndef FSW_BOARD_CLOCK_SYNC_HPP
2#define FSW_BOARD_CLOCK_SYNC_HPP
3
4/**
5 * @file BoardClockSync.hpp
6 * @brief Per-board clock sync for DAQ sample timestamping.
7 *
8 * Boards stamp each sensor chunk with uint32 millis() from a cheap local
9 * crystal: accurate RELATIVE spacing, meaningless absolute value, wraps at
10 * 2^32 ms (~49.7 days; faster on other firmware). The bridge historically
11 * stamped every sample in a UDP packet with one receive time, flattening a
12 * multi-chunk batch onto one instant and leaking network jitter into the
13 * recorded timeline.
14 *
15 * This module reconstructs true sample times server-side, one instance per
16 * source board:
17 * - UNWRAP: uint32 board ms extended to 64-bit via modular forward deltas.
18 * A forward step beyond max_plausible_gap is a reboot (or long stall) →
19 * resync. A wrap is just a small modular step → seamless, no resync.
20 * - OFFSET: sliding-window MINIMUM of residual = arrival − board_time.
21 * Network delay is one-sided noise (a packet can't arrive before it was
22 * sent), so residuals scatter ABOVE the true offset and the window floor
23 * is the best estimate; congestion cannot drag it. The window bounds
24 * drift staleness to ~window_seconds × crystal-ppm (≈0.5 ms at defaults).
25 * - SPREADING: each chunk stamped at board_time + offset, so batched scans
26 * keep their true spacing instead of collapsing onto arrival time.
27 * - INVARIANTS: because the current packet's residual participates in the
28 * window min, offset ≤ residual and the newest chunk never stamps in the
29 * future. Every stamp is additionally clamped to
30 * [arrival − max_batch_age, arrival] (violation → flat arrival fallback +
31 * counter: worst case equals the old behavior), and per-board emitted
32 * chunk timestamps are forced non-decreasing.
33 *
34 * Mode "arrival" reproduces the historical behavior exactly (revert switch,
35 * config.toml [time_sync]).
36 *
37 * Pure and deterministic: no sockets, no real clock — arrival times are
38 * inputs, so unit tests drive every edge case (see lib/test/
39 * test_board_clock_sync.cpp).
40 */
41
42#include <cstdint>
43#include <deque>
44#include <map>
45#include <string>
46#include <vector>
47
48namespace fsw {
49namespace time {
50
52 enum class Mode { BoardClock, Arrival };
54 uint32_t window_seconds = 10; // residual-min sliding window
55 uint32_t max_plausible_gap_s = 60; // modular forward step beyond this = reboot
56 uint32_t max_batch_age_s = 5; // stamps confined to [arrival - this, arrival]
57 uint32_t resync_threshold_ms = 1000; // |residual - offset| beyond this = lost lock
58 uint32_t log_interval_s = 10; // diagnostics cadence (used by the bridge)
59};
60
62public:
63 explicit BoardClockSync(const TimeSyncConfig& cfg) : cfg_(cfg) {
64 }
65
66 /**
67 * Stamp one packet's chunks for one board.
68 *
69 * @param board_key stable identity of the source board (source IP)
70 * @param arrival_ns server receive time of the packet (epoch ns)
71 * @param chunk_ms the packet's chunk timestamps in send order (board
72 * uint32 millis; may wrap mid-packet)
73 * @return one epoch-ns timestamp per chunk, same order as chunk_ms
74 */
75 std::vector<uint64_t> stamp_packet(const std::string& board_key, uint64_t arrival_ns,
76 const std::vector<uint32_t>& chunk_ms);
77
78 struct BoardStats {
79 double offset_ms = 0.0; // current arrival-vs-board offset estimate
80 double jitter_ms = 0.0; // EMA of residual excess over the offset floor
81 uint32_t resyncs = 0; // reboot / lost-lock re-initializations
82 uint32_t clamp_fallbacks = 0; // chunks stamped at flat arrival (safety clamp)
83 uint64_t packets = 0;
84 bool locked = false;
85 };
86
87 /** Stats for one board (default-constructed if unknown). */
88 BoardStats stats(const std::string& board_key) const;
89
90 /** All boards seen (for periodic diagnostics logging). */
91 std::vector<std::string> boards() const;
92
93private:
94 struct BoardState {
95 bool locked = false;
96 uint32_t last_raw_ms = 0; // newest chunk of the previous packet (raw)
97 uint64_t last_unwrapped_ms = 0; // 64-bit unwrapped counterpart
98 // Offset math is exact int64 ns — doubles cannot represent epoch-ns.
99 int64_t offset_ns = 0; // arrival_ns - unwrapped_board_ns (window min)
100 // (arrival_ns, residual_ns) per packet; pruned to window_seconds by arrival.
101 std::deque<std::pair<uint64_t, int64_t>> window;
102 uint64_t last_emitted_ns = 0; // per-board monotonic guard
104 };
105
106 void resync(BoardState& s, uint32_t newest_raw_ms, uint64_t arrival_ns, bool count);
107
109 std::map<std::string, BoardState> boards_;
110};
111
112} // namespace time
113} // namespace fsw
114
115#endif // FSW_BOARD_CLOCK_SYNC_HPP
BoardClockSync(const TimeSyncConfig &cfg)
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.
BoardClockSync(const TimeSyncConfig &cfg)
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_
size_type count(const key_type &key) const
Definition json.hpp:19264
Motorized ball valve position: raw ADC counts -> % open.
std::deque< std::pair< uint64_t, int64_t > > window