// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 Martino Ferrari #include "trigger.hpp" #include #include namespace udpscope { double TriggerConfig::post_span_s(double buffer_span_s) const { const double pre = std::clamp(pretrigger_pct, 0.0, 100.0) / 100.0; return buffer_span_s * (1.0 - pre); } void TriggerEngine::configure(const TriggerConfig& cfg, const std::string& channel_name, double post_seconds, double pre_seconds, double oldest_s) { const bool channel_changed = channel_name != channel_name_; if (channel_changed) channel_name_ = channel_name; cfg_ = cfg; post_seconds_ = post_seconds > 0.0 ? post_seconds : 0.0; pre_seconds_ = pre_seconds > 0.0 ? pre_seconds : 0.0; oldest_s_ = oldest_s; // A no-op unless the condition actually changed: this runs on every packet. detector_.configure(cfg_.condition); if (channel_changed) { detector_.reset(); pending_ = false; post_accum_count_ = 0; } } void TriggerEngine::arm(double now_s) noexcept { armed_ = true; pending_ = false; post_accum_count_ = 0.0; holdoff_until_s_ = now_s; detector_.reset(); } bool TriggerEngine::process(double value, double now_s) noexcept { if (!cfg_.enabled || !armed_ || cfg_.channel < 0) return false; // The detector is fed **first and unconditionally**, on every sample the engine sees -- // including during the hold-off and during an open capture, where its answer is thrown // away. It is a sequential machine: a pulse whose opening edge fell in a skipped stretch // can never be closed, and the symptom would be a width condition that silently stops // firing after the first hold-off. const double event_t = detector_.feed(value, now_s); // Hold-off: settled, waiting for the next event. if (!pending_ && now_s < holdoff_until_s_) { last_t_s_ = now_s; return false; } last_t_s_ = now_s; if (pending_) { // Accumulate the post-trigger window; latch once the stream clock has advanced // past it. Time, not a sample count: `now_s` is the same clock the x axis reads, // so the capture covers exactly the requested span whatever the cadence is. ++post_accum_count_; if (now_s - pending_start_s_ >= post_seconds_) { pending_ = false; post_accum_count_ = 0; if (cfg_.mode == TriggerMode::Single) { armed_ = false; } else { holdoff_until_s_ = now_s + std::max(0.0, cfg_.holdoff_s); } return true; } return false; } if (!std::isfinite(event_t)) return false; // The pre-trigger window has to already be behind the event, or the capture opens on a // half-filled pre window and the event is short on its left side -- the plot then shows a // window that is correctly sized but only partly drawn. The buffer fills whether or not the // trigger is armed, so this normally passes on the first event; it only bites just after a // rebuild (a CONFIG, or enabling the signal), when the history genuinely is not there yet. // // Measured from the *event's own* time, not from `now_s`: for a qualified condition the two // differ (a pulse is reported at its closing edge), and the pre window belongs to the event. if (event_t - oldest_s_ < pre_seconds_) return false; // The event's own time, which is what a trigger marker marks: the latch itself may happen // a post-window later. last_trigger_s_ = event_t; // The event always opens a capture; there is no "latch on the event itself" case. // A zero-length post window latches on the very next sample, which is the least a // capture can hold, and every longer window is just more samples -- no conversion // from seconds to a sample count, so no conversion to get wrong. pending_ = true; pending_start_s_ = event_t; post_accum_count_ = 0; return false; } } // namespace udpscope