// 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