flatudp: the signal-list codec, with a hand-computed decode check

This commit is contained in:
Martino Ferrari committed 2026-11-02 00:58:02 +01:00
1 parent dde2dad5b1
commit 6e8e71b32f
6 files changed
+311

No files matched your search

+179
View File
@@ -0,0 +1,179 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#include "source/flatudp/flatudp_signals.hpp"
#include <algorithm>
#include <bit>
#include <cctype>
#include <cstring>
#include <set>
namespace udpscope::flatudp {
namespace {
constexpr const char* kNames[] = {"uint8", "int8", "uint16", "int16", "uint32",
"int32", "uint64", "int64", "float32", "float64"};
constexpr std::size_t kSizes[] = {1, 1, 2, 2, 4, 4, 8, 8, 4, 8};
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);
}
/// A name must survive `format` and `parse`: none of the separators, no edge whitespace.
bool valid_name(const std::string& n) {
if (n.empty()) return false;
if (std::isspace(static_cast<unsigned char>(n.front())) ||
std::isspace(static_cast<unsigned char>(n.back())))
return false;
for (char c : n) {
if (c == ',' || c == ':' || c == '[' || c == ']' || static_cast<unsigned char>(c) < 0x20)
return false;
}
return true;
}
template <class T>
double load(const std::uint8_t* p, bool swap) {
unsigned char b[sizeof(T)];
std::memcpy(b, p, sizeof(T));
if (swap) std::reverse(b, b + sizeof(T));
T v;
std::memcpy(&v, b, sizeof(T));
return static_cast<double>(v);
}
double load_typed(Type t, const std::uint8_t* p, bool swap) {
switch (t) {
case Type::U8: return load<std::uint8_t>(p, swap);
case Type::I8: return load<std::int8_t>(p, swap);
case Type::U16: return load<std::uint16_t>(p, swap);
case Type::I16: return load<std::int16_t>(p, swap);
case Type::U32: return load<std::uint32_t>(p, swap);
case Type::I32: return load<std::int32_t>(p, swap);
case Type::U64: return load<std::uint64_t>(p, swap);
case Type::I64: return load<std::int64_t>(p, swap);
case Type::F32: return load<float>(p, swap);
case Type::F64: return load<double>(p, swap);
}
return 0.0;
}
} // namespace
std::size_t type_size(Type t) { return kSizes[static_cast<std::size_t>(t)]; }
const char* type_name(Type t) { return kNames[static_cast<std::size_t>(t)]; }
bool parse_type(const std::string& s, Type& out) {
for (std::size_t i = 0; i < 10; ++i) {
if (s == kNames[i]) {
out = static_cast<Type>(i);
return true;
}
}
return false;
}
std::size_t datagram_size(const std::vector<Row>& rows) {
std::size_t n = 0;
for (const Row& r : rows) n += type_size(r.type) * static_cast<std::size_t>(r.count);
return n;
}
bool all_scalar(const std::vector<Row>& rows) {
return std::all_of(rows.begin(), rows.end(), [](const Row& r) { return r.count == 1; });
}
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) + ']';
}
return out;
}
std::string parse(const std::string& text, std::vector<Row>& rows) {
rows.clear();
if (trim(text).empty()) return {};
std::size_t pos = 0;
int index = 0;
for (;;) {
std::size_t end = text.find(',', pos);
if (end == std::string::npos) end = text.size();
++index;
const std::string where = "signal " + std::to_string(index) + ": ";
const std::string tok = trim(text.substr(pos, end - pos));
const std::size_t colon = tok.find(':');
if (colon == std::string::npos) {
rows.clear();
return where + "expected name:type";
}
Row r;
r.name = trim(tok.substr(0, colon));
std::string ty = trim(tok.substr(colon + 1));
const std::size_t br = ty.find('[');
if (br != std::string::npos) {
if (ty.back() != ']') {
rows.clear();
return where + "missing ']' after the count";
}
const std::string n = trim(ty.substr(br + 1, ty.size() - br - 2));
const bool digits = !n.empty() && n.size() <= 6 &&
std::all_of(n.begin(), n.end(), [](char c) { return c >= '0' && c <= '9'; });
if (!digits) {
rows.clear();
return where + "the count must be a whole number";
}
r.count = static_cast<std::uint32_t>(std::stoul(n));
ty = trim(ty.substr(0, br));
}
if (!parse_type(ty, r.type)) {
rows.clear();
return where + "unknown type '" + ty + "'";
}
rows.push_back(r);
if (end >= text.size()) break;
pos = end + 1;
}
return {};
}
std::string problem(const std::vector<Row>& rows) {
if (rows.empty()) return "add at least one signal";
std::set<std::string> seen;
for (std::size_t i = 0; i < rows.size(); ++i) {
const Row& r = rows[i];
if (r.name.empty()) return "signal " + std::to_string(i + 1) + " has no name";
if (!valid_name(r.name))
return "'" + r.name + "': a name cannot hold , : [ ] or edge spaces";
if (!seen.insert(r.name).second) return "the name '" + r.name + "' is used twice";
if (r.count < 1 || r.count > kMaxCount)
return "'" + r.name + "': the count must be 1 to " + std::to_string(kMaxCount);
}
const std::size_t n = datagram_size(rows);
if (n > kMaxDatagram)
return std::to_string(n) + " bytes per datagram exceed the " + std::to_string(kMaxDatagram) +
" a UDP datagram can carry";
return {};
}
void decode_datagram(const std::vector<Row>& rows, const std::uint8_t* data, bool big_endian,
std::vector<std::vector<double>>& cols) {
if (cols.size() < rows.size()) cols.resize(rows.size());
const bool swap = big_endian != (std::endian::native == std::endian::big);
std::size_t off = 0;
for (std::size_t s = 0; s < rows.size(); ++s) {
const std::size_t w = type_size(rows[s].type);
for (std::uint32_t k = 0; k < rows[s].count; ++k) {
cols[s].push_back(load_typed(rows[s].type, data + off, swap));
off += w;
}
}
}
} // namespace udpscope::flatudp
+51
View File
@@ -0,0 +1,51 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
namespace udpscope::flatudp {
/// The numeric value is the `SignalInfo::type_code` (the order of the client library's `UDPS_T_*`).
enum class Type : std::uint8_t { U8 = 0, I8, U16, I16, U32, I32, U64, I64, F32, F64 };
constexpr std::uint32_t kMaxCount = 4096;
constexpr std::size_t kMaxDatagram = 65507; ///< the largest UDP payload over IPv4
/// 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;
bool operator==(const Row&) const = default;
};
std::size_t type_size(Type t);
const char* type_name(Type t); ///< "uint8" ... "float64"
bool parse_type(const std::string& s, Type& out);
/// The bytes one datagram must have: the sum of `type_size * count`.
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.
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
/// on success; `rows` is empty after a failure.
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 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,
std::vector<std::vector<double>>& cols);
} // namespace udpscope::flatudp
+3
View File
@@ -6118,6 +6118,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
if (const char* env = g_getenv("UDPSCOPE_PROBE_E1608_RECONNECT")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), e1608_probe_reconnect, &app);
}
if (const char* env = g_getenv("UDPSCOPE_PROBE_FLATUDP")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), flatudp_probe, &app);
}
// Our own options were consumed by parse_args(); GTK would reject them, so only
// the program name is handed to g_application_run().
+3
View File
@@ -1883,6 +1883,9 @@ gboolean e1608_probe(gpointer user);
gboolean e1608_probe_report(gpointer user);
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 wrong-size accounting and the silence timeout.
gboolean flatudp_probe(gpointer user);
gboolean pan_probe(gpointer user);
gboolean arm_probe(gpointer user);
gboolean trigrenum_probe_step3(gpointer user);
+74
View File
@@ -8,6 +8,7 @@
#include "source/probe_source.hpp"
#include "source/e1608/e1608_source.hpp"
#include "source/flatudp/flatudp_signals.hpp"
#include "source/registry.hpp"
#include "ui_app.hpp"
@@ -2037,6 +2038,70 @@ bool e1608_decode_check() {
return true;
}
/// The `signals` codec and the datagram decode against values worked out by hand. The row set
/// mixes every width and a vector: A:uint8, B:int16, C:float32, D:uint64[2], E:int8, 24 bytes.
/// A = 254, B = -2, C = 1.5, D = {1, 4294967298}, E = -1.
bool flatudp_codec_check() {
using namespace flatudp;
std::vector<Row> rows;
if (!parse(" A:uint8, B:int16,C : float32 ,D:uint64[2],E:int8", rows).empty()) return false;
const std::vector<Row> want = {{"A", Type::U8, 1}, {"B", Type::I16, 1}, {"C", Type::F32, 1},
{"D", Type::U64, 2}, {"E", Type::I8, 1}};
if (rows != want) return false;
if (format(rows) != "A:uint8,B:int16,C:float32,D:uint64[2],E:int8") return false;
if (datagram_size(rows) != 24 || all_scalar(rows) || !problem(rows).empty()) return false;
// The enum value is the type code the model keeps.
if (static_cast<int>(Type::F64) != 9 || static_cast<int>(Type::U64) != 6) return false;
const std::uint8_t le[24] = {0xFE, // A
0xFE, 0xFF, // B
0x00, 0x00, 0xC0, 0x3F, // C
0x01, 0, 0, 0, 0, 0, 0, 0, // D[0]
0x02, 0, 0, 0, 0x01, 0, 0, 0, // D[1]
0xFF}; // E
const std::uint8_t be[24] = {0xFE,
0xFF, 0xFE,
0x3F, 0xC0, 0x00, 0x00,
0, 0, 0, 0, 0, 0, 0, 0x01,
0, 0, 0, 0x01, 0, 0, 0, 0x02,
0xFF};
const double expect[5][2] = {{254, 0}, {-2, 0}, {1.5, 0}, {1, 4294967298.0}, {-1, 0}};
const std::size_t counts[5] = {1, 1, 1, 2, 1};
for (int pass = 0; pass < 2; ++pass) {
std::vector<std::vector<double>> cols;
decode_datagram(rows, pass == 0 ? le : be, pass == 1, cols);
if (cols.size() != 5) return false;
for (std::size_t s = 0; s < 5; ++s) {
if (cols[s].size() != counts[s]) return false;
for (std::size_t k = 0; k < counts[s]; ++k)
if (cols[s][k] != expect[s][k]) return false;
}
}
// Parse failures name the signal; a bad list never half-parses.
std::vector<Row> bad;
if (parse("A:uint8,B", bad).find("signal 2") == std::string::npos || !bad.empty()) return false;
if (parse("A:uint7", bad).find("uint7") == std::string::npos) return false;
if (parse("A:uint8[x]", bad).empty() || parse("A:uint8[3", bad).empty()) return false;
if (!parse("", bad).empty() || !bad.empty() || problem(bad).empty()) return false;
// The validity rules.
const auto prob = [](std::vector<Row> r) { return problem(r); };
// One call per statement: GCC warns (-Wdangling-pointer) on two list temporaries joined by `||`.
const auto valid = [](std::vector<Row> r) { return problem(r).empty(); };
if (prob({{"A", Type::U8, 1}, {"A", Type::U8, 1}}).find("twice") == std::string::npos) return false;
if (valid({{"", Type::U8, 1}})) return false;
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)
return false;
if (!valid({{"A", Type::F64, 4096}, {"B", Type::F32, 4096}})) return false;
return true;
}
} // namespace
/// Phase 2, 1 s after the controls were driven: the stream, the calibration, the capture button,
@@ -2182,6 +2247,15 @@ gboolean e1608_probe_reconnect(gpointer user) {
return G_SOURCE_REMOVE;
}
/// The flat UDP source's probe. For now only the pure codec check; it needs no flatudp source.
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);
std::fflush(stderr);
return G_SOURCE_REMOVE;
}
gboolean pan_probe(gpointer user) {
App* a = static_cast<App*>(user);
if (a->window == nullptr || a->rows->row_count() <= 0) return G_SOURCE_REMOVE;