Feature/source plugins #1
No files matched your search
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]`
|
||||
|
||||
```
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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 "
|
||||
|
||||
Reference in new issue
Block a user