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
@@ -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
|
||||
Reference in new issue
Block a user