flatudp: the signal-row editor form with a per-row Vector as drop-down, its probe line, README and architecture notes
Multicast was verified on a loopback group (239.255.42.99 on 127.0.0.1); a real network was not exercised. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
e3bc1f72f3
commit
d955baf612
6 files changed
+385
-20
No files matched your search
@@ -6,10 +6,14 @@
|
||||
|
||||
#include <adwaita.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace udpscope {
|
||||
namespace {
|
||||
@@ -23,6 +27,13 @@ std::string num(double 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<unsigned char>(s[a]))) ++a;
|
||||
while (b > a && std::isspace(static_cast<unsigned char>(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);
|
||||
@@ -66,18 +77,70 @@ void mark(GtkWidget* w, bool bad) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The form's widgets, shared by every signal closure and every `ConfigForm` closure.
|
||||
struct Form {
|
||||
GtkWidget *port, *bind, *multicast, *iface, *rate, *endian, *signals;
|
||||
/// 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<Form> {
|
||||
GtkWidget *port, *bind, *multicast, *iface, *rate, *endian, *group;
|
||||
std::vector<SigRow> sigs;
|
||||
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.
|
||||
std::vector<Row> rows_now() const {
|
||||
std::vector<Row> rows;
|
||||
for (const SigRow& s : sigs) {
|
||||
Row r;
|
||||
r.name = trim(text_of(s.row));
|
||||
r.type = static_cast<Type>(std::min<guint>(gtk_drop_down_get_selected(GTK_DROP_DOWN(s.type)), 9));
|
||||
r.count = static_cast<std::uint32_t>(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<n>` that no row uses.
|
||||
std::string next_name() const {
|
||||
const std::vector<Row> 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<Row> 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 {
|
||||
@@ -88,10 +151,13 @@ struct Form {
|
||||
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));
|
||||
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;
|
||||
@@ -101,7 +167,12 @@ struct Form {
|
||||
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());
|
||||
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();
|
||||
}
|
||||
@@ -111,21 +182,53 @@ struct Form {
|
||||
|
||||
using FormPtr = std::shared_ptr<Form>;
|
||||
|
||||
/// 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<FormPtr*>(user);
|
||||
if (!f.alive) return;
|
||||
f.refresh();
|
||||
if (!f.updating && f.changed && *f.changed) (*f.changed)();
|
||||
f.fire();
|
||||
}
|
||||
|
||||
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)();
|
||||
f.fire();
|
||||
}
|
||||
|
||||
void on_spin(GtkSpinButton*, gpointer user) {
|
||||
Form& f = **static_cast<FormPtr*>(user);
|
||||
if (!f.alive) return;
|
||||
f.refresh();
|
||||
f.fire();
|
||||
}
|
||||
|
||||
void on_add(GtkButton*, gpointer user) {
|
||||
FormPtr f = *static_cast<FormPtr*>(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<RowCtx*>(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<FormPtr*>(d); }
|
||||
void free_ctx(gpointer d, GClosure*) { delete static_cast<RowCtx*>(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; }
|
||||
@@ -145,6 +248,70 @@ void hook_entry(GtkWidget* w, const FormPtr& f) {
|
||||
static_cast<GConnectFlags>(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<Type>(i));
|
||||
names[10] = nullptr;
|
||||
GtkWidget* type = gtk_drop_down_new_from_strings(names);
|
||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(type), static_cast<guint>(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<GConnectFlags>(0));
|
||||
g_signal_connect_data(rm, "clicked", G_CALLBACK(on_remove), new RowCtx{self, row}, free_ctx,
|
||||
static_cast<GConnectFlags>(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"; }
|
||||
@@ -169,27 +336,34 @@ class FlatUdpKind final : public SourceKind {
|
||||
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});
|
||||
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(sig), TRUE);
|
||||
adw_expander_row_set_expanded(ADW_EXPANDER_ROW(f->group), 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);
|
||||
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<GConnectFlags>(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, sig};
|
||||
out.rows = {net, timing, f->group};
|
||||
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;
|
||||
"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(); };
|
||||
|
||||
@@ -2213,6 +2213,122 @@ long flatudp_breaks(App* a, std::size_t channel, long step) {
|
||||
return breaks;
|
||||
}
|
||||
|
||||
/// Every widget named `name` at or under `root`, in tree order (a signal row's widgets repeat).
|
||||
void find_all_named(GtkWidget* root, const char* name, std::vector<GtkWidget*>& out) {
|
||||
if (root == nullptr) return;
|
||||
if (std::strcmp(gtk_widget_get_name(root), name) == 0) out.push_back(root);
|
||||
for (GtkWidget* c = gtk_widget_get_first_child(root); c != nullptr;
|
||||
c = gtk_widget_get_next_sibling(c)) {
|
||||
find_all_named(c, name, out);
|
||||
}
|
||||
}
|
||||
|
||||
/// The connect form's validity rules and its signal-row editor, driven through the widgets: the
|
||||
/// empty form is not connectable; a good list is, and its subtitle names the channel total and the
|
||||
/// implied size; a duplicate name and an over-size list are not; "Add signal" appends `S1`, `S2`; a
|
||||
/// row's "Remove" button takes that row out. Then the vector rows: a waveform list is valid and
|
||||
/// round-trips with its `w`; a waveform beside a scalar (or of another length) is not, and the
|
||||
/// subtitle says so; the 256-channel cap counts a waveform as one; the "Vector as" drop-down is
|
||||
/// shown for a count above 1 only, writes `Row::wave`, and returns to Channels when the count does.
|
||||
void flatudp_form_probe() {
|
||||
SourceKind* k = find_kind("flatudp");
|
||||
if (k == nullptr) {
|
||||
std::fprintf(stderr, "[flatudp] form: no flatudp kind\n");
|
||||
return;
|
||||
}
|
||||
ConfigForm f = k->make_config_form();
|
||||
const bool initial = f.valid();
|
||||
const auto with = [&](const char* signals) {
|
||||
SourceConfig c;
|
||||
c.set("rate", "1000");
|
||||
c.set("signals", signals);
|
||||
f.write(c);
|
||||
return f.valid();
|
||||
};
|
||||
const auto subtitle = [&]() {
|
||||
const char* t = adw_expander_row_get_subtitle(ADW_EXPANDER_ROW(f.rows[2]));
|
||||
return std::string(t != nullptr ? t : "");
|
||||
};
|
||||
const auto has = [](const std::string& text, const char* part) {
|
||||
return text.find(part) != std::string::npos;
|
||||
};
|
||||
const bool good = with("A:uint8,B:float32,W:int16[4]");
|
||||
const std::string good_sub = subtitle();
|
||||
const bool size_ok = has(good_sub, "13 bytes") && has(good_sub, "6 channels");
|
||||
const bool dup = with("A:uint8,A:float32");
|
||||
const bool dup_ok = has(subtitle(), "twice");
|
||||
const bool big = with("A:float64[4096],B:float64[4096]");
|
||||
with("A:uint8,B:float32");
|
||||
|
||||
GtkWidget* add = find_named(f.rows[2], "flatudp-add");
|
||||
if (add != nullptr) {
|
||||
g_signal_emit_by_name(add, "clicked");
|
||||
g_signal_emit_by_name(add, "clicked");
|
||||
}
|
||||
const bool added = f.read().get("signals") == "A:uint8,B:float32,S1:float32,S2:float32";
|
||||
GtkWidget* rm = find_named(f.rows[2], "flatudp-remove"); // the first row's
|
||||
if (rm != nullptr) g_signal_emit_by_name(rm, "clicked");
|
||||
const bool removed = f.read().get("signals") == "B:float32,S1:float32,S2:float32";
|
||||
|
||||
// Waveform rows.
|
||||
const bool wave_valid = with("W1:int16[4]w,W2:float32[4]w");
|
||||
const bool wave_sub = has(subtitle(), "2 channels");
|
||||
const bool wave_rt = f.read().get("signals") == "W1:int16[4]w,W2:float32[4]w";
|
||||
const bool mixed = with("A:uint8,W:int16[4]w");
|
||||
const bool mixed_msg = has(subtitle(), "waveform");
|
||||
const bool length = with("W1:int16[4]w,W2:int16[8]w");
|
||||
const bool length_msg = has(subtitle(), "same length");
|
||||
const bool cap = with("A:float32[200],B:float32[100]");
|
||||
const bool cap_msg = has(subtitle(), "256");
|
||||
const bool cap_wave = with("A:float32[200]w,B:float32[200]w");
|
||||
|
||||
// The "Vector as" drop-down, driven.
|
||||
with("A:uint8,V1:int16[4],V2:int16[4]");
|
||||
std::vector<GtkWidget*> vec;
|
||||
find_all_named(f.rows[2], "flatudp-vector", vec);
|
||||
const bool vec_found = vec.size() == 3;
|
||||
const bool vec_shown = vec_found && !gtk_widget_get_visible(vec[0]) &&
|
||||
gtk_widget_get_visible(vec[1]) && gtk_widget_get_visible(vec[2]);
|
||||
bool vec_drive = false;
|
||||
bool vec_reset = false;
|
||||
if (vec_found) {
|
||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(vec[1]), 1);
|
||||
const bool half_valid = f.valid(); // one waveform beside a scalar and a channels row
|
||||
const std::string half = f.read().get("signals");
|
||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(vec[2]), 1);
|
||||
const bool scalar_left = f.valid(); // the scalar A still breaks the all-waveform rule
|
||||
vec_drive = !half_valid && half == "A:uint8,V1:int16[4]w,V2:int16[4]" && !scalar_left &&
|
||||
f.read().get("signals") == "A:uint8,V1:int16[4]w,V2:int16[4]w";
|
||||
// Bring V1 back to one element: its drop-down hides and returns to Channels.
|
||||
std::vector<GtkWidget*> counts;
|
||||
find_all_named(f.rows[2], "flatudp-count", counts);
|
||||
if (counts.size() == 3) gtk_spin_button_set_value(GTK_SPIN_BUTTON(counts[1]), 1);
|
||||
vec_reset = counts.size() == 3 && !gtk_widget_get_visible(vec[1]) &&
|
||||
gtk_drop_down_get_selected(GTK_DROP_DOWN(vec[1])) == 0 &&
|
||||
f.read().get("signals") == "A:uint8,V1:int16,V2:int16[4]w";
|
||||
}
|
||||
|
||||
const bool ok = !initial && good && size_ok && !dup && dup_ok && !big && add != nullptr && added &&
|
||||
rm != nullptr && removed && wave_valid && wave_sub && wave_rt && !mixed &&
|
||||
mixed_msg && !length && length_msg && !cap && cap_msg && cap_wave && vec_shown &&
|
||||
vec_drive && vec_reset;
|
||||
std::fprintf(stderr,
|
||||
"[flatudp] form: initial_valid=%d good_valid=%d size_ok=%d dup_valid=%d dup_msg=%d "
|
||||
"oversize_valid=%d add=%d remove=%d wave_valid=%d wave_sub=%d wave_roundtrip=%d "
|
||||
"mixed_valid=%d mixed_msg=%d length_valid=%d length_msg=%d cap_valid=%d cap_msg=%d "
|
||||
"cap_wave_valid=%d vec_shown=%d vec_drive=%d vec_reset=%d ok=%d\n",
|
||||
initial ? 1 : 0, good ? 1 : 0, size_ok ? 1 : 0, dup ? 1 : 0, dup_ok ? 1 : 0,
|
||||
big ? 1 : 0, added ? 1 : 0, removed ? 1 : 0, wave_valid ? 1 : 0, wave_sub ? 1 : 0,
|
||||
wave_rt ? 1 : 0, mixed ? 1 : 0, mixed_msg ? 1 : 0, length ? 1 : 0, length_msg ? 1 : 0,
|
||||
cap ? 1 : 0, cap_msg ? 1 : 0, cap_wave ? 1 : 0, vec_shown ? 1 : 0, vec_drive ? 1 : 0,
|
||||
vec_reset ? 1 : 0, ok ? 1 : 0);
|
||||
// The rows were never parented; sink and release them so the form leaves nothing behind.
|
||||
for (GtkWidget* row : f.rows) {
|
||||
g_object_ref_sink(row);
|
||||
g_object_unref(row);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/// Phase 2, 1 s after the controls were driven: the stream, the calibration, the capture button,
|
||||
@@ -2585,6 +2701,7 @@ gboolean flatudp_probe(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
std::fprintf(stderr, "[flatudp] codec: ok=%d\n", flatudp_codec_check() ? 1 : 0);
|
||||
flatudp_form_probe();
|
||||
if (flatudp_source(a) == nullptr) {
|
||||
std::fprintf(stderr, "[flatudp] no flatudp source (run with --source flatudp)\n");
|
||||
std::fflush(stderr);
|
||||
|
||||
Reference in new issue
Block a user