From 84aebf41167cd530585f5200e866e22f39a51575 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Tue, 13 Oct 2026 15:42:26 +0200 Subject: [PATCH] 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. --- docs/claude/probe-readings.md | 6 +++++- src/layout_store.cpp | 13 ++++++++++++- src/ui.cpp | 25 ++++++++++++++++++++++--- src/ui_app.hpp | 6 ++++++ src/ui_measure.cpp | 6 ++++++ src/ui_probe.cpp | 14 ++++++++++++-- 6 files changed, 63 insertions(+), 7 deletions(-) diff --git a/docs/claude/probe-readings.md b/docs/claude/probe-readings.md index 6c84e3a..1bb6f8f 100644 --- a/docs/claude/probe-readings.md +++ b/docs/claude/probe-readings.md @@ -851,9 +851,13 @@ popover row reads `failed: ` — one stderr line, no retry storm again [defaults] calibrated: 2.5 -1.25 model=2.5 -1.25 [configs] before=default active=default [configs] save-as=1 names=probe,default active=probe -[configs] round trip read=1 comment='written by the configs probe' buffer_mb=8 relative=0 capture=0 plots=1 signals=4 measures=0 math=0 +[configs] round trip read=1 comment='written by the configs probe' buffer_mb=8 relative=0 capture=0 plots=1 signals=4 measures=0 math=0 noise=-1 hf=-1 [configs] after forget names=default ``` +`noise=` and `hf=` are the first measurement's band and corner as they came back from the file. The probe +sets them to **7.5 and 250** before saving, so a key that never reached the file reads back as the defaults +(10 and 0) instead -- which is the only way a round trip can tell a written key from an absent one. They read +`-1` when the saved configuration holds no measurement, which is what the run above does. | field | value it must read | the defect a wrong reading names | | --- | --- | --- | diff --git a/src/layout_store.cpp b/src/layout_store.cpp index 1e692f3..ebb6d42 100644 --- a/src/layout_store.cpp +++ b/src/layout_store.cpp @@ -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 diff --git a/src/ui.cpp b/src/ui.cpp index 2a4a630..8768f47 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -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 { diff --git a/src/ui_app.hpp b/src/ui_app.hpp index 4d7bee4..539beb4 100644 --- a/src/ui_app.hpp +++ b/src/ui_app.hpp @@ -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 diff --git a/src/ui_measure.cpp b/src/ui_measure.cpp index 7c9009c..7dbab0c 100644 --- a/src/ui_measure.cpp +++ b/src/ui_measure.cpp @@ -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(d->edit) < app.measures.size()) { app.measures[static_cast(d->edit)] = std::move(m); } else { diff --git a/src/ui_probe.cpp b/src/ui_probe.cpp index 76e0f30..d98adcd 100644 --- a/src/ui_probe.cpp +++ b/src/ui_probe.cpp @@ -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());