feat: a pulse whose width is wrong can be the trigger's event, so a glitch can be caught
This commit is contained in:
1 parent
b3fd9ae00f
commit
d147678b1c
3 files changed
+245
-1
No files matched your search
@@ -14,6 +14,9 @@ void EventDetector::configure(const EventCondition& c) noexcept {
|
||||
void EventDetector::reset() noexcept {
|
||||
have_prev_ = false;
|
||||
prev_ = 0.0;
|
||||
in_pulse_ = false;
|
||||
pulse_up_ = false;
|
||||
pulse_start_s_ = 0.0;
|
||||
}
|
||||
|
||||
double EventDetector::feed(double value, double t) noexcept {
|
||||
@@ -28,6 +31,8 @@ double EventDetector::feed(double value, double t) noexcept {
|
||||
event = feed_edge(value, t);
|
||||
break;
|
||||
case TriggerType::PulseWidth:
|
||||
event = feed_pulse(value, t);
|
||||
break;
|
||||
case TriggerType::Window:
|
||||
case TriggerType::Dropout:
|
||||
break; // implemented in the tasks that add their panel rows
|
||||
@@ -46,4 +51,51 @@ double EventDetector::feed_edge(double value, double t) noexcept {
|
||||
return fired ? t : kNoEvent;
|
||||
}
|
||||
|
||||
bool EventDetector::qualifies(double width_s) const noexcept {
|
||||
const double a = cond_.width_s < cond_.width2_s ? cond_.width_s : cond_.width2_s;
|
||||
const double b = cond_.width_s < cond_.width2_s ? cond_.width2_s : cond_.width_s;
|
||||
switch (cond_.cmp) {
|
||||
case WidthCmp::Less:
|
||||
return width_s < cond_.width_s;
|
||||
case WidthCmp::Greater:
|
||||
return width_s > cond_.width_s;
|
||||
case WidthCmp::Within:
|
||||
return width_s >= a && width_s <= b;
|
||||
case WidthCmp::Outside:
|
||||
return width_s < a || width_s > b;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
double EventDetector::feed_pulse(double value, double t) noexcept {
|
||||
const bool up = crossed_up(value);
|
||||
const bool down = crossed_down(value);
|
||||
if (!in_pulse_) {
|
||||
// Which crossing opens a pulse is what `edge` selects: Rising measures the time spent
|
||||
// above the level, Falling the time below, Both every excursion either way.
|
||||
const bool opens = (cond_.edge == TriggerEdge::Rising && up) ||
|
||||
(cond_.edge == TriggerEdge::Falling && down) ||
|
||||
(cond_.edge == TriggerEdge::Both && (up || down));
|
||||
if (opens) {
|
||||
in_pulse_ = true;
|
||||
pulse_up_ = up;
|
||||
pulse_start_s_ = t;
|
||||
}
|
||||
return kNoEvent;
|
||||
}
|
||||
const bool closes = pulse_up_ ? down : up;
|
||||
if (!closes) return kNoEvent;
|
||||
const double width = t - pulse_start_s_;
|
||||
if (cond_.edge == TriggerEdge::Both) {
|
||||
// The closing edge of one excursion is the opening edge of the next, so a square wave
|
||||
// measures *every* half-period. Dropping the state here instead would measure every
|
||||
// other one, and the hits would come out half as dense as the waveform.
|
||||
pulse_up_ = !pulse_up_;
|
||||
pulse_start_s_ = t;
|
||||
} else {
|
||||
in_pulse_ = false;
|
||||
}
|
||||
return qualifies(width) ? t : kNoEvent;
|
||||
}
|
||||
|
||||
} // namespace udpscope
|
||||
@@ -82,10 +82,18 @@ class EventDetector {
|
||||
return prev_ > cond_.level && v <= cond_.level;
|
||||
}
|
||||
double feed_edge(double value, double t) noexcept;
|
||||
double feed_pulse(double value, double t) noexcept;
|
||||
/// 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
|
||||
/// figure into the first spin.
|
||||
bool qualifies(double width_s) const noexcept;
|
||||
|
||||
EventCondition cond_{};
|
||||
bool have_prev_ = false;
|
||||
double prev_ = 0.0;
|
||||
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.
|
||||
double pulse_start_s_ = 0.0;
|
||||
};
|
||||
|
||||
} // namespace udpscope
|
||||
+185
-1
@@ -131,6 +131,16 @@ struct App {
|
||||
GtkWidget* sidebar_toggle = nullptr;
|
||||
GtkWidget* side_scroll = nullptr;
|
||||
GtkWidget* trigger_edge_drop = nullptr;
|
||||
GtkWidget* trigger_type_drop = nullptr;
|
||||
GtkWidget* trigger_cmp_drop = nullptr;
|
||||
GtkWidget* trigger_width_spin = nullptr;
|
||||
GtkWidget* trigger_width2_spin = nullptr;
|
||||
/// 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
|
||||
/// does not lose what was typed.
|
||||
GtkWidget* trigger_cmp_row = nullptr;
|
||||
GtkWidget* trigger_width_row = nullptr;
|
||||
GtkWidget* trigger_width2_row = nullptr;
|
||||
GtkWidget* trigger_mode_drop = nullptr;
|
||||
GtkWidget* trigger_level_spin = nullptr;
|
||||
GtkWidget* trigger_pre_spin = nullptr;
|
||||
@@ -1596,6 +1606,32 @@ void on_arm_clicked(GtkButton* button, gpointer user) {
|
||||
gtk_button_set_label(GTK_BUTTON(app->arm_button), armed ? "Disarm" : "Arm");
|
||||
}
|
||||
|
||||
/// The trigger type dropdown's order, as a table rather than a cast: the dropdown lists
|
||||
/// only the conditions that are implemented, so its index is not `TriggerType`'s value.
|
||||
TriggerType trigger_type_at(guint i) {
|
||||
switch (i) {
|
||||
case 1:
|
||||
return TriggerType::PulseWidth;
|
||||
default:
|
||||
return TriggerType::Edge;
|
||||
}
|
||||
}
|
||||
|
||||
/// The width-comparison dropdown's order. `<` first, because that is the glitch trigger
|
||||
/// and the reason this condition exists.
|
||||
WidthCmp width_cmp_at(guint i) {
|
||||
switch (i) {
|
||||
case 0:
|
||||
return WidthCmp::Less;
|
||||
case 2:
|
||||
return WidthCmp::Within;
|
||||
case 3:
|
||||
return WidthCmp::Outside;
|
||||
default:
|
||||
return WidthCmp::Greater;
|
||||
}
|
||||
}
|
||||
|
||||
void publish_trigger_fields(App* app) {
|
||||
if (app->updating_panels) return;
|
||||
// Read the widgets first (they may be touched again by a re-entrant handler if a
|
||||
@@ -1606,6 +1642,10 @@ void publish_trigger_fields(App* app) {
|
||||
const double holdoff = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_holdoff_spin));
|
||||
const guint edge = gtk_drop_down_get_selected(GTK_DROP_DOWN(app->trigger_edge_drop));
|
||||
const guint mode = gtk_drop_down_get_selected(GTK_DROP_DOWN(app->trigger_mode_drop));
|
||||
const guint type = gtk_drop_down_get_selected(GTK_DROP_DOWN(app->trigger_type_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 width2 = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_width2_spin));
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(app->model->mutex());
|
||||
TriggerConfig& tc = app->model->trigger();
|
||||
@@ -1616,9 +1656,29 @@ void publish_trigger_fields(App* app) {
|
||||
: edge == 1 ? TriggerEdge::Falling
|
||||
: TriggerEdge::Both;
|
||||
tc.mode = mode == 0 ? TriggerMode::Normal : TriggerMode::Single;
|
||||
tc.condition.type = trigger_type_at(type);
|
||||
tc.condition.cmp = width_cmp_at(cmp);
|
||||
tc.condition.width_s = width;
|
||||
tc.condition.width2_s = width2;
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows only the rows the selected condition reads, so the panel never offers a figure the
|
||||
/// condition ignores. The widgets keep their values while hidden: `EventCondition` carries
|
||||
/// all eight fields whatever its type, and switching away and back must not lose a setting.
|
||||
void sync_trigger_rows(App& app) {
|
||||
if (app.trigger_type_drop == nullptr) return;
|
||||
const TriggerType type =
|
||||
trigger_type_at(gtk_drop_down_get_selected(GTK_DROP_DOWN(app.trigger_type_drop)));
|
||||
const WidthCmp cmp =
|
||||
width_cmp_at(gtk_drop_down_get_selected(GTK_DROP_DOWN(app.trigger_cmp_drop)));
|
||||
const bool pulse = type == TriggerType::PulseWidth;
|
||||
gtk_widget_set_visible(app.trigger_cmp_row, pulse);
|
||||
gtk_widget_set_visible(app.trigger_width_row, pulse);
|
||||
gtk_widget_set_visible(app.trigger_width2_row,
|
||||
pulse && (cmp == WidthCmp::Within || cmp == WidthCmp::Outside));
|
||||
}
|
||||
|
||||
void on_trigger_field_changed(GtkWidget*, gpointer user) {
|
||||
publish_trigger_fields(static_cast<App*>(user));
|
||||
}
|
||||
@@ -1631,6 +1691,14 @@ void on_trigger_drop_changed(GObject*, GParamSpec*, gpointer user) {
|
||||
publish_trigger_fields(static_cast<App*>(user));
|
||||
}
|
||||
|
||||
/// The two dropdowns that change *which rows exist*, as opposed to a value within them.
|
||||
/// Same `notify::selected` signature trap as `on_trigger_drop_changed`.
|
||||
void on_trigger_shape_changed(GObject*, GParamSpec*, gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
publish_trigger_fields(app);
|
||||
sync_trigger_rows(*app);
|
||||
}
|
||||
|
||||
void on_trigger_signal_changed(GObject* obj, GParamSpec*, gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
if (app->updating_panels) return;
|
||||
@@ -6546,10 +6614,35 @@ GtkWidget* build_trigger_panel(App& app) {
|
||||
G_CALLBACK(on_trigger_signal_changed), &app);
|
||||
gtk_box_append(GTK_BOX(box), labeled_box("signal", app.trigger_signal_drop));
|
||||
|
||||
const char* types[] = {"edge", "pulse width", nullptr};
|
||||
app.trigger_type_drop = gtk_drop_down_new_from_strings(types);
|
||||
gtk_widget_set_tooltip_text(app.trigger_type_drop,
|
||||
"what counts as an event: a level crossing, or a pulse whose "
|
||||
"width satisfies the comparison below");
|
||||
gtk_box_append(GTK_BOX(box), labeled_box("condition", app.trigger_type_drop));
|
||||
|
||||
const char* edges[] = {"rising", "falling", "both", nullptr};
|
||||
app.trigger_edge_drop = gtk_drop_down_new_from_strings(edges);
|
||||
gtk_box_append(GTK_BOX(box), labeled_box("edge", app.trigger_edge_drop));
|
||||
|
||||
const char* cmps[] = {"<", ">", "within", "outside", nullptr};
|
||||
app.trigger_cmp_drop = gtk_drop_down_new_from_strings(cmps);
|
||||
gtk_widget_set_tooltip_text(app.trigger_cmp_drop,
|
||||
"how the measured pulse width is compared: '<' is the glitch "
|
||||
"trigger -- a pulse shorter than it should be");
|
||||
app.trigger_cmp_row = labeled_box("width is", app.trigger_cmp_drop);
|
||||
gtk_box_append(GTK_BOX(box), app.trigger_cmp_row);
|
||||
|
||||
app.trigger_width_spin = spin(0.0, 3600.0, 0.001, 6);
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_width_spin), 0.001);
|
||||
app.trigger_width_row = labeled_box("width s", app.trigger_width_spin);
|
||||
gtk_box_append(GTK_BOX(box), app.trigger_width_row);
|
||||
|
||||
app.trigger_width2_spin = spin(0.0, 3600.0, 0.001, 6);
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_width2_spin), 0.002);
|
||||
app.trigger_width2_row = labeled_box("and s", app.trigger_width2_spin);
|
||||
gtk_box_append(GTK_BOX(box), app.trigger_width2_row);
|
||||
|
||||
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) {
|
||||
static_cast<App*>(u)->trigger_level_touched = true;
|
||||
@@ -6580,13 +6673,19 @@ GtkWidget* build_trigger_panel(App& app) {
|
||||
g_signal_connect(live, "clicked", G_CALLBACK(on_go_live), &app);
|
||||
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}) {
|
||||
g_signal_connect(w, "value-changed", G_CALLBACK(on_trigger_field_changed), &app);
|
||||
}
|
||||
g_signal_connect(app.trigger_edge_drop, "notify::selected",
|
||||
G_CALLBACK(on_trigger_drop_changed), &app);
|
||||
g_signal_connect(app.trigger_mode_drop, "notify::selected",
|
||||
G_CALLBACK(on_trigger_drop_changed), &app);
|
||||
g_signal_connect(app.trigger_type_drop, "notify::selected",
|
||||
G_CALLBACK(on_trigger_shape_changed), &app);
|
||||
g_signal_connect(app.trigger_cmp_drop, "notify::selected",
|
||||
G_CALLBACK(on_trigger_shape_changed), &app);
|
||||
sync_trigger_rows(app);
|
||||
return box;
|
||||
}
|
||||
|
||||
@@ -8302,6 +8401,88 @@ gboolean fields_probe(gpointer user) {
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// Headless self-check for the qualified conditions: arms a pulse-width condition on the
|
||||
/// mock's `square`, whose narrow pulse the mock injects once per square period, and reports
|
||||
/// the latched t0 against where the pulse's closing edge must be.
|
||||
///
|
||||
/// The assertion is the *phase* rather than an absolute t0: the probe arms at whatever stream
|
||||
/// time the app happened to reach, so which glitch it catches is not fixed, but every glitch
|
||||
/// closes at `--glitch-at + --glitch-width + k * (2 / --freq)`. A `phase` of a few sample
|
||||
/// periods is the pulse being found exactly; a phase near half the square period means the
|
||||
/// condition fired on an ordinary edge instead.
|
||||
struct ConditionProbe {
|
||||
double glitch_at = 0.25;
|
||||
double glitch_width = 0.002;
|
||||
double square_period = 0.4;
|
||||
int waited = 0;
|
||||
};
|
||||
ConditionProbe g_condition;
|
||||
|
||||
gboolean condition_probe_report(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
const double t0 = a->model->trigger_time();
|
||||
if (!std::isfinite(t0) || t0 <= 0.0) {
|
||||
if (++g_condition.waited < 200) return G_SOURCE_CONTINUE;
|
||||
std::fprintf(stderr, "[condition-probe] no latch after %d polls\n", g_condition.waited);
|
||||
std::fflush(stderr);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
const double want = g_condition.glitch_at + g_condition.glitch_width;
|
||||
double phase = std::fmod(t0 - want, g_condition.square_period);
|
||||
if (phase < 0.0) phase += g_condition.square_period;
|
||||
if (phase > 0.5 * g_condition.square_period) phase -= g_condition.square_period;
|
||||
std::fprintf(stderr, "[condition-probe] type=%d cmp=%d width=%.6f t0=%.6f phase=%.6f mode=%d\n",
|
||||
int(a->model->trigger().condition.type), int(a->model->trigger().condition.cmp),
|
||||
a->model->trigger().condition.width_s, t0, phase, int(a->model->display_mode()));
|
||||
std::fflush(stderr);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
gboolean condition_probe_arm(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
// A window short enough that the pre-trigger share is already behind the glitch.
|
||||
apply_time_window(*a, 0.2);
|
||||
const std::vector<ChannelInfo>& channels = a->model->channels();
|
||||
int sel = -1;
|
||||
for (std::size_t i = 0; i < channels.size(); ++i) {
|
||||
if (channels[i].name == "square") sel = static_cast<int>(i);
|
||||
}
|
||||
std::fprintf(stderr, "[condition-probe] square=%d of %zu channels\n", sel, channels.size());
|
||||
if (sel >= 0) {
|
||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_signal_drop),
|
||||
static_cast<guint>(sel));
|
||||
}
|
||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_type_drop), 1); // pulse width
|
||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_cmp_drop), 0); // <
|
||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_edge_drop), 0); // rising
|
||||
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_pre_spin), 10.0);
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_holdoff_spin), 0.0);
|
||||
publish_trigger_fields(a);
|
||||
if (a->trigger_toggle != nullptr)
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->trigger_toggle), TRUE);
|
||||
g_condition.waited = 0;
|
||||
g_timeout_add(25, condition_probe_report, a);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
gboolean condition_probe(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
// `square` is not acquired until it is enabled, and the trigger can only name a channel
|
||||
// that is: enabling is what starts acquisition.
|
||||
const std::vector<SignalInfo>& signals = a->model->signals();
|
||||
for (std::size_t i = 0; i < signals.size(); ++i) {
|
||||
if (signals[i].name == "square") a->model->set_signal_enabled(i, true);
|
||||
}
|
||||
// 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);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// Headless self-check: drives the window control the four ways a user can -- type a number,
|
||||
/// change the unit under it, and step a decade either way -- and prints what each produced.
|
||||
/// Each line reports the typed request, the window the plot took and the trigger's event
|
||||
@@ -9678,6 +9859,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_TRIGRENUM_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), trigrenum_probe, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_CONDITION_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), condition_probe, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_FIELDS_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), fields_probe, &app);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user