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:
Martino FerrariandClaude Opus 4.6 committed 2026-11-02 01:23:51 +01:00
1 parent e3bc1f72f3
commit d955baf612
6 files changed
+385 -20

No files matched your search

+117
View File
@@ -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);