// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 Martino Ferrari #include "source/flatudp/flatudp_source.hpp" #include "source/registry.hpp" #include #include #include #include #include #include #include #include #include namespace udpscope { namespace { using namespace flatudp; using ChangeCb = std::shared_ptr>; std::string num(double v) { char b[48]; std::snprintf(b, sizeof b, "%.10g", v); return b; } std::string trim(const std::string& s) { std::size_t a = 0, b = s.size(); while (a < b && std::isspace(static_cast(s[a]))) ++a; while (b > a && std::isspace(static_cast(s[b - 1]))) --b; return s.substr(a, b - a); } GtkWidget* entry_row(const char* title) { GtkWidget* r = adw_entry_row_new(); adw_preferences_row_set_title(ADW_PREFERENCES_ROW(r), title); adw_preferences_row_set_use_underline(ADW_PREFERENCES_ROW(r), TRUE); return r; } GtkWidget* spin_row(const char* title, double lo, double hi, double step, double def, int digits) { GtkWidget* r = GTK_WIDGET(adw_spin_row_new_with_range(lo, hi, step)); adw_preferences_row_set_title(ADW_PREFERENCES_ROW(r), title); adw_preferences_row_set_use_underline(ADW_PREFERENCES_ROW(r), TRUE); adw_spin_row_set_digits(ADW_SPIN_ROW(r), static_cast(digits)); adw_spin_row_set_value(ADW_SPIN_ROW(r), def); return r; } GtkWidget* combo_row(const char* title, const char* const* items) { GtkWidget* r = adw_combo_row_new(); adw_preferences_row_set_title(ADW_PREFERENCES_ROW(r), title); adw_preferences_row_set_use_underline(ADW_PREFERENCES_ROW(r), TRUE); adw_combo_row_set_model(ADW_COMBO_ROW(r), G_LIST_MODEL(gtk_string_list_new(items))); return r; } GtkWidget* expander(const char* title, std::initializer_list rows) { GtkWidget* e = adw_expander_row_new(); adw_preferences_row_set_title(ADW_PREFERENCES_ROW(e), title); for (GtkWidget* r : rows) adw_expander_row_add_row(ADW_EXPANDER_ROW(e), r); return e; } std::string text_of(GtkWidget* w) { return gtk_editable_get_text(GTK_EDITABLE(w)); } double value_of(GtkWidget* w) { return adw_spin_row_get_value(ADW_SPIN_ROW(w)); } guint sel_of(GtkWidget* w) { return adw_combo_row_get_selected(ADW_COMBO_ROW(w)); } void mark(GtkWidget* w, bool bad) { if (bad) { gtk_widget_add_css_class(w, "error"); } else { gtk_widget_remove_css_class(w, "error"); } } /// One signal's editing widgets; `row` is the `AdwEntryRow` that holds the others. `vec` ("Vector as": /// Channels or Waveform) is shown only while `count` is above 1. struct SigRow { GtkWidget *row, *type, *count, *vec; }; /// The form's widgets, shared by every signal closure and every `ConfigForm` closure. The raw /// widget pointers are valid while the chooser keeps the rows. struct Form : std::enable_shared_from_this
{ GtkWidget *port, *bind, *multicast, *iface, *rate, *endian, *group; std::vector sigs; ChangeCb changed = std::make_shared>(); bool updating = false; bool alive = true; ///< cleared when the chooser destroys the rows; handlers then touch nothing std::vector rows_now() const { std::vector rows; for (const SigRow& s : sigs) { Row r; r.name = trim(text_of(s.row)); r.type = static_cast(std::min(gtk_drop_down_get_selected(GTK_DROP_DOWN(s.type)), 9)); r.count = static_cast(gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(s.count))); r.wave = r.count > 1 && gtk_drop_down_get_selected(GTK_DROP_DOWN(s.vec)) == 1; rows.push_back(r); } return rows; } /// The first `S` that no row uses. std::string next_name() const { const std::vector rows = rows_now(); for (int n = 1;; ++n) { const std::string name = "S" + std::to_string(n); bool used = false; for (const Row& r : rows) used = used || r.name == name; if (!used) return name; } } /// Bad addresses are marked with the `error` style; the group's subtitle carries the signal list's /// own state, the first rule it breaks or the size it implies. void refresh() { mark(bind, !text_of(bind).empty() && !valid_ipv4(text_of(bind))); mark(multicast, !text_of(multicast).empty() && !valid_multicast(text_of(multicast))); mark(iface, !text_of(iface).empty() && !valid_ipv4(text_of(iface))); // "Vector as" belongs to a vector row only; a row back at one element returns to Channels. for (const SigRow& s : sigs) { const bool vec = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(s.count)) > 1; gtk_widget_set_visible(s.vec, vec); if (!vec && gtk_drop_down_get_selected(GTK_DROP_DOWN(s.vec)) != 0) gtk_drop_down_set_selected(GTK_DROP_DOWN(s.vec), 0); } const std::vector rows = rows_now(); const std::string p = flatudp::problem(rows); std::string sub = p; if (p.empty()) { sub = std::to_string(channel_count(rows)) + (channel_count(rows) == 1 ? " channel, " : " channels, ") + std::to_string(datagram_size(rows)) + " bytes per datagram"; } adw_expander_row_set_subtitle(ADW_EXPANDER_ROW(group), sub.c_str()); } void fire() { if (!updating && changed && *changed) (*changed)(); } SourceConfig read() const { SourceConfig c; c.set("port", num(value_of(port))); if (!text_of(bind).empty()) c.set("bind", text_of(bind)); if (!text_of(multicast).empty()) c.set("multicast", text_of(multicast)); if (!text_of(iface).empty()) c.set("iface", text_of(iface)); c.set("rate", num(value_of(rate))); c.set("endian", sel_of(endian) == 1 ? "big" : "little"); c.set("signals", format(rows_now())); return c; } void add_row(const Row& r); void remove_row(GtkWidget* row); void write(const SourceConfig& cfg) { const FlatUdpSettings s = FlatUdpSettings::from(cfg); updating = true; adw_spin_row_set_value(ADW_SPIN_ROW(port), s.port); gtk_editable_set_text(GTK_EDITABLE(bind), s.bind.c_str()); gtk_editable_set_text(GTK_EDITABLE(multicast), s.multicast.c_str()); gtk_editable_set_text(GTK_EDITABLE(iface), s.iface.c_str()); if (s.rate > 0.0) adw_spin_row_set_value(ADW_SPIN_ROW(rate), s.rate); adw_combo_row_set_selected(ADW_COMBO_ROW(endian), s.big_endian ? 1 : 0); while (!sigs.empty()) { GtkWidget* row = sigs.back().row; sigs.pop_back(); adw_expander_row_remove(ADW_EXPANDER_ROW(group), row); } for (const Row& r : s.rows) add_row(r); updating = false; refresh(); } bool valid() const { return FlatUdpSettings::from(read()).problem().empty(); } }; using FormPtr = std::shared_ptr; /// A remove button's context: which form, which row. struct RowCtx { FormPtr form; GtkWidget* row; }; void on_notify(GObject*, GParamSpec*, gpointer user) { Form& f = **static_cast(user); if (!f.alive) return; f.refresh(); f.fire(); } void on_edited(GtkEditable*, gpointer user) { Form& f = **static_cast(user); if (!f.alive) return; f.refresh(); f.fire(); } void on_spin(GtkSpinButton*, gpointer user) { Form& f = **static_cast(user); if (!f.alive) return; f.refresh(); f.fire(); } void on_add(GtkButton*, gpointer user) { FormPtr f = *static_cast(user); if (!f->alive) return; f->add_row(Row{f->next_name(), Type::F32, 1}); adw_expander_row_set_expanded(ADW_EXPANDER_ROW(f->group), TRUE); f->refresh(); f->fire(); } void on_remove(GtkButton*, gpointer user) { // Copy first: removing the row finalizes this button, and with it this context. RowCtx* c = static_cast(user); FormPtr f = c->form; GtkWidget* row = c->row; if (!f->alive) return; f->remove_row(row); } void free_form(gpointer d, GClosure*) { delete static_cast(d); } void free_ctx(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 hook_destroy(GtkWidget* w, const FormPtr& f) { g_signal_connect_data(w, "destroy", G_CALLBACK(on_form_destroy), new FormPtr(f), free_form, static_cast(0)); } void hook_notify(GtkWidget* w, const char* signal, const FormPtr& f) { g_signal_connect_data(w, signal, G_CALLBACK(on_notify), new FormPtr(f), free_form, static_cast(0)); } void hook_entry(GtkWidget* w, const FormPtr& f) { g_signal_connect_data(w, "changed", G_CALLBACK(on_edited), new FormPtr(f), free_form, static_cast(0)); } void Form::add_row(const Row& r) { FormPtr self = shared_from_this(); GtkWidget* row = adw_entry_row_new(); adw_preferences_row_set_title(ADW_PREFERENCES_ROW(row), "Name"); gtk_editable_set_text(GTK_EDITABLE(row), r.name.c_str()); const char* names[11]; for (std::size_t i = 0; i < 10; ++i) names[i] = type_name(static_cast(i)); names[10] = nullptr; GtkWidget* type = gtk_drop_down_new_from_strings(names); gtk_drop_down_set_selected(GTK_DROP_DOWN(type), static_cast(r.type)); gtk_widget_set_valign(type, GTK_ALIGN_CENTER); gtk_widget_set_name(type, "flatudp-type"); // The spin clamps to 1..4096, so a hand-edited session with a count outside it is repaired here. GtkWidget* count = gtk_spin_button_new_with_range(1, kMaxCount, 1); gtk_spin_button_set_value(GTK_SPIN_BUTTON(count), r.count); gtk_widget_set_valign(count, GTK_ALIGN_CENTER); gtk_widget_set_tooltip_text(count, "Elements: 1 is a scalar signal, more make a vector signal"); gtk_widget_set_name(count, "flatudp-count"); static const char* const kVec[] = {"Channels", "Waveform", nullptr}; GtkWidget* vec = gtk_drop_down_new_from_strings(kVec); gtk_drop_down_set_selected(GTK_DROP_DOWN(vec), r.wave && r.count > 1 ? 1 : 0); gtk_widget_set_valign(vec, GTK_ALIGN_CENTER); gtk_widget_set_visible(vec, r.count > 1); gtk_widget_set_tooltip_text(vec, "Vector as: Channels makes one channel per element, one sample per cycle; " "Waveform makes one channel of all the elements per cycle"); gtk_widget_set_name(vec, "flatudp-vector"); GtkWidget* rm = gtk_button_new_with_label("Remove"); gtk_widget_set_valign(rm, GTK_ALIGN_CENTER); gtk_widget_set_name(rm, "flatudp-remove"); adw_entry_row_add_suffix(ADW_ENTRY_ROW(row), type); adw_entry_row_add_suffix(ADW_ENTRY_ROW(row), count); adw_entry_row_add_suffix(ADW_ENTRY_ROW(row), vec); adw_entry_row_add_suffix(ADW_ENTRY_ROW(row), rm); hook_entry(row, self); hook_notify(type, "notify::selected", self); hook_notify(vec, "notify::selected", self); g_signal_connect_data(count, "value-changed", G_CALLBACK(on_spin), new FormPtr(self), free_form, static_cast(0)); g_signal_connect_data(rm, "clicked", G_CALLBACK(on_remove), new RowCtx{self, row}, free_ctx, static_cast(0)); adw_expander_row_add_row(ADW_EXPANDER_ROW(group), row); sigs.push_back(SigRow{row, type, count, vec}); } void Form::remove_row(GtkWidget* row) { for (auto it = sigs.begin(); it != sigs.end(); ++it) { if (it->row == row) { sigs.erase(it); adw_expander_row_remove(ADW_EXPANDER_ROW(group), row); break; } } refresh(); fire(); } class FlatUdpKind final : public SourceKind { public: std::string id() const override { return "flatudp"; } std::string display_name() const override { return "Flat UDP (MARTe2 UDPSender)"; } bool has_datagrams() const override { return true; } std::string identity(const SourceConfig& cfg) const override { return FlatUdpSettings::from(cfg).identity(); } std::unique_ptr make(const SourceConfig& cfg) const override { return std::make_unique(cfg); } ConfigForm make_config_form() const override { auto f = std::make_shared(); static const char* const kEndian[] = {"Little endian", "Big endian", nullptr}; f->port = spin_row("_Port", 1, 65535, 1, kDefaultPort, 0); f->bind = entry_row("_Bind address"); f->multicast = entry_row("_Multicast group"); f->iface = entry_row("Multicast _interface"); f->rate = spin_row("Cycle _rate (Hz)", 0.001, 1000000, 1, 1000, 3); f->endian = combo_row("_Byte order", kEndian); GtkWidget* net = expander("Network", {f->port, f->bind, f->multicast, f->iface}); GtkWidget* timing = expander("Timing and format", {f->rate, f->endian}); f->group = adw_expander_row_new(); adw_preferences_row_set_title(ADW_PREFERENCES_ROW(f->group), "Signals"); adw_expander_row_set_expanded(ADW_EXPANDER_ROW(net), TRUE); adw_expander_row_set_expanded(ADW_EXPANDER_ROW(f->group), TRUE); GtkWidget* add = gtk_button_new_with_label("Add signal"); gtk_widget_set_valign(add, GTK_ALIGN_CENTER); gtk_widget_set_name(add, "flatudp-add"); adw_expander_row_add_suffix(ADW_EXPANDER_ROW(f->group), add); g_signal_connect_data(add, "clicked", G_CALLBACK(on_add), new FormPtr(f), free_form, static_cast(0)); for (GtkWidget* w : {net, timing, f->group}) hook_destroy(w, f); for (GtkWidget* w : {f->bind, f->multicast, f->iface}) hook_entry(w, f); for (GtkWidget* w : {f->port, f->rate}) hook_notify(w, "notify::value", f); hook_notify(f->endian, "notify::selected", f); f->refresh(); ConfigForm out; out.rows = {net, timing, f->group}; out.description = "The signal list must match the sender's exactly, in order: the datagrams carry no " "description, and a datagram of any other size is dropped. A vector signal is either " "one channel per element or, as a waveform, one channel of all its elements"; out.focus = f->rate; out.write = [f](const SourceConfig& c) { f->write(c); }; out.read = [f]() { return f->read(); }; out.valid = [f]() { return f->valid(); }; ChangeCb cb = f->changed; out.on_change = [cb](std::function fn) { *cb = std::move(fn); }; return out; } }; } // namespace std::unique_ptr make_flatudp_kind() { return std::make_unique(); } } // namespace udpscope