refactor: what counts as an event moves into its own evaluator, so a search can ask the same question the trigger asks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Sonnet 4.6 committed 2026-10-07 17:15:37 +02:00
1 parent 178cf54ef6
commit b3fd9ae00f
6 files changed
+189 -50

No files matched your search

+91
View File
@@ -0,0 +1,91 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#pragma once
#include <limits>
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<double>::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_; }
const EventCondition& condition() const noexcept { return cond_; }
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;
EventCondition cond_{};
bool have_prev_ = false;
double prev_ = 0.0;
};
} // namespace udpscope