1010#include " cbdev/clock_sync.h"
1111#include < algorithm>
1212#include < cmath>
13+ #include < numeric>
14+ #include < vector>
1315
1416namespace cbdev {
1517
@@ -107,6 +109,21 @@ std::optional<int64_t> ClockSync::getUncertaintyNs() const {
107109 return m_current_uncertainty_ns;
108110}
109111
112+ void ClockSync::setExternalOffset (std::optional<int64_t > offset_ns,
113+ std::optional<int64_t > uncertainty_ns) {
114+ std::lock_guard<std::mutex> lock (m_mutex);
115+ m_external_offset_ns = offset_ns;
116+ m_external_uncertainty_ns = uncertainty_ns;
117+ if (offset_ns) {
118+ m_current_offset_ns = offset_ns;
119+ if (uncertainty_ns)
120+ m_current_uncertainty_ns = uncertainty_ns;
121+ } else {
122+ // Cleared — revert to internal estimate
123+ recomputeEstimate ();
124+ }
125+ }
126+
110127bool ClockSync::probesAreReliable () const {
111128 std::lock_guard<std::mutex> lock (m_mutex);
112129 return probeSpreadOk ();
@@ -116,11 +133,9 @@ void ClockSync::addDataPacketSample(const uint64_t device_time_ns, const time_po
116133 std::lock_guard<std::mutex> lock (m_mutex);
117134
118135 const int64_t recv_ns = to_ns (recv_local);
119- // offset = device_time - recv_time.
120- // This overestimates the true offset by the one-way network delay
121- // (host→device path isn't measured). Over many samples the minimum
122- // offset will approach the true value because the minimum corresponds
123- // to the least-queued packet.
136+ // raw_offset = device_time - recv_time = true_offset - one_way_delay.
137+ // The maximum raw_offset (minimum one-way delay) is closest to the
138+ // true offset, regardless of clock epoch.
124139 const int64_t offset_ns = static_cast <int64_t >(device_time_ns) - recv_ns;
125140
126141 // Detect clock jumps (same logic as addProbeSample)
@@ -144,61 +159,117 @@ void ClockSync::addDataPacketSample(const uint64_t device_time_ns, const time_po
144159 m_data_samples.pop_front ();
145160 }
146161
147- // Pick minimum offset (closest to true offset = least network delay).
148- // Add a constant to compensate for the one-way acquisition-to-
149- // transmission delay that this method can't measure. Without
150- // this correction the converted timestamps appear in the future.
151- // 0.7 ms is the observed lower bound on the device's capture-to-
152- // send latency (ADC sample → UDP datagram on the wire).
153- //
154- // Pick minimum offset (closest to true offset = least network delay).
155- // Once established, only allow the offset to DECREASE (= converted
156- // timestamps move forward in time, never backwards).
157- // device_to_monotonic(D) = D - offset, so a smaller offset shifts
158- // results later. When old minimum-delay samples expire the raw
159- // minimum jumps up, but we hold the previous lower value to
160- // preserve monotonicity.
161- //
162- // The monotonicity constraint is tracked separately from probe-based
163- // offsets (m_data_floor_ns) so that the initial transition from a
164- // stale probe offset to the data-based estimate isn't blocked.
162+ // Compute fallback offset: max raw_offset with gap-based glitch
163+ // rejection, plus a small correction for residual network delay.
164+ // Recomputed from the current window each time (no permanent floor).
165165 if (!m_data_samples.empty ()) {
166166 constexpr int64_t ONE_WAY_DELAY_ESTIMATE_NS = 700'000 ; // 0.7 ms
167- int64_t best = m_data_samples.front ().offset_ns ;
167+ constexpr int64_t DATA_GAP_THRESHOLD_NS = 5'000'000 ; // 5 ms
168+
169+ // Sort offsets to find the glitch-filtered max.
170+ std::vector<int64_t > offsets;
171+ offsets.reserve (m_data_samples.size ());
168172 for (const auto & s : m_data_samples)
169- best = std::min (best, s.offset_ns );
170- const int64_t candidate = best + ONE_WAY_DELAY_ESTIMATE_NS ;
171- if (!m_data_floor_ns || candidate < *m_data_floor_ns) {
172- m_data_floor_ns = candidate;
173+ offsets.push_back (s.offset_ns );
174+ std::sort (offsets.begin (), offsets.end ());
175+
176+ // Strip isolated upward outliers (same gap logic as probes).
177+ size_t top = offsets.size ();
178+ while (top > 1 ) {
179+ if (offsets[top - 1 ] - offsets[top - 2 ] > DATA_GAP_THRESHOLD_NS )
180+ --top;
181+ else
182+ break ;
173183 }
174- m_current_offset_ns = *m_data_floor_ns;
175- m_current_uncertainty_ns = ONE_WAY_DELAY_ESTIMATE_NS ;
184+
185+ m_data_floor_ns = offsets[top - 1 ] + ONE_WAY_DELAY_ESTIMATE_NS ;
176186 }
187+
188+ recomputeEstimate ();
177189}
178190
179191void ClockSync::recomputeEstimate () {
180- // If data-packet fallback has been activated, never revert to probes.
181- // Data timestamps (from the ADC/PTP clock) are always reliable, while
182- // probe timestamps (from header->time) may be broken on some firmware.
183- if (m_data_floor_ns.has_value ())
192+ // External offset (from peer device shmem) takes unconditional priority.
193+ if (m_external_offset_ns) {
194+ m_current_offset_ns = m_external_offset_ns;
195+ if (m_external_uncertainty_ns)
196+ m_current_uncertainty_ns = m_external_uncertainty_ns;
184197 return ;
198+ }
185199
186- // Find probe with minimum RTT — least queuing/jitter gives most reliable estimate
187- const ProbeSample* best_probe = nullptr ;
188- for (const auto & p : m_probe_samples) {
189- if (!best_probe || p.rtt_ns < best_probe->rtt_ns ) {
190- best_probe = &p;
200+ // Strategy:
201+ // 1. If probes are reliable (tight spread), use glitch-filtered
202+ // max-offset probe. This is the HUB1 path.
203+ // 2. Otherwise, if data-packet samples are available, use the
204+ // glitch-filtered max raw_offset from data packets. This is
205+ // the NSP path — data-packet timestamps (from the ADC/PTP
206+ // clock) are more stable than probe header->time on the NSP.
207+ // 3. If neither is available, use probes anyway (unreliable but
208+ // better than nothing).
209+
210+ // Check if probes are reliable enough to use directly.
211+ if (!m_probe_samples.empty () && probeSpreadOk ()) {
212+ // Probes are tight — use glitch-filtered max-offset.
213+ std::vector<size_t > indices (m_probe_samples.size ());
214+ std::iota (indices.begin (), indices.end (), 0 );
215+ std::sort (indices.begin (), indices.end (),
216+ [this ](size_t a, size_t b) {
217+ return m_probe_samples[a].offset_ns
218+ < m_probe_samples[b].offset_ns ;
219+ });
220+
221+ constexpr int64_t GAP_THRESHOLD_NS = 10'000'000 ; // 10 ms
222+ size_t top = indices.size ();
223+ while (top > 1 ) {
224+ if (m_probe_samples[indices[top - 1 ]].offset_ns -
225+ m_probe_samples[indices[top - 2 ]].offset_ns > GAP_THRESHOLD_NS )
226+ --top;
227+ else
228+ break ;
191229 }
230+
231+ const auto & best = m_probe_samples[indices[top - 1 ]];
232+ m_current_offset_ns = best.offset_ns ;
233+ m_current_uncertainty_ns = best.rtt_ns / 2 ;
234+ return ;
192235 }
193236
194- if (best_probe) {
195- m_current_offset_ns = best_probe->offset_ns ;
196- m_current_uncertainty_ns = best_probe->rtt_ns / 2 ;
197- } else if (m_data_samples.empty ()) {
198- // Only clear if no data-packet fallback either
199- m_current_offset_ns = std::nullopt ;
200- m_current_uncertainty_ns = std::nullopt ;
237+ // Probes unreliable or absent — use data-packet fallback.
238+ if (m_data_floor_ns.has_value ()) {
239+ m_current_offset_ns = *m_data_floor_ns;
240+ m_current_uncertainty_ns = 700'000 ; // ONE_WAY_DELAY_ESTIMATE_NS
241+ return ;
201242 }
243+
244+ // Last resort: use probes even though they're unreliable.
245+ if (!m_probe_samples.empty ()) {
246+ // Same glitch-filtered max-offset as above.
247+ std::vector<size_t > indices (m_probe_samples.size ());
248+ std::iota (indices.begin (), indices.end (), 0 );
249+ std::sort (indices.begin (), indices.end (),
250+ [this ](size_t a, size_t b) {
251+ return m_probe_samples[a].offset_ns
252+ < m_probe_samples[b].offset_ns ;
253+ });
254+
255+ constexpr int64_t GAP_THRESHOLD_NS = 10'000'000 ;
256+ size_t top = indices.size ();
257+ while (top > 1 ) {
258+ if (m_probe_samples[indices[top - 1 ]].offset_ns -
259+ m_probe_samples[indices[top - 2 ]].offset_ns > GAP_THRESHOLD_NS )
260+ --top;
261+ else
262+ break ;
263+ }
264+
265+ const auto & best = m_probe_samples[indices[top - 1 ]];
266+ m_current_offset_ns = best.offset_ns ;
267+ m_current_uncertainty_ns = best.rtt_ns / 2 ;
268+ return ;
269+ }
270+
271+ m_current_offset_ns = std::nullopt ;
272+ m_current_uncertainty_ns = std::nullopt ;
202273}
203274
204275bool ClockSync::probeSpreadOk () const {
0 commit comments