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