feat: the trigger can ignore a noise band and reject HF before comparing

This commit is contained in:
Martino Ferrari committed 2026-10-13 15:04:52 +02:00
1 parent d5a9b3c5d5
commit d34bff3f03
9 files changed
+99 -16

No files matched your search

+2 -1
View File
@@ -728,7 +728,8 @@ void Acquisition::on_data(const udps_frame_t* frame) {
trig_channel >= 0 && trig_channel < static_cast<int>(channels.size()) trig_channel >= 0 && trig_channel < static_cast<int>(channels.size())
? channels[static_cast<std::size_t>(trig_channel)].name ? channels[static_cast<std::size_t>(trig_channel)].name
: kNoTriggerChannel; : kNoTriggerChannel;
engine.configure(tc, trig_name, post_want, event_span - post_want, oldest_s); engine.configure(tc, trig_name, post_want, event_span - post_want, oldest_s,
model_.partition().slot_dt());
// The engine's armed state follows the panel's, both ways: without the disarm // The engine's armed state follows the panel's, both ways: without the disarm
// half, Disarm left the engine firing (its own `armed_` was only ever set), and // half, Disarm left the engine firing (its own `armed_` was only ever set), and
// re-entering trigger mode kept the stale prev/edge/hold-off state. // re-entering trigger mode kept the stale prev/edge/hold-off state.
+59 -5
View File
@@ -3,11 +3,22 @@
#include "condition.hpp" #include "condition.hpp"
#include <cmath>
namespace udpscope { namespace udpscope {
void EventDetector::configure(const EventCondition& c) noexcept { void EventDetector::configure(const EventCondition& c, double dt) noexcept {
if (c == cond_) return; if (c == cond_ && dt == dt_) return;
cond_ = c; cond_ = c;
dt_ = dt;
// a = 1 - exp(-2*pi*fc*dt), clamped just under Nyquist so a never exceeds 1.
// fc == 0 leaves a == 1, which is a pass-through.
pole_a_ = 1.0;
if (cond_.hf_fc > 0.0 && dt > 0.0) {
const double nyquist = 0.5 / dt;
const double fc = cond_.hf_fc < nyquist * 0.99 ? cond_.hf_fc : nyquist * 0.99;
pole_a_ = 1.0 - std::exp(-2.0 * 3.14159265358979323846 * fc * dt);
}
reset(); reset();
} }
@@ -20,9 +31,45 @@ void EventDetector::reset() noexcept {
inside_ = false; inside_ = false;
last_cross_s_ = 0.0; last_cross_s_ = 0.0;
dropped_ = false; dropped_ = false;
pole_y_ = 0.0;
pole_have_ = false;
side_ = 0;
}
double EventDetector::filter(double v) noexcept {
// A NaN must stay a NaN and must not poison the state: a dropout is a real
// reading here, not an error.
if (!std::isfinite(v)) return v;
if (!pole_have_) {
pole_y_ = v;
pole_have_ = true;
return pole_y_;
}
pole_y_ += pole_a_ * (v - pole_y_);
return pole_y_;
}
void EventDetector::decide(double v, bool& up, bool& down) noexcept {
up = false;
down = false;
if (cond_.noise_band <= 0.0) {
up = crossed_up(v);
down = crossed_down(v);
return;
}
// A Schmitt trigger: a sample inside the band decides nothing, and a crossing
// is a change between two *decided* sides. Seeding from the undecided side
// would fire on a signal that starts inside the band and leaves it without
// ever reaching `level`.
const int was = side_;
if (v >= cond_.level + cond_.noise_band) side_ = 1;
else if (v <= cond_.level - cond_.noise_band) side_ = -1;
up = was == -1 && side_ == 1;
down = was == 1 && side_ == -1;
} }
double EventDetector::feed(double value, double t) noexcept { double EventDetector::feed(double value, double t) noexcept {
value = filter(value);
if (!have_prev_) { if (!have_prev_) {
prev_ = value; prev_ = value;
have_prev_ = true; have_prev_ = true;
@@ -57,8 +104,9 @@ double EventDetector::feed(double value, double t) noexcept {
} }
double EventDetector::feed_edge(double value, double t) noexcept { double EventDetector::feed_edge(double value, double t) noexcept {
const bool up = crossed_up(value); bool up = false;
const bool down = crossed_down(value); bool down = false;
decide(value, up, down);
const bool fired = (cond_.edge == TriggerEdge::Rising && up) || const bool fired = (cond_.edge == TriggerEdge::Rising && up) ||
(cond_.edge == TriggerEdge::Falling && down) || (cond_.edge == TriggerEdge::Falling && down) ||
(cond_.edge == TriggerEdge::Both && (up || down)); (cond_.edge == TriggerEdge::Both && (up || down));
@@ -115,7 +163,13 @@ double EventDetector::feed_pulse(double value, double t) noexcept {
double EventDetector::feed_window(double value, double t) noexcept { double EventDetector::feed_window(double value, double t) noexcept {
const double lo = cond_.level < cond_.level2 ? cond_.level : cond_.level2; const double lo = cond_.level < cond_.level2 ? cond_.level : cond_.level2;
const double hi = cond_.level < cond_.level2 ? cond_.level2 : cond_.level; const double hi = cond_.level < cond_.level2 ? cond_.level2 : cond_.level;
const bool inside = value >= lo && value <= hi; // The band is applied on the edge the signal must *leave*, and clamped to just
// under half the window so `lo + band` can never pass `hi - band`.
double band = cond_.noise_band > 0.0 ? cond_.noise_band : 0.0;
const double room = 0.49 * (hi - lo);
if (band > room) band = room;
const bool inside = inside_ ? (value >= lo - band && value <= hi + band)
: (value >= lo + band && value <= hi - band);
if (inside == inside_) return kNoEvent; if (inside == inside_) return kNoEvent;
inside_ = inside; inside_ = inside;
// The event is the *change of side*, so `Rising` reads as "into the band" and `Falling` // The event is the *change of side*, so `Rising` reads as "into the band" and `Falling`
+24 -2
View File
@@ -33,11 +33,17 @@ struct EventCondition {
double width_s = 1.0e-3; ///< PulseWidth only. double width_s = 1.0e-3; ///< PulseWidth only.
double width2_s = 2.0e-3; ///< PulseWidth, Within/Outside only. double width2_s = 2.0e-3; ///< PulseWidth, Within/Outside only.
double timeout_s = 1.0e-3; ///< Dropout only. double timeout_s = 1.0e-3; ///< Dropout only.
/// Half-width of the amplitude band around `level` a sample must leave before
/// a crossing counts. 0 is the bare comparator. In signal units.
double noise_band = 0.0;
/// Corner of a one-pole low pass applied to the sample before any comparison.
/// 0 is off. In Hz.
double hf_fc = 0.0;
bool operator==(const EventCondition& o) const noexcept { bool operator==(const EventCondition& o) const noexcept {
return type == o.type && level == o.level && level2 == o.level2 && edge == o.edge && 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 && cmp == o.cmp && width_s == o.width_s && width2_s == o.width2_s &&
timeout_s == o.timeout_s; timeout_s == o.timeout_s && noise_band == o.noise_band && hf_fc == o.hf_fc;
} }
bool operator!=(const EventCondition& o) const noexcept { return !(*this == o); } bool operator!=(const EventCondition& o) const noexcept { return !(*this == o); }
}; };
@@ -60,7 +66,10 @@ class EventDetector {
/// Re-applying the same condition is a no-op, deliberately: the producer calls this on /// 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 /// every packet, and resetting there would restart the machine before any pulse could
/// close. A genuinely different condition resets, because its state means nothing. /// close. A genuinely different condition resets, because its state means nothing.
void configure(const EventCondition& c) noexcept; ///
/// `dt` is the stored sample interval (`Partition::slot_dt()`), needed for the
/// HF reject's pole. A no-op unless the condition or the interval changed.
void configure(const EventCondition& c, double dt) noexcept;
void reset() noexcept; void reset() noexcept;
/// One sample, in stream order. Returns the event's own stream time, or `kNoEvent`. /// One sample, in stream order. Returns the event's own stream time, or `kNoEvent`.
@@ -80,6 +89,10 @@ class EventDetector {
bool crossed_down(double v) const noexcept { bool crossed_down(double v) const noexcept {
return prev_ > cond_.level && v <= cond_.level; return prev_ > cond_.level && v <= cond_.level;
} }
/// The HF reject, applied to every sample before any comparison.
double filter(double v) noexcept;
/// `up`/`down` for the edge condition, through the band if there is one.
void decide(double v, bool& up, bool& down) noexcept;
double feed_edge(double value, double t) noexcept; double feed_edge(double value, double t) noexcept;
double feed_pulse(double value, double t) noexcept; double feed_pulse(double value, double t) noexcept;
double feed_window(double value, double t) noexcept; double feed_window(double value, double t) noexcept;
@@ -98,6 +111,15 @@ class EventDetector {
bool inside_ = false; ///< Window: which side of the band the previous sample was. 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. double last_cross_s_ = 0.0; ///< Dropout: when the signal was last seen crossing.
bool dropped_ = false; ///< Dropout: the timeout has already been reported. bool dropped_ = false; ///< Dropout: the timeout has already been reported.
/// One-pole prefilter. `pole_a_ == 1` passes the input through, so "off"
/// costs no branch. `pole_y_` is the filtered value.
double dt_ = 0.0;
double pole_a_ = 1.0;
double pole_y_ = 0.0;
bool pole_have_ = false;
/// Which side of the band the signal was last *decided* to be on: +1 above,
/// -1 below, 0 undecided. A sample inside the band decides nothing.
int side_ = 0;
}; };
} // namespace udpscope } // namespace udpscope
+2 -2
View File
@@ -16,11 +16,11 @@ double ms_since(const std::chrono::steady_clock::time_point& t0) {
} }
} // namespace } // namespace
void BufferSearch::start(const EventCondition& c, int channel) { void BufferSearch::start(const EventCondition& c, int channel, double dt) {
clear(); clear();
channel_ = channel; channel_ = channel;
detector_.reset(); detector_.reset();
detector_.configure(c); detector_.configure(c, dt);
// `configure()` is a no-op for an unchanged condition, so the explicit reset above is what // `configure()` is a no-op for an unchanged condition, so the explicit reset above is what
// makes a re-run of the *same* search start from a clean machine. // makes a re-run of the *same* search start from a clean machine.
detector_.reset(); detector_.reset();
+1 -1
View File
@@ -48,7 +48,7 @@ class BufferSearch {
}; };
/// Begins a scan of the channel for the condition. Discards any previous result. /// Begins a scan of the channel for the condition. Discards any previous result.
void start(const EventCondition& c, int channel); void start(const EventCondition& c, int channel, double dt);
void cancel() noexcept; void cancel() noexcept;
void clear() noexcept; void clear() noexcept;
+3 -2
View File
@@ -14,7 +14,8 @@ double TriggerConfig::post_span_s(double buffer_span_s) const {
} }
void TriggerEngine::configure(const TriggerConfig& cfg, const std::string& channel_name, void TriggerEngine::configure(const TriggerConfig& cfg, const std::string& channel_name,
double post_seconds, double pre_seconds, double oldest_s) { double post_seconds, double pre_seconds, double oldest_s,
double dt) {
const bool channel_changed = channel_name != channel_name_; const bool channel_changed = channel_name != channel_name_;
if (channel_changed) channel_name_ = channel_name; if (channel_changed) channel_name_ = channel_name;
cfg_ = cfg; cfg_ = cfg;
@@ -22,7 +23,7 @@ void TriggerEngine::configure(const TriggerConfig& cfg, const std::string& chann
pre_seconds_ = pre_seconds > 0.0 ? pre_seconds : 0.0; pre_seconds_ = pre_seconds > 0.0 ? pre_seconds : 0.0;
oldest_s_ = oldest_s; oldest_s_ = oldest_s;
// A no-op unless the condition actually changed: this runs on every packet. // A no-op unless the condition actually changed: this runs on every packet.
detector_.configure(cfg_.condition); detector_.configure(cfg_.condition, dt);
if (channel_changed) { if (channel_changed) {
detector_.reset(); detector_.reset();
pending_ = false; pending_ = false;
+2 -1
View File
@@ -69,7 +69,8 @@ class TriggerEngine {
/// buffer's, not a timer since arming -- the ring keeps filling with the trigger /// buffer's, not a timer since arming -- the ring keeps filling with the trigger
/// off, so the history is usually already there and waiting would be wrong. /// off, so the history is usually already there and waiting would be wrong.
void configure(const TriggerConfig& cfg, const std::string& channel_name, void configure(const TriggerConfig& cfg, const std::string& channel_name,
double post_seconds, double pre_seconds, double oldest_s); double post_seconds, double pre_seconds, double oldest_s,
double dt);
/// Disarms without forgetting the configuration. /// Disarms without forgetting the configuration.
void disarm() noexcept { armed_ = false; pending_ = false; } void disarm() noexcept { armed_ = false; pending_ = false; }
void arm(double now_s) noexcept; void arm(double now_s) noexcept;
+3 -1
View File
@@ -1421,12 +1421,14 @@ gboolean search_probe_run(gpointer user) {
g_search_t0 = a->model->trigger_time(); g_search_t0 = a->model->trigger_time();
EventCondition c; EventCondition c;
int channel = -1; int channel = -1;
double dt = 0.0;
{ {
std::lock_guard<std::mutex> lock(a->model->mutex()); std::lock_guard<std::mutex> lock(a->model->mutex());
c = a->model->trigger().condition; c = a->model->trigger().condition;
channel = a->model->trigger().channel; channel = a->model->trigger().channel;
dt = a->model->partition().slot_dt();
} }
a->search.start(c, channel); a->search.start(c, channel, dt);
std::fprintf(stderr, "[search-probe] started on channel %d, armed t0=%.6f\n", channel, std::fprintf(stderr, "[search-probe] started on channel %d, armed t0=%.6f\n", channel,
g_search_t0); g_search_t0);
std::fflush(stderr); std::fflush(stderr);
+3 -1
View File
@@ -341,12 +341,14 @@ GtkWidget* build_trigger_panel(App& app) {
publish_trigger_fields(a); publish_trigger_fields(a);
EventCondition c; EventCondition c;
int channel = -1; int channel = -1;
double dt = 0.0;
{ {
std::lock_guard<std::mutex> lock(a->model->mutex()); std::lock_guard<std::mutex> lock(a->model->mutex());
c = a->model->trigger().condition; c = a->model->trigger().condition;
channel = a->model->trigger().channel; channel = a->model->trigger().channel;
dt = a->model->partition().slot_dt();
} }
a->search.start(c, channel); a->search.start(c, channel, dt);
update_search_panel(*a); update_search_panel(*a);
}), }),
&app); &app);