Feature/source plugins #1

Merged
martino merged 130 commits from feature/source-plugins into main 2026-11-04 07:35:21 +01:00
9 changed files with 185 additions and 30 deletions
Showing only changes of commit bbef7faf3d - Show all commits

No files matched your search

+1 -1
View File
@@ -132,7 +132,7 @@ divider's `+` for a new math channel; when every signal is acquired a dim "every
stands in for the list. While the bar has overflowed into the "N signals" badge the page lists the acquired
signals too, since their rows are then the only way to each signal's page. There is no signal lock: a
signal's page and the list always allow acquire, colour, calibration, the math editor and delete.
A signal badge opens that signal's own page in the slide-over -- colour, calibration and the *Plot on N*
A signal badge opens that signal's own page in the slide-over -- colour, the gain and offset (two aligned numbers, a pencil beside each editing just that one) and the *Plot on N*
buttons -- and is also the drag handle: drag it onto a plot to draw the signal there, or onto another
badge to reorder them. Past **six** signal badges the bar shows a single **"N signals"** badge in their
place (it opens the list, and `+` is hidden): the E-1608's 8 analog inputs and counter would not fit
+2 -2
View File
@@ -66,8 +66,8 @@ the same signal twice, or onto another badge to reorder the badges. The colour i
the same signal is the same colour wherever it appears.
Clicking a signal's badge opens its own page: the colour, **Plot on** buttons for the rows, and
**Calibrate** — gain and offset, applied at ingest, so the measurements and the export carry the same
numbers the plot shows.
**Gain** and **Offset**, shown as two aligned numbers with a pencil button each that edits that one value.
They are applied at ingest, so the measurements and the export carry the same numbers the plot shows.
## 4. The window and navigation
+1 -1
View File
@@ -307,7 +307,7 @@ the assertion: `finite` must be about `M` minus the hole share, and `finite=0` b
non-activatable -- a heading that highlighted under the pointer would read as a row you can plot --
and it carries the `+` that creates a channel, appended even when there are none, because a `+` that
waited for a channel to exist could never create the first one. A math row's menu is *Edit graph…*,
*Default colour*, *Delete*: no Information and no Calibrate, because calibration is applied at
*Default colour*, *Delete*: no Information and no Gain/Offset rows, because calibration is applied at
ingest and a derived channel has no stream to describe, and no rename, because the node editor owns
a channel's name and unit and a second place to change them would be a second answer to the same
question. A row's tick is the channel's own switch and must reach `App::math` -- the copy the
+7 -1
View File
@@ -1802,7 +1802,7 @@ direct manipulation, and `panes:` no longer carries a `signals=` field.)
- **`body_visible=0`** is the settings being off screen, which is what makes the pane a couple of hundred
pixels instead of 580 in `panels:`.
- **The signals list has no lock.** It lists what can be *added* (see `signals panel:`), and the signal
pages always allow acquire, colour, calibrate, math edit and delete.
pages always allow acquire, colour, gain/offset, math edit and delete.
## `panel switches:` and the trigger panel's default sensitivity
@@ -2761,6 +2761,12 @@ the slide-over closed and the page was dropped by `signal_pages_sync()`. `pruned
channel removed) and the slide-over was still on `trigger`, both by `slideover_page()` and by the stack's visible child; `0` is `signal_pages_sync()` parking the stack on the
signals list while the title and the badge marker still name `trigger`; `-1` when there is no `trigger` page or no math channel could be added.
A second line, `[signalpage] calibration identity=1/0 set=2.5/-1.25 pencils=2 pencil_edits_one=1`, reads the page's gain and offset rows
(`udpscope-gain` / `udpscope-offset` labels, one string rule: `format_calibration()`, up to six decimals with trailing zeros dropped): `identity` is what an
uncalibrated signal shows, `set` what the page shows after `set_calibration(2.5, -1.25)` and `signal_pages_sync()` (the Apply path), `pencils=2` the two edit
buttons, and `pencil_edits_one=1` that the gain pencil opens a dialog titled "Gain" with exactly one spin button and that Apply changed the gain only
(offset stayed -1.25) and refilled the label. `-1` is no pencil / no dialog; `0` is a dialog that edits both or a label that did not follow the model.
## `[sigbadges]`
```
+6 -4
View File
@@ -30,8 +30,9 @@
</item>
<item>
<p>Once a signal is acquired its badge appears. Click it to open that signal's page: its
colour, <gui>Plot on</gui> buttons for the rows, and <gui>Information</gui> and
<gui>Calibrate</gui>, where the signal's details and its calibration live.</p>
colour, <gui>Plot on</gui> buttons for the rows, <gui>Information</gui> for the signal's
details, and its <gui>Gain</gui> and <gui>Offset</gui>, each with a pencil button that
edits that one number.</p>
</item>
</steps>
</section>
@@ -42,8 +43,9 @@
<item><p>Signals start disabled, and enabling one is what starts acquiring it. A trigger
on a signal nobody acquires can never fire, so arming a trigger enables its signal for
you.</p></item>
<item><p><gui>Calibrate</gui> on a signal's page opens the gain and offset it is
<em>currently</em> drawn through, so a trace already scaled by a gain of 2 shows 2 —
<item><p>A signal's page shows the gain and offset it is <em>currently</em> drawn
through, so a trace already scaled by a gain of 2 reads 2. The pencil beside a number opens
an editor for that number only, at its current value —
pressing <gui>Apply</gui> confirms it rather than resetting it. A signal nobody
calibrated shows the identity, 1 and 0.</p></item>
<item><p>Calibration is applied <em>at ingest</em>, so changing it re-plans the buffer
+20 -8
View File
@@ -865,15 +865,21 @@ void on_info_clicked(GtkButton* button, gpointer user) {
void on_calibrate_apply(GtkButton*, gpointer user) {
CalibContext* ctx = static_cast<CalibContext*>(user);
ctx->app->model->set_calibration(ctx->signal, gtk_spin_button_get_value(GTK_SPIN_BUTTON(ctx->gain)),
gtk_spin_button_get_value(GTK_SPIN_BUTTON(ctx->offset)));
// A single-field dialog has only its own spin; the other half is the model's record, unchanged.
std::pair<double, double> cal = ctx->app->model->calibration(ctx->signal);
if (ctx->gain != nullptr) cal.first = gtk_spin_button_get_value(GTK_SPIN_BUTTON(ctx->gain));
if (ctx->offset != nullptr) cal.second = gtk_spin_button_get_value(GTK_SPIN_BUTTON(ctx->offset));
ctx->app->model->set_calibration(ctx->signal, cal.first, cal.second);
schedule_layout_save(*ctx->app);
// The signal page quotes the calibration; refill it before the dialog goes.
signal_pages_sync(*ctx->app);
gtk_window_destroy(GTK_WINDOW(ctx->window));
}
void open_calibration(App& app, GtkWindow* parent, std::size_t signal) {
void open_calibration(App& app, GtkWindow* parent, std::size_t signal, CalibField field) {
const SignalInfo& sig = app.model->signals()[signal];
GtkWidget* window = dialog_window(GTK_WIDGET(parent), "Calibrate");
const char* heading = field == CalibField::Gain ? "Gain" : field == CalibField::Offset ? "Offset" : "Calibrate";
GtkWidget* window = dialog_window(GTK_WIDGET(parent), heading);
GtkWidget* box = gtk_window_get_child(GTK_WINDOW(window));
GtkWidget* title = gtk_label_new(sig.name.c_str());
gtk_widget_set_halign(title, GTK_ALIGN_START);
@@ -884,10 +890,16 @@ void open_calibration(App& app, GtkWindow* parent, std::size_t signal) {
// and the values are the model's record, so a signal that is not acquired still shows the
// calibration that will apply to it when it is.
const std::pair<double, double> cal = app.model->calibration(signal);
GtkWidget* gain = spin(-1.0e12, 1.0e12, 0.001, 6, cal.first);
GtkWidget* offset = spin(-1.0e12, 1.0e12, 0.001, 6, cal.second);
gtk_box_append(GTK_BOX(box), labeled_box("gain", gain));
gtk_box_append(GTK_BOX(box), labeled_box("offset", offset));
GtkWidget* gain = nullptr;
GtkWidget* offset = nullptr;
if (field != CalibField::Offset) {
gain = spin(-1.0e12, 1.0e12, 0.001, 6, cal.first);
gtk_box_append(GTK_BOX(box), labeled_box("gain", gain));
}
if (field != CalibField::Gain) {
offset = spin(-1.0e12, 1.0e12, 0.001, 6, cal.second);
gtk_box_append(GTK_BOX(box), labeled_box("offset", offset));
}
auto* ctx = new CalibContext{&app, signal, gain, offset, window};
GtkWidget* cancel = gtk_button_new_with_label("Cancel");
+3 -1
View File
@@ -1711,7 +1711,9 @@ void on_trigger_signal_changed(GObject* obj, GParamSpec*, gpointer user);
void show_info_dialog(GtkWindow* parent, const SignalInfo& sig);
void on_info_clicked(GtkButton* button, gpointer user);
void on_calibrate_apply(GtkButton*, gpointer user);
void open_calibration(App& app, GtkWindow* parent, std::size_t signal);
/// Which of the calibration's two numbers a dialog edits; `Both` is the full dialog.
enum class CalibField { Both, Gain, Offset };
void open_calibration(App& app, GtkWindow* parent, std::size_t signal, CalibField field = CalibField::Both);
GdkContentProvider* on_drag_prepare(GtkDragSource*, double, double, gpointer user);
int signal_color_index(const std::vector<SignalInfo>& signals, std::size_t signal);
std::string color_hex(plot::Color c);
+2 -2
View File
@@ -123,8 +123,8 @@ constexpr HelpHint kHelpHints[] = {
"every row.",
"", &App::trigger_search},
{"signals_config", "Configuring signals",
"A signal's own page, opened by its badge or by its row here, holds the calibration that maps "
"raw counts to a unit, the colour a trace is drawn in, and which plot a signal is drawn on. A "
"A signal's own page, opened by its badge or by its row here, shows the gain and offset that map "
"raw counts to a unit (a pencil edits each), the colour a trace is drawn in, and which plot a signal is drawn on. A "
"signal nothing measures can be switched off there to stop acquiring it.",
"", &App::signals_note},
{"window", "The time window",
+143 -10
View File
@@ -2,13 +2,14 @@
// SPDX-FileCopyrightText: 2026 Martino Ferrari
//
// The per-signal slide-over pages (`signal:<name>`): one signal's acquire switch, colour, the plot it is on,
// its information and calibration (a math channel's editor instead). Opened from a row of the signal list;
// its information, gain and offset (a math channel's editor instead). Opened from a row of the signal list;
// kept in step with the model by `signal_pages_sync()`, which `rebuild_signal_panels()` ends with.
#include "ui_app.hpp"
#include <gtk/gtk.h>
#include <cmath>
#include <cstdio>
#include <mutex>
#include <string>
@@ -31,6 +32,25 @@ std::size_t signal_index(App& app, const std::string& name) {
return std::string::npos;
}
/// A calibration number as written on the page: up to six decimals (what the editor's spin carries),
/// trailing zeros dropped, so the identity reads `1` and `0` and a trim of 0.000125 is not rounded away;
/// out of that range, three-to-six significant digits in exponent form.
std::string format_calibration(double v) {
if (!std::isfinite(v)) return "--";
char buf[64];
const double mag = std::fabs(v);
if (mag >= 1.0e9 || (mag > 0.0 && mag < 1.0e-6)) {
std::snprintf(buf, sizeof(buf), "%.6g", v);
return buf;
}
std::snprintf(buf, sizeof(buf), "%.6f", v);
std::string num = buf;
while (num.size() > 1 && num.back() == '0') num.pop_back();
if (num.back() == '.') num.pop_back();
if (num == "-0") num = "0";
return num;
}
GtkWidget* text_button(const char* label, GtkWidget* parent) {
GtkWidget* b = gtk_button_new_with_label(label);
gtk_box_append(GTK_BOX(parent), b);
@@ -173,7 +193,7 @@ void fill(App& app, const std::string& name, GtkWidget* page) {
}),
&app);
// Information (a streamed signal's), calibration (ditto), and a math channel's editor.
// Information (a streamed signal's), then its gain and offset below, and a math channel's editor.
GtkWidget* more_head = gtk_label_new(sig.math ? "Math channel" : "Signal");
gtk_widget_add_css_class(more_head, "heading");
gtk_widget_set_halign(more_head, GTK_ALIGN_START);
@@ -191,16 +211,65 @@ void fill(App& app, const std::string& name, GtkWidget* page) {
GtkWidget* info = text_button("Information", more);
tag(info, index);
g_signal_connect(info, "clicked", G_CALLBACK(on_info_clicked), &app);
GtkWidget* cal = text_button("Calibrate...", more);
gtk_widget_set_tooltip_text(cal, "gain and offset, applied at ingest");
tag(cal, index);
g_signal_connect(cal, "clicked", G_CALLBACK(+[](GtkButton* b, gpointer user) {
App* a = static_cast<App*>(user);
open_calibration(*a, GTK_WINDOW(a->window), tagged(b));
}),
&app);
}
gtk_box_append(GTK_BOX(page), more);
// Gain and offset, applied at ingest: two rows, labels left, numbers right-aligned in tabular figures, the
// offset's unit in its own column so the digits line up, and a pencil that edits that one value.
if (!sig.math) {
const std::pair<double, double> cal = app.model->calibration(index);
GtkWidget* grid = gtk_grid_new();
gtk_widget_set_name(grid, "udpscope-calibration"); // the probe reads its labels
gtk_grid_set_column_spacing(GTK_GRID(grid), 8);
gtk_grid_set_row_spacing(GTK_GRID(grid), 4);
const struct {
const char* label;
double value;
const std::string unit;
CalibField field;
const char* tip;
} rows[] = {
{"Gain:", cal.first, "", CalibField::Gain, "edit the gain, applied at ingest"},
{"Offset:", cal.second, sig.unit, CalibField::Offset, "edit the offset, applied at ingest"},
};
int r = 0;
for (const auto& row : rows) {
GtkWidget* name_label = gtk_label_new(row.label);
gtk_widget_set_halign(name_label, GTK_ALIGN_START);
gtk_grid_attach(GTK_GRID(grid), name_label, 0, r, 1, 1);
GtkWidget* value = gtk_label_new(format_calibration(row.value).c_str());
gtk_widget_add_css_class(value, "numeric");
gtk_widget_set_name(value, row.field == CalibField::Gain ? "udpscope-gain" : "udpscope-offset");
gtk_label_set_xalign(GTK_LABEL(value), 1.0f);
gtk_label_set_width_chars(GTK_LABEL(value), 12);
gtk_label_set_ellipsize(GTK_LABEL(value), PANGO_ELLIPSIZE_START);
gtk_widget_set_hexpand(value, true);
gtk_widget_set_halign(value, GTK_ALIGN_END);
gtk_grid_attach(GTK_GRID(grid), value, 1, r, 1, 1);
GtkWidget* unit = gtk_label_new(row.unit.c_str());
gtk_widget_add_css_class(unit, "dim-label");
gtk_label_set_xalign(GTK_LABEL(unit), 0.0f);
gtk_label_set_width_chars(GTK_LABEL(unit), 4);
gtk_label_set_ellipsize(GTK_LABEL(unit), PANGO_ELLIPSIZE_END);
gtk_grid_attach(GTK_GRID(grid), unit, 2, r, 1, 1);
GtkWidget* edit = gtk_button_new_from_icon_name("document-edit-symbolic");
gtk_widget_add_css_class(edit, "flat");
gtk_widget_set_tooltip_text(edit, row.tip);
gtk_accessible_update_property(GTK_ACCESSIBLE(edit), GTK_ACCESSIBLE_PROPERTY_LABEL, row.tip, -1);
tag(edit, index);
g_object_set_data(G_OBJECT(edit), "udpscope-field", GINT_TO_POINTER(static_cast<int>(row.field)));
g_signal_connect(edit, "clicked", G_CALLBACK(+[](GtkButton* b, gpointer user) {
App* a = static_cast<App*>(user);
open_calibration(*a, GTK_WINDOW(a->window), tagged(b),
static_cast<CalibField>(GPOINTER_TO_INT(
g_object_get_data(G_OBJECT(b), "udpscope-field"))));
}),
&app);
gtk_grid_attach(GTK_GRID(grid), edit, 3, r, 1, 1);
++r;
}
gtk_box_append(GTK_BOX(page), grid);
}
}
} // namespace
@@ -437,6 +506,70 @@ gboolean signal_page_probe(gpointer user) {
}
slideover_close(a);
}
// Gain and offset on the page: the labels follow the model, and a pencil edits just its own number.
const std::pair<double, double> cal0 = a.model->calibration(index);
const auto label_text = [&](const char* id) -> std::string {
auto it = a.signal_pages.find(name);
if (it == a.signal_pages.end()) return "?";
GtkWidget* l = find_descendant(it->second, [&](GtkWidget* w) {
return GTK_IS_LABEL(w) && std::string(gtk_widget_get_name(w)) == id;
});
return l == nullptr ? "?" : gtk_label_get_text(GTK_LABEL(l));
};
const std::string cal_identity = label_text("udpscope-gain") + "/" + label_text("udpscope-offset");
a.model->set_calibration(index, 2.5, -1.25);
signal_pages_sync(a);
const std::string cal_set = label_text("udpscope-gain") + "/" + label_text("udpscope-offset");
int pencils = 0;
int pencil_edits_one = -1;
{
auto it = a.signal_pages.find(name);
GtkWidget* grid = it == a.signal_pages.end() ? nullptr : find_descendant(it->second, [](GtkWidget* w) {
return std::string(gtk_widget_get_name(w)) == "udpscope-calibration";
});
GtkWidget* gain_pencil = nullptr;
if (grid != nullptr) {
for (GtkWidget* c = gtk_widget_get_first_child(grid); c != nullptr; c = gtk_widget_get_next_sibling(c)) {
if (GTK_IS_BUTTON(c) && gtk_button_get_icon_name(GTK_BUTTON(c)) != nullptr) {
if (pencils++ == 0) gain_pencil = c;
}
}
}
if (gain_pencil != nullptr) {
g_signal_emit_by_name(gain_pencil, "clicked");
GListModel* windows = gtk_window_get_toplevels();
for (guint i = 0; i < g_list_model_get_n_items(windows); ++i) {
GtkWindow* w = GTK_WINDOW(g_list_model_get_item(windows, i));
const char* title = gtk_window_get_title(w);
if (title != nullptr && std::string(title) == "Gain") {
GtkWidget* body = gtk_window_get_child(w);
int spins = 0;
find_descendant(body, [&](GtkWidget* x) {
if (GTK_IS_SPIN_BUTTON(x)) ++spins;
return false;
});
GtkWidget* spin_w = find_descendant(body, [](GtkWidget* x) { return GTK_IS_SPIN_BUTTON(x); });
GtkWidget* apply = find_descendant(body, [](GtkWidget* x) { return is_button_labelled(x, "Apply"); });
if (spin_w != nullptr && apply != nullptr) {
gtk_spin_button_set_value(GTK_SPIN_BUTTON(spin_w), 4.0);
g_signal_emit_by_name(apply, "clicked");
const std::pair<double, double> after = a.model->calibration(index);
pencil_edits_one = spins == 1 && after.first == 4.0 && after.second == -1.25 &&
label_text("udpscope-gain") == "4"
? 1
: 0;
} else {
gtk_window_close(w);
}
}
g_object_unref(w);
}
}
}
a.model->set_calibration(index, cal0.first, cal0.second);
signal_pages_sync(a);
std::fprintf(stderr, "[signalpage] calibration identity=%s set=%s pencils=%d pencil_edits_one=%d\n",
cal_identity.c_str(), cal_set.c_str(), pencils, pencil_edits_one);
std::fprintf(stderr,
"[signalpage] name=%s open=%d pages=%d enable_visible=%d plot_buttons=%d plot_on_row0=%d "
"plot_on_row0_disabled=%d "