From 24bdd6b9301a393462827a7aa8d920d93861c89e Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Mon, 2 Nov 2026 10:17:01 +0100 Subject: [PATCH] e1608: a Scan network row in the connection form, with click-to-fill results Co-Authored-By: Claude Opus 4.6 --- src/source/e1608/e1608_kind.cpp | 137 +++++++++++++++++++++++++++++++- 1 file changed, 133 insertions(+), 4 deletions(-) diff --git a/src/source/e1608/e1608_kind.cpp b/src/source/e1608/e1608_kind.cpp index 2f89931..5dc847b 100644 --- a/src/source/e1608/e1608_kind.cpp +++ b/src/source/e1608/e1608_kind.cpp @@ -11,6 +11,7 @@ #include #include #include +#include namespace udpscope { namespace { @@ -82,6 +83,11 @@ struct Form { ChangeCb changed = std::make_shared>(); bool updating = false; bool alive = true; ///< cleared when the chooser destroys the rows; handlers then touch nothing + GtkWidget* scan_row = nullptr; ///< the "Scan network" expander; the results are its rows + std::vector results; ///< the result rows of the current scan + std::unique_ptr scan; ///< non-null while a scan is running + guint timer = 0; ///< the GLib source polling `scan`, 0 when none + int scan_ms = 0; ///< time spent in the running scan bool diff() const { return sel_of(mode) == 1; } int enabled_ai() const { @@ -164,6 +170,24 @@ struct Form { refresh(); } + /// A result click: puts one way of naming the board in and the other two fields out, under the + /// `updating` guard so the dialog is told once. + void fill(const std::string& addr, const std::string& mac_text) { + if (!alive) return; + updating = true; + gtk_editable_set_text(GTK_EDITABLE(address), addr.c_str()); + gtk_editable_set_text(GTK_EDITABLE(mac), mac_text.c_str()); + gtk_editable_set_text(GTK_EDITABLE(subnet), ""); + updating = false; + if (changed && *changed) (*changed)(); + } + + void stop_scan() { + if (timer != 0) g_source_remove(timer); + timer = 0; + scan.reset(); + } + bool valid() const { return enabled_ai() > 0 && E1608Settings::from(read()).problem().empty(); } }; @@ -185,7 +209,11 @@ void on_edited(GtkEditable*, gpointer user) { void free_form(gpointer d, GClosure*) { delete static_cast(d); } /// "destroy" on a row group: the siblings the handlers reach by raw pointer may already be gone. -void on_form_destroy(GtkWidget*, gpointer user) { (**static_cast(user)).alive = false; } +void on_form_destroy(GtkWidget*, gpointer user) { + Form& f = **static_cast(user); + f.alive = false; + f.stop_scan(); // the poll must not outlive the rows it writes to +} void hook_destroy(GtkWidget* w, const FormPtr& f) { g_signal_connect_data(w, "destroy", G_CALLBACK(on_form_destroy), new FormPtr(f), free_form, @@ -202,6 +230,96 @@ void hook_entry(GtkWidget* w, const FormPtr& f) { static_cast(0)); } +constexpr int kScanMs = 1500; ///< how long a scan listens +constexpr int kTickMs = 50; ///< how often it drains the sockets + +/// A result row's two ways to name the board, with the form they fill. +struct Pick { + FormPtr form; + std::string ip, mac; +}; + +void free_pick(gpointer d, GClosure*) { delete static_cast(d); } + +void on_pick_address(AdwActionRow*, gpointer user) { + Pick& p = *static_cast(user); + p.form->fill(p.ip, ""); +} + +void on_pick_mac(GtkButton*, gpointer user) { + Pick& p = *static_cast(user); + p.form->fill("", p.mac); +} + +void add_result(const FormPtr& f, const DeviceInfo& d) { + const std::string mac = format_mac(d.mac); + GtkWidget* row = adw_action_row_new(); + gtk_widget_set_name(row, "e1608-result"); + adw_preferences_row_set_title(ADW_PREFERENCES_ROW(row), d.ip.c_str()); + adw_action_row_set_subtitle(ADW_ACTION_ROW(row), mac.c_str()); + gtk_list_box_row_set_activatable(GTK_LIST_BOX_ROW(row), TRUE); + GtkWidget* use_mac = gtk_button_new_with_label("Use MAC"); + gtk_widget_set_name(use_mac, "e1608-use-mac"); + gtk_widget_set_valign(use_mac, GTK_ALIGN_CENTER); + adw_action_row_add_suffix(ADW_ACTION_ROW(row), use_mac); + g_signal_connect_data(row, "activated", G_CALLBACK(on_pick_address), + new Pick{f, d.ip, mac}, free_pick, static_cast(0)); + g_signal_connect_data(use_mac, "clicked", G_CALLBACK(on_pick_mac), new Pick{f, d.ip, mac}, + free_pick, static_cast(0)); + adw_expander_row_add_row(ADW_EXPANDER_ROW(f->scan_row), row); + adw_expander_row_set_expanded(ADW_EXPANDER_ROW(f->scan_row), TRUE); + f->results.push_back(row); +} + +void set_scan_status(Form& f, const std::string& text) { + adw_expander_row_set_subtitle(ADW_EXPANDER_ROW(f.scan_row), text.c_str()); +} + +/// 50 ms poll of the running scan. The rows appear as boards answer; at `kScanMs` the status is +/// settled and the scan (its sockets) is released. +gboolean on_scan_tick(gpointer user) { + const FormPtr& fp = *static_cast(user); + Form& f = *fp; + if (!f.alive || !f.scan) { + f.timer = 0; + return G_SOURCE_REMOVE; + } + const std::vector found = f.scan->poll(); + while (f.results.size() < found.size()) add_result(fp, found[f.results.size()]); + f.scan_ms += kTickMs; + if (f.scan_ms < kScanMs) return G_SOURCE_CONTINUE; + if (found.empty()) + set_scan_status(f, "No board found"); + else if (found.size() == 1) + set_scan_status(f, "1 board found"); + else + set_scan_status(f, std::to_string(found.size()) + " boards found"); + f.scan.reset(); + f.timer = 0; + return G_SOURCE_REMOVE; +} + +void free_form_ptr(gpointer d) { delete static_cast(d); } + +/// A new scan on the Port field's value: the previous results go, a running scan is replaced. +void start_scan(const FormPtr& fp) { + Form& f = *fp; + if (!f.alive) return; + f.stop_scan(); + for (GtkWidget* r : f.results) adw_expander_row_remove(ADW_EXPANDER_ROW(f.scan_row), r); + f.results.clear(); + adw_expander_row_set_expanded(ADW_EXPANDER_ROW(f.scan_row), FALSE); + f.scan = std::make_unique(static_cast(value_of(f.port))); + f.scan_ms = 0; + set_scan_status(f, "Scanning..."); + f.timer = g_timeout_add_full(G_PRIORITY_DEFAULT, kTickMs, on_scan_tick, new FormPtr(fp), + free_form_ptr); +} + +void on_scan_clicked(GtkButton*, gpointer user) { + start_scan(*static_cast(user)); +} + class E1608Kind final : public SourceKind { public: std::string id() const override { return "e1608"; } @@ -253,8 +371,18 @@ class E1608Kind final : public SourceKind { f->ao_init[0] = spin_row("Initial DAC 0 (V)", -10, 10, 0.1, 0, 3); f->ao_init[1] = spin_row("Initial DAC 1 (V)", -10, 10, 0.1, 0, 3); - GtkWidget* conn = expander("Connection", {f->address, f->mac, f->subnet, f->port, f->code, - f->timeout}); + f->scan_row = adw_expander_row_new(); + gtk_widget_set_name(f->scan_row, "e1608-scan-row"); + adw_preferences_row_set_title(ADW_PREFERENCES_ROW(f->scan_row), "Scan network"); + GtkWidget* scan_button = gtk_button_new_with_label("Scan"); + gtk_widget_set_name(scan_button, "e1608-scan"); + gtk_widget_set_valign(scan_button, GTK_ALIGN_CENTER); + adw_expander_row_add_suffix(ADW_EXPANDER_ROW(f->scan_row), scan_button); + g_signal_connect_data(scan_button, "clicked", G_CALLBACK(on_scan_clicked), new FormPtr(f), + free_form, static_cast(0)); + + GtkWidget* conn = expander("Connection", {f->scan_row, f->address, f->mac, f->subnet, f->port, + f->code, f->timeout}); GtkWidget* acq = expander("Acquisition", {f->rate, f->ext_clock, f->trigger, f->auto_restart, f->calibrate, f->poll_rate}); GtkWidget* ai = expander("Analog inputs", {f->mode, f->ai[0], f->ai[1], f->ai[2], f->ai[3], @@ -283,7 +411,8 @@ class E1608Kind final : public SourceKind { ConfigForm out; out.rows = {conn, acq, ai, dio}; out.description = - "Give an address, or a MAC address to look up (optionally with a subnet such as 192.168.1)"; + "Give an address, or a MAC address to look up (optionally with a subnet such as 192.168.1), " + "or scan the network for boards"; out.focus = f->address; out.write = [f](const SourceConfig& c) { f->write(c); }; out.read = [f]() { return f->read(); };