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

+31 -1
View File
@@ -145,11 +145,41 @@ Errors (no device, a rejected connection code, a timeout, the scan socket closin
`[e1608] ...` once per distinct message (an overrun is logged each time it happens); the window shows only that
the source is disconnected (the status popover reads `waiting`, and the panels go insensitive), not why.
The third built-in kind is **Flat UDP**, a receiver for the MARTe2 `UDPSender` datasource: one datagram per
cycle, the signals concatenated in declaration order with no header, no padding and no timestamp, in the
sender's byte order. The datagram describes nothing, so **the signal list in the connection form is the
whole contract and is your responsibility**: one row per signal (a name, a type from `uint8` to `float64`,
and an element count), in the sender's order, including the `Trigger` byte an IndependentThread `UDPSender`
puts first. A row with a count above 1 is a vector, and a "Vector as" drop-down on that row says how to
show it: as **Channels** (the default), where `Wave:float32[16]` becomes 16 scalar channels `Wave[0]` ..
`Wave[15]` of one sample per cycle each, or as a **Waveform**, one channel that receives a block of 16
consecutive samples per cycle (its declared rate is the cycle rate times the count). The waveform is written
`Wave:float32[16]w` in the `signals` text, and the same text is what a session file and `--source-opt
signals=` carry. A list with a waveform row may hold **only waveform rows, all of the same count** (a
scalar or a channels row beside one, or two lengths, would need zero padding or held values, so the form
refuses it and says so), and a list may make **at most 256 channels** (a scalar is one, a channels vector is
`count`, a waveform is one). The form's subtitle shows the channel total and the size the list implies
("6 channels, 13 bytes per datagram"), or the first rule the list breaks; a datagram of any other size is
dropped, counted as stale, and reported once per distinct size on stderr as `[flatudp] ...`. The form also
takes the port (44488, the sender's own default), an optional bind address, an optional multicast group with
the interface to join it on, the byte order, and the sender's **cycle rate in Hz, which is required**: it
sizes the ring and the ingest decimation and spaces the samples within a frame. Time is stamped by the
existing pipeline from arrival and that declared rate (and `--relative-time` gives a counted clock); there is
no timestamp in the stream to prefer. **Loss is never reported** -- with no counter in the datagram, a missed
cycle is indistinguishable from a slow one -- and neither is a changed sender: a list that does not match
reads as "everything dropped". Source-address filtering, detecting the list from the traffic and importing a
MARTe2 `.cfg` are not provided. A list with no waveform row is delivered in frames of about 10 ms
(`round(rate / 100)` cycles, at most 1024; a partial frame is flushed after 100 ms); a list of waveforms
delivers one frame per datagram. The source is connected on the first valid datagram and disconnected after
2 s without one. It is verified against `tools/mock_flatudp.py`, not against a MARTe2 application. Multicast
was verified on a loopback group (`239.255.42.99` joined on `127.0.0.1`, although this machine's `lo` does not
carry the `MULTICAST` flag); a real network, its switches and IGMP were not exercised.
A session file written before kinds existed has no `source_kind`, and is read as UDPS; a UDPS file is
written exactly as before. A kind's capability panels (for instance a source's own controls) appear as
pages of the side stack, and are insensitive rather than hidden while no source is connected.
Planned, each as its own sub-project: a flat UDP kind and ABScope. ABScope has more than one
Planned, as its own sub-project: ABScope. It has more than one
timebase, so it will change the frame and the data path together rather than fit the present single-timebase
frame.
+27
View File
@@ -61,6 +61,33 @@ What feeds `Acquisition` is a `DataSource`, one of the registered *kinds*.
only through `post()`. `Acquisition::on_event` ignores the detail string and does nothing for `Error`,
so the source also writes each new error to stderr as `[e1608] ...` (once per distinct message, not per
retry); that line is where a user sees why a connection failed.
- **The flat UDP kind is three files and no thread.** `flatudp_signals` is the `signals` string codec,
the validity rules and the decoders, with no GTK and no sockets (the probe round-trips it);
`flatudp_source` is `FlatUdpSettings` plus the `DataSource`; `flatudp_kind` is the form, a signal-row
editor (a name, a type, a count and, while the count is above 1, a "Vector as" drop-down per row). The
datagram has no description, so a list that is wrong is invisible except as dropped datagrams: the size
check is the only defence, and it is why a wrong-size datagram is counted (`stale`) and reported once per
size.
- A vector row is shown one of two ways, chosen per row (`Row::wave`, the `w` suffix in the text). As
**channels** it is `count` scalar channels of one sample per cycle, so the list batches like an
all-scalar one. As a **waveform** it is one channel with `rows = count` and
`sampling_rate = rate * count`, one datagram per `Frame` (`num_samples = 1`). The two cannot share a
list because the model spans a frame by its largest element block and `Frame::value` is 0 past a scalar's
one value: a scalar beside a vector block would be zero-padded, so `problem()` demands that a list with a
waveform row be waveform rows of one count, and the form shows that message in the subtitle. The channel
total is capped at 256.
- Its frames are **fixed-size blocks** like the E-1608's for the same reason (`build_stamps` warns when the
sample count varies): `round(rate / 100)` datagrams for a list with no waveform row, a partial block
flushed after 100 ms. `rate` is required because `sampling_rate = 0` makes the model size the ring and
the ingest decimation for 1 kHz; there is no time-base switch, since `acquisition.cpp` has no stamping
path that ignores the declared cadence.
- `Disconnected` after 2 s without a *valid* datagram (wrong-size ones do not refresh the clock); it resets
`Acquisition`'s per-session state, so the next datagram re-anchors rather than reading as a gap, and the
history survives (`CONFIG` is sent once, from the first `poll()`).
- The form's row editor removes a row from inside its own button's `clicked` emission (GTK keeps the emitter
alive to the end of the emission, and the handler copies the form and the row out of its context first);
every handler tests `Form::alive`, which the groups' `destroy` clears, because the chooser may have
destroyed the sibling widgets the handlers reach by raw pointer.
### Data path
+17
View File
@@ -1912,6 +1912,7 @@ the one `[e1608] handshake with ...: Connection refused` line, `stream: ... sign
```
[flatudp] codec: ok=1
[flatudp] form: initial_valid=0 good_valid=1 size_ok=1 dup_valid=0 dup_msg=1 oversize_valid=0 add=1 remove=1 wave_valid=1 wave_sub=1 wave_roundtrip=1 mixed_valid=0 mixed_msg=1 length_valid=0 length_msg=1 cap_valid=0 cap_msg=1 cap_wave_valid=1 vec_shown=1 vec_drive=1 vec_reset=1 ok=1
[flatudp] datagram of 22 bytes, expected 21, check the signal list
[flatudp] stream: connected=1 signals=7 names=Trigger,Ia,Cnt,Wave[0],Wave[1],Wave[2],Wave[3] expect=Trigger,Ia,Cnt,Wave[0],Wave[1],Wave[2],Wave[3] frames=368 rate=1000.000 ok=1
[flatudp] channels: count=7 expect=7 ok=1
@@ -1945,6 +1946,22 @@ same reason.
rules refuse what they should (a waveform of count 1, a waveform mixed with another row or of another length,
257 channels). It needs no source, so `--source probe` shows it too. `ok=0` is a codec defect: a swapped byte
order, a wrong width or a rule that does not fire.
- `form: ... ok=1`: driven through the widgets, not the settings. `initial_valid=0` -- an empty list is not
connectable (`1` is a form that lets a source with no signals through). `good_valid=1 size_ok=1` -- a
13-byte list is valid and its subtitle says `13 bytes` and `6 channels` (`W:int16[4]` as channels).
`dup_valid=0 dup_msg=1` -- a duplicate name is refused *and the subtitle says why* ("used twice").
`oversize_valid=0` -- two `float64[4096]` rows (65536 bytes) exceed a datagram. `add=1` -- two clicks on
"Add signal" append `S1` and `S2`; `remove=1` -- the first row's "Remove" button takes that row out. A `0`
on `add`/`remove` with a `Gtk-CRITICAL` is a row handled after it was removed. The vector fields:
`wave_valid=1 wave_sub=1 wave_roundtrip=1` -- two waveform rows of one length are valid, the subtitle says
`2 channels`, and `signals` reads back with its `w`; `mixed_valid=0 mixed_msg=1` -- a waveform beside a
scalar is refused and the subtitle names the waveform rule; `length_valid=0 length_msg=1` -- waveforms of
two lengths are refused ("same length"); `cap_valid=0 cap_msg=1 cap_wave_valid=1` -- 300 channels are over
the 256 cap and the subtitle says `256`, while two `[200]w` waveforms (2 channels) are fine; `vec_shown=1`
-- the "Vector as" drop-down (`flatudp-vector`) is shown on a count above 1 and hidden on a scalar row;
`vec_drive=1` -- setting it to Waveform writes `Row::wave` (one waveform beside a channels row, then beside
a scalar, are both still invalid, and the text gains its `w`); `vec_reset=1` -- a row whose count returns
to 1 hides its drop-down and returns to Channels. Like `codec`, it needs no flatudp source.
- `stream: ... ok=1`: the model's signals are the list's channels in order, each at its declared rate: `rate`
for a scalar or a `channels` element (named `Name[k]`), `rate * count` for a waveform. `names` out of order is a
decode that mixed rows; a rate that is not the expected one is a `sampling_rate` that did not reach the model
+2 -2
View File
@@ -442,8 +442,8 @@ precedent, not an exception.
- `UDPSCOPE_PROBE_E1608_RECONNECT=8000` -- reads the source after the mock dropped the link and 500 ms
later checks the frame count moved on. The reading's own entry is in `probe-readings.md`.
- `UDPSCOPE_PROBE_FLATUDP=2500` with `UDPSCOPE_EXIT_AFTER_MS`, run as `--source flatudp` against
`tools/mock_flatudp.py` -- the flat UDP source end to end: a hand-computed codec and decode check (it
needs no source; it covers the `w` suffix, the waveform and 256-channel rules, the channel decode and the
`tools/mock_flatudp.py` -- the flat UDP source end to end: a hand-computed codec and decode check and the connect form's validity rules and signal-row editor (both
need no source; it covers the `w` suffix, the waveform and 256-channel rules, the channel decode and the
float64/uint16/int32/uint32/int64 types in both byte orders), the stream's channel names (`Wave[0]`..
for a `channels` vector) and declared rates (`rate * count` for a waveform), the channel count, value
continuity per row (and the offset between a vector's elements, or a waveform's block phase), the
+191 -17
View File
@@ -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(); };
+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);