feat: a level band and a signal that stops can be the trigger's event too
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
0876ecfe22
commit
a26770ec32
3 files changed
+192
-35
No files matched your search
+50
-1
@@ -17,12 +17,23 @@ void EventDetector::reset() noexcept {
|
|||||||
in_pulse_ = false;
|
in_pulse_ = false;
|
||||||
pulse_up_ = false;
|
pulse_up_ = false;
|
||||||
pulse_start_s_ = 0.0;
|
pulse_start_s_ = 0.0;
|
||||||
|
inside_ = false;
|
||||||
|
last_cross_s_ = 0.0;
|
||||||
|
dropped_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
double EventDetector::feed(double value, double t) noexcept {
|
double EventDetector::feed(double value, double t) noexcept {
|
||||||
if (!have_prev_) {
|
if (!have_prev_) {
|
||||||
prev_ = value;
|
prev_ = value;
|
||||||
have_prev_ = true;
|
have_prev_ = true;
|
||||||
|
// The window condition needs a side to change *from*, and the dropout a last-seen time
|
||||||
|
// to count from; neither can be derived from a crossing that has not happened. Seeding
|
||||||
|
// them here is what lets both handlers run without a "have we started" guard.
|
||||||
|
const double lo = cond_.level < cond_.level2 ? cond_.level : cond_.level2;
|
||||||
|
const double hi = cond_.level < cond_.level2 ? cond_.level2 : cond_.level;
|
||||||
|
inside_ = value >= lo && value <= hi;
|
||||||
|
last_cross_s_ = t;
|
||||||
|
dropped_ = false;
|
||||||
return kNoEvent;
|
return kNoEvent;
|
||||||
}
|
}
|
||||||
double event = kNoEvent;
|
double event = kNoEvent;
|
||||||
@@ -34,8 +45,11 @@ double EventDetector::feed(double value, double t) noexcept {
|
|||||||
event = feed_pulse(value, t);
|
event = feed_pulse(value, t);
|
||||||
break;
|
break;
|
||||||
case TriggerType::Window:
|
case TriggerType::Window:
|
||||||
|
event = feed_window(value, t);
|
||||||
|
break;
|
||||||
case TriggerType::Dropout:
|
case TriggerType::Dropout:
|
||||||
break; // implemented in the tasks that add their panel rows
|
event = feed_dropout(value, t);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
// After the handler: every crossing test above reads `prev_` as the previous sample.
|
// After the handler: every crossing test above reads `prev_` as the previous sample.
|
||||||
prev_ = value;
|
prev_ = value;
|
||||||
@@ -98,4 +112,39 @@ double EventDetector::feed_pulse(double value, double t) noexcept {
|
|||||||
return qualifies(width) ? t : kNoEvent;
|
return qualifies(width) ? t : kNoEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double EventDetector::feed_window(double value, double t) noexcept {
|
||||||
|
const double lo = cond_.level < cond_.level2 ? cond_.level : cond_.level2;
|
||||||
|
const double hi = cond_.level < cond_.level2 ? cond_.level2 : cond_.level;
|
||||||
|
const bool inside = value >= lo && value <= hi;
|
||||||
|
if (inside == inside_) return kNoEvent;
|
||||||
|
inside_ = inside;
|
||||||
|
// The event is the *change of side*, so `Rising` reads as "into the band" and `Falling`
|
||||||
|
// as "out of it". A band of zero width therefore never fires at all, because no sample
|
||||||
|
// can be on a different side of it than its neighbour: the panel refuses one.
|
||||||
|
const bool fired = (cond_.edge == TriggerEdge::Rising && inside) ||
|
||||||
|
(cond_.edge == TriggerEdge::Falling && !inside) ||
|
||||||
|
cond_.edge == TriggerEdge::Both;
|
||||||
|
return fired ? t : kNoEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
double EventDetector::feed_dropout(double value, double t) noexcept {
|
||||||
|
const bool up = crossed_up(value);
|
||||||
|
const bool down = crossed_down(value);
|
||||||
|
const bool crossed = (cond_.edge == TriggerEdge::Rising && up) ||
|
||||||
|
(cond_.edge == TriggerEdge::Falling && down) ||
|
||||||
|
(cond_.edge == TriggerEdge::Both && (up || down));
|
||||||
|
if (crossed) {
|
||||||
|
last_cross_s_ = t;
|
||||||
|
dropped_ = false;
|
||||||
|
return kNoEvent;
|
||||||
|
}
|
||||||
|
// One event per silence, not one per sample of it: without the latch a dead signal would
|
||||||
|
// report an event on every sample after the timeout and fill the search's hit list with
|
||||||
|
// one answer repeated.
|
||||||
|
if (dropped_) return kNoEvent;
|
||||||
|
if (t - last_cross_s_ <= cond_.timeout_s) return kNoEvent;
|
||||||
|
dropped_ = true;
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace udpscope
|
} // namespace udpscope
|
||||||
@@ -83,6 +83,8 @@ class EventDetector {
|
|||||||
}
|
}
|
||||||
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_dropout(double value, double t) noexcept;
|
||||||
/// Whether a measured width satisfies `cond_.cmp`. `Within`/`Outside` take their two
|
/// 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
|
/// bounds in either order, so a panel cannot make an empty band by typing the larger
|
||||||
/// figure into the first spin.
|
/// figure into the first spin.
|
||||||
@@ -94,6 +96,9 @@ class EventDetector {
|
|||||||
bool in_pulse_ = false; ///< PulseWidth: an opening edge has been seen.
|
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.
|
bool pulse_up_ = false; ///< PulseWidth: its polarity, so we know which edge closes it.
|
||||||
double pulse_start_s_ = 0.0;
|
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
|
} // namespace udpscope
|
||||||
+137
-34
@@ -135,12 +135,16 @@ struct App {
|
|||||||
GtkWidget* trigger_cmp_drop = nullptr;
|
GtkWidget* trigger_cmp_drop = nullptr;
|
||||||
GtkWidget* trigger_width_spin = nullptr;
|
GtkWidget* trigger_width_spin = nullptr;
|
||||||
GtkWidget* trigger_width2_spin = nullptr;
|
GtkWidget* trigger_width2_spin = nullptr;
|
||||||
|
GtkWidget* trigger_level2_spin = nullptr;
|
||||||
|
GtkWidget* trigger_timeout_spin = nullptr;
|
||||||
/// The `labeled_box` rows a condition may not use: held so `sync_trigger_rows()` can
|
/// The `labeled_box` rows a condition may not use: held so `sync_trigger_rows()` can
|
||||||
/// hide them. A hidden row keeps its widget's value, so switching condition and back
|
/// hide them. A hidden row keeps its widget's value, so switching condition and back
|
||||||
/// does not lose what was typed.
|
/// does not lose what was typed.
|
||||||
GtkWidget* trigger_cmp_row = nullptr;
|
GtkWidget* trigger_cmp_row = nullptr;
|
||||||
GtkWidget* trigger_width_row = nullptr;
|
GtkWidget* trigger_width_row = nullptr;
|
||||||
GtkWidget* trigger_width2_row = nullptr;
|
GtkWidget* trigger_width2_row = nullptr;
|
||||||
|
GtkWidget* trigger_level2_row = nullptr;
|
||||||
|
GtkWidget* trigger_timeout_row = nullptr;
|
||||||
GtkWidget* trigger_mode_drop = nullptr;
|
GtkWidget* trigger_mode_drop = nullptr;
|
||||||
GtkWidget* trigger_level_spin = nullptr;
|
GtkWidget* trigger_level_spin = nullptr;
|
||||||
GtkWidget* trigger_pre_spin = nullptr;
|
GtkWidget* trigger_pre_spin = nullptr;
|
||||||
@@ -1612,6 +1616,10 @@ TriggerType trigger_type_at(guint i) {
|
|||||||
switch (i) {
|
switch (i) {
|
||||||
case 1:
|
case 1:
|
||||||
return TriggerType::PulseWidth;
|
return TriggerType::PulseWidth;
|
||||||
|
case 2:
|
||||||
|
return TriggerType::Window;
|
||||||
|
case 3:
|
||||||
|
return TriggerType::Dropout;
|
||||||
default:
|
default:
|
||||||
return TriggerType::Edge;
|
return TriggerType::Edge;
|
||||||
}
|
}
|
||||||
@@ -1646,6 +1654,8 @@ void publish_trigger_fields(App* app) {
|
|||||||
const guint cmp = gtk_drop_down_get_selected(GTK_DROP_DOWN(app->trigger_cmp_drop));
|
const guint cmp = gtk_drop_down_get_selected(GTK_DROP_DOWN(app->trigger_cmp_drop));
|
||||||
const double width = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_width_spin));
|
const double width = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_width_spin));
|
||||||
const double width2 = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_width2_spin));
|
const double width2 = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_width2_spin));
|
||||||
|
const double level2 = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_level2_spin));
|
||||||
|
const double timeout = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_timeout_spin));
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(app->model->mutex());
|
std::lock_guard<std::mutex> lock(app->model->mutex());
|
||||||
TriggerConfig& tc = app->model->trigger();
|
TriggerConfig& tc = app->model->trigger();
|
||||||
@@ -1660,6 +1670,8 @@ void publish_trigger_fields(App* app) {
|
|||||||
tc.condition.cmp = width_cmp_at(cmp);
|
tc.condition.cmp = width_cmp_at(cmp);
|
||||||
tc.condition.width_s = width;
|
tc.condition.width_s = width;
|
||||||
tc.condition.width2_s = width2;
|
tc.condition.width2_s = width2;
|
||||||
|
tc.condition.level2 = level2;
|
||||||
|
tc.condition.timeout_s = timeout;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1673,10 +1685,27 @@ void sync_trigger_rows(App& app) {
|
|||||||
const WidthCmp cmp =
|
const WidthCmp cmp =
|
||||||
width_cmp_at(gtk_drop_down_get_selected(GTK_DROP_DOWN(app.trigger_cmp_drop)));
|
width_cmp_at(gtk_drop_down_get_selected(GTK_DROP_DOWN(app.trigger_cmp_drop)));
|
||||||
const bool pulse = type == TriggerType::PulseWidth;
|
const bool pulse = type == TriggerType::PulseWidth;
|
||||||
|
const bool window = type == TriggerType::Window;
|
||||||
gtk_widget_set_visible(app.trigger_cmp_row, pulse);
|
gtk_widget_set_visible(app.trigger_cmp_row, pulse);
|
||||||
gtk_widget_set_visible(app.trigger_width_row, pulse);
|
gtk_widget_set_visible(app.trigger_width_row, pulse);
|
||||||
gtk_widget_set_visible(app.trigger_width2_row,
|
gtk_widget_set_visible(app.trigger_width2_row,
|
||||||
pulse && (cmp == WidthCmp::Within || cmp == WidthCmp::Outside));
|
pulse && (cmp == WidthCmp::Within || cmp == WidthCmp::Outside));
|
||||||
|
gtk_widget_set_visible(app.trigger_level2_row, window);
|
||||||
|
gtk_widget_set_visible(app.trigger_timeout_row, type == TriggerType::Dropout);
|
||||||
|
// A band of zero width is not a degenerate edge trigger -- a window event is a change of
|
||||||
|
// *side*, so equal levels silently never fire, which looks exactly like a broken trigger.
|
||||||
|
// The panel is where that is refused: the second level is pushed at least one increment
|
||||||
|
// clear of the first, in whichever direction it already lay.
|
||||||
|
if (window) {
|
||||||
|
GtkSpinButton* l2 = GTK_SPIN_BUTTON(app.trigger_level2_spin);
|
||||||
|
const double level = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app.trigger_level_spin));
|
||||||
|
double step = 0.0;
|
||||||
|
gtk_spin_button_get_increments(l2, &step, nullptr);
|
||||||
|
const double other = gtk_spin_button_get_value(l2);
|
||||||
|
if (std::fabs(other - level) < step) {
|
||||||
|
gtk_spin_button_set_value(l2, other < level ? level - step : level + step);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void on_trigger_field_changed(GtkWidget*, gpointer user) {
|
void on_trigger_field_changed(GtkWidget*, gpointer user) {
|
||||||
@@ -6614,7 +6643,7 @@ GtkWidget* build_trigger_panel(App& app) {
|
|||||||
G_CALLBACK(on_trigger_signal_changed), &app);
|
G_CALLBACK(on_trigger_signal_changed), &app);
|
||||||
gtk_box_append(GTK_BOX(box), labeled_box("signal", app.trigger_signal_drop));
|
gtk_box_append(GTK_BOX(box), labeled_box("signal", app.trigger_signal_drop));
|
||||||
|
|
||||||
const char* types[] = {"edge", "pulse width", nullptr};
|
const char* types[] = {"edge", "pulse width", "window", "dropout", nullptr};
|
||||||
app.trigger_type_drop = gtk_drop_down_new_from_strings(types);
|
app.trigger_type_drop = gtk_drop_down_new_from_strings(types);
|
||||||
gtk_widget_set_tooltip_text(app.trigger_type_drop,
|
gtk_widget_set_tooltip_text(app.trigger_type_drop,
|
||||||
"what counts as an event: a level crossing, or a pulse whose "
|
"what counts as an event: a level crossing, or a pulse whose "
|
||||||
@@ -6643,6 +6672,19 @@ GtkWidget* build_trigger_panel(App& app) {
|
|||||||
app.trigger_width2_row = labeled_box("and s", app.trigger_width2_spin);
|
app.trigger_width2_row = labeled_box("and s", app.trigger_width2_spin);
|
||||||
gtk_box_append(GTK_BOX(box), app.trigger_width2_row);
|
gtk_box_append(GTK_BOX(box), app.trigger_width2_row);
|
||||||
|
|
||||||
|
app.trigger_level2_spin = spin(-1.0e12, 1.0e12, 0.01, 4);
|
||||||
|
gtk_widget_set_tooltip_text(app.trigger_level2_spin,
|
||||||
|
"the band's other edge; it is kept at least one increment away "
|
||||||
|
"from the level, because a band of zero width can never be "
|
||||||
|
"crossed");
|
||||||
|
app.trigger_level2_row = labeled_box("to level", app.trigger_level2_spin);
|
||||||
|
gtk_box_append(GTK_BOX(box), app.trigger_level2_row);
|
||||||
|
|
||||||
|
app.trigger_timeout_spin = spin(0.0, 3600.0, 0.001, 6);
|
||||||
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_timeout_spin), 0.001);
|
||||||
|
app.trigger_timeout_row = labeled_box("timeout s", app.trigger_timeout_spin);
|
||||||
|
gtk_box_append(GTK_BOX(box), app.trigger_timeout_row);
|
||||||
|
|
||||||
app.trigger_level_spin = spin(-1.0e12, 1.0e12, 0.01, 4);
|
app.trigger_level_spin = spin(-1.0e12, 1.0e12, 0.01, 4);
|
||||||
g_signal_connect(app.trigger_level_spin, "value-changed", G_CALLBACK(+[](GtkWidget*, gpointer u) {
|
g_signal_connect(app.trigger_level_spin, "value-changed", G_CALLBACK(+[](GtkWidget*, gpointer u) {
|
||||||
static_cast<App*>(u)->trigger_level_touched = true;
|
static_cast<App*>(u)->trigger_level_touched = true;
|
||||||
@@ -6674,7 +6716,8 @@ GtkWidget* build_trigger_panel(App& app) {
|
|||||||
gtk_box_append(GTK_BOX(box), live);
|
gtk_box_append(GTK_BOX(box), live);
|
||||||
|
|
||||||
for (GtkWidget* w : {app.trigger_level_spin, app.trigger_pre_spin, app.trigger_holdoff_spin,
|
for (GtkWidget* w : {app.trigger_level_spin, app.trigger_pre_spin, app.trigger_holdoff_spin,
|
||||||
app.trigger_width_spin, app.trigger_width2_spin}) {
|
app.trigger_width_spin, app.trigger_width2_spin,
|
||||||
|
app.trigger_level2_spin, app.trigger_timeout_spin}) {
|
||||||
g_signal_connect(w, "value-changed", G_CALLBACK(on_trigger_field_changed), &app);
|
g_signal_connect(w, "value-changed", G_CALLBACK(on_trigger_field_changed), &app);
|
||||||
}
|
}
|
||||||
g_signal_connect(app.trigger_edge_drop, "notify::selected",
|
g_signal_connect(app.trigger_edge_drop, "notify::selected",
|
||||||
@@ -6685,6 +6728,14 @@ GtkWidget* build_trigger_panel(App& app) {
|
|||||||
G_CALLBACK(on_trigger_shape_changed), &app);
|
G_CALLBACK(on_trigger_shape_changed), &app);
|
||||||
g_signal_connect(app.trigger_cmp_drop, "notify::selected",
|
g_signal_connect(app.trigger_cmp_drop, "notify::selected",
|
||||||
G_CALLBACK(on_trigger_shape_changed), &app);
|
G_CALLBACK(on_trigger_shape_changed), &app);
|
||||||
|
// The clamp lives in `sync_trigger_rows()`, so the two level spins have to run it rather
|
||||||
|
// than the plain field publish.
|
||||||
|
for (GtkWidget* w : {app.trigger_level_spin, app.trigger_level2_spin}) {
|
||||||
|
g_signal_connect(w, "value-changed", G_CALLBACK(+[](GtkWidget*, gpointer u) {
|
||||||
|
sync_trigger_rows(*static_cast<App*>(u));
|
||||||
|
}),
|
||||||
|
&app);
|
||||||
|
}
|
||||||
sync_trigger_rows(app);
|
sync_trigger_rows(app);
|
||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
@@ -8401,28 +8452,39 @@ gboolean fields_probe(gpointer user) {
|
|||||||
return G_SOURCE_REMOVE;
|
return G_SOURCE_REMOVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Headless self-check for the qualified conditions: arms a pulse-width condition on the
|
/// Headless self-check for the qualified conditions. Selected by
|
||||||
/// mock's `square`, whose narrow pulse the mock injects once per square period, and reports
|
/// `UDPSCOPE_PROBE_CONDITION_SHAPE` (0 = pulse `<`, 1 = pulse `>`, 2 = window, 3 = dropout).
|
||||||
/// the measured pulse width from the held event snapshot.
|
|
||||||
///
|
///
|
||||||
/// The assertion is snapshot-relative rather than an absolute t0. The app's stream clock
|
/// The assertion is snapshot-relative rather than an absolute t0. The app's stream clock
|
||||||
/// epoch is the first packet it received, not the mock's t = 0; the two clocks differ by
|
/// epoch is the first packet it received, not the mock's t = 0; the two clocks differ by
|
||||||
/// however many packets the CONNECT handshake cost (typically a whole number of 100-sample /
|
/// however many packets the CONNECT handshake cost (typically a whole number of 100-sample /
|
||||||
/// 10 ms packets), so comparing trigger_time() against a mock-clock target is meaningless.
|
/// 10 ms packets), so comparing trigger_time() against a mock-clock target is meaningless.
|
||||||
///
|
///
|
||||||
/// A held event snapshot's stamps are edge-relative: stamps == 0 is the latching event,
|
/// Shape 0 (pulse `<`): x = 0 is the glitch's closing edge. Scans backwards for the most
|
||||||
/// which for a pulse-width condition is the pulse's *closing* edge. The pulse's opening
|
/// recent upward crossing; `measured = -stamp` is the glitch width (≈ 0.002 s).
|
||||||
/// edge therefore sits at stamps ≈ -glitch_width. The probe scans backwards from x = 0 to
|
|
||||||
/// find the most recent upward crossing of level 0 (the same prev < level && value >= level
|
|
||||||
/// test the detector uses) and negates the crossing's stamp to get the measured pulse width.
|
|
||||||
///
|
///
|
||||||
/// A `measured` near 0.002 s means the condition caught the 2 ms glitch pulse exactly.
|
/// Shape 1 (pulse `>`): same scan; `measured` is the ordinary square half-period (≈ 0.200 s).
|
||||||
/// A `measured` near 0.200 s (the ordinary square half-period) means the condition fired on
|
///
|
||||||
/// an ordinary edge rather than the narrow pulse -- that is the wrong answer to distinguish
|
/// Shape 2 (window): x = 0 is the moment the signal left the band. Reports `at0` (the
|
||||||
/// against. If no snapshot is available or no upward crossing is found before x = 0, the
|
/// sample value at x = 0, which must be outside the band) and `earlier` (a sample a few
|
||||||
/// line still prints with `measured=nan` rather than being skipped, so a failure is still
|
/// stored periods back, which must be inside).
|
||||||
/// one readable line.
|
///
|
||||||
|
/// Shape 3 (dropout): x = 0 is `quiet_at + timeout`, the moment the timeout expired. Scans
|
||||||
|
/// backwards for the most recent level crossing; `measured = -stamp` should equal `timeout`.
|
||||||
|
///
|
||||||
|
/// All legs print one `[condition-probe]` line with `nan` for any unmeasurable field.
|
||||||
struct ConditionProbe {
|
struct ConditionProbe {
|
||||||
|
double glitch_at = 0.25;
|
||||||
|
double glitch_width = 0.002;
|
||||||
|
double square_period = 0.4;
|
||||||
|
/// With `--quiet-at`, the sine stops at a level on the other side of zero, so `quiet_at`
|
||||||
|
/// is its last crossing and the dropout's event time is exactly `quiet_at + timeout`.
|
||||||
|
/// Unlike the glitch this happens *once*, so the assertion is the absolute t0.
|
||||||
|
double quiet_at = -1.0;
|
||||||
|
double timeout = 0.050;
|
||||||
|
/// 0 = pulse width `<` (the glitch), 1 = pulse width `>`, 2 = window, 3 = dropout. Selected
|
||||||
|
/// by `UDPSCOPE_PROBE_CONDITION_SHAPE`; the default keeps Task 3's glitch leg.
|
||||||
|
int shape = 0;
|
||||||
int waited = 0;
|
int waited = 0;
|
||||||
};
|
};
|
||||||
ConditionProbe g_condition;
|
ConditionProbe g_condition;
|
||||||
@@ -8437,23 +8499,36 @@ gboolean condition_probe_report(gpointer user) {
|
|||||||
std::fflush(stderr);
|
std::fflush(stderr);
|
||||||
return G_SOURCE_REMOVE;
|
return G_SOURCE_REMOVE;
|
||||||
}
|
}
|
||||||
// Measure the pulse width from the snapshot's own edge-relative stamps rather than
|
const double nan = std::numeric_limits<double>::quiet_NaN();
|
||||||
// comparing t0 against a mock-clock target. stamps == 0 is the closing edge (the
|
|
||||||
// latching event for a pulse-width condition); the opening edge sits at stamps ≈ -width.
|
|
||||||
// Scan backwards from the last sample at or before x = 0 for the most recent upward
|
|
||||||
// crossing of the condition level, then negate the crossing stamp to get the width.
|
|
||||||
double measured = std::numeric_limits<double>::quiet_NaN();
|
|
||||||
const double level = a->model->trigger().condition.level;
|
const double level = a->model->trigger().condition.level;
|
||||||
|
const double level2 = a->model->trigger().condition.level2;
|
||||||
const std::size_t trig_ch = static_cast<std::size_t>(a->model->trigger().channel);
|
const std::size_t trig_ch = static_cast<std::size_t>(a->model->trigger().channel);
|
||||||
|
|
||||||
|
// Shape 2 reports two sample values rather than a measured width.
|
||||||
|
double at0 = nan;
|
||||||
|
double earlier = nan;
|
||||||
|
double measured = nan;
|
||||||
|
|
||||||
if (std::shared_ptr<const EventSnapshot> snap = a->model->display_snapshot()) {
|
if (std::shared_ptr<const EventSnapshot> snap = a->model->display_snapshot()) {
|
||||||
if (snap != nullptr && trig_ch < snap->values.size()) {
|
if (snap != nullptr && trig_ch < snap->values.size()) {
|
||||||
const std::vector<f64>& st = snap->stamps;
|
const std::vector<f64>& st = snap->stamps;
|
||||||
const std::vector<f32>& vals = snap->values[trig_ch];
|
const std::vector<f32>& vals = snap->values[trig_ch];
|
||||||
if (vals.size() == st.size() && !st.empty()) {
|
if (vals.size() == st.size() && !st.empty()) {
|
||||||
// Find the last index whose stamp is <= 0.
|
// Index of the last stamp <= 0 (the latching event).
|
||||||
const auto it = std::upper_bound(st.begin(), st.end(), 0.0);
|
const auto it = std::upper_bound(st.begin(), st.end(), 0.0);
|
||||||
std::size_t end = static_cast<std::size_t>(it - st.begin());
|
const std::size_t end = static_cast<std::size_t>(it - st.begin());
|
||||||
// Scan backwards for the most recent upward crossing (prev < level, value >= level).
|
|
||||||
|
if (g_condition.shape == 2) {
|
||||||
|
// Window leg: x=0 is the moment the signal left the band.
|
||||||
|
// `at0` is the sample at x=0 (must be outside [lo,hi]).
|
||||||
|
// `earlier` is a sample a few stored periods back (must be inside).
|
||||||
|
if (end >= 1) at0 = static_cast<double>(vals[end - 1]);
|
||||||
|
// "A few stored periods" back: use half the snapshot length as a rough offset.
|
||||||
|
const std::size_t back = end > 4 ? end / 2 : 0;
|
||||||
|
if (back < vals.size()) earlier = static_cast<double>(vals[back]);
|
||||||
|
} else {
|
||||||
|
// Pulse and dropout legs: scan backwards for the most recent upward crossing
|
||||||
|
// (prev < level, value >= level) and negate the stamp to get a duration.
|
||||||
for (std::size_t i = end; i >= 2; --i) {
|
for (std::size_t i = end; i >= 2; --i) {
|
||||||
const double prev = static_cast<double>(vals[i - 2]);
|
const double prev = static_cast<double>(vals[i - 2]);
|
||||||
const double val = static_cast<double>(vals[i - 1]);
|
const double val = static_cast<double>(vals[i - 1]);
|
||||||
@@ -8465,9 +8540,12 @@ gboolean condition_probe_report(gpointer user) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
std::fprintf(stderr, "[condition-probe] type=%d cmp=%d width=%.6f t0=%.6f measured=%.6f mode=%d\n",
|
}
|
||||||
|
std::fprintf(stderr,
|
||||||
|
"[condition-probe] type=%d cmp=%d width=%.6f t0=%.6f measured=%.6f "
|
||||||
|
"at0=%.6f earlier=%.6f level2=%.6f mode=%d\n",
|
||||||
int(a->model->trigger().condition.type), int(a->model->trigger().condition.cmp),
|
int(a->model->trigger().condition.type), int(a->model->trigger().condition.cmp),
|
||||||
a->model->trigger().condition.width_s, t0, measured,
|
a->model->trigger().condition.width_s, t0, measured, at0, earlier, level2,
|
||||||
int(a->model->display_mode()));
|
int(a->model->display_mode()));
|
||||||
std::fflush(stderr);
|
std::fflush(stderr);
|
||||||
return G_SOURCE_REMOVE;
|
return G_SOURCE_REMOVE;
|
||||||
@@ -8480,19 +8558,35 @@ gboolean condition_probe_arm(gpointer user) {
|
|||||||
apply_time_window(*a, 0.2);
|
apply_time_window(*a, 0.2);
|
||||||
const std::vector<ChannelInfo>& channels = a->model->channels();
|
const std::vector<ChannelInfo>& channels = a->model->channels();
|
||||||
int sel = -1;
|
int sel = -1;
|
||||||
|
const bool dropout = g_condition.shape == 3;
|
||||||
|
const char* want_name = dropout ? "sine" : "square";
|
||||||
for (std::size_t i = 0; i < channels.size(); ++i) {
|
for (std::size_t i = 0; i < channels.size(); ++i) {
|
||||||
if (channels[i].name == "square") sel = static_cast<int>(i);
|
if (channels[i].name == want_name) sel = static_cast<int>(i);
|
||||||
}
|
}
|
||||||
std::fprintf(stderr, "[condition-probe] square=%d of %zu channels\n", sel, channels.size());
|
std::fprintf(stderr, "[condition-probe] %s=%d of %zu channels\n", want_name, sel,
|
||||||
|
channels.size());
|
||||||
if (sel >= 0) {
|
if (sel >= 0) {
|
||||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_signal_drop),
|
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_signal_drop),
|
||||||
static_cast<guint>(sel));
|
static_cast<guint>(sel));
|
||||||
}
|
}
|
||||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_type_drop), 1); // pulse width
|
// Shape 2 is `window`, which the dropdown numbers 2; the two pulse-width legs share type 1
|
||||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_cmp_drop), 0); // <
|
// and differ only in the comparison.
|
||||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_edge_drop), 0); // rising
|
const guint type = g_condition.shape == 2 ? 2u : (dropout ? 3u : 1u);
|
||||||
|
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_type_drop), type);
|
||||||
|
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_cmp_drop),
|
||||||
|
g_condition.shape == 1 ? 1u : 0u); // `>` for leg 1, `<` otherwise
|
||||||
|
// `both` for the window and the dropout: a band is entered *and* left, and a dropout has no
|
||||||
|
// direction at all. Rising for the two pulse-width legs, which open on a rising excursion.
|
||||||
|
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_edge_drop),
|
||||||
|
(dropout || g_condition.shape == 2) ? 2u : 0u);
|
||||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_width_spin), 0.010);
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_width_spin), 0.010);
|
||||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_level_spin), 0.0);
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_timeout_spin), g_condition.timeout);
|
||||||
|
// The band for the window leg: straddling zero, well inside the square's +/-1, so each
|
||||||
|
// transition both enters and leaves it. Harmless for the other legs, which ignore `level2`.
|
||||||
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_level2_spin), 0.5);
|
||||||
|
// Zero for every leg but the window, whose band is `[-0.5, +0.5]`.
|
||||||
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_level_spin),
|
||||||
|
g_condition.shape == 2 ? -0.5 : 0.0);
|
||||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_pre_spin), 10.0);
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_pre_spin), 10.0);
|
||||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_holdoff_spin), 0.0);
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_holdoff_spin), 0.0);
|
||||||
publish_trigger_fields(a);
|
publish_trigger_fields(a);
|
||||||
@@ -8506,11 +8600,20 @@ gboolean condition_probe_arm(gpointer user) {
|
|||||||
gboolean condition_probe(gpointer user) {
|
gboolean condition_probe(gpointer user) {
|
||||||
App* a = static_cast<App*>(user);
|
App* a = static_cast<App*>(user);
|
||||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||||
|
// `UDPSCOPE_PROBE_QUIET_AT` mirrors the mock's `--quiet-at`: it selects the dropout leg of
|
||||||
|
// the probe and tells it where the sine's last crossing is. Absent, the glitch leg runs.
|
||||||
|
if (const char* q = g_getenv("UDPSCOPE_PROBE_QUIET_AT")) g_condition.quiet_at = std::atof(q);
|
||||||
|
if (const char* s = g_getenv("UDPSCOPE_PROBE_CONDITION_SHAPE")) {
|
||||||
|
g_condition.shape = std::atoi(s);
|
||||||
|
} else if (g_condition.quiet_at >= 0.0) {
|
||||||
|
g_condition.shape = 3;
|
||||||
|
}
|
||||||
// `square` is not acquired until it is enabled, and the trigger can only name a channel
|
// `square` is not acquired until it is enabled, and the trigger can only name a channel
|
||||||
// that is: enabling is what starts acquisition.
|
// that is: enabling is what starts acquisition.
|
||||||
|
const char* want = g_condition.shape == 3 ? "sine" : "square";
|
||||||
const std::vector<SignalInfo>& signals = a->model->signals();
|
const std::vector<SignalInfo>& signals = a->model->signals();
|
||||||
for (std::size_t i = 0; i < signals.size(); ++i) {
|
for (std::size_t i = 0; i < signals.size(); ++i) {
|
||||||
if (signals[i].name == "square") a->model->set_signal_enabled(i, true);
|
if (signals[i].name == want) a->model->set_signal_enabled(i, true);
|
||||||
}
|
}
|
||||||
// One beat for the channel list to be rebuilt and the ring to refill behind the glitch.
|
// One beat for the channel list to be rebuilt and the ring to refill behind the glitch.
|
||||||
g_timeout_add(1500, condition_probe_arm, a);
|
g_timeout_add(1500, condition_probe_arm, a);
|
||||||
|
|||||||
Reference in new issue
Block a user