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:
1 parent
178cf54ef6
commit
b3fd9ae00f
6 files changed
+189
-50
No files matched your search
@@ -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
|
||||
Reference in new issue
Block a user