feat: the trigger can ignore a noise band and reject HF before comparing
This commit is contained in:
1 parent
d5a9b3c5d5
commit
d34bff3f03
9 files changed
+99
-16
No files matched your search
+2
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||||
|
|||||||
Reference in new issue
Block a user