flatudp: the signal-list codec, with a hand-computed decode check
This commit is contained in:
1 parent
dde2dad5b1
commit
6e8e71b32f
6 files changed
+311
No files matched your search
@@ -247,6 +247,7 @@ add_executable(udpscope
|
|||||||
src/source/e1608/e1608_source.cpp
|
src/source/e1608/e1608_source.cpp
|
||||||
src/source/e1608/e1608_kind.cpp
|
src/source/e1608/e1608_kind.cpp
|
||||||
src/source/e1608/e1608_panels.cpp
|
src/source/e1608/e1608_panels.cpp
|
||||||
|
src/source/flatudp/flatudp_signals.cpp
|
||||||
src/plot_host.cpp
|
src/plot_host.cpp
|
||||||
src/export.cpp
|
src/export.cpp
|
||||||
src/search.cpp
|
src/search.cpp
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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")) {
|
if (const char* env = g_getenv("UDPSCOPE_PROBE_E1608_RECONNECT")) {
|
||||||
g_timeout_add(static_cast<guint>(std::atoi(env)), e1608_probe_reconnect, &app);
|
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
|
// Our own options were consumed by parse_args(); GTK would reject them, so only
|
||||||
// the program name is handed to g_application_run().
|
// the program name is handed to g_application_run().
|
||||||
|
|||||||
@@ -1883,6 +1883,9 @@ gboolean e1608_probe(gpointer user);
|
|||||||
gboolean e1608_probe_report(gpointer user);
|
gboolean e1608_probe_report(gpointer user);
|
||||||
gboolean e1608_probe_reconnect(gpointer user);
|
gboolean e1608_probe_reconnect(gpointer user);
|
||||||
gboolean e1608_probe_resumed(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 pan_probe(gpointer user);
|
||||||
gboolean arm_probe(gpointer user);
|
gboolean arm_probe(gpointer user);
|
||||||
gboolean trigrenum_probe_step3(gpointer user);
|
gboolean trigrenum_probe_step3(gpointer user);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include "source/probe_source.hpp"
|
#include "source/probe_source.hpp"
|
||||||
#include "source/e1608/e1608_source.hpp"
|
#include "source/e1608/e1608_source.hpp"
|
||||||
|
#include "source/flatudp/flatudp_signals.hpp"
|
||||||
#include "source/registry.hpp"
|
#include "source/registry.hpp"
|
||||||
#include "ui_app.hpp"
|
#include "ui_app.hpp"
|
||||||
|
|
||||||
@@ -2037,6 +2038,70 @@ bool e1608_decode_check() {
|
|||||||
return true;
|
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
|
} // namespace
|
||||||
|
|
||||||
/// Phase 2, 1 s after the controls were driven: the stream, the calibration, the capture button,
|
/// 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;
|
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) {
|
gboolean pan_probe(gpointer user) {
|
||||||
App* a = static_cast<App*>(user);
|
App* a = static_cast<App*>(user);
|
||||||
if (a->window == nullptr || a->rows->row_count() <= 0) return G_SOURCE_REMOVE;
|
if (a->window == nullptr || a->rows->row_count() <= 0) return G_SOURCE_REMOVE;
|
||||||
|
|||||||
Reference in new issue
Block a user