ui: a measurement's noise band and HF reject are settable and persisted

Two rows in the measure dialog, built the way the threshold and side rows are and appended
beside them: the band as a percentage of the window's own peak-to-peak (the unit the engine
takes), the corner in Hz with 0 off. They are seeded when editing, shown only where they do
something -- the band for the crossing types, since it moves the edge decision and nothing
else, the reject for all of them, since it sits before both walks -- and read into
`MeasureOptions` on apply.

They persist as two *named* keys, `noise` and `hf`, on the same `o <<` chain as the keys
beside them and read back through the same keyed reader, so a session written before this
change loads with 10.0 and 0.0: today's behaviour exactly, with no version check.

[configs] now proves the round trip rather than assuming it. The probe sets the first
measurement's band and corner to 7.5 and 250 before saving, so a key that never reached the
file reads back as the defaults:

    [configs] round trip read=1 comment='written by the configs probe' ... measures=4 math=0 noise=7.5 hf=250

The rest of that line is unchanged, and the run is free of Gtk-CRITICAL.
This commit is contained in:
Martino Ferrari committed 2026-10-13 15:42:26 +02:00
1 parent 380ddb9a77
commit 84aebf4116
6 files changed
+63 -7

No files matched your search

+12 -1
View File
@@ -556,6 +556,16 @@ bool load_layout(const std::string& path, LayoutState& out) {
m.options.threshold_pct = std::stod(value);
} catch (...) {
}
} else if (key == "noise") {
try {
m.options.noise_pct = std::stod(value);
} catch (...) {
}
} else if (key == "hf") {
try {
m.options.hf_fc = std::stod(value);
} catch (...) {
}
} else if (key == "above") {
m.options.above = value == "true";
} else if (key == "show") {
@@ -668,7 +678,8 @@ bool save_layout(const std::string& path, const LayoutState& state) {
o << "\n[[measure]]\nsignal = " << toml_escape(m.signal) << "\ntype = " << kind
<< "\nthreshold = " << m.options.threshold_pct
<< "\nabove = " << (m.options.above ? "true" : "false") << "\nshow = "
<< (m.options.show_on_plot ? "true" : "false") << "\n";
<< (m.options.show_on_plot ? "true" : "false") << "\nnoise = " << m.options.noise_pct
<< "\nhf = " << m.options.hf_fc << "\n";
}
// A sibling temp file + rename: a crash mid-write leaves the old layout, not a
+22 -3
View File
@@ -1678,15 +1678,32 @@ void open_measure_dialog(App& app, int edit_index) {
GtkWidget* side_row = labelled_row("pulse above level", above, &side_label);
gtk_box_append(GTK_BOX(box), side_row);
// A percentage of the window's own peak-to-peak, which is the unit the engine takes.
GtkWidget* noise = spin(0.0, 50.0, 1.0, 1, 10.0);
// `labelled_row()` writes the label out unconditionally, and these two never retitle, so the
// out-parameters are named and then explicitly unused rather than passed as nullptr.
GtkWidget* noise_label = nullptr;
GtkWidget* noise_row = labelled_row("noise band %", noise, &noise_label);
gtk_box_append(GTK_BOX(box), noise_row);
(void)noise_label;
// 0 is off. The engine clamps to just under the stream's Nyquist, which the dialog cannot know.
GtkWidget* hf = spin(0.0, 1.0e7, 1.0, 3, 0.0);
GtkWidget* hf_label = nullptr;
GtkWidget* hf_row = labelled_row("HF reject Hz", hf, &hf_label);
gtk_box_append(GTK_BOX(box), hf_row);
(void)hf_label;
GtkWidget* show = gtk_check_button_new_with_label("show on plot");
gtk_widget_set_tooltip_text(show,
"draw the result on the plot: a ruler at the level, a marker "
"at the time it happened, the pulse's edges as markers");
gtk_box_append(GTK_BOX(box), show);
auto* d = new MeasureDialog{&app, edit_index, signal_drop, type_drop,
threshold, above, show, window,
threshold_row, threshold_label, side_row, side_label,
auto* d = new MeasureDialog{&app, edit_index, signal_drop, type_drop,
threshold, above, show, window,
threshold_row, threshold_label, side_row, side_label,
noise, noise_row, hf, hf_row,
sig_names};
app.measure_dialog = window;
g_signal_connect(window, "destroy", G_CALLBACK(+[](GtkWidget*, gpointer user) {
@@ -1709,6 +1726,8 @@ void open_measure_dialog(App& app, int edit_index) {
}
}
gtk_spin_button_set_value(GTK_SPIN_BUTTON(threshold), editing->options.threshold_pct);
gtk_spin_button_set_value(GTK_SPIN_BUTTON(noise), editing->options.noise_pct);
gtk_spin_button_set_value(GTK_SPIN_BUTTON(hf), editing->options.hf_fc);
gtk_switch_set_active(GTK_SWITCH(above), editing->options.above);
gtk_check_button_set_active(GTK_CHECK_BUTTON(show), editing->options.show_on_plot);
} else {
+6
View File
@@ -910,6 +910,12 @@ struct MeasureDialog {
GtkWidget* threshold_label = nullptr;
GtkWidget* side_row = nullptr;
GtkWidget* side_label = nullptr;
/// The two noise-rejection rows. `noise` is a percentage of the window's peak-to-peak, `hf` a corner in
/// Hz with 0 off; both are shown only where they do something (see `measure_dialog_sync`).
GtkWidget* noise = nullptr;
GtkWidget* noise_row = nullptr;
GtkWidget* hf = nullptr;
GtkWidget* hf_row = nullptr;
/// The names the signal dropdown was built from, in order. `apply` resolves an index
/// through *this*, not through the model's signal list: the dropdown lists the plotted
/// traces, so index `i` here is the i-th trace -- indexing the CONFIG with it selected
+6
View File
@@ -94,6 +94,10 @@ void measure_dialog_sync(MeasureDialog* d) {
const bool side = measure_uses_side(type);
gtk_widget_set_visible(d->threshold_row, threshold);
gtk_widget_set_visible(d->side_row, side);
// The band moves the edge decision, so it is shown for exactly the crossing types. The HF reject sits
// before both walks, so it is shown for all of them.
gtk_widget_set_visible(d->noise_row, measure_uses_noise(type));
gtk_widget_set_visible(d->hf_row, TRUE);
gtk_widget_set_visible(d->show, measure_can_show_on_plot(type));
// The words follow the type: one row, several meanings.
@@ -166,6 +170,8 @@ void on_measure_dialog_apply(GtkButton*, gpointer user) {
m.options.threshold_pct = gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->threshold));
m.options.above = gtk_switch_get_active(GTK_SWITCH(d->above));
m.options.show_on_plot = gtk_check_button_get_active(GTK_CHECK_BUTTON(d->show));
m.options.noise_pct = gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->noise));
m.options.hf_fc = gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->hf));
if (d->edit >= 0 && static_cast<std::size_t>(d->edit) < app.measures.size()) {
app.measures[static_cast<std::size_t>(d->edit)] = std::move(m);
} else {
+12 -2
View File
@@ -2953,6 +2953,13 @@ gboolean configs_probe(gpointer user) {
std::fprintf(stderr, "[configs] before=%s active=%s\n", joined().c_str(),
active_configuration(a->source_host, a->source_port).c_str());
// A non-default band and corner before the save, so the round trip below proves the two keys are
// *written*: left at their defaults, a key that never made it to the file would read back the same.
const bool has_measure = !a->measures.empty();
if (has_measure) {
a->measures.front().options.noise_pct = 7.5;
a->measures.front().options.hf_fc = 250.0;
}
const bool saved = save_configuration_as(*a, "probe", "written by the configs probe");
std::fprintf(stderr, "[configs] save-as=%d names=%s active=%s\n", saved ? 1 : 0,
joined().c_str(), active_configuration(a->source_host, a->source_port).c_str());
@@ -2967,11 +2974,14 @@ gboolean configs_probe(gpointer user) {
if (!sig.empty()) ++signals;
}
}
const double noise_back = st.measures.empty() ? -1.0 : st.measures.front().options.noise_pct;
const double hf_back = st.measures.empty() ? -1.0 : st.measures.front().options.hf_fc;
std::fprintf(stderr,
"[configs] round trip read=%d comment='%s' buffer_mb=%.6g relative=%d "
"capture=%u plots=%zu signals=%d measures=%zu math=%zu\n",
"capture=%u plots=%zu signals=%d measures=%zu math=%zu noise=%.6g hf=%.6g\n",
read ? 1 : 0, st.comment.c_str(), st.buffer_mb, st.relative_time ? 1 : 0,
st.capture_packets, st.rows.size(), signals, st.measures.size(), st.math.size());
st.capture_packets, st.rows.size(), signals, st.measures.size(), st.math.size(),
noise_back, hf_back);
forget_configuration(a->source_host, a->source_port, "probe");
std::fprintf(stderr, "[configs] after forget names=%s\n", joined().c_str());