flatudp: vector rows as channels or as a waveform, a re-openable source, probes for both

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:18:43 +01:00
1 parent a523bc2bd9
commit e3bc1f72f3
10 files changed
+446 -100

No files matched your search

+62 -3
View File
@@ -87,12 +87,23 @@ bool all_scalar(const std::vector<Row>& rows) {
return std::all_of(rows.begin(), rows.end(), [](const Row& r) { return r.count == 1; });
}
bool any_wave(const std::vector<Row>& rows) {
return std::any_of(rows.begin(), rows.end(), [](const Row& r) { return r.wave; });
}
int channel_count(const std::vector<Row>& rows) {
long n = 0;
for (const Row& r : rows) n += r.wave ? 1 : static_cast<long>(r.count);
return static_cast<int>(std::min<long>(n, 1L << 30));
}
std::string format(const std::vector<Row>& rows) {
std::string out;
for (const Row& r : rows) {
if (!out.empty()) out += ',';
out += r.name + ':' + type_name(r.type);
if (r.count != 1) out += '[' + std::to_string(r.count) + ']';
if (r.count != 1 || r.wave) out += '[' + std::to_string(r.count) + ']';
if (r.wave) out += 'w';
}
return out;
}
@@ -118,11 +129,19 @@ std::string parse(const std::string& text, std::vector<Row>& rows) {
std::string ty = trim(tok.substr(colon + 1));
const std::size_t br = ty.find('[');
if (br != std::string::npos) {
if (ty.back() != ']') {
const std::size_t cl = ty.rfind(']');
if (cl == std::string::npos || cl < br) {
rows.clear();
return where + "missing ']' after the count";
}
const std::string n = trim(ty.substr(br + 1, ty.size() - br - 2));
const std::string tail = trim(ty.substr(cl + 1));
if (tail == "w") {
r.wave = true;
} else if (!tail.empty()) {
rows.clear();
return where + "unexpected '" + tail + "' after the count (only w, for a waveform)";
}
const std::string n = trim(ty.substr(br + 1, cl - br - 1));
const bool digits = !n.empty() && n.size() <= 6 &&
std::all_of(n.begin(), n.end(), [](char c) { return c >= '0' && c <= '9'; });
if (!digits) {
@@ -159,6 +178,30 @@ std::string problem(const std::vector<Row>& rows) {
if (n > kMaxDatagram)
return std::to_string(n) + " bytes per datagram exceed the " + std::to_string(kMaxDatagram) +
" a UDP datagram can carry";
// A waveform is one channel of `count` samples per datagram; it cannot share a frame with a
// channel of another length, so a list of waveforms is all waveforms of one length.
const Row* first_wave = nullptr;
for (const Row& r : rows) {
if (!r.wave) continue;
if (r.count < 2) return "signal '" + r.name + "' is a waveform and needs a count above 1";
if (first_wave == nullptr) first_wave = &r;
}
if (first_wave != nullptr) {
for (const Row& r : rows) {
if (!r.wave)
return "signal '" + r.name + "' is not a waveform but '" + first_wave->name +
"' is; every row must be a waveform of the same length";
if (r.count != first_wave->count)
return "signal '" + r.name + "' is a waveform of " + std::to_string(r.count) +
" values but '" + first_wave->name + "' has " + std::to_string(first_wave->count) +
"; every row must be a waveform of the same length";
}
}
const int channels = channel_count(rows);
if (channels > kMaxChannels)
return std::to_string(channels) + " channels exceed the " + std::to_string(kMaxChannels) +
" one list may make (a vector row makes one per element unless it is a waveform)";
return {};
}
@@ -176,4 +219,20 @@ void decode_datagram(const std::vector<Row>& rows, const std::uint8_t* data, boo
}
}
void decode_channels(const std::vector<Row>& rows, const std::uint8_t* data, bool big_endian,
std::vector<std::vector<double>>& cols) {
const std::size_t channels = static_cast<std::size_t>(channel_count(rows));
if (cols.size() < channels) cols.resize(channels);
const bool swap = big_endian != (std::endian::native == std::endian::big);
std::size_t off = 0, ch = 0;
for (const Row& r : rows) {
const std::size_t w = type_size(r.type);
for (std::uint32_t k = 0; k < r.count; ++k) {
cols[r.wave ? ch : ch + k].push_back(load_typed(r.type, data + off, swap));
off += w;
}
ch += r.wave ? 1 : r.count;
}
}
} // namespace udpscope::flatudp
+17 -1
View File
@@ -15,12 +15,16 @@ enum class Type : std::uint8_t { U8 = 0, I8, U16, I16, U32, I32, U64, I64, F32,
constexpr std::uint32_t kMaxCount = 4096;
constexpr std::size_t kMaxDatagram = 65507; ///< the largest UDP payload over IPv4
constexpr int kMaxChannels = 256; ///< the most channels one list may make
/// One signal of the sender's datagram: `count` consecutive values of `type`.
struct Row {
std::string name;
Type type = Type::F32;
std::uint32_t count = 1;
/// A vector row (count > 1) only: true is ONE channel of `count` consecutive samples per
/// datagram (a waveform); false is `count` channels `name[0]`.. of one sample each.
bool wave = false;
bool operator==(const Row&) const = default;
};
@@ -33,7 +37,13 @@ std::size_t datagram_size(const std::vector<Row>& rows);
/// True when no row is a vector. Such a list can be batched, several datagrams to a frame.
bool all_scalar(const std::vector<Row>& rows);
/// `name:type[,name:type[count]...]`; a count of 1 is written without brackets.
/// True when some row is a waveform (`wave`).
bool any_wave(const std::vector<Row>& rows);
/// The channels the list makes: a scalar 1, a non-wave vector `count`, a waveform 1.
int channel_count(const std::vector<Row>& rows);
/// `name:type[,name:type[count][w]...]`; a count of 1 is written without brackets, and the `w`
/// of a waveform row follows its count.
std::string format(const std::vector<Row>& rows);
/// The inverse of `format`. Whitespace around names, types and counts is ignored. An empty text
/// is an empty list (and not an error: `problem` reports it). Returns an error in words, empty
@@ -43,6 +53,12 @@ std::string parse(const std::string& text, std::vector<Row>& rows);
/// The first reason this list cannot describe a datagram, in words for the user; empty when valid.
std::string problem(const std::vector<Row>& rows);
/// Decodes one datagram of `datagram_size(rows)` bytes to one column per *channel* (see
/// `channel_count`): a non-wave vector row feeds `count` columns of one value each, a waveform row
/// one column of `count` values. Appends; the caller clears the columns between uses.
void decode_channels(const std::vector<Row>& rows, const std::uint8_t* data, bool big_endian,
std::vector<std::vector<double>>& cols);
/// Decodes one datagram of `datagram_size(rows)` bytes to doubles, row by row, byte-swapping when
/// `big_endian` differs from the host. Appends to `cols[s]`; the caller clears them between uses.
void decode_datagram(const std::vector<Row>& rows, const std::uint8_t* data, bool big_endian,
+38 -9
View File
@@ -90,7 +90,7 @@ std::string FlatUdpSettings::identity() const {
}
std::size_t FlatUdpSettings::block() const {
if (!all_scalar(rows) || !(rate > 0.0) || !std::isfinite(rate)) return 1;
if (any_wave(rows) || !(rate > 0.0) || !std::isfinite(rate)) return 1;
const long b = std::lround(rate / 100.0);
return static_cast<std::size_t>(std::min(1024L, std::max(1L, b)));
}
@@ -127,20 +127,44 @@ void FlatUdpSource::open(SourceSink& sink) {
if (!p.empty()) throw std::runtime_error(p);
sink_ = &sink;
// A fresh session: Stop then Go live opens the same object again, and the model needs its CONFIG.
configured_ = false;
connected_.store(false, std::memory_order_relaxed);
ever_connected_ = false;
recv_error_noted_ = false;
reported_sizes_.clear();
frame_counter_ = 0;
frames_ = 0;
reconnects_ = 0;
valid_.store(0, std::memory_order_relaxed);
wrong_.store(0, std::memory_order_relaxed);
// One SignalInfo per channel: a scalar, one element of a `channels` vector (`Name[k]`), or a whole
// waveform (rows = count consecutive samples at rate * count).
signals_.clear();
for (const Row& r : cfg_.rows) {
const auto add = [&](const std::string& name, const Row& r, std::uint32_t rows, double rate) {
SignalInfo s;
s.name = r.name;
s.name = name;
s.type_code = static_cast<std::uint8_t>(r.type);
s.rows = r.count;
s.rows = rows;
s.cols = 1;
s.time_mode = kTimePacket;
s.sampling_rate = cfg_.rate;
s.sampling_rate = rate;
signals_.push_back(s);
};
for (const Row& r : cfg_.rows) {
if (r.wave) {
add(r.name, r, r.count, cfg_.rate * static_cast<double>(r.count));
} else if (r.count == 1) {
add(r.name, r, 1, cfg_.rate);
} else {
for (std::uint32_t k = 0; k < r.count; ++k)
add(r.name + "[" + std::to_string(k) + "]", r, 1, cfg_.rate);
}
}
expected_ = datagram_size(cfg_.rows);
block_ = cfg_.block();
pending_.assign(cfg_.rows.size(), {});
pending_.assign(signals_.size(), {});
pending_n_ = 0;
rx_.assign(65536, 0);
@@ -223,14 +247,19 @@ void FlatUdpSource::poll(int timeout_ms) {
void FlatUdpSource::drain_socket() {
for (int i = 0; i < 8192; ++i) { // bounded, so a flood cannot starve the timers
const ssize_t n = ::recv(fd_, rx_.data(), rx_.size(), 0);
// MSG_TRUNC: the return value is the datagram's real length even if it did not fit `rx_`.
const ssize_t n = ::recv(fd_, rx_.data(), rx_.size(), MSG_TRUNC);
if (n < 0) {
if (errno == EINTR) continue;
if (errno != EAGAIN && errno != EWOULDBLOCK && !recv_error_noted_) {
recv_error_noted_ = true;
note(std::string("receive failed: ") + std::strerror(errno));
}
break; // EAGAIN: drained
}
const double t = epoch_seconds();
const std::size_t len = static_cast<std::size_t>(n);
if (tap_) tap_(rx_.data(), len, t);
if (tap_) tap_(rx_.data(), std::min(len, rx_.size()), t);
if (len == expected_) {
accept(rx_.data());
} else {
@@ -248,7 +277,7 @@ void FlatUdpSource::accept(const std::uint8_t* data) {
connected_.store(true, std::memory_order_relaxed);
sink_->on_event(SourceEvent::Connected, "");
}
decode_datagram(cfg_.rows, data, cfg_.big_endian, pending_);
decode_channels(cfg_.rows, data, cfg_.big_endian, pending_);
if (pending_n_ == 0) batch_start_ = last_valid_;
if (++pending_n_ >= block_) flush();
}
+2 -1
View File
@@ -40,7 +40,7 @@ struct FlatUdpSettings {
std::string problem() const;
/// `[multicast:]port`; names the session file.
std::string identity() const;
/// Datagrams per frame: 1 for a list with a vector, else `clamp(round(rate / 100), 1, 1024)`.
/// Datagrams per frame: 1 for a list with a waveform row, else `clamp(round(rate / 100), 1, 1024)`.
std::size_t block() const;
};
@@ -78,6 +78,7 @@ class FlatUdpSource final : public DataSource {
int fd_ = -1;
bool configured_ = false;
bool ever_connected_ = false;
bool recv_error_noted_ = false; ///< a failing `recv` is logged once per open
std::size_t expected_ = 0; ///< bytes of a valid datagram
std::size_t block_ = 1;
+3 -2
View File
@@ -1885,8 +1885,9 @@ gboolean e1608_probe_reconnect(gpointer user);
gboolean e1608_probe_resumed(gpointer user);
/// The flat UDP (MARTe2 UDPSender) source: the signal-list codec and a hand-computed decode, then,
/// against `tools/mock_flatudp.py`, the stream, the value continuity, the wrong-size accounting and
/// the capture button (`_report`, a second later). `_gap` reads the source inside the mock's silence;
/// `_resumed` shows the stream came back.
/// the capture button (`_report`, a second later). `_gap` starts a 100 ms poll that waits for the
/// stream, then for the mock's silence to drop the source (whenever it falls), then (`_resumed`) for
/// the stream to come back.
gboolean flatudp_probe(gpointer user);
gboolean flatudp_probe_report(gpointer user);
gboolean flatudp_probe_gap(gpointer user);
+240 -30
View File
@@ -2103,11 +2103,90 @@ bool flatudp_codec_check() {
if (valid({{"a,b", Type::U8, 1}})) return false;
if (valid({{"A", Type::U8, 0}})) return false;
if (valid({{"A", Type::U8, 4097}})) return false;
if (!valid({{"A", Type::U8, 4096}})) return false;
// 2 x 32768 = 65536 bytes is over the datagram limit; 32768 + 16384 is not.
if (prob({{"A", Type::F64, 4096}, {"B", Type::F64, 4096}}).find("65536") == std::string::npos)
// 4096 is the count limit; as `channels` it would be 4096 channels, so it is a waveform here.
if (!valid({{"A", Type::U8, 4096, true}})) return false;
if (valid({{"A", Type::U8, 4096, false}})) return false;
// 2 x 32768 = 65536 bytes is over the datagram limit; 32768 + 16384 is not (waveforms of one length).
if (prob({{"A", Type::F64, 4096, true}, {"B", Type::F64, 4096, true}}).find("65536") == std::string::npos)
return false;
if (!valid({{"A", Type::F64, 4096}, {"B", Type::F32, 4096}})) return false;
if (!valid({{"A", Type::F64, 4096, true}, {"B", Type::F32, 4096, true}})) return false;
// The `w` suffix: a waveform row. It round-trips, `format` writes it only for a waveform, and a
// count of 1 is still bracketed when the row is one.
const std::vector<Row> mixed = {{"S", Type::U8, 1, false}, {"V", Type::F32, 3, false},
{"W", Type::I16, 16, true}, {"X", Type::U8, 1, true}};
const std::string text = format(mixed);
if (text != "S:uint8,V:float32[3],W:int16[16]w,X:uint8[1]w") return false;
std::vector<Row> back;
if (!parse(text, back).empty() || back != mixed) return false;
if (!parse("W : int16 [ 4 ] w", back).empty() || back.size() != 1 || !back[0].wave ||
back[0].count != 4 || format(back) != "W:int16[4]w")
return false;
if (parse("W:int16[4]x", back).empty() || !back.empty()) return false;
if (parse("W:int16w", back).empty()) return false;
if (parse("W:int16[4", back).empty()) return false;
if (!any_wave(mixed) || any_wave(want)) return false;
// The waveform rules: count above 1; all rows waveforms, of one length.
if (valid({{"W", Type::I16, 1, true}})) return false;
if (prob({{"W", Type::I16, 1, true}}).find("'W'") == std::string::npos) return false;
if (!valid({{"W", Type::I16, 4, true}})) return false;
if (!valid({{"W", Type::I16, 4, true}, {"X", Type::F32, 4, true}})) return false;
if (valid({{"W", Type::I16, 4, true}, {"X", Type::F32, 8, true}})) return false;
if (valid({{"W", Type::I16, 4, true}, {"S", Type::F32, 1, false}})) return false;
if (valid({{"S", Type::F32, 1, false}, {"W", Type::I16, 4, true}})) return false;
if (valid({{"W", Type::I16, 4, true}, {"V", Type::F32, 4, false}})) return false;
if (prob({{"W", Type::I16, 4, true}, {"S", Type::F32, 1, false}}).find("'S'") == std::string::npos)
return false;
if (prob({{"W", Type::I16, 4, true}, {"X", Type::F32, 8, true}}).find("'X'") == std::string::npos)
return false;
// The channel total: scalar 1, vector `count`, waveform 1; at most kMaxChannels.
if (channel_count({{"S", Type::U8, 1, false}, {"V", Type::U8, 5, false}, {"W", Type::U8, 5, true}}) != 7)
return false;
if (!valid({{"V", Type::U8, 256, false}})) return false;
if (valid({{"V", Type::U8, 257, false}})) return false;
if (valid({{"V", Type::U8, 256, false}, {"S", Type::U8, 1, false}})) return false;
if (prob({{"V", Type::U8, 257, false}}).find("256") == std::string::npos) return false;
// Channel decode: a `channels` vector feeds `count` columns of one value, a waveform one column of
// `count`. U16 = 0x1234, I32 = -2, then V[3] of U8 = 7 8 9, then a waveform of two I16 = 1 -1.
const std::vector<Row> cr = {{"A", Type::U16, 1, false}, {"B", Type::I32, 1, false},
{"V", Type::U8, 3, false}, {"W", Type::I16, 2, true}};
const std::uint8_t cd[] = {0x34, 0x12, 0xFE, 0xFF, 0xFF, 0xFF, 7, 8, 9, 0x01, 0x00, 0xFF, 0xFF};
if (datagram_size(cr) != sizeof cd || channel_count(cr) != 6) return false;
std::vector<std::vector<double>> cc;
decode_channels(cr, cd, false, cc);
const std::vector<std::vector<double>> cwant = {{4660}, {-2}, {7}, {8}, {9}, {1, -1}};
if (cc != cwant) return false;
// The wide and unsigned types, little then big endian: a float64, uint16, int32, uint32 and int64.
// F64 -2.5, U16 65535, I32 -123456, U32 4000000000, I64 -5000000000.
const std::vector<Row> tr = {{"F", Type::F64}, {"U", Type::U16}, {"I", Type::I32},
{"L", Type::U32}, {"Q", Type::I64}};
const std::uint8_t tle[] = {0, 0, 0, 0, 0, 0, 0x04, 0xC0, // -2.5 as 0xC004000000000000
0xFF, 0xFF, // 65535
0xC0, 0x1D, 0xFE, 0xFF, // -123456 = 0xFFFE1DC0
0x00, 0x28, 0x6B, 0xEE, // 4000000000 = 0xEE6B2800
0x00, 0x0E, 0xFA, 0xD5, 0xFE, 0xFF, 0xFF, 0xFF}; // -5e9 = 0xFFFFFFFED5FA0E00
if (datagram_size(tr) != sizeof tle) return false;
std::uint8_t tbe[sizeof tle];
{
std::size_t off = 0;
for (const Row& r : tr) {
const std::size_t w = type_size(r.type);
for (std::size_t b = 0; b < w; ++b) tbe[off + b] = tle[off + w - 1 - b];
off += w;
}
}
const double twant[5] = {-2.5, 65535.0, -123456.0, 4000000000.0, -5000000000.0};
for (int pass = 0; pass < 2; ++pass) {
std::vector<std::vector<double>> cols;
decode_datagram(tr, pass == 0 ? tle : tbe, pass == 1, cols);
if (cols.size() != 5) return false;
for (std::size_t k = 0; k < 5; ++k)
if (cols[k].size() != 1 || cols[k][0] != twant[k]) return false;
}
return true;
}
@@ -2115,8 +2194,6 @@ flatudp::FlatUdpSource* flatudp_source(App* a) {
return a->acq != nullptr ? dynamic_cast<flatudp::FlatUdpSource*>(a->acq->source()) : nullptr;
}
std::uint64_t g_flatudp_frames_before = 0;
/// The mock's signal k carries (i * (k + 1) + element) % 100 in packet i, so two consecutive
/// samples of a scalar signal differ by k + 1 modulo 100. Returns the number of steps that do not,
/// over a window clear of the head the producer is writing; -1 when it cannot be read.
@@ -2281,7 +2358,91 @@ gboolean e1608_probe_reconnect(gpointer user) {
return G_SOURCE_REMOVE;
}
/// After the mock's gap: the stream's frame count moved on and the source is connected again.
/// The mock's channels, as the model must list them: a scalar `Name`, the elements of a `channels`
/// vector `Name[k]`, a waveform `Name` at `rate * count`.
struct FlatudpExpect {
std::vector<std::string> names;
std::vector<double> rates;
};
static FlatudpExpect flatudp_expect(const flatudp::FlatUdpSettings& s) {
FlatudpExpect e;
for (const flatudp::Row& r : s.rows) {
if (r.wave) {
e.names.push_back(r.name);
e.rates.push_back(s.rate * double(r.count));
} else if (r.count == 1) {
e.names.push_back(r.name);
e.rates.push_back(s.rate);
} else {
for (std::uint32_t k = 0; k < r.count; ++k) {
e.names.push_back(r.name + "[" + std::to_string(k) + "]");
e.rates.push_back(s.rate);
}
}
}
return e;
}
/// Breaks in a waveform channel as the mock emits it: datagram i carries i, i+1 .. i+count-1, so
/// consecutive samples differ by 1 and the step into the next datagram by 2 - count (mod 100).
/// The block phase is read from the first such step, then held for the whole window.
static long flatudp_wave_breaks(App* a, std::size_t channel, long count) {
if (channel >= a->model->channels().size()) return -1;
const ChannelBuffer& ch = a->model->live().channel(channel);
const u64 n = ch.size();
const u64 want = 400;
if (n <= want + 50) return -1;
std::vector<f32> v(static_cast<std::size_t>(want));
ch.raw(n - want - 50, n - 50, v.data());
const auto step = [&](std::size_t i) {
const long d = std::lround(double(v[i])) - std::lround(double(v[i - 1]));
return ((d % 100) + 100) % 100;
};
const long jump = (((2 - count) % 100) + 100) % 100;
long phase = -1;
for (std::size_t i = 1; i < v.size() && phase < 0; ++i) {
if (step(i) == jump) phase = static_cast<long>(i % std::size_t(count));
}
if (phase < 0) return static_cast<long>(v.size());
long breaks = 0;
for (std::size_t i = 1; i < v.size(); ++i) {
const long want_step = long(i % std::size_t(count)) == phase ? jump : 1;
if (step(i) != want_step) ++breaks;
}
return breaks;
}
/// Samples of two channels at the same index, `b` minus `a`, modulo 100: the mock's element e of a
/// vector row adds e to the row's value, so element k+1 minus element k is 1 at every sample.
static long flatudp_offset_breaks(App* app, std::size_t a, std::size_t b, long offset) {
if (a >= app->model->channels().size() || b >= app->model->channels().size()) return -1;
const ChannelBuffer& ca = app->model->live().channel(a);
const ChannelBuffer& cb = app->model->live().channel(b);
const u64 n = std::min(ca.size(), cb.size());
const u64 want = 400;
if (n <= want + 50) return -1;
std::vector<f32> va(static_cast<std::size_t>(want)), vb(va.size());
ca.raw(n - want - 50, n - 50, va.data());
cb.raw(n - want - 50, n - 50, vb.data());
long breaks = 0;
for (std::size_t i = 0; i < va.size(); ++i) {
const long d = std::lround(double(vb[i])) - std::lround(double(va[i]));
if (((d % 100) + 100) % 100 != offset) ++breaks;
}
return breaks;
}
/// The silence probe is a small state machine on a 100 ms tick, so it needs no knowledge of when the
/// sender's gap falls (the mock's clock starts seconds before the app's): wait until the stream is
/// up, then for the 2 s silence to take the source down, then for it to come back.
struct FlatudpGapState {
int phase = 0; // 0 waiting for the stream, 1 for the silence, 2 for the return
int ticks = 0;
std::uint64_t frames_before = 0;
};
static FlatudpGapState g_flatudp_gap;
gboolean flatudp_probe_resumed(gpointer user) {
App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE;
@@ -2289,27 +2450,55 @@ gboolean flatudp_probe_resumed(gpointer user) {
const std::uint64_t after = a->model->stats().frames.load(std::memory_order_relaxed);
const bool up = src != nullptr && src->connected() &&
a->model->stats().connected.load(std::memory_order_relaxed);
if (!(up && after > g_flatudp_gap.frames_before) && ++g_flatudp_gap.ticks < 200) return G_SOURCE_CONTINUE;
std::fprintf(stderr, "[flatudp] resumed: connected=%d frames_before=%" G_GUINT64_FORMAT
" frames_after=%" G_GUINT64_FORMAT " ok=%d\n",
up ? 1 : 0, g_flatudp_frames_before, after,
up && after > g_flatudp_frames_before ? 1 : 0);
up ? 1 : 0, g_flatudp_gap.frames_before, after,
up && after > g_flatudp_gap.frames_before ? 1 : 0);
std::fflush(stderr);
return G_SOURCE_REMOVE;
}
/// Inside the mock's gap: 2 s of silence must have raised `Disconnected`.
gboolean flatudp_probe_gap(gpointer user) {
/// Polls the silence: the source must go down within 20 s of the stream coming up.
static gboolean flatudp_probe_gap_poll(gpointer user) {
App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE;
flatudp::FlatUdpSource* src = flatudp_source(a);
if (src == nullptr) return G_SOURCE_REMOVE;
FlatudpGapState& g = g_flatudp_gap;
const bool src_up = src->connected();
const bool model_up = a->model->stats().connected.load(std::memory_order_relaxed);
g_flatudp_frames_before = a->model->stats().frames.load(std::memory_order_relaxed);
std::fprintf(stderr, "[flatudp] silence: connected=%d model_connected=%d ok=%d\n", src_up ? 1 : 0,
model_up ? 1 : 0, !src_up && !model_up ? 1 : 0);
std::fflush(stderr);
g_timeout_add(4000, flatudp_probe_resumed, a);
++g.ticks;
if (g.phase == 0) {
if (src_up && model_up) {
g.phase = 1;
g.ticks = 0;
} else if (g.ticks >= 200) {
std::fprintf(stderr, "[flatudp] silence: never connected ok=0\n");
std::fflush(stderr);
return G_SOURCE_REMOVE;
}
return G_SOURCE_CONTINUE;
}
if (!src_up || g.ticks >= 200) {
// The model learns of it through the sink a moment after the source drops: read it a tick later.
if (!src_up && model_up && g.ticks < 200) return G_SOURCE_CONTINUE;
g.frames_before = a->model->stats().frames.load(std::memory_order_relaxed);
std::fprintf(stderr, "[flatudp] silence: connected=%d model_connected=%d after_ms=%d ok=%d\n",
src_up ? 1 : 0, model_up ? 1 : 0, g.ticks * 100, !src_up && !model_up ? 1 : 0);
std::fflush(stderr);
g.ticks = 0;
g.phase = 2;
g_timeout_add(100, flatudp_probe_resumed, a);
return G_SOURCE_REMOVE;
}
return G_SOURCE_CONTINUE;
}
/// Starts the silence probe (see `FlatudpGapState`).
gboolean flatudp_probe_gap(gpointer user) {
g_flatudp_gap = FlatudpGapState{};
g_timeout_add(100, flatudp_probe_gap_poll, user);
return G_SOURCE_REMOVE;
}
@@ -2324,16 +2513,17 @@ gboolean flatudp_probe_report(gpointer user) {
const bool up = a->model->stats().connected.load(std::memory_order_relaxed);
const std::vector<SignalInfo>& sigs = a->model->signals();
const FlatudpExpect ex = flatudp_expect(s);
std::string names, expect_names;
std::size_t expect_channels = 0; // one channel per signal: a vector's elements are consecutive samples
for (const SignalInfo& si : sigs) names += (names.empty() ? "" : ",") + si.name;
for (const flatudp::Row& r : s.rows) {
expect_names += (expect_names.empty() ? "" : ",") + r.name;
++expect_channels;
bool rates_ok = sigs.size() == ex.rates.size();
for (std::size_t i = 0; i < sigs.size(); ++i) {
names += (names.empty() ? "" : ",") + sigs[i].name;
if (i < ex.rates.size() && std::fabs(sigs[i].sampling_rate - ex.rates[i]) > 1e-6) rates_ok = false;
}
for (const std::string& n : ex.names) expect_names += (expect_names.empty() ? "" : ",") + n;
const double rate = sigs.empty() ? 0.0 : sigs.front().sampling_rate;
const std::uint64_t frames = a->model->stats().frames.load(std::memory_order_relaxed);
const bool stream_ok = up && names == expect_names && std::fabs(rate - s.rate) < 1e-6 && frames > 0;
const bool stream_ok = up && names == expect_names && rates_ok && frames > 0;
std::fprintf(stderr,
"[flatudp] stream: connected=%d signals=%zu names=%s expect=%s frames=%" G_GUINT64_FORMAT
" rate=%.3f ok=%d\n",
@@ -2341,18 +2531,38 @@ gboolean flatudp_probe_report(gpointer user) {
stream_ok ? 1 : 0);
const std::size_t channels = a->model->channels().size();
const std::size_t expect_channels = static_cast<std::size_t>(flatudp::channel_count(s.rows));
std::fprintf(stderr, "[flatudp] channels: count=%zu expect=%zu ok=%d\n", channels, expect_channels,
channels == expect_channels ? 1 : 0);
// Only the first two signals are checked, and only in an all-scalar list: a vector row makes the
// model span each frame by its elements, and the scalar channels are then padded with zeros.
if (up && s.rows.size() >= 2 && flatudp::all_scalar(s.rows)) {
const long b0 = flatudp_breaks(a, 0, 1);
const long b1 = flatudp_breaks(a, 1, 2);
std::fprintf(stderr, "[flatudp] continuity: ch0_breaks=%ld ch1_breaks=%ld ok=%d\n", b0, b1,
b0 == 0 && b1 == 0 ? 1 : 0);
// The mock's row k, element e, carries (i * (k + 1) + e) % 100 in datagram i, so consecutive
// samples of a channel from row k differ by k + 1 (mod 100), and element e + 1 of a `channels`
// vector reads one more than element e at the same sample. A waveform channel steps by 1 inside
// a datagram and by 2 - count between two.
if (up && !s.rows.empty() && flatudp::any_wave(s.rows)) {
const long wb = flatudp_wave_breaks(a, 0, static_cast<long>(s.rows[0].count));
std::fprintf(stderr, "[flatudp] continuity: wave_breaks=%ld ok=%d\n", wb, wb == 0 ? 1 : 0);
} else if (up && !s.rows.empty()) {
std::string out;
bool ok = true;
std::size_t base = 0;
bool elem_done = false;
for (std::size_t k = 0; k < s.rows.size(); ++k) {
const flatudp::Row& r = s.rows[k];
const long b = flatudp_breaks(a, base, static_cast<long>(k) + 1);
out += " " + r.name + (r.count > 1 ? "[0]" : "") + "_breaks=" + std::to_string(b);
ok = ok && b == 0;
if (r.count > 1 && !elem_done) {
elem_done = true;
const long ob = flatudp_offset_breaks(a, base, base + 1, 1);
out += " " + r.name + "[1]-[0]_breaks=" + std::to_string(ob);
ok = ok && ob == 0;
}
base += r.count;
}
std::fprintf(stderr, "[flatudp] continuity:%s ok=%d\n", out.c_str(), ok ? 1 : 0);
} else {
std::fprintf(stderr, "[flatudp] continuity: skipped (needs a connected all-scalar list of two or more)\n");
std::fprintf(stderr, "[flatudp] continuity: skipped (not connected)\n");
}
const std::uint64_t wrong = src->wrong_size_datagrams();