From 407051959adb35855c4361af90c486bf8076bca2 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Sun, 1 Nov 2026 09:46:26 +0100 Subject: [PATCH] e1608: the Analog out and Digital I/O panels Co-Authored-By: Claude Sonnet 4.6 --- CMakeLists.txt | 1 + src/source/e1608/e1608_kind.cpp | 5 + src/source/e1608/e1608_panels.cpp | 345 ++++++++++++++++++++++++++++++ src/source/e1608/e1608_panels.hpp | 16 ++ 4 files changed, 367 insertions(+) create mode 100644 src/source/e1608/e1608_panels.cpp create mode 100644 src/source/e1608/e1608_panels.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ad6fb97..01e1bf4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -246,6 +246,7 @@ add_executable(udpscope src/source/e1608/e1608_device.cpp src/source/e1608/e1608_source.cpp src/source/e1608/e1608_kind.cpp + src/source/e1608/e1608_panels.cpp src/plot_host.cpp src/export.cpp src/search.cpp diff --git a/src/source/e1608/e1608_kind.cpp b/src/source/e1608/e1608_kind.cpp index 5e9efc6..8d0c7f0 100644 --- a/src/source/e1608/e1608_kind.cpp +++ b/src/source/e1608/e1608_kind.cpp @@ -3,6 +3,7 @@ #include "source/e1608/e1608_source.hpp" #include "source/registry.hpp" +#include "source/e1608/e1608_panels.hpp" #include @@ -279,6 +280,10 @@ class E1608Kind final : public SourceKind { out.on_change = [cb](std::function fn) { *cb = std::move(fn); }; return out; } + + std::vector make_panels(DataSource& source) const override { + return make_e1608_panels(source); + } }; } // namespace diff --git a/src/source/e1608/e1608_panels.cpp b/src/source/e1608/e1608_panels.cpp new file mode 100644 index 0000000..50e82c9 --- /dev/null +++ b/src/source/e1608/e1608_panels.cpp @@ -0,0 +1,345 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: 2026 Martino Ferrari + +#include "source/e1608/e1608_panels.hpp" + +#include "source/e1608/e1608_source.hpp" + +#include + +#include +#include +#include +#include +#include + +namespace udpscope { +namespace { + +using namespace e1608; + +constexpr gint64 kRefreshUs = 100000; // the panels redraw their readouts at 10 Hz + +/// A signal closure's argument: the panel's context and the index (DAC or bit) it concerns. +struct Hook { + void* ctx = nullptr; + int idx = 0; +}; + +GtkWidget* spin(const std::string& name, const char* title, double lo, double hi, double step, + double def, guint digits) { + GtkWidget* r = GTK_WIDGET(adw_spin_row_new_with_range(lo, hi, step)); + gtk_widget_set_name(r, name.c_str()); + adw_preferences_row_set_title(ADW_PREFERENCES_ROW(r), title); + adw_spin_row_set_digits(ADW_SPIN_ROW(r), digits); + adw_spin_row_set_value(ADW_SPIN_ROW(r), def); + return r; +} + +GtkWidget* switch_row(const std::string& name, const char* title, const char* subtitle) { + GtkWidget* r = adw_switch_row_new(); + gtk_widget_set_name(r, name.c_str()); + adw_preferences_row_set_title(ADW_PREFERENCES_ROW(r), title); + if (subtitle != nullptr) adw_action_row_set_subtitle(ADW_ACTION_ROW(r), subtitle); + return r; +} + +bool due(gint64& last_us) { + const gint64 now = g_get_monotonic_time(); + if (now - last_us < kRefreshUs) return false; + last_us = now; + return true; +} + +void set_text(GtkWidget* label, const char* text) { + if (std::strcmp(gtk_label_get_text(GTK_LABEL(label)), text) != 0) gtk_label_set_text(GTK_LABEL(label), text); +} + +GtkWidget* panel_root(GtkWidget* a, GtkWidget* b = nullptr) { + GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 18); + gtk_widget_set_margin_top(box, 12); + gtk_widget_set_margin_bottom(box, 12); + gtk_widget_set_margin_start(box, 12); + gtk_widget_set_margin_end(box, 12); + gtk_box_append(GTK_BOX(box), a); + if (b != nullptr) gtk_box_append(GTK_BOX(box), b); + return box; +} + +// --- Analog out ------------------------------------------------------------------------------- + +struct AoWidgets { + GtkWidget *mode = nullptr, *level = nullptr, *amp = nullptr, *offset = nullptr, *freq = nullptr, + *run = nullptr, *last = nullptr; +}; + +struct AoCtx { + E1608Source* src = nullptr; + std::shared_ptr ro; + AoWidgets w[2]; + Hook hooks[2]; + bool updating = false; ///< set while the panel writes its own widgets: those writes post nothing + unsigned epoch_seen = 0; + gint64 last_us = 0; +}; + +void ao_sensitivity(const AoWidgets& w) { + const bool level = adw_combo_row_get_selected(ADW_COMBO_ROW(w.mode)) == 0; + gtk_widget_set_sensitive(w.level, level); + for (GtkWidget* x : {w.amp, w.offset, w.freq, w.run}) gtk_widget_set_sensitive(x, !level); +} + +/// Back to what the source holds after a (re)connect. +void ao_reset(AoCtx* c) { + c->updating = true; + for (int ch = 0; ch < 2; ++ch) { + const AoWidgets& w = c->w[ch]; + adw_combo_row_set_selected(ADW_COMBO_ROW(w.mode), 0); + adw_spin_row_set_value(ADW_SPIN_ROW(w.level), c->src->settings().ao_init[ch]); + adw_spin_row_set_value(ADW_SPIN_ROW(w.amp), 1.0); + adw_spin_row_set_value(ADW_SPIN_ROW(w.offset), 0.0); + adw_spin_row_set_value(ADW_SPIN_ROW(w.freq), 1.0); + adw_switch_row_set_active(ADW_SWITCH_ROW(w.run), FALSE); + ao_sensitivity(w); + } + c->updating = false; +} + +void ao_push(AoCtx* c, int ch) { + if (c->updating) return; + const AoWidgets& w = c->w[ch]; + const guint mode = adw_combo_row_get_selected(ADW_COMBO_ROW(w.mode)); + if (mode == 0 && adw_switch_row_get_active(ADW_SWITCH_ROW(w.run))) { // Level has no generator + c->updating = true; + adw_switch_row_set_active(ADW_SWITCH_ROW(w.run), FALSE); + c->updating = false; + } + ao_sensitivity(w); + AoSettings s; + s.mode = static_cast(mode); + s.level = adw_spin_row_get_value(ADW_SPIN_ROW(w.level)); + s.amplitude = adw_spin_row_get_value(ADW_SPIN_ROW(w.amp)); + s.offset = adw_spin_row_get_value(ADW_SPIN_ROW(w.offset)); + s.freq_hz = adw_spin_row_get_value(ADW_SPIN_ROW(w.freq)); + s.run = adw_switch_row_get_active(ADW_SWITCH_ROW(w.run)); + E1608Source* src = c->src; + src->post([src, ch, s] { src->set_analog_out(ch, s); }); +} + +void on_ao_changed(GObject*, GParamSpec*, gpointer user) { + const Hook* h = static_cast(user); + ao_push(static_cast(h->ctx), h->idx); +} + +void ao_refresh(AoCtx* c) { + const unsigned ep = c->ro->epoch.load(); + if (ep != c->epoch_seen) { + c->epoch_seen = ep; + ao_reset(c); + } + for (int ch = 0; ch < 2; ++ch) { + const double v = c->ro->ao[ch].load(); + char b[48]; + if (std::isnan(v)) + std::snprintf(b, sizeof b, "not written"); + else + std::snprintf(b, sizeof b, "%+.4f V", v); + const char* cur = adw_action_row_get_subtitle(ADW_ACTION_ROW(c->w[ch].last)); + if (cur == nullptr || std::strcmp(cur, b) != 0) + adw_action_row_set_subtitle(ADW_ACTION_ROW(c->w[ch].last), b); + } +} + +gboolean ao_tick(GtkWidget*, GdkFrameClock*, gpointer user) { + AoCtx* c = static_cast(user); + if (due(c->last_us)) ao_refresh(c); + return G_SOURCE_CONTINUE; +} + +GtkWidget* build_ao(E1608Source* src) { + auto* c = new AoCtx; + c->src = src; + c->ro = src->readouts(); + + GtkWidget* groups[2]; + static const char* const kModes[] = {"Level", "Sine", "Square", "Triangle", "Ramp", nullptr}; + for (int ch = 0; ch < 2; ++ch) { + const std::string p = "e1608-ao" + std::to_string(ch); + AoWidgets& w = c->w[ch]; + w.mode = adw_combo_row_new(); + gtk_widget_set_name(w.mode, (p + "-mode").c_str()); + adw_preferences_row_set_title(ADW_PREFERENCES_ROW(w.mode), "Mode"); + adw_combo_row_set_model(ADW_COMBO_ROW(w.mode), G_LIST_MODEL(gtk_string_list_new(kModes))); + w.level = spin(p + "-level", "Level (V)", -10, 10, 0.1, src->settings().ao_init[ch], 3); + w.amp = spin(p + "-amp", "Amplitude (V)", 0, 10, 0.1, 1.0, 3); + w.offset = spin(p + "-offset", "Offset (V)", -10, 10, 0.1, 0.0, 3); + w.freq = spin(p + "-freq", "Frequency (Hz)", 0.1, 10, 0.1, 1.0, 1); + w.run = switch_row(p + "-run", "Run", nullptr); + w.last = adw_action_row_new(); + gtk_widget_set_name(w.last, (p + "-last").c_str()); + adw_preferences_row_set_title(ADW_PREFERENCES_ROW(w.last), "Last written"); + adw_action_row_set_subtitle(ADW_ACTION_ROW(w.last), "not written"); + + GtkWidget* g = adw_preferences_group_new(); + const std::string title = "DAC " + std::to_string(ch); + adw_preferences_group_set_title(ADW_PREFERENCES_GROUP(g), title.c_str()); + for (GtkWidget* r : {w.mode, w.level, w.amp, w.offset, w.freq, w.run, w.last}) + adw_preferences_group_add(ADW_PREFERENCES_GROUP(g), r); + groups[ch] = g; + + c->hooks[ch] = {c, ch}; + g_signal_connect_data(w.mode, "notify::selected", G_CALLBACK(on_ao_changed), &c->hooks[ch], + nullptr, static_cast(0)); + for (GtkWidget* x : {w.level, w.amp, w.offset, w.freq}) + g_signal_connect_data(x, "notify::value", G_CALLBACK(on_ao_changed), &c->hooks[ch], nullptr, + static_cast(0)); + g_signal_connect_data(w.run, "notify::active", G_CALLBACK(on_ao_changed), &c->hooks[ch], + nullptr, static_cast(0)); + ao_sensitivity(w); + } + + GtkWidget* root = panel_root(groups[0], groups[1]); + g_object_set_data_full(G_OBJECT(root), "e1608-ao-ctx", c, + +[](gpointer p) { delete static_cast(p); }); + gtk_widget_add_tick_callback(root, ao_tick, c, nullptr); + return root; +} + +// --- Digital I/O ------------------------------------------------------------------------------ + +struct DioCtx { + E1608Source* src = nullptr; + std::shared_ptr ro; + GtkWidget* led[8] = {}; ///< input bits + GtkWidget* out[8] = {}; ///< output bits (switch rows) + GtkWidget* counter = nullptr; + Hook hooks[8]; + bool updating = false; + unsigned epoch_seen = 0; + gint64 last_us = 0; +}; + +void set_led(GtkWidget* l, bool on) { + if (gtk_widget_has_css_class(l, "success") == on) return; + if (on) { + gtk_widget_remove_css_class(l, "dim-label"); + gtk_widget_add_css_class(l, "success"); + } else { + gtk_widget_remove_css_class(l, "success"); + gtk_widget_add_css_class(l, "dim-label"); + } +} + +void dio_refresh(DioCtx* c) { + const unsigned ep = c->ro->epoch.load(); + if (ep != c->epoch_seen) { // a (re)connect re-latched the outputs from the form + c->epoch_seen = ep; + const unsigned latch = c->ro->do_latch.load(); + c->updating = true; + for (int b = 0; b < 8; ++b) { + if (c->out[b] != nullptr) + adw_switch_row_set_active(ADW_SWITCH_ROW(c->out[b]), ((latch >> b) & 1u) != 0); + } + c->updating = false; + } + const unsigned di = c->ro->di.load(); + for (int b = 0; b < 8; ++b) { + if (c->led[b] != nullptr) set_led(c->led[b], ((di >> b) & 1u) != 0); + } + char buf[24]; + std::snprintf(buf, sizeof buf, "%u", static_cast(c->ro->counter.load())); + set_text(c->counter, buf); +} + +gboolean dio_tick(GtkWidget*, GdkFrameClock*, gpointer user) { + DioCtx* c = static_cast(user); + if (due(c->last_us)) dio_refresh(c); + return G_SOURCE_CONTINUE; +} + +void on_do_changed(GObject* obj, GParamSpec*, gpointer user) { + const Hook* h = static_cast(user); + DioCtx* c = static_cast(h->ctx); + if (c->updating) return; + const bool on = adw_switch_row_get_active(ADW_SWITCH_ROW(obj)); + E1608Source* src = c->src; + const int bit = h->idx; + src->post([src, bit, on] { src->set_output_bit(bit, on); }); +} + +void on_counter_reset(GtkButton*, gpointer user) { + DioCtx* c = static_cast(user); + E1608Source* src = c->src; + src->post([src] { src->reset_counter(); }); +} + +GtkWidget* build_dio(E1608Source* src) { + auto* c = new DioCtx; + c->src = src; + c->ro = src->readouts(); + + GtkWidget* lines = adw_preferences_group_new(); + adw_preferences_group_set_title(ADW_PREFERENCES_GROUP(lines), "Digital lines"); + for (int b = 0; b < 8; ++b) { + const std::string title = "Bit " + std::to_string(b); + if (src->settings().dir_out[b]) { + GtkWidget* row = switch_row("e1608-do" + std::to_string(b), title.c_str(), "Output"); + c->out[b] = row; + c->hooks[b] = {c, b}; + g_signal_connect_data(row, "notify::active", G_CALLBACK(on_do_changed), &c->hooks[b], nullptr, + static_cast(0)); + adw_preferences_group_add(ADW_PREFERENCES_GROUP(lines), row); + } else { + GtkWidget* row = adw_action_row_new(); + adw_preferences_row_set_title(ADW_PREFERENCES_ROW(row), title.c_str()); + adw_action_row_set_subtitle(ADW_ACTION_ROW(row), "Input"); + GtkWidget* led = gtk_label_new("\u25cf"); + gtk_widget_set_name(led, ("e1608-di" + std::to_string(b)).c_str()); + gtk_widget_add_css_class(led, "dim-label"); + adw_action_row_add_suffix(ADW_ACTION_ROW(row), led); + c->led[b] = led; + adw_preferences_group_add(ADW_PREFERENCES_GROUP(lines), row); + } + } + + GtkWidget* counter_group = adw_preferences_group_new(); + adw_preferences_group_set_title(ADW_PREFERENCES_GROUP(counter_group), "Counter"); + GtkWidget* crow = adw_action_row_new(); + adw_preferences_row_set_title(ADW_PREFERENCES_ROW(crow), "Counts"); + c->counter = gtk_label_new("0"); + gtk_widget_set_name(c->counter, "e1608-counter"); + gtk_widget_add_css_class(c->counter, "numeric"); + adw_action_row_add_suffix(ADW_ACTION_ROW(crow), c->counter); + GtkWidget* reset = gtk_button_new_with_label("Reset"); + gtk_widget_set_name(reset, "e1608-counter-reset"); + gtk_widget_set_valign(reset, GTK_ALIGN_CENTER); + g_signal_connect_data(reset, "clicked", G_CALLBACK(on_counter_reset), c, nullptr, + static_cast(0)); + adw_action_row_add_suffix(ADW_ACTION_ROW(crow), reset); + adw_preferences_group_add(ADW_PREFERENCES_GROUP(counter_group), crow); + + GtkWidget* root = panel_root(lines, counter_group); + g_object_set_data_full(G_OBJECT(root), "e1608-dio-ctx", c, + +[](gpointer p) { delete static_cast(p); }); + gtk_widget_add_tick_callback(root, dio_tick, c, nullptr); + return root; +} + +} // namespace + +std::vector make_e1608_panels(DataSource& source) { + E1608Source* src = static_cast(&source); + SourcePanel ao; + ao.id = "ao"; + ao.title = "Analog out"; + ao.icon = "audio-volume-high-symbolic"; + ao.widget = build_ao(src); + SourcePanel dio; + dio.id = "dio"; + dio.title = "Digital I/O"; + dio.icon = "input-dialpad-symbolic"; + dio.widget = build_dio(src); + return {ao, dio}; +} + +} // namespace udpscope diff --git a/src/source/e1608/e1608_panels.hpp b/src/source/e1608/e1608_panels.hpp new file mode 100644 index 0000000..aec0948 --- /dev/null +++ b/src/source/e1608/e1608_panels.hpp @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: 2026 Martino Ferrari + +#pragma once + +#include "source/source_kind.hpp" + +#include + +namespace udpscope { + +/// The E-1608's two capability panels, "Analog out" and "Digital I/O". `source` must be an +/// `e1608::E1608Source`. The widgets reach it only through `source.post()`. +std::vector make_e1608_panels(DataSource& source); + +} // namespace udpscope