Feature/source plugins #1

Merged
martino merged 130 commits from feature/source-plugins into main 2026-11-04 07:35:21 +01:00
8 changed files with 156 additions and 164 deletions
Showing only changes of commit 504bc193ea - Show all commits

No files matched your search

+76 -137
View File
@@ -5,9 +5,8 @@
#include <limits> #include <limits>
#include <udps_client.h>
#include "layout_store.hpp" #include "layout_store.hpp"
#include "source/registry.hpp"
#include <glib/gstdio.h> #include <glib/gstdio.h>
@@ -29,22 +28,6 @@ namespace {
/// ring to make room for samples that are not coming. /// ring to make room for samples that are not coming.
constexpr double kGlitchPackets = 256.0; constexpr double kGlitchPackets = 256.0;
SignalInfo to_info(const udps_signal_t& s) {
SignalInfo info;
info.name = s.name;
info.unit = s.unit;
info.type_code = s.type_code;
info.quant_type = s.quant_type;
info.rows = s.num_rows;
info.cols = s.num_cols;
info.range_min = s.range_min;
info.range_max = s.range_max;
info.time_mode = s.time_mode;
info.sampling_rate = s.sampling_rate;
info.time_signal_idx = s.time_signal_idx;
return info;
}
} // namespace } // namespace
Acquisition::Acquisition(ScopeModel& model, const Config& cfg) : model_(model), cfg_(cfg) {} Acquisition::Acquisition(ScopeModel& model, const Config& cfg) : model_(model), cfg_(cfg) {}
@@ -52,31 +35,21 @@ Acquisition::Acquisition(ScopeModel& model, const Config& cfg) : model_(model),
Acquisition::~Acquisition() { stop(); } Acquisition::~Acquisition() { stop(); }
void Acquisition::start() { void Acquisition::start() {
// A started -- or *restarted* -- client is a new stream. Going live from a file view is the restart that // A started -- or *restarted* -- source is a new stream. Going live from a file view is the restart that
// matters: the store still holds the event's samples, and the new stream's clock can sit behind their // matters: the store still holds the event's samples, and the new stream's clock can sit behind their
// stamps, so the guard below has to re-base rather than clamp. `ring=[2.5200..2.5200]` with `n` climbing // stamps, so the guard below has to re-base rather than clamp. `ring=[2.5200..2.5200]` with `n` climbing
// was the symptom -- every packet pinned flat at the file view's last stamp. // was the symptom -- every packet pinned flat at the file view's last stamp.
model_.reset_stream(); model_.reset_stream();
last_stamp_ = -std::numeric_limits<double>::infinity(); last_stamp_ = -std::numeric_limits<double>::infinity();
udps_client_config_t c; if (!source_) {
udps_client_config_init(&c); SourceKind* kind = find_kind(cfg_.source_kind);
c.server_addr = cfg_.server.c_str(); if (kind == nullptr) throw std::runtime_error("unknown source kind: " + cfg_.source_kind);
c.server_port = cfg_.port; source_ = kind->make(cfg_.source_config());
c.multicast_group = cfg_.multicast.empty() ? nullptr : cfg_.multicast.c_str();
c.interface_addr = cfg_.interface_addr.empty() ? nullptr : cfg_.interface_addr.c_str();
c.recv_buffer_bytes = static_cast<std::uint32_t>(cfg_.recv_buffer_bytes);
c.silence_timeout_s = 2.0;
c.reconnect_delay_s = 0.5;
c.keepalive_interval_s = 1.0;
client_ = udps_client_create(&c);
if (client_ == nullptr) {
throw std::runtime_error("could not create the UDPS client for " + cfg_.server + ":" +
std::to_string(cfg_.port));
} }
udps_client_set_callbacks(client_, &Acquisition::config_trampoline, &Acquisition::data_trampoline, source_->set_datagram_tap([this](const std::uint8_t* bytes, std::size_t len, double recv_time) {
&Acquisition::event_trampoline, this); on_datagram(bytes, len, recv_time);
udps_client_set_datagram_cb(client_, &Acquisition::datagram_trampoline, this); });
source_->open(*this);
// `--capture-packets`: armed before the first datagram, so the first CONNECT and // `--capture-packets`: armed before the first datagram, so the first CONNECT and
// its CONFIG -- the hardest strange behaviours to reproduce -- are in the ring. // its CONFIG -- the hardest strange behaviours to reproduce -- are in the ring.
// The open itself still happens on the producer thread, at the first datagram. // The open itself still happens on the producer thread, at the first datagram.
@@ -90,35 +63,24 @@ void Acquisition::start() {
} }
void Acquisition::stop() { void Acquisition::stop() {
if (!running_.exchange(false, std::memory_order_acq_rel)) { if (running_.exchange(false, std::memory_order_acq_rel) && thread_.joinable()) thread_.join();
if (client_ != nullptr) { if (source_) source_->close();
udps_client_destroy(client_);
client_ = nullptr;
}
return;
}
if (thread_.joinable()) thread_.join();
if (client_ != nullptr) {
udps_client_destroy(client_);
client_ = nullptr;
}
} }
void Acquisition::run() { void Acquisition::run() {
while (running_.load(std::memory_order_acquire)) { while (running_.load(std::memory_order_acquire)) {
udps_client_poll(client_, 50); source_->poll(50);
udps_stats_t s; const SourceStats s = source_->stats();
udps_client_stats(client_, &s);
StreamStats& out = model_.stats(); StreamStats& out = model_.stats();
out.packets.store(s.packets_received, std::memory_order_relaxed); out.packets.store(s.packets, std::memory_order_relaxed);
out.frames.store(s.frames_delivered, std::memory_order_relaxed); out.frames.store(s.frames, std::memory_order_relaxed);
out.gaps.store(s.counter_gaps, std::memory_order_relaxed); out.gaps.store(s.gaps, std::memory_order_relaxed);
out.resequenced.store(s.late_resequenced, std::memory_order_relaxed); out.resequenced.store(s.resequenced, std::memory_order_relaxed);
out.late.store(s.late_delivered, std::memory_order_relaxed); out.late.store(s.late, std::memory_order_relaxed);
out.stale.store(s.stale_packets, std::memory_order_relaxed); out.stale.store(s.stale, std::memory_order_relaxed);
out.overflows.store(s.reorder_overflows, std::memory_order_relaxed); out.overflows.store(s.overflows, std::memory_order_relaxed);
out.reconnects.store(s.reconnects, std::memory_order_relaxed); out.reconnects.store(s.reconnects, std::memory_order_relaxed);
out.connected.store(udps_client_is_connected(client_) != 0, std::memory_order_relaxed); out.connected.store(s.connected, std::memory_order_relaxed);
} }
} }
@@ -127,19 +89,19 @@ void Acquisition::set_capture_want(bool want, std::uint32_t slots) {
if (slots > 0) capture_slots_ = slots; if (slots > 0) capture_slots_ = slots;
if (capture_path_.empty()) { if (capture_path_.empty()) {
// Keyed on the source: one path per source, the previous capture kept one deep. // Keyed on the source: one path per source, the previous capture kept one deep.
std::string host = cfg_.server; std::string host = cfg_.session_host();
for (char& ch : host) { for (char& ch : host) {
if (ch == ':' || ch == '/' || ch == ' ') ch = '_'; if (ch == ':' || ch == '/' || ch == ' ') ch = '_';
} }
std::string dir = cache_dir() + "/" + host + "-" + std::to_string(cfg_.port); std::string dir = cache_dir() + "/" + host + "-" + std::to_string(cfg_.session_port());
g_mkdir_with_parents(dir.c_str(), 0700); g_mkdir_with_parents(dir.c_str(), 0700);
capture_path_ = dir + "/capture.bin"; capture_path_ = dir + "/capture.bin";
std::uint32_t addr = 0; std::uint32_t addr = 0;
struct ::in_addr a; struct ::in_addr a;
if (::inet_pton(AF_INET, cfg_.server.c_str(), &a) == 1) { if (cfg_.source_kind == "udps" && ::inet_pton(AF_INET, cfg_.server.c_str(), &a) == 1) {
addr = a.s_addr; // already network order, which is what the header wants addr = a.s_addr; // already network order, which is what the header wants
} }
capture_.set_peer(addr, cfg_.port); capture_.set_peer(addr, cfg_.source_kind == "udps" ? cfg_.port : 0);
} }
} }
capture_want_.store(want, std::memory_order_release); capture_want_.store(want, std::memory_order_release);
@@ -171,29 +133,20 @@ void Acquisition::on_datagram(const std::uint8_t* bytes, std::size_t len, double
} }
} }
void Acquisition::datagram_trampoline(const std::uint8_t* bytes, std::size_t len, void Acquisition::on_config(const std::vector<SignalInfo>& info, std::uint8_t publish_mode) {
double recv_time, void* user) {
static_cast<Acquisition*>(user)->on_datagram(bytes, len, recv_time);
}
void Acquisition::on_config(const udps_signal_t* signals, std::uint32_t count,
std::uint8_t publish_mode) {
std::vector<SignalInfo> info;
info.reserve(count);
for (std::uint32_t i = 0; i < count; ++i) info.push_back(to_info(signals[i]));
// One line per CONFIG on stderr: what the stream says it is. This is the first // One line per CONFIG on stderr: what the stream says it is. This is the first
// thing to look at when a signal refuses to plot. // thing to look at when a signal refuses to plot.
static const char* const kModes[] = {"strict", "accumulate", "decimate"}; static const char* const kModes[] = {"strict", "accumulate", "decimate"};
std::fprintf(stderr, "[udpscope] CONFIG: %u signals, publish=%s\n", count, std::fprintf(stderr, "[udpscope] CONFIG: %u signals, publish=%s\n",
static_cast<unsigned>(info.size()),
publish_mode < 3 ? kModes[publish_mode] : "?"); publish_mode < 3 ? kModes[publish_mode] : "?");
for (const SignalInfo& sig : info) { for (const SignalInfo& sig : info) {
std::fprintf(stderr, std::fprintf(stderr,
"[udpscope] '%s' %ux%u (%u elem) rate=%g Hz time=%s idx=%u unit='%s'\n", "[udpscope] '%s' %ux%u (%u elem) rate=%g Hz time=%s idx=%u unit='%s'\n",
sig.name.c_str(), sig.rows, sig.cols, sig.elements(), sig.sampling_rate, sig.name.c_str(), sig.rows, sig.cols, sig.elements(), sig.sampling_rate,
sig.time_mode == UDPS_TIME_PACKET ? "packet" sig.time_mode == kTimePacket ? "packet"
: sig.time_mode == UDPS_TIME_FULL_ARRAY ? "full-array" : sig.time_mode == kTimeFullArray ? "full-array"
: sig.time_mode == UDPS_TIME_FIRST_SAMPLE ? "first-sample" : sig.time_mode == kTimeFirstSample ? "first-sample"
: "last-sample", : "last-sample",
sig.time_signal_idx, sig.unit.c_str()); sig.time_signal_idx, sig.unit.c_str());
} }
@@ -213,14 +166,14 @@ void Acquisition::on_config(const udps_signal_t* signals, std::uint32_t count,
time_source_ = 0; time_source_ = 0;
reset_session_state(); reset_session_state();
model_.stats().have_config.store(true, std::memory_order_relaxed); model_.stats().have_config.store(true, std::memory_order_relaxed);
model_.set_signals(std::move(info)); model_.set_signals(info);
} }
void Acquisition::on_event(int event, const char* detail) { void Acquisition::on_event(SourceEvent event, const char* detail) {
(void)detail; (void)detail;
if (event == UDPS_EVENT_CONNECTED) { if (event == SourceEvent::Connected) {
model_.stats().connected.store(true, std::memory_order_relaxed); model_.stats().connected.store(true, std::memory_order_relaxed);
} else if (event == UDPS_EVENT_DISCONNECTED) { } else if (event == SourceEvent::Disconnected) {
model_.stats().connected.store(false, std::memory_order_relaxed); model_.stats().connected.store(false, std::memory_order_relaxed);
// The session dropped mid-flight: everything the producer knows about *that* // The session dropped mid-flight: everything the producer knows about *that*
// session's numbering and clocks is now a lie about the next one. Clearing it here // session's numbering and clocks is now a lie about the next one. Clearing it here
@@ -300,15 +253,15 @@ bool Acquisition::gap_plausible(std::uint32_t missing, double elapsed, const Par
return true; return true;
} }
std::uint32_t Acquisition::build_stamps(const udps_frame_t* frame, int time_signal, std::uint32_t Acquisition::build_stamps(const Frame& frame, int time_signal,
bool counter_gap_refused) { bool counter_gap_refused) {
// The frame's span in base ticks: a scalar signal carries `num_samples` values // The frame's span in base ticks: a scalar signal carries `num_samples` values
// (one per batched cycle), a block signal (rows*cols > 1) carries its whole // (one per batched cycle), a block signal (rows*cols > 1) carries its whole
// element block. The largest of those is how many base ticks this frame spans -- // element block. The largest of those is how many base ticks this frame spans --
// in `strict` mode `num_samples` is 1 while a 1x1000 block carries 1000 samples. // in `strict` mode `num_samples` is 1 while a 1x1000 block carries 1000 samples.
std::uint32_t n = frame->num_samples; std::uint32_t n = frame.num_samples;
for (std::uint32_t i = 0; i < frame->num_signals; ++i) { for (std::uint32_t i = 0; i < frame.num_signals; ++i) {
n = std::max(n, udps_signal_num_elements(&frame->signals[i])); n = std::max(n, frame.signals[i].elements());
} }
stamps_.assign(n, 0.0); stamps_.assign(n, 0.0);
abs_stamps_.assign(n, 0.0); abs_stamps_.assign(n, 0.0);
@@ -359,9 +312,9 @@ std::uint32_t Acquisition::build_stamps(const udps_frame_t* frame, int time_sign
// refused, because the advance and the reservation are the *same* claim about the wire: // refused, because the advance and the reservation are the *same* claim about the wire:
// believing a phantom gap here would stretch the timeline by exactly the loss that did not // believing a phantom gap here would stretch the timeline by exactly the loss that did not
// happen. // happen.
if (!frame->late && frame->lost > 0 && batch_ > 0 && !counter_gap_refused && if (!frame.late && frame.lost > 0 && batch_ > 0 && !counter_gap_refused &&
double(frame->lost) <= kGlitchPackets) { double(frame.lost) <= kGlitchPackets) {
rel_base_s_ += double(frame->lost) * double(batch_) * dt; rel_base_s_ += double(frame.lost) * double(batch_) * dt;
} }
for (std::uint32_t k = 0; k < n; ++k) { for (std::uint32_t k = 0; k < n; ++k) {
stamps_[k] = rel_base_s_ + double(k) * dt; stamps_[k] = rel_base_s_ + double(k) * dt;
@@ -370,14 +323,14 @@ std::uint32_t Acquisition::build_stamps(const udps_frame_t* frame, int time_sign
// The absolute clock is still read, for export only. When the stream carries no usable // The absolute clock is still read, for export only. When the stream carries no usable
// time signal there is no absolute clock to record and the relative stamp is the honest // time signal there is no absolute clock to record and the relative stamp is the honest
// answer -- an export then says what the counter says, which is all anyone had. // answer -- an export then says what the counter says, which is all anyone had.
if (time_signal >= 0 && time_signal < static_cast<int>(frame->num_signals)) { if (time_signal >= 0 && time_signal < static_cast<int>(frame.num_signals)) {
const udps_signal_t& ts = frame->signals[time_signal]; const SignalInfo& ts = frame.signals[time_signal];
const bool scalar_time = udps_signal_num_elements(&ts) == 1; const bool scalar_time = ts.elements() == 1;
for (std::uint32_t k = 0; k < n; ++k) { for (std::uint32_t k = 0; k < n; ++k) {
abs_stamps_[k] = abs_stamps_[k] =
scalar_time scalar_time
? udps_frame_value(frame, static_cast<std::uint32_t>(time_signal), k, 0) ? frame.value(static_cast<std::uint32_t>(time_signal), k, 0)
: udps_frame_value(frame, static_cast<std::uint32_t>(time_signal), 0, k); : frame.value(static_cast<std::uint32_t>(time_signal), 0, k);
} }
} else { } else {
for (std::uint32_t k = 0; k < n; ++k) abs_stamps_[k] = stamps_[k]; for (std::uint32_t k = 0; k < n; ++k) abs_stamps_[k] = stamps_[k];
@@ -411,8 +364,8 @@ std::uint32_t Acquisition::build_stamps(const udps_frame_t* frame, int time_sign
// arrival clock for the rest of its life (until a CONFIG says otherwise): letting // arrival clock for the rest of its life (until a CONFIG says otherwise): letting
// the decision flip per frame is what interleaved two timelines in one ring. // the decision flip per frame is what interleaved two timelines in one ring.
if (time_source_ != 2 && time_signal >= 0 && if (time_source_ != 2 && time_signal >= 0 &&
time_signal < static_cast<int>(frame->num_signals)) { time_signal < static_cast<int>(frame.num_signals)) {
const udps_signal_t& ts = frame->signals[time_signal]; const SignalInfo& ts = frame.signals[time_signal];
// How the client indexes a signal's values depends on its shape // How the client indexes a signal's values depends on its shape
// (`udps_frame_value`): a scalar carries one value per batched cycle, so the // (`udps_frame_value`): a scalar carries one value per batched cycle, so the
// stamp of sample k is (sample=k, elem=0); a multi-element signal carries one // stamp of sample k is (sample=k, elem=0); a multi-element signal carries one
@@ -420,18 +373,18 @@ std::uint32_t Acquisition::build_stamps(const udps_frame_t* frame, int time_sign
// (sample=0, elem=k). Passing the scalar form for both is why FULL_ARRAY time // (sample=0, elem=k). Passing the scalar form for both is why FULL_ARRAY time
// signals -- the only mode that carries a stamp per element -- read as // signals -- the only mode that carries a stamp per element -- read as
// element 0 for every sample and were then judged degenerate. // element 0 for every sample and were then judged degenerate.
const bool scalar_time = udps_signal_num_elements(&ts) == 1; const bool scalar_time = ts.elements() == 1;
auto value_of = [&](std::uint32_t k) { auto value_of = [&](std::uint32_t k) {
return scalar_time return scalar_time
? udps_frame_value(frame, static_cast<std::uint32_t>(time_signal), k, 0) ? frame.value(static_cast<std::uint32_t>(time_signal), k, 0)
: udps_frame_value(frame, static_cast<std::uint32_t>(time_signal), 0, k); : frame.value(static_cast<std::uint32_t>(time_signal), 0, k);
}; };
switch (ts.time_mode) { switch (ts.time_mode) {
case UDPS_TIME_FULL_ARRAY: case kTimeFullArray:
for (std::uint32_t k = 0; k < n; ++k) stamps_[k] = value_of(k); for (std::uint32_t k = 0; k < n; ++k) stamps_[k] = value_of(k);
used_stream_time = true; used_stream_time = true;
break; break;
case UDPS_TIME_FIRST_SAMPLE: { case kTimeFirstSample: {
// Only element 0 is meaningful here: the producer stamps the batch's first // Only element 0 is meaningful here: the producer stamps the batch's first
// sample and says nothing about the rest, so the cadence has to space them -- // sample and says nothing about the rest, so the cadence has to space them --
// exactly as LAST_SAMPLE below dates its batch backwards from its anchor. // exactly as LAST_SAMPLE below dates its batch backwards from its anchor.
@@ -445,7 +398,7 @@ std::uint32_t Acquisition::build_stamps(const udps_frame_t* frame, int time_sign
used_stream_time = true; used_stream_time = true;
break; break;
} }
case UDPS_TIME_LAST_SAMPLE: { case kTimeLastSample: {
const double anchor = value_of(n > 0 ? n - 1 : 0); const double anchor = value_of(n > 0 ? n - 1 : 0);
for (std::uint32_t k = 0; k < n; ++k) for (std::uint32_t k = 0; k < n; ++k)
stamps_[k] = anchor - double(n - 1 - k) * cadence; stamps_[k] = anchor - double(n - 1 - k) * cadence;
@@ -549,7 +502,7 @@ std::uint32_t Acquisition::build_stamps(const udps_frame_t* frame, int time_sign
// was produced, and the earlier ones are dated backwards. A gap (`lost`) // was produced, and the earlier ones are dated backwards. A gap (`lost`)
// shows up as extra elapsed time between arrivals, which is exactly the // shows up as extra elapsed time between arrivals, which is exactly the
// spacing the samples had. // spacing the samples had.
const double end = frame->recv_time; const double end = frame.recv_time;
if (!have_arrival_epoch_) { if (!have_arrival_epoch_) {
arrival_epoch_ = end; arrival_epoch_ = end;
have_arrival_epoch_ = true; have_arrival_epoch_ = true;
@@ -605,7 +558,7 @@ std::uint32_t Acquisition::build_stamps(const udps_frame_t* frame, int time_sign
// to a dead clock; they do not, and the wipe is what made a single glitching packet // to a dead clock; they do not, and the wipe is what made a single glitching packet
// look like the stream restarting from zero. // look like the stream restarting from zero.
time_source_ = 2; time_source_ = 2;
const double end = frame->recv_time; const double end = frame.recv_time;
arrival_epoch_ = end - (last_stamp_ + model_.epoch()); arrival_epoch_ = end - (last_stamp_ + model_.epoch());
arrival_prev_ = end; arrival_prev_ = end;
} }
@@ -625,7 +578,7 @@ std::uint32_t Acquisition::build_stamps(const udps_frame_t* frame, int time_sign
return n; return n;
} }
void Acquisition::on_data(const udps_frame_t* frame) { void Acquisition::on_data(const Frame& frame) {
std::lock_guard<std::mutex> lock(model_.mutex()); std::lock_guard<std::mutex> lock(model_.mutex());
if (!running_.load(std::memory_order_relaxed)) return; if (!running_.load(std::memory_order_relaxed)) return;
@@ -633,8 +586,8 @@ void Acquisition::on_data(const udps_frame_t* frame) {
// The counter is this frame's identity and the hole's address. A *late* frame's counter is // The counter is this frame's identity and the hole's address. A *late* frame's counter is
// behind the sequence, so it must not move the mark -- the hole it belongs in was addressed // behind the sequence, so it must not move the mark -- the hole it belongs in was addressed
// from the counter that revealed it. // from the counter that revealed it.
if (!have_counter_ || static_cast<std::int32_t>(frame->counter - last_counter_) > 0) { if (!have_counter_ || static_cast<std::int32_t>(frame.counter - last_counter_) > 0) {
last_counter_ = frame->counter; last_counter_ = frame.counter;
have_counter_ = true; have_counter_ = true;
} }
@@ -668,7 +621,7 @@ void Acquisition::on_data(const udps_frame_t* frame) {
// Placed here and not earlier because `part`/`channels` are not in scope above, and not // Placed here and not earlier because `part`/`channels` are not in scope above, and not
// later because `build_stamps()` is stateful: a late frame's stamps would walk the clock // later because `build_stamps()` is stateful: a late frame's stamps would walk the clock
// backwards and can throw the whole stream onto the arrival clock. // backwards and can throw the whole stream onto the arrival clock.
if (frame->late) { if (frame.late) {
backfill_from(frame, part, channels, model_.live(), part.time_stride); backfill_from(frame, part, channels, model_.live(), part.time_stride);
return; return;
} }
@@ -688,7 +641,7 @@ void Acquisition::on_data(const udps_frame_t* frame) {
// (full-array, more than one element, no declared rate). // (full-array, more than one element, no declared rate).
for (std::size_t i = 0; i < model_.signals().size(); ++i) { for (std::size_t i = 0; i < model_.signals().size(); ++i) {
const SignalInfo& sig = model_.signals()[i]; const SignalInfo& sig = model_.signals()[i];
if (sig.time_mode == UDPS_TIME_FULL_ARRAY && sig.elements() > 1 && if (sig.time_mode == kTimeFullArray && sig.elements() > 1 &&
sig.sampling_rate == 0.0) { sig.sampling_rate == 0.0) {
time_signal = static_cast<int>(i); time_signal = static_cast<int>(i);
break; break;
@@ -702,11 +655,11 @@ void Acquisition::on_data(const udps_frame_t* frame) {
// let one burst raise the very number that is supposed to corroborate it -- a 200-packet outage // let one burst raise the very number that is supposed to corroborate it -- a 200-packet outage
// smooths the interval to 40 ms and then refuses itself. // smooths the interval to 40 ms and then refuses itself.
const bool first_frame = !have_prev_frame_recv_; const bool first_frame = !have_prev_frame_recv_;
const double since_prev = first_frame ? 0.0 : frame->recv_time - prev_frame_recv_; const double since_prev = first_frame ? 0.0 : frame.recv_time - prev_frame_recv_;
const std::uint32_t missing = frame->lost; const std::uint32_t missing = frame.lost;
const bool gap_refused = const bool gap_refused =
missing > 0 && !gap_plausible(missing, since_prev, part, part.time_stride); missing > 0 && !gap_plausible(missing, since_prev, part, part.time_stride);
note_frame_arrival(frame->recv_time); note_frame_arrival(frame.recv_time);
const std::uint32_t n = build_stamps(frame, time_signal, gap_refused); const std::uint32_t n = build_stamps(frame, time_signal, gap_refused);
if (n == 0) return; if (n == 0) return;
@@ -721,7 +674,7 @@ void Acquisition::on_data(const udps_frame_t* frame) {
epoch_adopted_ = true; epoch_adopted_ = true;
model_.set_epoch_if_unset(stamps_[0]); model_.set_epoch_if_unset(stamps_[0]);
std::fprintf(stderr, "[udpscope] time base: epoch=%.3f, first frame recv=%.3f, n=%u\n", std::fprintf(stderr, "[udpscope] time base: epoch=%.3f, first frame recv=%.3f, n=%u\n",
model_.epoch(), frame->recv_time, n); model_.epoch(), frame.recv_time, n);
std::fflush(stderr); std::fflush(stderr);
} }
const double epoch = model_.epoch(); const double epoch = model_.epoch();
@@ -930,7 +883,7 @@ void Acquisition::on_data(const udps_frame_t* frame) {
edge, last_stamp_, span - post_s, post_s, snap ? snap->pre_s : 0.0, edge, last_stamp_, span - post_s, post_s, snap ? snap->pre_s : 0.0,
snap ? snap->post_s : 0.0, snap ? snap->post_s : 0.0,
static_cast<unsigned long long>(snap ? snap->stamps.size() : 0), static_cast<unsigned long long>(snap ? snap->stamps.size() : 0),
frame->recv_time, static_cast<unsigned long long>(tick_), frame.recv_time, static_cast<unsigned long long>(tick_),
std::chrono::duration<double>(std::chrono::steady_clock::now() - started_) std::chrono::duration<double>(std::chrono::steady_clock::now() - started_)
.count()); .count());
std::fflush(stderr); std::fflush(stderr);
@@ -1012,9 +965,9 @@ void Acquisition::on_data(const udps_frame_t* frame) {
// the cycle's sample at element `k`. Either way element 0 is the channel. // the cycle's sample at element `k`. Either way element 0 is the channel.
const double raw = const double raw =
ch.elements > 1 ch.elements > 1
? udps_frame_value(frame, static_cast<std::uint32_t>(ch.signal), 0, ? frame.value(static_cast<std::uint32_t>(ch.signal), 0,
std::min<std::uint32_t>(k, ch.elements - 1)) std::min<std::uint32_t>(k, ch.elements - 1))
: udps_frame_value(frame, static_cast<std::uint32_t>(ch.signal), k, 0); : frame.value(static_cast<std::uint32_t>(ch.signal), k, 0);
// Every sample is read and accumulated even when only one in `period` is stored: // Every sample is read and accumulated even when only one in `period` is stored:
// that is the whole point of the filter. Skipping the read -- which is what the // that is the whole point of the filter. Skipping the read -- which is what the
// old stride did -- is what made decimation alias. // old stride did -- is what made decimation alias.
@@ -1091,7 +1044,7 @@ void Acquisition::on_data(const udps_frame_t* frame) {
if (engine.pending()) model_.set_capture_edge(engine.last_trigger_time()); if (engine.pending()) model_.set_capture_edge(engine.last_trigger_time());
model_.set_trigger_pending(engine.pending()); model_.set_trigger_pending(engine.pending());
static std::int64_t next_engine_log = 0; static std::int64_t next_engine_log = 0;
const std::int64_t now_e = static_cast<std::int64_t>(frame->recv_time * 1.0e6); const std::int64_t now_e = static_cast<std::int64_t>(frame.recv_time * 1.0e6);
if (now_e >= next_engine_log) { if (now_e >= next_engine_log) {
next_engine_log = now_e + 1000000; next_engine_log = now_e + 1000000;
// `elapsed/post_want` is the capture's progress in the only unit that matters now // `elapsed/post_want` is the capture's progress in the only unit that matters now
@@ -1196,7 +1149,7 @@ void Acquisition::reserve_hole(std::uint32_t missing, const Partition& part,
model_.stats().hole_slots.fetch_add(h.slot_count, std::memory_order_relaxed); model_.stats().hole_slots.fetch_add(h.slot_count, std::memory_order_relaxed);
} }
void Acquisition::backfill_from(const udps_frame_t* frame, const Partition& part, void Acquisition::backfill_from(const Frame& frame, const Partition& part,
const std::vector<ChannelInfo>& channels, BufferStore& store, const std::vector<ChannelInfo>& channels, BufferStore& store,
u64 g) { u64 g) {
// `batch_` is learned in `build_stamps()`, which a late frame never reaches. A late frame // `batch_` is learned in `build_stamps()`, which a late frame never reaches. A late frame
@@ -1213,7 +1166,7 @@ void Acquisition::backfill_from(const udps_frame_t* frame, const Partition& part
// late packets would be dropped on the next line. // late packets would be dropped on the next line.
Hole* hole = nullptr; Hole* hole = nullptr;
for (Hole& h : model_.holes()) { for (Hole& h : model_.holes()) {
const std::int32_t into = static_cast<std::int32_t>(frame->counter - h.counter_first); const std::int32_t into = static_cast<std::int32_t>(frame.counter - h.counter_first);
if (into >= 0 && into < static_cast<std::int32_t>(h.packets)) { if (into >= 0 && into < static_cast<std::int32_t>(h.packets)) {
hole = &h; hole = &h;
break; break;
@@ -1224,7 +1177,7 @@ void Acquisition::backfill_from(const udps_frame_t* frame, const Partition& part
// The packet's own span in absolute base ticks. `tick_begin` is the hole's first raw sample // The packet's own span in absolute base ticks. `tick_begin` is the hole's first raw sample
// and the packets inside a hole are consecutive, so the counter's distance from // and the packets inside a hole are consecutive, so the counter's distance from
// `counter_first` places this one exactly. // `counter_first` places this one exactly.
const u64 lo = hole->tick_begin + u64(frame->counter - hole->counter_first) * u64(batch_); const u64 lo = hole->tick_begin + u64(frame.counter - hole->counter_first) * u64(batch_);
const u64 hi = lo + u64(batch_); const u64 hi = lo + u64(batch_);
u64 filled = 0; u64 filled = 0;
@@ -1248,10 +1201,10 @@ void Acquisition::backfill_from(const udps_frame_t* frame, const Partition& part
const u64 k = s * period + j - lo; // this packet's own sample index, 0..batch_-1 const u64 k = s * period + j - lo; // this packet's own sample index, 0..batch_-1
const double raw = const double raw =
channels[i].elements > 1 channels[i].elements > 1
? udps_frame_value(frame, static_cast<std::uint32_t>(channels[i].signal), 0, ? frame.value(static_cast<std::uint32_t>(channels[i].signal), 0,
std::min<std::uint32_t>(static_cast<std::uint32_t>(k), std::min<std::uint32_t>(static_cast<std::uint32_t>(k),
channels[i].elements - 1)) channels[i].elements - 1))
: udps_frame_value(frame, static_cast<std::uint32_t>(channels[i].signal), : frame.value(static_cast<std::uint32_t>(channels[i].signal),
static_cast<std::uint32_t>(k), 0); static_cast<std::uint32_t>(k), 0);
acc += raw * channels[i].gain + channels[i].offset; acc += raw * channels[i].gain + channels[i].offset;
} }
@@ -1274,18 +1227,4 @@ void Acquisition::backfill_from(const udps_frame_t* frame, const Partition& part
model_.stats().backfilled.fetch_add(filled, std::memory_order_relaxed); model_.stats().backfilled.fetch_add(filled, std::memory_order_relaxed);
} }
// Static trampolines: the client's C ABI cannot call a member function directly.
void Acquisition::config_trampoline(const udps_signal_t* signals, std::uint32_t count,
std::uint8_t publish_mode, void* user) {
static_cast<Acquisition*>(user)->on_config(signals, count, publish_mode);
}
void Acquisition::data_trampoline(const udps_frame_t* frame, void* user) {
static_cast<Acquisition*>(user)->on_data(frame);
}
void Acquisition::event_trampoline(udps_event_t event, const char* detail, void* user) {
static_cast<Acquisition*>(user)->on_event(static_cast<int>(event), detail);
}
} // namespace udpscope } // namespace udpscope
+15 -17
View File
@@ -8,21 +8,22 @@
#include "model.hpp" #include "model.hpp"
#include "trigger.hpp" #include "trigger.hpp"
#include <udps_client.h> #include "source/data_source.hpp"
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <cstdint> #include <cstdint>
#include <limits> #include <limits>
#include <memory>
#include <thread> #include <thread>
#include <vector> #include <vector>
namespace udpscope { namespace udpscope {
/// Owns the UDPStreamer client and the producer thread that feeds the model's /// Owns the data source and the producer thread that feeds the model's
/// acquisition buffer. Everything it touches from the thread is serialised on /// acquisition buffer. Everything it touches from the thread is serialised on
/// `ScopeModel::mutex()`, so a UI-side structural change cannot race a frame. /// `ScopeModel::mutex()`, so a UI-side structural change cannot race a frame.
class Acquisition { class Acquisition : private SourceSink {
public: public:
Acquisition(ScopeModel& model, const Config& cfg); Acquisition(ScopeModel& model, const Config& cfg);
~Acquisition(); ~Acquisition();
@@ -62,22 +63,19 @@ class Acquisition {
/// read by a packet whose engine was still disarmed, and then dropped. /// read by a packet whose engine was still disarmed, and then dropped.
void request_force() noexcept { force_want_.store(true, std::memory_order_release); } void request_force() noexcept { force_want_.store(true, std::memory_order_release); }
/// The source this acquisition drives, for the UI's capability panels: it may only call
/// `post()` on it. Null until `start()`.
DataSource* source() noexcept { return source_.get(); }
/// The capture, for the popover's row and the probe. Counters and errors are read /// The capture, for the popover's row and the probe. Counters and errors are read
/// cross-thread through PacketCapture's own published fields. /// cross-thread through PacketCapture's own published fields.
const PacketCapture& capture() const noexcept { return capture_; } const PacketCapture& capture() const noexcept { return capture_; }
private: private:
void run(); void run();
void on_config(const udps_signal_t* signals, std::uint32_t count, std::uint8_t publish_mode); void on_config(const std::vector<SignalInfo>& signals, std::uint8_t publish_mode) override;
void on_data(const udps_frame_t* frame); void on_data(const Frame& frame) override;
void on_event(int event, const char* detail); void on_event(SourceEvent event, const char* detail) override;
static void config_trampoline(const udps_signal_t* signals, std::uint32_t count,
std::uint8_t publish_mode, void* user);
static void data_trampoline(const udps_frame_t* frame, void* user);
static void event_trampoline(udps_event_t event, const char* detail, void* user);
static void datagram_trampoline(const std::uint8_t* bytes, std::size_t len,
double recv_time, void* user);
/// Drops the producer state that is per *connection*: the counter mark, the time /// Drops the producer state that is per *connection*: the counter mark, the time
/// anchors and the clock choice. A reconnect re-sends the CONFIG and the stream may /// anchors and the clock choice. A reconnect re-sends the CONFIG and the stream may
/// have restarted its numbering; carrying the old session's marks across makes the /// have restarted its numbering; carrying the old session's marks across makes the
@@ -89,9 +87,9 @@ class Acquisition {
void on_datagram(const std::uint8_t* bytes, std::size_t len, double recv_time); void on_datagram(const std::uint8_t* bytes, std::size_t len, double recv_time);
/// Fills `stamps_` with this frame's base-tick timestamps, in stream seconds, /// Fills `stamps_` with this frame's base-tick timestamps, in stream seconds,
/// and returns how many were written (`frame->num_samples` unless the frame is /// and returns how many were written (`frame.num_samples` unless the frame is
/// malformed). /// malformed).
std::uint32_t build_stamps(const udps_frame_t* frame, int time_signal, std::uint32_t build_stamps(const Frame& frame, int time_signal,
bool counter_gap_refused); bool counter_gap_refused);
/// Pushes NaN for the `missing` packets' worth of samples that were never delivered, /// Pushes NaN for the `missing` packets' worth of samples that were never delivered,
@@ -104,7 +102,7 @@ class Acquisition {
/// Writes a late frame's values into the NaN-filled holes their counter addresses, /// Writes a late frame's values into the NaN-filled holes their counter addresses,
/// amending the value ring directly (producer path) and queuing pyramid amends for /// amending the value ring directly (producer path) and queuing pyramid amends for
/// `ChannelBuffer::acquire()` to apply on the consumer thread. /// `ChannelBuffer::acquire()` to apply on the consumer thread.
void backfill_from(const udps_frame_t* frame, const Partition& part, void backfill_from(const Frame& frame, const Partition& part,
const std::vector<ChannelInfo>& channels, BufferStore& store, u64 g); const std::vector<ChannelInfo>& channels, BufferStore& store, u64 g);
/// Keeps the wire's own cadence up to date from the arrival times, in the modes where /// Keeps the wire's own cadence up to date from the arrival times, in the modes where
@@ -118,7 +116,7 @@ class Acquisition {
ScopeModel& model_; ScopeModel& model_;
Config cfg_; Config cfg_;
udps_client* client_ = nullptr; std::unique_ptr<DataSource> source_;
std::thread thread_; std::thread thread_;
std::atomic<bool> running_{false}; std::atomic<bool> running_{false};
std::atomic<bool> paused_{false}; std::atomic<bool> paused_{false};
+36 -2
View File
@@ -6,6 +6,7 @@
// For `RowSet::kMaxRows`: `--plots` names a row count, and one constant for the ceiling // For `RowSet::kMaxRows`: `--plots` names a row count, and one constant for the ceiling
// is what stops the message and the clamp from drifting apart. // is what stops the message and the clamp from drifting apart.
#include "plot_host.hpp" #include "plot_host.hpp"
#include "source/registry.hpp"
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
@@ -40,14 +41,35 @@ double parse_double(const char* name, const std::string& v) {
} // namespace } // namespace
SourceConfig Config::source_config() const {
SourceConfig out;
if (source_kind == "udps") {
out.set("server", server);
out.set("port", std::to_string(port));
if (!multicast.empty()) out.set("multicast", multicast);
if (!interface_addr.empty()) out.set("interface", interface_addr);
out.set("recv_buffer", std::to_string(recv_buffer_bytes));
}
for (const auto& kv : source_opts.items()) out.set(kv.first, kv.second);
return out;
}
std::string Config::session_host() const { return udpscope::session_host(source_kind, source_config()); }
std::uint16_t Config::session_port() const { return udpscope::session_port(source_kind, source_config()); }
void print_usage(std::FILE* out, const char* argv0) { void print_usage(std::FILE* out, const char* argv0) {
std::string names;
for (const SourceKind* k : kinds()) names += (names.empty() ? "" : ", ") + k->id();
std::fprintf(out, std::fprintf(out,
"usage: %s --server ADDR --port PORT [options]\n" "usage: %s --server ADDR --port PORT [--source NAME] [options]\n"
"\n" "\n"
" --server ADDR stream source IPv4 address (default 127.0.0.1)\n" " --server ADDR stream source IPv4 address (default 127.0.0.1)\n"
" --port PORT UDP port (unicast) or TCP port (multicast) (default 8000)\n" " --port PORT UDP port (unicast) or TCP port (multicast) (default 8000)\n"
" --multicast GROUP join GROUP for DATA instead of unicast\n" " --multicast GROUP join GROUP for DATA instead of unicast\n"
" --interface ADDR local IPv4 of the interface to join on\n" " --interface ADDR local IPv4 of the interface to join on\n"
" --source NAME source kind (%s; default udps). --server/--port/\n"
" --multicast/--interface configure udps\n"
" --source-opt K=V a setting for the source kind; repeatable\n"
" --buffer-mb MB size of each of the two sample buffers (default 200)\n" " --buffer-mb MB size of each of the two sample buffers (default 200)\n"
" --max-window SEC largest time window the toolbar offers (default 600)\n" " --max-window SEC largest time window the toolbar offers (default 600)\n"
" --holdoff SEC default trigger hold-off (default 0.010)\n" " --holdoff SEC default trigger hold-off (default 0.010)\n"
@@ -69,7 +91,7 @@ void print_usage(std::FILE* out, const char* argv0) {
" and the GL area has no hardware context, so the window\n" " and the GL area has no hardware context, so the window\n"
" never paints or allocates\n" " never paints or allocates\n"
" -h, --help this text\n", " -h, --help this text\n",
argv0); argv0, names.c_str());
} }
Config parse_args(int argc, char** argv) { Config parse_args(int argc, char** argv) {
@@ -91,6 +113,18 @@ Config parse_args(int argc, char** argv) {
c.multicast = value_of(argc, argv, i, "--multicast"); c.multicast = value_of(argc, argv, i, "--multicast");
} else if (a == "--interface" || a.rfind("--interface=", 0) == 0) { } else if (a == "--interface" || a.rfind("--interface=", 0) == 0) {
c.interface_addr = value_of(argc, argv, i, "--interface"); c.interface_addr = value_of(argc, argv, i, "--interface");
} else if (a == "--source" || a.rfind("--source=", 0) == 0) {
const std::string v = value_of(argc, argv, i, "--source");
if (find_kind(v) == nullptr) {
std::string names;
for (const SourceKind* k : kinds()) names += (names.empty() ? "" : ", ") + k->id();
fail("unknown source '" + v + "' (available: " + names + ")");
}
c.source_kind = v;
c.source_specified = true;
} else if (a == "--source-opt" || a.rfind("--source-opt=", 0) == 0) {
const std::string v = value_of(argc, argv, i, "--source-opt");
if (!c.source_opts.parse_assignment(v)) fail("--source-opt must be key=value: " + v);
} else if (a == "--buffer-mb" || a.rfind("--buffer-mb=", 0) == 0) { } else if (a == "--buffer-mb" || a.rfind("--buffer-mb=", 0) == 0) {
c.buffer_mb = parse_double("--buffer-mb", value_of(argc, argv, i, "--buffer-mb")); c.buffer_mb = parse_double("--buffer-mb", value_of(argc, argv, i, "--buffer-mb"));
c.buffer_mb_specified = true; c.buffer_mb_specified = true;
+19
View File
@@ -6,6 +6,8 @@
#include <cstdint> #include <cstdint>
#include <string> #include <string>
#include "source/source_config.hpp"
namespace udpscope { namespace udpscope {
/// Everything the command line can say. Defaults are the ones the spec names /// Everything the command line can say. Defaults are the ones the spec names
@@ -21,6 +23,23 @@ struct Config {
/// `server` valid". The default stays a usable value for every other path. /// `server` valid". The default stays a usable value for every other path.
bool server_specified = false; bool server_specified = false;
/// `--source NAME`: which registered kind opens. `udps` is the default, and `--server/--port/
/// --multicast/--interface` keep meaning UDPS.
std::string source_kind = "udps";
/// Whether `--source` was given (the other half of "did the user choose a source").
bool source_specified = false;
/// Repeatable `--source-opt k=v`: the kind's own settings. For `udps` they overlay the fields above.
SourceConfig source_opts;
/// A source was named on the command line (`--server` or `--source`), so the chooser is skipped.
bool has_source() const noexcept { return server_specified || source_specified; }
/// The settings the kind's `make()` receives: for UDPS the four connection fields plus the receive
/// buffer, then `source_opts` on top.
SourceConfig source_config() const;
/// The (host, port) that name this source's session file; see `session_host()` in the registry.
std::string session_host() const;
std::uint16_t session_port() const;
/// Size of *each* of the two sample buffers, in megabytes. The application /// Size of *each* of the two sample buffers, in megabytes. The application
/// always allocates two identical buffers (spec item 1). /// always allocates two identical buffers (spec item 1).
double buffer_mb = 200.0; double buffer_mb = 200.0;
+4 -2
View File
@@ -7,6 +7,7 @@
#include "waveform.hpp" #include "waveform.hpp"
#include "model.hpp" #include "model.hpp"
#include "plot_host.hpp" #include "plot_host.hpp"
#include "source/registry.hpp"
#include "source_dialog.hpp" #include "source_dialog.hpp"
#include "ui.hpp" #include "ui.hpp"
@@ -18,6 +19,7 @@
int main(int argc, char** argv) { int main(int argc, char** argv) {
udpscope::Config cfg; udpscope::Config cfg;
try { try {
udpscope::register_builtin_sources();
cfg = udpscope::parse_args(argc, argv); cfg = udpscope::parse_args(argc, argv);
// The preferences fill in what the command line did not decide. `--relative-time` is an explicit // The preferences fill in what the command line did not decide. `--relative-time` is an explicit
// instruction for this run, so a preference can only turn the mode on, never off -- which is also // instruction for this run, so a preference can only turn the mode on, never off -- which is also
@@ -57,13 +59,13 @@ int main(int argc, char** argv) {
// declining must cost no buffer and no socket, and nothing below has to cope with "no source". // declining must cost no buffer and no socket, and nothing below has to cope with "no source".
// `--open` is the other answer to that question, and the chooser knows it: its Waveforms page // `--open` is the other answer to that question, and the chooser knows it: its Waveforms page
// fills `open_waveform` in instead of the connection fields. // fills `open_waveform` in instead of the connection fields.
const bool asked = !cfg.server_specified && cfg.open_waveform.empty(); const bool asked = !cfg.has_source() && cfg.open_waveform.empty();
if (asked && !udpscope::choose_source(cfg)) return 0; if (asked && !udpscope::choose_source(cfg)) return 0;
// A source named on the command line skips the chooser, but its *stored* configuration still // A source named on the command line skips the chooser, but its *stored* configuration still
// applies: the buffer size, the clock and the capture ring are decided here, before the model -- // applies: the buffer size, the clock and the capture ring are decided here, before the model --
// which is the only moment they can be -- and a saved host whose buffer was ignored because the // which is the only moment they can be -- and a saved host whose buffer was ignored because the
// address came from the command line is an edit that looks like it never took effect. // address came from the command line is an edit that looks like it never took effect.
if (cfg.server_specified) udpscope::apply_stored_settings(cfg); if (cfg.has_source()) udpscope::apply_stored_settings(cfg);
// Everything from here to `gtk_window_present()` inside `run_ui()` runs with no main loop: the // Everything from here to `gtk_window_present()` inside `run_ui()` runs with no main loop: the
// buffer is allocated, a recording is read, then the whole UI is built. The chooser's window is // buffer is allocated, a recording is read, then the whole UI is built. The chooser's window is
+2 -2
View File
@@ -1129,10 +1129,10 @@ gboolean pick_probe(gpointer user) {
} // namespace } // namespace
void apply_stored_settings(Config& cfg) { void apply_stored_settings(Config& cfg) {
const std::string want = active_configuration(cfg.server, cfg.port); const std::string want = active_configuration(cfg.session_host(), cfg.session_port());
if (want.empty()) return; if (want.empty()) return;
LayoutState st; LayoutState st;
if (!load_layout(configuration_path(cfg.server, cfg.port, want), st)) return; if (!load_layout(configuration_path(cfg.session_host(), cfg.session_port(), want), st)) return;
// Only where nothing newer was said: a flag the user typed, or the preferences they just applied, // Only where nothing newer was said: a flag the user typed, or the preferences they just applied,
// wins -- because it is the more recent statement of intent, and the file is not a place to hide a // wins -- because it is the more recent statement of intent, and the file is not a place to hide a
// surprise from whoever just made it. // surprise from whoever just made it.
+3 -3
View File
@@ -2494,7 +2494,7 @@ void on_about_clicked(GtkButton*, gpointer user) {
App* app = static_cast<App*>(user); App* app = static_cast<App*>(user);
close_overflow_menu(*app); close_overflow_menu(*app);
show_about(GTK_WINDOW(app->window), show_about(GTK_WINDOW(app->window),
layout_path(app->model->config().server, app->model->config().port)); layout_path(app->model->config().session_host(), app->model->config().session_port()));
} }
void on_help_clicked(GtkButton*, gpointer user) { void on_help_clicked(GtkButton*, gpointer user) {
@@ -5914,8 +5914,8 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
// both overrode the arrangement the file was opened with and could clobber a real session. With no // both overrode the arrangement the file was opened with and could clobber a real session. With no
// port, `save_layout_now()` refuses and the first-CONFIG layout load never runs, which is exactly // port, `save_layout_now()` refuses and the first-CONFIG layout load never runs, which is exactly
// a source-less run. // a source-less run.
app.source_host = cfg.open_waveform.empty() ? cfg.server : std::string(); app.source_host = cfg.open_waveform.empty() ? cfg.session_host() : std::string();
app.source_port = cfg.open_waveform.empty() ? cfg.port : 0; app.source_port = cfg.open_waveform.empty() ? cfg.session_port() : 0;
app.source_multicast = cfg.open_waveform.empty() ? cfg.multicast : std::string(); app.source_multicast = cfg.open_waveform.empty() ? cfg.multicast : std::string();
app.source_interface = cfg.interface_addr; app.source_interface = cfg.interface_addr;
// The event cache is keyed on the source, like the packet capture. A waveform has no source, so a // The event cache is keyed on the source, like the packet capture. A waveform has no source, so a
+1 -1
View File
@@ -5411,7 +5411,7 @@ gboolean help_probe(gpointer user) {
App* a = static_cast<App*>(user); App* a = static_cast<App*>(user);
const HelpReport report = help_self_check(); const HelpReport report = help_self_check();
const std::string session = const std::string session =
layout_path(a->model->config().server, a->model->config().port); layout_path(a->model->config().session_host(), a->model->config().session_port());
// The manual is *launched* here and read no further: it opens in another process (Yelp, through // The manual is *launched* here and read no further: it opens in another process (Yelp, through
// the `help:` URI) and neither it nor this box can draw a window for it, so there is nothing to // the `help:` URI) and neither it nor this box can draw a window for it, so there is nothing to