flatudp: the source, kind, mock sender and stream probes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
6e8e71b32f
commit
78a29fbc39
13 files changed
+962
-7
No files matched your search
@@ -0,0 +1,206 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||
|
||||
#include "source/flatudp/flatudp_source.hpp"
|
||||
#include "source/registry.hpp"
|
||||
|
||||
#include <adwaita.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace udpscope {
|
||||
namespace {
|
||||
|
||||
using namespace flatudp;
|
||||
using ChangeCb = std::shared_ptr<std::function<void()>>;
|
||||
|
||||
std::string num(double v) {
|
||||
char b[48];
|
||||
std::snprintf(b, sizeof b, "%.10g", v);
|
||||
return b;
|
||||
}
|
||||
|
||||
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<guint>(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<GtkWidget*> 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");
|
||||
}
|
||||
}
|
||||
|
||||
/// The form's widgets, shared by every signal closure and every `ConfigForm` closure.
|
||||
struct Form {
|
||||
GtkWidget *port, *bind, *multicast, *iface, *rate, *endian, *signals;
|
||||
ChangeCb changed = std::make_shared<std::function<void()>>();
|
||||
bool updating = false;
|
||||
bool alive = true; ///< cleared when the chooser destroys the rows; handlers then touch nothing
|
||||
|
||||
/// A bad address is marked with the `error` style; the rest of the problem is `valid()`'s.
|
||||
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)));
|
||||
}
|
||||
|
||||
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", text_of(signals));
|
||||
return c;
|
||||
}
|
||||
|
||||
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);
|
||||
gtk_editable_set_text(GTK_EDITABLE(signals), cfg.get("signals").c_str());
|
||||
updating = false;
|
||||
refresh();
|
||||
}
|
||||
|
||||
bool valid() const { return FlatUdpSettings::from(read()).problem().empty(); }
|
||||
};
|
||||
|
||||
using FormPtr = std::shared_ptr<Form>;
|
||||
|
||||
void on_notify(GObject*, GParamSpec*, gpointer user) {
|
||||
Form& f = **static_cast<FormPtr*>(user);
|
||||
if (!f.alive) return;
|
||||
f.refresh();
|
||||
if (!f.updating && f.changed && *f.changed) (*f.changed)();
|
||||
}
|
||||
|
||||
void on_edited(GtkEditable*, gpointer user) {
|
||||
Form& f = **static_cast<FormPtr*>(user);
|
||||
if (!f.alive) return;
|
||||
f.refresh();
|
||||
if (!f.updating && f.changed && *f.changed) (*f.changed)();
|
||||
}
|
||||
|
||||
void free_form(gpointer d, GClosure*) { delete static_cast<FormPtr*>(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<FormPtr*>(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<GConnectFlags>(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<GConnectFlags>(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<GConnectFlags>(0));
|
||||
}
|
||||
|
||||
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<DataSource> make(const SourceConfig& cfg) const override {
|
||||
return std::make_unique<FlatUdpSource>(cfg);
|
||||
}
|
||||
|
||||
ConfigForm make_config_form() const override {
|
||||
auto f = std::make_shared<Form>();
|
||||
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);
|
||||
f->signals = entry_row("_Signals");
|
||||
|
||||
GtkWidget* net = expander("Network", {f->port, f->bind, f->multicast, f->iface});
|
||||
GtkWidget* timing = expander("Timing and format", {f->rate, f->endian});
|
||||
GtkWidget* sig = expander("Signals", {f->signals});
|
||||
adw_expander_row_set_expanded(ADW_EXPANDER_ROW(net), TRUE);
|
||||
adw_expander_row_set_expanded(ADW_EXPANDER_ROW(sig), TRUE);
|
||||
|
||||
for (GtkWidget* w : {net, timing, sig}) hook_destroy(w, f);
|
||||
for (GtkWidget* w : {f->bind, f->multicast, f->iface, f->signals}) 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, sig};
|
||||
out.description =
|
||||
"The signal list must match the sender's exactly, in order: the datagrams carry no "
|
||||
"description. Write name:type[count], comma separated, e.g. Trigger:uint8,Ia:float32,"
|
||||
"Wave:float32[16]";
|
||||
out.focus = f->signals;
|
||||
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<void()> fn) { *cb = std::move(fn); };
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<SourceKind> make_flatudp_kind() { return std::make_unique<FlatUdpKind>(); }
|
||||
|
||||
} // namespace udpscope
|
||||
Reference in new issue
Block a user