flatudp: the source, kind, mock sender and stream probes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
6e8e71b32f
commit
78a29fbc39
13 files changed
+962
-7
No files matched your search
@@ -0,0 +1,206 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||
|
||||
#include "source/flatudp/flatudp_source.hpp"
|
||||
#include "source/registry.hpp"
|
||||
|
||||
#include <adwaita.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace udpscope {
|
||||
namespace {
|
||||
|
||||
using namespace flatudp;
|
||||
using ChangeCb = std::shared_ptr<std::function<void()>>;
|
||||
|
||||
std::string num(double v) {
|
||||
char b[48];
|
||||
std::snprintf(b, sizeof b, "%.10g", v);
|
||||
return b;
|
||||
}
|
||||
|
||||
GtkWidget* entry_row(const char* title) {
|
||||
GtkWidget* r = adw_entry_row_new();
|
||||
adw_preferences_row_set_title(ADW_PREFERENCES_ROW(r), title);
|
||||
adw_preferences_row_set_use_underline(ADW_PREFERENCES_ROW(r), TRUE);
|
||||
return r;
|
||||
}
|
||||
|
||||
GtkWidget* spin_row(const char* title, double lo, double hi, double step, double def, int digits) {
|
||||
GtkWidget* r = GTK_WIDGET(adw_spin_row_new_with_range(lo, hi, step));
|
||||
adw_preferences_row_set_title(ADW_PREFERENCES_ROW(r), title);
|
||||
adw_preferences_row_set_use_underline(ADW_PREFERENCES_ROW(r), TRUE);
|
||||
adw_spin_row_set_digits(ADW_SPIN_ROW(r), static_cast<guint>(digits));
|
||||
adw_spin_row_set_value(ADW_SPIN_ROW(r), def);
|
||||
return r;
|
||||
}
|
||||
|
||||
GtkWidget* combo_row(const char* title, const char* const* items) {
|
||||
GtkWidget* r = adw_combo_row_new();
|
||||
adw_preferences_row_set_title(ADW_PREFERENCES_ROW(r), title);
|
||||
adw_preferences_row_set_use_underline(ADW_PREFERENCES_ROW(r), TRUE);
|
||||
adw_combo_row_set_model(ADW_COMBO_ROW(r), G_LIST_MODEL(gtk_string_list_new(items)));
|
||||
return r;
|
||||
}
|
||||
|
||||
GtkWidget* expander(const char* title, std::initializer_list<GtkWidget*> rows) {
|
||||
GtkWidget* e = adw_expander_row_new();
|
||||
adw_preferences_row_set_title(ADW_PREFERENCES_ROW(e), title);
|
||||
for (GtkWidget* r : rows) adw_expander_row_add_row(ADW_EXPANDER_ROW(e), r);
|
||||
return e;
|
||||
}
|
||||
|
||||
std::string text_of(GtkWidget* w) { return gtk_editable_get_text(GTK_EDITABLE(w)); }
|
||||
double value_of(GtkWidget* w) { return adw_spin_row_get_value(ADW_SPIN_ROW(w)); }
|
||||
guint sel_of(GtkWidget* w) { return adw_combo_row_get_selected(ADW_COMBO_ROW(w)); }
|
||||
|
||||
void mark(GtkWidget* w, bool bad) {
|
||||
if (bad) {
|
||||
gtk_widget_add_css_class(w, "error");
|
||||
} else {
|
||||
gtk_widget_remove_css_class(w, "error");
|
||||
}
|
||||
}
|
||||
|
||||
/// The form's widgets, shared by every signal closure and every `ConfigForm` closure.
|
||||
struct Form {
|
||||
GtkWidget *port, *bind, *multicast, *iface, *rate, *endian, *signals;
|
||||
ChangeCb changed = std::make_shared<std::function<void()>>();
|
||||
bool updating = false;
|
||||
bool alive = true; ///< cleared when the chooser destroys the rows; handlers then touch nothing
|
||||
|
||||
/// A bad address is marked with the `error` style; the rest of the problem is `valid()`'s.
|
||||
void refresh() {
|
||||
mark(bind, !text_of(bind).empty() && !valid_ipv4(text_of(bind)));
|
||||
mark(multicast, !text_of(multicast).empty() && !valid_multicast(text_of(multicast)));
|
||||
mark(iface, !text_of(iface).empty() && !valid_ipv4(text_of(iface)));
|
||||
}
|
||||
|
||||
SourceConfig read() const {
|
||||
SourceConfig c;
|
||||
c.set("port", num(value_of(port)));
|
||||
if (!text_of(bind).empty()) c.set("bind", text_of(bind));
|
||||
if (!text_of(multicast).empty()) c.set("multicast", text_of(multicast));
|
||||
if (!text_of(iface).empty()) c.set("iface", text_of(iface));
|
||||
c.set("rate", num(value_of(rate)));
|
||||
c.set("endian", sel_of(endian) == 1 ? "big" : "little");
|
||||
c.set("signals", text_of(signals));
|
||||
return c;
|
||||
}
|
||||
|
||||
void write(const SourceConfig& cfg) {
|
||||
const FlatUdpSettings s = FlatUdpSettings::from(cfg);
|
||||
updating = true;
|
||||
adw_spin_row_set_value(ADW_SPIN_ROW(port), s.port);
|
||||
gtk_editable_set_text(GTK_EDITABLE(bind), s.bind.c_str());
|
||||
gtk_editable_set_text(GTK_EDITABLE(multicast), s.multicast.c_str());
|
||||
gtk_editable_set_text(GTK_EDITABLE(iface), s.iface.c_str());
|
||||
if (s.rate > 0.0) adw_spin_row_set_value(ADW_SPIN_ROW(rate), s.rate);
|
||||
adw_combo_row_set_selected(ADW_COMBO_ROW(endian), s.big_endian ? 1 : 0);
|
||||
gtk_editable_set_text(GTK_EDITABLE(signals), cfg.get("signals").c_str());
|
||||
updating = false;
|
||||
refresh();
|
||||
}
|
||||
|
||||
bool valid() const { return FlatUdpSettings::from(read()).problem().empty(); }
|
||||
};
|
||||
|
||||
using FormPtr = std::shared_ptr<Form>;
|
||||
|
||||
void on_notify(GObject*, GParamSpec*, gpointer user) {
|
||||
Form& f = **static_cast<FormPtr*>(user);
|
||||
if (!f.alive) return;
|
||||
f.refresh();
|
||||
if (!f.updating && f.changed && *f.changed) (*f.changed)();
|
||||
}
|
||||
|
||||
void on_edited(GtkEditable*, gpointer user) {
|
||||
Form& f = **static_cast<FormPtr*>(user);
|
||||
if (!f.alive) return;
|
||||
f.refresh();
|
||||
if (!f.updating && f.changed && *f.changed) (*f.changed)();
|
||||
}
|
||||
|
||||
void free_form(gpointer d, GClosure*) { delete static_cast<FormPtr*>(d); }
|
||||
|
||||
/// "destroy" on a row group: the siblings the handlers reach by raw pointer may already be gone.
|
||||
void on_form_destroy(GtkWidget*, gpointer user) { (**static_cast<FormPtr*>(user)).alive = false; }
|
||||
|
||||
void hook_destroy(GtkWidget* w, const FormPtr& f) {
|
||||
g_signal_connect_data(w, "destroy", G_CALLBACK(on_form_destroy), new FormPtr(f), free_form,
|
||||
static_cast<GConnectFlags>(0));
|
||||
}
|
||||
|
||||
void hook_notify(GtkWidget* w, const char* signal, const FormPtr& f) {
|
||||
g_signal_connect_data(w, signal, G_CALLBACK(on_notify), new FormPtr(f), free_form,
|
||||
static_cast<GConnectFlags>(0));
|
||||
}
|
||||
|
||||
void hook_entry(GtkWidget* w, const FormPtr& f) {
|
||||
g_signal_connect_data(w, "changed", G_CALLBACK(on_edited), new FormPtr(f), free_form,
|
||||
static_cast<GConnectFlags>(0));
|
||||
}
|
||||
|
||||
class FlatUdpKind final : public SourceKind {
|
||||
public:
|
||||
std::string id() const override { return "flatudp"; }
|
||||
std::string display_name() const override { return "Flat UDP (MARTe2 UDPSender)"; }
|
||||
bool has_datagrams() const override { return true; }
|
||||
|
||||
std::string identity(const SourceConfig& cfg) const override {
|
||||
return FlatUdpSettings::from(cfg).identity();
|
||||
}
|
||||
|
||||
std::unique_ptr<DataSource> make(const SourceConfig& cfg) const override {
|
||||
return std::make_unique<FlatUdpSource>(cfg);
|
||||
}
|
||||
|
||||
ConfigForm make_config_form() const override {
|
||||
auto f = std::make_shared<Form>();
|
||||
static const char* const kEndian[] = {"Little endian", "Big endian", nullptr};
|
||||
|
||||
f->port = spin_row("_Port", 1, 65535, 1, kDefaultPort, 0);
|
||||
f->bind = entry_row("_Bind address");
|
||||
f->multicast = entry_row("_Multicast group");
|
||||
f->iface = entry_row("Multicast _interface");
|
||||
f->rate = spin_row("Cycle _rate (Hz)", 0.001, 1000000, 1, 1000, 3);
|
||||
f->endian = combo_row("_Byte order", kEndian);
|
||||
f->signals = entry_row("_Signals");
|
||||
|
||||
GtkWidget* net = expander("Network", {f->port, f->bind, f->multicast, f->iface});
|
||||
GtkWidget* timing = expander("Timing and format", {f->rate, f->endian});
|
||||
GtkWidget* sig = expander("Signals", {f->signals});
|
||||
adw_expander_row_set_expanded(ADW_EXPANDER_ROW(net), TRUE);
|
||||
adw_expander_row_set_expanded(ADW_EXPANDER_ROW(sig), TRUE);
|
||||
|
||||
for (GtkWidget* w : {net, timing, sig}) hook_destroy(w, f);
|
||||
for (GtkWidget* w : {f->bind, f->multicast, f->iface, f->signals}) hook_entry(w, f);
|
||||
for (GtkWidget* w : {f->port, f->rate}) hook_notify(w, "notify::value", f);
|
||||
hook_notify(f->endian, "notify::selected", f);
|
||||
f->refresh();
|
||||
|
||||
ConfigForm out;
|
||||
out.rows = {net, timing, sig};
|
||||
out.description =
|
||||
"The signal list must match the sender's exactly, in order: the datagrams carry no "
|
||||
"description. Write name:type[count], comma separated, e.g. Trigger:uint8,Ia:float32,"
|
||||
"Wave:float32[16]";
|
||||
out.focus = f->signals;
|
||||
out.write = [f](const SourceConfig& c) { f->write(c); };
|
||||
out.read = [f]() { return f->read(); };
|
||||
out.valid = [f]() { return f->valid(); };
|
||||
ChangeCb cb = f->changed;
|
||||
out.on_change = [cb](std::function<void()> fn) { *cb = std::move(fn); };
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<SourceKind> make_flatudp_kind() { return std::make_unique<FlatUdpKind>(); }
|
||||
|
||||
} // namespace udpscope
|
||||
@@ -0,0 +1,286 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||
|
||||
#include "source/flatudp/flatudp_source.hpp"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
#include <poll.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cerrno>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
|
||||
namespace udpscope::flatudp {
|
||||
namespace {
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
std::string lower(std::string s) {
|
||||
for (char& c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
return s;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
double epoch_seconds() {
|
||||
return std::chrono::duration<double>(std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
constexpr std::size_t kMaxReportedSizes = 8; ///< a sender of random sizes must not flood stderr
|
||||
|
||||
} // namespace
|
||||
|
||||
bool valid_ipv4(const std::string& s) {
|
||||
in_addr a{};
|
||||
return inet_pton(AF_INET, s.c_str(), &a) == 1;
|
||||
}
|
||||
|
||||
bool valid_multicast(const std::string& s) {
|
||||
in_addr a{};
|
||||
if (inet_pton(AF_INET, s.c_str(), &a) != 1) return false;
|
||||
return (ntohl(a.s_addr) >> 28) == 0xE;
|
||||
}
|
||||
|
||||
// --- FlatUdpSettings ------------------------------------------------------------------------
|
||||
|
||||
FlatUdpSettings FlatUdpSettings::from(const SourceConfig& c) {
|
||||
FlatUdpSettings s;
|
||||
s.port = static_cast<int>(c.get_int("port", kDefaultPort));
|
||||
s.bind = trim(c.get("bind"));
|
||||
s.multicast = trim(c.get("multicast"));
|
||||
s.iface = trim(c.get("iface"));
|
||||
s.rate = c.get_double("rate", 0.0);
|
||||
const std::string e = lower(trim(c.get("endian", "little")));
|
||||
s.endian_ok = e == "little" || e == "big";
|
||||
s.big_endian = e == "big";
|
||||
s.signals_error = parse(c.get("signals"), s.rows);
|
||||
return s;
|
||||
}
|
||||
|
||||
std::string FlatUdpSettings::problem() const {
|
||||
if (port < 1 || port > 65535) return "the port must be 1 to 65535";
|
||||
if (!signals_error.empty()) return signals_error;
|
||||
const std::string p = flatudp::problem(rows);
|
||||
if (!p.empty()) return p;
|
||||
if (!(rate > 0.0) || !std::isfinite(rate)) return "give the sender's cycle rate in Hz (rate)";
|
||||
if (!endian_ok) return "endian must be little or big";
|
||||
if (!bind.empty() && !valid_ipv4(bind)) return "the bind address '" + bind + "' is not an IPv4 address";
|
||||
if (!multicast.empty() && !valid_multicast(multicast))
|
||||
return "'" + multicast + "' is not a multicast group (224.0.0.0 to 239.255.255.255)";
|
||||
if (!iface.empty() && !valid_ipv4(iface))
|
||||
return "the interface address '" + iface + "' is not an IPv4 address";
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string FlatUdpSettings::identity() const {
|
||||
return (multicast.empty() ? std::string() : multicast + ":") + std::to_string(port);
|
||||
}
|
||||
|
||||
std::size_t FlatUdpSettings::block() const {
|
||||
if (!all_scalar(rows) || !(rate > 0.0) || !std::isfinite(rate)) return 1;
|
||||
const long b = std::lround(rate / 100.0);
|
||||
return static_cast<std::size_t>(std::min(1024L, std::max(1L, b)));
|
||||
}
|
||||
|
||||
// --- FlatUdpSource --------------------------------------------------------------------------
|
||||
|
||||
FlatUdpSource::FlatUdpSource(const SourceConfig& cfg) : cfg_(FlatUdpSettings::from(cfg)) {}
|
||||
|
||||
FlatUdpSource::~FlatUdpSource() { close_fd(); }
|
||||
|
||||
std::string FlatUdpSource::last_message() const {
|
||||
std::lock_guard<std::mutex> lk(msg_mu_);
|
||||
return last_message_;
|
||||
}
|
||||
|
||||
void FlatUdpSource::note(const std::string& msg) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(msg_mu_);
|
||||
last_message_ = msg;
|
||||
}
|
||||
std::fprintf(stderr, "[flatudp] %s\n", msg.c_str());
|
||||
std::fflush(stderr);
|
||||
}
|
||||
|
||||
void FlatUdpSource::close_fd() {
|
||||
if (fd_ >= 0) {
|
||||
::close(fd_);
|
||||
fd_ = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void FlatUdpSource::open(SourceSink& sink) {
|
||||
const std::string p = cfg_.problem();
|
||||
if (!p.empty()) throw std::runtime_error(p);
|
||||
sink_ = &sink;
|
||||
|
||||
signals_.clear();
|
||||
for (const Row& r : cfg_.rows) {
|
||||
SignalInfo s;
|
||||
s.name = r.name;
|
||||
s.type_code = static_cast<std::uint8_t>(r.type);
|
||||
s.rows = r.count;
|
||||
s.cols = 1;
|
||||
s.time_mode = kTimePacket;
|
||||
s.sampling_rate = cfg_.rate;
|
||||
signals_.push_back(s);
|
||||
}
|
||||
expected_ = datagram_size(cfg_.rows);
|
||||
block_ = cfg_.block();
|
||||
pending_.assign(cfg_.rows.size(), {});
|
||||
pending_n_ = 0;
|
||||
rx_.assign(65536, 0);
|
||||
|
||||
const auto fail = [this](const std::string& what) {
|
||||
const std::string msg = what + ": " + std::strerror(errno);
|
||||
close_fd();
|
||||
throw std::runtime_error(msg);
|
||||
};
|
||||
|
||||
fd_ = ::socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
|
||||
if (fd_ < 0) fail("cannot create a UDP socket");
|
||||
// Receivers of one multicast group share its port; a unicast port is the one app's.
|
||||
if (!cfg_.multicast.empty()) {
|
||||
const int one = 1;
|
||||
::setsockopt(fd_, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
|
||||
}
|
||||
const int rcvbuf = 4 << 20; // best effort: a 1 kHz sender at 64 KiB would not fit a default one
|
||||
::setsockopt(fd_, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof rcvbuf);
|
||||
|
||||
sockaddr_in addr{};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(static_cast<std::uint16_t>(cfg_.port));
|
||||
addr.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
if (!cfg_.bind.empty()) inet_pton(AF_INET, cfg_.bind.c_str(), &addr.sin_addr);
|
||||
if (::bind(fd_, reinterpret_cast<const sockaddr*>(&addr), sizeof addr) < 0)
|
||||
fail("cannot bind UDP port " + std::to_string(cfg_.port));
|
||||
|
||||
if (!cfg_.multicast.empty()) {
|
||||
ip_mreq mreq{};
|
||||
inet_pton(AF_INET, cfg_.multicast.c_str(), &mreq.imr_multiaddr);
|
||||
if (cfg_.iface.empty()) {
|
||||
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
|
||||
} else {
|
||||
inet_pton(AF_INET, cfg_.iface.c_str(), &mreq.imr_interface);
|
||||
}
|
||||
if (::setsockopt(fd_, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof mreq) < 0)
|
||||
fail("cannot join multicast group " + cfg_.multicast);
|
||||
}
|
||||
}
|
||||
|
||||
void FlatUdpSource::close() {
|
||||
close_fd();
|
||||
connected_.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
SourceStats FlatUdpSource::stats() const {
|
||||
SourceStats s;
|
||||
s.packets = valid_.load(std::memory_order_relaxed) + wrong_.load(std::memory_order_relaxed);
|
||||
s.frames = frames_;
|
||||
s.stale = wrong_.load(std::memory_order_relaxed);
|
||||
s.reconnects = reconnects_;
|
||||
s.connected = connected_.load(std::memory_order_relaxed);
|
||||
return s;
|
||||
}
|
||||
|
||||
void FlatUdpSource::poll(int timeout_ms) {
|
||||
drain_commands();
|
||||
// Never wait long: the partial-block flush and the silence timeout are checked on this cadence.
|
||||
const int wait = std::clamp(timeout_ms, 0, 20);
|
||||
if (fd_ < 0 || sink_ == nullptr) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(wait));
|
||||
return;
|
||||
}
|
||||
if (!configured_) {
|
||||
configured_ = true;
|
||||
sink_->on_config(signals_, 1);
|
||||
}
|
||||
pollfd p{};
|
||||
p.fd = fd_;
|
||||
p.events = POLLIN;
|
||||
if (::poll(&p, 1, wait) > 0 && (p.revents & POLLIN)) drain_socket();
|
||||
|
||||
const auto now = Clock::now();
|
||||
if (pending_n_ > 0 && now - batch_start_ >= 100ms) flush(); // a stopped sender holds nothing back
|
||||
if (connected_.load(std::memory_order_relaxed) && now - last_valid_ >= 2s) {
|
||||
connected_.store(false, std::memory_order_relaxed);
|
||||
sink_->on_event(SourceEvent::Disconnected, "no datagram for 2 s");
|
||||
}
|
||||
}
|
||||
|
||||
void FlatUdpSource::drain_socket() {
|
||||
for (int i = 0; i < 8192; ++i) { // bounded, so a flood cannot starve the timers
|
||||
const ssize_t n = ::recv(fd_, rx_.data(), rx_.size(), 0);
|
||||
if (n < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
break; // EAGAIN: drained
|
||||
}
|
||||
const double t = epoch_seconds();
|
||||
const std::size_t len = static_cast<std::size_t>(n);
|
||||
if (tap_) tap_(rx_.data(), len, t);
|
||||
if (len == expected_) {
|
||||
accept(rx_.data());
|
||||
} else {
|
||||
reject(len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FlatUdpSource::accept(const std::uint8_t* data) {
|
||||
valid_.fetch_add(1, std::memory_order_relaxed);
|
||||
last_valid_ = Clock::now();
|
||||
if (!connected_.load(std::memory_order_relaxed)) {
|
||||
if (ever_connected_) ++reconnects_;
|
||||
ever_connected_ = true;
|
||||
connected_.store(true, std::memory_order_relaxed);
|
||||
sink_->on_event(SourceEvent::Connected, "");
|
||||
}
|
||||
decode_datagram(cfg_.rows, data, cfg_.big_endian, pending_);
|
||||
if (pending_n_ == 0) batch_start_ = last_valid_;
|
||||
if (++pending_n_ >= block_) flush();
|
||||
}
|
||||
|
||||
void FlatUdpSource::reject(std::size_t len) {
|
||||
wrong_.fetch_add(1, std::memory_order_relaxed);
|
||||
if (reported_sizes_.size() >= kMaxReportedSizes || !reported_sizes_.insert(len).second) return;
|
||||
const std::string msg = "datagram of " + std::to_string(len) + " bytes, expected " +
|
||||
std::to_string(expected_) + ", check the signal list";
|
||||
note(msg);
|
||||
sink_->on_event(SourceEvent::Error, msg.c_str());
|
||||
}
|
||||
|
||||
void FlatUdpSource::flush() {
|
||||
const std::size_t n = signals_.size();
|
||||
views_.resize(n);
|
||||
for (std::size_t i = 0; i < n; ++i)
|
||||
views_[i] = SignalValues{pending_[i].data(), static_cast<std::uint32_t>(pending_[i].size())};
|
||||
Frame f;
|
||||
f.counter = frame_counter_++;
|
||||
f.lost = 0;
|
||||
f.late = false;
|
||||
f.recv_time = epoch_seconds();
|
||||
f.publish_mode = 1;
|
||||
f.num_samples = static_cast<std::uint32_t>(pending_n_);
|
||||
f.num_signals = static_cast<std::uint32_t>(n);
|
||||
f.signals = signals_.data();
|
||||
f.values = views_.data();
|
||||
++frames_;
|
||||
sink_->on_data(f);
|
||||
for (auto& c : pending_) c.clear();
|
||||
pending_n_ = 0;
|
||||
}
|
||||
|
||||
} // namespace udpscope::flatudp
|
||||
@@ -0,0 +1,104 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "source/data_source.hpp"
|
||||
#include "source/flatudp/flatudp_signals.hpp"
|
||||
#include "source/source_config.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace udpscope::flatudp {
|
||||
|
||||
constexpr int kDefaultPort = 44488; ///< `UDPSender`'s own default
|
||||
|
||||
bool valid_ipv4(const std::string& s);
|
||||
/// An IPv4 address in 224.0.0.0/4.
|
||||
bool valid_multicast(const std::string& s);
|
||||
|
||||
/// Everything a configured flat UDP source needs, parsed once from the flat `SourceConfig`.
|
||||
struct FlatUdpSettings {
|
||||
int port = kDefaultPort;
|
||||
std::string bind; ///< local address; empty is any
|
||||
std::string multicast; ///< group to join; empty is plain unicast
|
||||
std::string iface; ///< local interface address for the join; empty is the system's choice
|
||||
double rate = 0.0; ///< the sender's cycle rate in Hz; 0 means not given
|
||||
bool big_endian = false;
|
||||
bool endian_ok = true; ///< false when `endian` is neither "little" nor "big"
|
||||
std::vector<Row> rows;
|
||||
std::string signals_error; ///< why the `signals` text did not parse; `rows` is empty then
|
||||
|
||||
static FlatUdpSettings from(const SourceConfig& cfg);
|
||||
/// The first reason this cannot connect, in words for the user; empty when it can.
|
||||
std::string problem() const;
|
||||
/// `[multicast:]port`; names the session file.
|
||||
std::string identity() const;
|
||||
/// Datagrams per frame: 1 for a list with a vector, else `clamp(round(rate / 100), 1, 1024)`.
|
||||
std::size_t block() const;
|
||||
};
|
||||
|
||||
class FlatUdpSource final : public DataSource {
|
||||
public:
|
||||
explicit FlatUdpSource(const SourceConfig& cfg);
|
||||
~FlatUdpSource() override;
|
||||
|
||||
void open(SourceSink& sink) override;
|
||||
void poll(int timeout_ms) override;
|
||||
void close() override;
|
||||
SourceStats stats() const override;
|
||||
bool connected() const override { return connected_.load(std::memory_order_relaxed); }
|
||||
void set_datagram_tap(DatagramTap tap) override { tap_ = std::move(tap); }
|
||||
|
||||
const FlatUdpSettings& settings() const { return cfg_; }
|
||||
/// Datagrams of the expected size / of any other size; for the probe, readable from any thread.
|
||||
std::uint64_t valid_datagrams() const { return valid_.load(std::memory_order_relaxed); }
|
||||
std::uint64_t wrong_size_datagrams() const { return wrong_.load(std::memory_order_relaxed); }
|
||||
std::string last_message() const;
|
||||
|
||||
private:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
void close_fd();
|
||||
void drain_socket();
|
||||
void accept(const std::uint8_t* data);
|
||||
void reject(std::size_t len);
|
||||
void flush();
|
||||
void note(const std::string& msg);
|
||||
|
||||
FlatUdpSettings cfg_;
|
||||
SourceSink* sink_ = nullptr;
|
||||
DatagramTap tap_;
|
||||
int fd_ = -1;
|
||||
bool configured_ = false;
|
||||
bool ever_connected_ = false;
|
||||
std::size_t expected_ = 0; ///< bytes of a valid datagram
|
||||
std::size_t block_ = 1;
|
||||
|
||||
std::vector<SignalInfo> signals_;
|
||||
std::vector<std::uint8_t> rx_;
|
||||
std::vector<std::vector<double>> pending_; ///< decoded, not yet framed: [signal][value]
|
||||
std::vector<SignalValues> views_;
|
||||
std::size_t pending_n_ = 0; ///< datagrams in `pending_`
|
||||
Clock::time_point batch_start_;
|
||||
Clock::time_point last_valid_;
|
||||
std::set<std::size_t> reported_sizes_;
|
||||
|
||||
std::atomic<bool> connected_{false};
|
||||
std::atomic<std::uint64_t> valid_{0};
|
||||
std::atomic<std::uint64_t> wrong_{0};
|
||||
mutable std::mutex msg_mu_;
|
||||
std::string last_message_;
|
||||
|
||||
std::uint32_t frame_counter_ = 0;
|
||||
std::uint64_t frames_ = 0;
|
||||
std::uint64_t reconnects_ = 0;
|
||||
};
|
||||
|
||||
} // namespace udpscope::flatudp
|
||||
@@ -51,6 +51,7 @@ void register_builtin_sources() {
|
||||
done = true;
|
||||
register_kind(make_udps_kind());
|
||||
register_kind(make_e1608_kind());
|
||||
register_kind(make_flatudp_kind());
|
||||
if (std::getenv("UDPSCOPE_PROBE_SOURCE_KINDS") != nullptr) register_kind(make_probe_kind());
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,11 @@ const std::vector<SourceKind*>& kinds();
|
||||
/// may drop. A later `dlopen` loader would only call `register_kind()`.
|
||||
void register_builtin_sources();
|
||||
|
||||
/// The built-in kinds, each defined in its own file (`udps_kind.cpp`, `probe_source.cpp`, `e1608/e1608_kind.cpp`).
|
||||
/// The built-in kinds, each defined in its own file (`udps_kind.cpp`, `probe_source.cpp`, `e1608/e1608_kind.cpp`, `flatudp/flatudp_kind.cpp`).
|
||||
std::unique_ptr<SourceKind> make_udps_kind();
|
||||
std::unique_ptr<SourceKind> make_probe_kind();
|
||||
std::unique_ptr<SourceKind> make_e1608_kind();
|
||||
std::unique_ptr<SourceKind> make_flatudp_kind();
|
||||
|
||||
/// The (host, port) pair that names a source's session file. UDPS keeps its legacy
|
||||
/// `server`/`port`; any other kind is `"<kind>-<identity>"` (characters outside `[A-Za-z0-9._-]`
|
||||
|
||||
@@ -6121,6 +6121,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_FLATUDP")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), flatudp_probe, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_FLATUDP_GAP")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), flatudp_probe_gap, &app);
|
||||
}
|
||||
|
||||
// Our own options were consumed by parse_args(); GTK would reject them, so only
|
||||
// the program name is handed to g_application_run().
|
||||
|
||||
+6
-1
@@ -1884,8 +1884,13 @@ 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.
|
||||
/// against `tools/mock_flatudp.py`, the stream, the value continuity, the wrong-size accounting and
|
||||
/// the capture button (`_report`, a second later). `_gap` reads the source inside the mock's silence;
|
||||
/// `_resumed` shows the stream came back.
|
||||
gboolean flatudp_probe(gpointer user);
|
||||
gboolean flatudp_probe_report(gpointer user);
|
||||
gboolean flatudp_probe_gap(gpointer user);
|
||||
gboolean flatudp_probe_resumed(gpointer user);
|
||||
gboolean pan_probe(gpointer user);
|
||||
gboolean arm_probe(gpointer user);
|
||||
gboolean trigrenum_probe_step3(gpointer user);
|
||||
|
||||
+133
-1
@@ -9,6 +9,7 @@
|
||||
#include "source/probe_source.hpp"
|
||||
#include "source/e1608/e1608_source.hpp"
|
||||
#include "source/flatudp/flatudp_signals.hpp"
|
||||
#include "source/flatudp/flatudp_source.hpp"
|
||||
#include "source/registry.hpp"
|
||||
#include "ui_app.hpp"
|
||||
|
||||
@@ -1938,6 +1939,14 @@ gboolean source_kinds_probe(gpointer user) {
|
||||
c.set("trigger", "rising");
|
||||
c.set("dir1", "o");
|
||||
c.set("plot0", "1");
|
||||
} else if (id == "flatudp") {
|
||||
c.set("port", "44600");
|
||||
c.set("bind", "10.0.0.8");
|
||||
c.set("multicast", "239.1.2.3");
|
||||
c.set("iface", "10.0.0.9");
|
||||
c.set("rate", "2500");
|
||||
c.set("endian", "big");
|
||||
c.set("signals", "Trigger:uint8,Ia:float32,Wave:int16[4]");
|
||||
}
|
||||
return c;
|
||||
};
|
||||
@@ -2102,6 +2111,31 @@ bool flatudp_codec_check() {
|
||||
return true;
|
||||
}
|
||||
|
||||
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.
|
||||
long flatudp_breaks(App* a, std::size_t channel, long step) {
|
||||
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());
|
||||
long breaks = 0;
|
||||
for (std::size_t i = 1; i < v.size(); ++i) {
|
||||
const long d = std::lround(double(v[i])) - std::lround(double(v[i - 1]));
|
||||
if (((d % 100) + 100) % 100 != step) ++breaks;
|
||||
}
|
||||
return breaks;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/// Phase 2, 1 s after the controls were driven: the stream, the calibration, the capture button,
|
||||
@@ -2247,12 +2281,110 @@ 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.
|
||||
/// After the mock's gap: the stream's frame count moved on and the source is connected again.
|
||||
gboolean flatudp_probe_resumed(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
flatudp::FlatUdpSource* src = flatudp_source(a);
|
||||
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);
|
||||
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);
|
||||
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) {
|
||||
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;
|
||||
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);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// Phase 2, 1 s after the signals were enabled: the stream, its channels, the value continuity, the
|
||||
/// wrong-size accounting and the capture button.
|
||||
gboolean flatudp_probe_report(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;
|
||||
const flatudp::FlatUdpSettings& s = src->settings();
|
||||
const bool up = a->model->stats().connected.load(std::memory_order_relaxed);
|
||||
|
||||
const std::vector<SignalInfo>& sigs = a->model->signals();
|
||||
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;
|
||||
}
|
||||
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;
|
||||
std::fprintf(stderr,
|
||||
"[flatudp] stream: connected=%d signals=%zu names=%s expect=%s frames=%" G_GUINT64_FORMAT
|
||||
" rate=%.3f ok=%d\n",
|
||||
up ? 1 : 0, sigs.size(), names.c_str(), expect_names.c_str(), frames, rate,
|
||||
stream_ok ? 1 : 0);
|
||||
|
||||
const std::size_t channels = a->model->channels().size();
|
||||
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);
|
||||
} else {
|
||||
std::fprintf(stderr, "[flatudp] continuity: skipped (needs a connected all-scalar list of two or more)\n");
|
||||
}
|
||||
|
||||
const std::uint64_t wrong = src->wrong_size_datagrams();
|
||||
const std::string msg = src->last_message();
|
||||
std::fprintf(stderr, "[flatudp] wrong size: count=%" G_GUINT64_FORMAT " message='%s' ok=%d\n", wrong,
|
||||
msg.c_str(), wrong >= 1 && msg.find("expected") != std::string::npos ? 1 : 0);
|
||||
|
||||
std::fprintf(stderr, "[flatudp] capture button: present=%d sensitive=%d\n",
|
||||
a->capture_button != nullptr ? 1 : 0,
|
||||
a->capture_button != nullptr && gtk_widget_get_sensitive(a->capture_button) ? 1 : 0);
|
||||
std::fprintf(stderr, "[flatudp] state: valid=%" G_GUINT64_FORMAT " wrong=%" G_GUINT64_FORMAT "\n",
|
||||
src->valid_datagrams(), wrong);
|
||||
std::fflush(stderr);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// Phase 1: the codec check (it needs no flatudp source), then every signal enabled in one structural
|
||||
/// change, and the report a second later.
|
||||
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);
|
||||
if (flatudp_source(a) == nullptr) {
|
||||
std::fprintf(stderr, "[flatudp] no flatudp source (run with --source flatudp)\n");
|
||||
std::fflush(stderr);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
std::vector<std::size_t> all(a->model->signals().size());
|
||||
for (std::size_t i = 0; i < all.size(); ++i) all[i] = i;
|
||||
if (!all.empty()) a->model->set_signals_enabled(all, true);
|
||||
std::fflush(stderr);
|
||||
g_timeout_add(1000, flatudp_probe_report, a);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user