// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 Martino Ferrari #pragma once #include namespace udpscope { /// Which way through a level counts. Shared by every condition type: for `Window` it is /// the direction through the *band* (into it, out of it, or either), and for `Dropout` it /// selects which crossings count as the signal still being alive. /// /// It lives here rather than in `trigger.hpp` because the buffer search needs the /// evaluator without the engine's arming state. enum class TriggerEdge { Rising, Falling, Both }; /// What shape in the signal counts as an event. enum class TriggerType { Edge, PulseWidth, Window, Dropout }; /// How a measured pulse width is compared with the condition's bounds. `Less` is the /// glitch trigger: "a pulse shorter than it should be". enum class WidthCmp { Less, Greater, Within, Outside }; /// Everything that decides whether a sample is an event. No arming, no capture window: /// those belong to `TriggerConfig`. struct EventCondition { TriggerType type = TriggerType::Edge; double level = 0.0; double level2 = 0.0; ///< Window: the band's other edge. Ignored otherwise. TriggerEdge edge = TriggerEdge::Rising; WidthCmp cmp = WidthCmp::Greater; ///< PulseWidth only. double width_s = 1.0e-3; ///< PulseWidth only. double width2_s = 2.0e-3; ///< PulseWidth, Within/Outside only. double timeout_s = 1.0e-3; ///< Dropout only. bool operator==(const EventCondition& o) const noexcept { return type == o.type && level == o.level && level2 == o.level2 && edge == o.edge && cmp == o.cmp && width_s == o.width_s && width2_s == o.width2_s && timeout_s == o.timeout_s; } bool operator!=(const EventCondition& o) const noexcept { return !(*this == o); } }; /// The event condition's state machine: one sample in, an event time or nothing out. /// /// It holds no arming state and no capture state -- that is `TriggerEngine`'s job -- which /// is what lets the buffer search run the *same* evaluator over retained samples and get /// the answers the live trigger would have given. /// /// Every condition but `Edge` is sequential: a pulse's width is only known at its closing /// edge, and a dropout is a measured absence. Callers must therefore feed it **every** /// sample in stream order, including samples they intend to ignore; a gap is a pulse it can /// never close. class EventDetector { public: /// What `feed()` returns when the sample is not an event. static constexpr double kNoEvent = std::numeric_limits::quiet_NaN(); /// Re-applying the same condition is a no-op, deliberately: the producer calls this on /// every packet, and resetting there would restart the machine before any pulse could /// close. A genuinely different condition resets, because its state means nothing. void configure(const EventCondition& c) noexcept; void reset() noexcept; /// One sample, in stream order. Returns the event's own stream time, or `kNoEvent`. /// /// There is no finite-value check: a NaN sample is benign here (every comparison against /// it is false, so it cannot fire), and rejecting one would leave `prev_` at the last good /// value and let the sample *after* the NaN fire a crossing that today cannot happen. double feed(double value, double t) noexcept; double last_value() const noexcept { return prev_; } bool have_last() const noexcept { return have_prev_; } private: bool crossed_up(double v) const noexcept { return prev_ < cond_.level && v >= cond_.level; } bool crossed_down(double v) const noexcept { return prev_ > cond_.level && v <= cond_.level; } double feed_edge(double value, double t) noexcept; double feed_pulse(double value, double t) noexcept; double feed_window(double value, double t) noexcept; double feed_dropout(double value, double t) noexcept; /// Whether a measured width satisfies `cond_.cmp`. `Within`/`Outside` take their two /// bounds in either order, so a panel cannot make an empty band by typing the larger /// figure into the first spin. bool qualifies(double width_s) const noexcept; EventCondition cond_{}; bool have_prev_ = false; double prev_ = 0.0; bool in_pulse_ = false; ///< PulseWidth: an opening edge has been seen. bool pulse_up_ = false; ///< PulseWidth: its polarity, so we know which edge closes it. double pulse_start_s_ = 0.0; bool inside_ = false; ///< Window: which side of the band the previous sample was. double last_cross_s_ = 0.0; ///< Dropout: when the signal was last seen crossing. bool dropped_ = false; ///< Dropout: the timeout has already been reported. }; } // namespace udpscope