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:
1 parent
a523bc2bd9
commit
e3bc1f72f3
10 files changed
+446
-100
No files matched your search
+240
-30
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user