Initial commit
This commit is contained in:
commit
8059b90de7
29 files changed
+7893
No files matched your search
@@ -0,0 +1,476 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||
|
||||
#include "acquisition.hpp"
|
||||
|
||||
#include <udps_client.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace udpscope {
|
||||
namespace {
|
||||
|
||||
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
|
||||
|
||||
Acquisition::Acquisition(ScopeModel& model, const Config& cfg) : model_(model), cfg_(cfg) {}
|
||||
|
||||
Acquisition::~Acquisition() { stop(); }
|
||||
|
||||
void Acquisition::start() {
|
||||
udps_client_config_t c;
|
||||
udps_client_config_init(&c);
|
||||
c.server_addr = cfg_.server.c_str();
|
||||
c.server_port = cfg_.port;
|
||||
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,
|
||||
&Acquisition::event_trampoline, this);
|
||||
|
||||
started_ = std::chrono::steady_clock::now();
|
||||
running_.store(true, std::memory_order_release);
|
||||
thread_ = std::thread([this] { run(); });
|
||||
}
|
||||
|
||||
void Acquisition::stop() {
|
||||
if (!running_.exchange(false, std::memory_order_acq_rel)) {
|
||||
if (client_ != nullptr) {
|
||||
udps_client_destroy(client_);
|
||||
client_ = nullptr;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (thread_.joinable()) thread_.join();
|
||||
if (client_ != nullptr) {
|
||||
udps_client_destroy(client_);
|
||||
client_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void Acquisition::run() {
|
||||
while (running_.load(std::memory_order_acquire)) {
|
||||
udps_client_poll(client_, 50);
|
||||
udps_stats_t s;
|
||||
udps_client_stats(client_, &s);
|
||||
StreamStats& out = model_.stats();
|
||||
out.packets.store(s.packets_received, std::memory_order_relaxed);
|
||||
out.frames.store(s.frames_delivered, std::memory_order_relaxed);
|
||||
out.gaps.store(s.counter_gaps, 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);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// thing to look at when a signal refuses to plot.
|
||||
static const char* const kModes[] = {"strict", "accumulate", "decimate"};
|
||||
std::fprintf(stderr, "[udpscope] CONFIG: %u signals, publish=%s\n", count,
|
||||
publish_mode < 3 ? kModes[publish_mode] : "?");
|
||||
for (const SignalInfo& sig : info) {
|
||||
std::fprintf(stderr,
|
||||
"[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.time_mode == UDPS_TIME_PACKET ? "packet"
|
||||
: sig.time_mode == UDPS_TIME_FULL_ARRAY ? "full-array"
|
||||
: sig.time_mode == UDPS_TIME_FIRST_SAMPLE ? "first-sample"
|
||||
: "last-sample",
|
||||
sig.time_signal_idx, sig.unit.c_str());
|
||||
}
|
||||
std::fflush(stderr);
|
||||
model_.stats().have_config.store(true, std::memory_order_relaxed);
|
||||
model_.set_signals(std::move(info));
|
||||
}
|
||||
|
||||
void Acquisition::on_event(int event, const char* detail) {
|
||||
(void)detail;
|
||||
if (event == UDPS_EVENT_CONNECTED) {
|
||||
model_.stats().connected.store(true, std::memory_order_relaxed);
|
||||
} else if (event == UDPS_EVENT_DISCONNECTED) {
|
||||
model_.stats().connected.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
std::uint32_t Acquisition::build_stamps(const udps_frame_t* frame, int time_signal) {
|
||||
// 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
|
||||
// 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.
|
||||
std::uint32_t n = frame->num_samples;
|
||||
for (std::uint32_t i = 0; i < frame->num_signals; ++i) {
|
||||
n = std::max(n, udps_signal_num_elements(&frame->signals[i]));
|
||||
}
|
||||
stamps_.assign(n, 0.0);
|
||||
|
||||
double period = model_.partition().base_dt;
|
||||
if (!(period > 0.0)) period = 1.0e-3;
|
||||
|
||||
bool used_stream_time = false;
|
||||
if (time_signal >= 0 && time_signal < static_cast<int>(frame->num_signals)) {
|
||||
const udps_signal_t& ts = frame->signals[time_signal];
|
||||
// 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
|
||||
// stamp of sample k is (sample=k, elem=0); a multi-element signal carries one
|
||||
// *sample* of `elements` values per packet, so the stamp of sample k is
|
||||
// (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
|
||||
// element 0 for every sample and were then judged degenerate.
|
||||
const bool scalar_time = udps_signal_num_elements(&ts) == 1;
|
||||
auto value_of = [&](std::uint32_t k) {
|
||||
return scalar_time
|
||||
? udps_frame_value(frame, static_cast<std::uint32_t>(time_signal), k, 0)
|
||||
: udps_frame_value(frame, static_cast<std::uint32_t>(time_signal), 0, k);
|
||||
};
|
||||
switch (ts.time_mode) {
|
||||
case UDPS_TIME_FULL_ARRAY:
|
||||
for (std::uint32_t k = 0; k < n; ++k) stamps_[k] = value_of(k);
|
||||
used_stream_time = true;
|
||||
break;
|
||||
case UDPS_TIME_FIRST_SAMPLE: {
|
||||
const double anchor = value_of(0);
|
||||
const double step = n > 1 ? (value_of(n - 1) - anchor) / double(n - 1) : period;
|
||||
for (std::uint32_t k = 0; k < n; ++k) stamps_[k] = anchor + double(k) * step;
|
||||
used_stream_time = true;
|
||||
break;
|
||||
}
|
||||
case UDPS_TIME_LAST_SAMPLE: {
|
||||
const double anchor = value_of(n > 0 ? n - 1 : 0);
|
||||
for (std::uint32_t k = 0; k < n; ++k)
|
||||
stamps_[k] = anchor - double(n - 1 - k) * period;
|
||||
used_stream_time = true;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (used_stream_time && n > 0) {
|
||||
// A time signal that does not actually vary -- a vector time signal, whose
|
||||
// values the client reads one element at a time, or a producer that leaves it
|
||||
// zero -- would put every sample at one x and collapse the axis to a point.
|
||||
// The arrival estimate is a worse clock but a usable one; a degenerate one is
|
||||
// not a time base at all.
|
||||
bool usable = std::isfinite(stamps_[0]);
|
||||
double lo = stamps_[0];
|
||||
double hi = stamps_[0];
|
||||
for (std::uint32_t k = 1; usable && k < n; ++k) {
|
||||
if (!std::isfinite(stamps_[k])) {
|
||||
usable = false;
|
||||
break;
|
||||
}
|
||||
lo = std::min(lo, stamps_[k]);
|
||||
hi = std::max(hi, stamps_[k]);
|
||||
}
|
||||
if (usable && hi > lo) {
|
||||
// The stamps must also ADVANCE across packets. A producer that restarts its
|
||||
// stamps every packet (or a signal mislabelled as time) keeps handing back
|
||||
// the same values; the max-clamp would then pin every sample at one x, and
|
||||
// the trigger's hold-off clock with it -- the engine waited forever and no
|
||||
// second event ever fired.
|
||||
if (have_stream_anchor_ && !(stamps_[n - 1] > last_stream_anchor_)) {
|
||||
usable = false;
|
||||
} else {
|
||||
last_stream_anchor_ = stamps_[n - 1];
|
||||
have_stream_anchor_ = true;
|
||||
}
|
||||
}
|
||||
if (!usable || !(hi > lo)) {
|
||||
used_stream_time = false; // no span to time with; fall back to arrival
|
||||
static bool warned = false;
|
||||
if (!warned) {
|
||||
warned = true;
|
||||
std::fprintf(stderr,
|
||||
"[udpscope] warning: the stream's time signal carries no usable span "
|
||||
"(%g..%g) or does not advance across packets; timing samples by "
|
||||
"arrival instead. Check the time signal's mode and index in the "
|
||||
"CONFIG dump above.\n",
|
||||
lo, hi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!used_stream_time) {
|
||||
// Arrival-anchored: the packet is assumed to have arrived as its last element
|
||||
// was produced, and the earlier ones are dated backwards. A gap (`lost`)
|
||||
// shows up as extra elapsed time between arrivals, which is exactly the
|
||||
// spacing the samples had.
|
||||
const double end = frame->recv_time;
|
||||
if (!have_arrival_epoch_) {
|
||||
arrival_epoch_ = end;
|
||||
have_arrival_epoch_ = true;
|
||||
}
|
||||
const double base = end - arrival_epoch_;
|
||||
double step = period;
|
||||
if (have_recv_ && n > 0) {
|
||||
const double elapsed = end - last_recv_;
|
||||
const double cycles = double(n) * double(frame->lost + 1);
|
||||
if (elapsed > 0.0 && cycles > 0.0) step = elapsed / cycles;
|
||||
}
|
||||
for (std::uint32_t k = 0; k < n; ++k) stamps_[k] = base - double(n - 1 - k) * step;
|
||||
}
|
||||
|
||||
// A stream-time signal may be declared in a time unit rather than seconds
|
||||
// ('ns' is the common one): scale it to seconds so the x axis is in seconds
|
||||
// either way. The scale applies ONLY to stream time: the arrival fallback
|
||||
// already produces seconds off the socket clock -- scaling those by the time
|
||||
// signal's unit shrank the whole time base by 1e9 on 'ns' streams, which
|
||||
// froze the trigger's hold-off clock and pinned the axis at ~0.
|
||||
if (used_stream_time && time_signal >= 0 &&
|
||||
time_signal < static_cast<int>(model_.signals().size())) {
|
||||
double unit_scale = 1.0;
|
||||
const std::string& unit = model_.signals()[static_cast<std::size_t>(time_signal)].unit;
|
||||
if (unit == "ns") unit_scale = 1e-9;
|
||||
else if (unit == "us" || unit == "\u00b5s" || unit == "\xce\xbc" "s") unit_scale = 1e-6;
|
||||
else if (unit == "ms") unit_scale = 1e-3;
|
||||
if (unit_scale != 1.0) {
|
||||
for (std::uint32_t k = 0; k < n; ++k) stamps_[k] *= unit_scale;
|
||||
}
|
||||
}
|
||||
|
||||
last_recv_ = frame->recv_time;
|
||||
have_recv_ = true;
|
||||
return n;
|
||||
}
|
||||
|
||||
void Acquisition::on_data(const udps_frame_t* frame) {
|
||||
if (paused_.load(std::memory_order_relaxed)) return;
|
||||
|
||||
std::lock_guard<std::mutex> lock(model_.mutex());
|
||||
if (!running_.load(std::memory_order_relaxed)) return;
|
||||
|
||||
const Partition& part = model_.partition();
|
||||
const std::vector<ChannelInfo>& channels = model_.channels();
|
||||
if (part.time_capacity == 0 || channels.empty()) return;
|
||||
|
||||
// The time reference, in order of preference: the signal the first channel's
|
||||
// CONFIG entry names via `time_signal_idx`, or -- when the stream instead
|
||||
// carries its time base as an explicit full-array signal -- that signal.
|
||||
int time_signal = -1;
|
||||
{
|
||||
const SignalInfo& sig = model_.signals()[channels[0].signal];
|
||||
if (sig.time_signal_idx != 0xFFFFFFFFu && sig.time_signal_idx < model_.signals().size()) {
|
||||
time_signal = static_cast<int>(sig.time_signal_idx);
|
||||
}
|
||||
}
|
||||
if (time_signal < 0) {
|
||||
// No channel names a time signal: look for the standalone timestamp signal
|
||||
// (full-array, more than one element, no declared rate).
|
||||
for (std::size_t i = 0; i < model_.signals().size(); ++i) {
|
||||
const SignalInfo& sig = model_.signals()[i];
|
||||
if (sig.time_mode == UDPS_TIME_FULL_ARRAY && sig.elements() > 1 &&
|
||||
sig.sampling_rate == 0.0) {
|
||||
time_signal = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const std::uint32_t n = build_stamps(frame, time_signal);
|
||||
if (n == 0) return;
|
||||
|
||||
if (model_.epoch() == 0.0) {
|
||||
model_.set_epoch_if_unset(stamps_[0]);
|
||||
std::fprintf(stderr, "[udpscope] time base: epoch=%.3f, first frame recv=%.3f, n=%u\n",
|
||||
model_.epoch(), frame->recv_time, n);
|
||||
std::fflush(stderr);
|
||||
}
|
||||
const double epoch = model_.epoch();
|
||||
|
||||
TriggerConfig& tc = model_.trigger();
|
||||
// One ring, always filling, whatever the trigger is doing: the pre-trigger
|
||||
// history has to already be there when an edge arrives, and the first latch after
|
||||
// arming would otherwise open on however little had accumulated since. What
|
||||
// freezes in trigger mode is the *view*, not acquisition -- the latch copies the
|
||||
// event window out into an immutable `EventSnapshot`.
|
||||
BufferStore& store = model_.live();
|
||||
const u64 g = part.time_stride;
|
||||
|
||||
// Calibration is read per frame; it only changes under the same lock.
|
||||
TriggerEngine& engine = engine_;
|
||||
const int trig_channel = tc.channel;
|
||||
// The post-trigger window straight through in seconds. There used to be a measured,
|
||||
// smoothed samples-per-second estimate here, converting the window into a sample
|
||||
// count for the engine; the estimate is what sized captures wrong. Its floor
|
||||
// (`inst >= 1.0`) turned any stream whose time signal advances in large steps into
|
||||
// 1 S/s, the count came out as 1, and the engine latched on the edge sample itself
|
||||
// -- an event with no post-trigger data, which is exactly the "post window is
|
||||
// empty" defect. The engine is fed stream time per sample anyway, so it can count
|
||||
// the window itself and no conversion has to exist.
|
||||
//
|
||||
// The oldest buffered stamp goes with it, so the engine can refuse an edge the
|
||||
// history does not reach back far enough behind. Read from the ring rather than
|
||||
// timed from the arm: the ring fills with the trigger off too, so the pre window is
|
||||
// usually already there and a timer would delay the first event for nothing.
|
||||
const double event_span = model_.event_span();
|
||||
const double post_want = tc.post_span_s(event_span);
|
||||
const double oldest_s =
|
||||
store.time().size() > 0 ? double(store.time()[0]) : last_stamp_;
|
||||
engine.configure(tc, post_want, event_span - post_want, oldest_s);
|
||||
// The engine's armed state follows the panel's, both ways: without the disarm
|
||||
// half, Disarm left the engine firing (its own `armed_` was only ever set), and
|
||||
// re-entering trigger mode kept the stale prev/edge/hold-off state.
|
||||
if (tc.enabled && tc.armed) {
|
||||
if (!engine.armed()) {
|
||||
engine.arm(last_stamp_);
|
||||
model_.trigger_armed().store(true, std::memory_order_relaxed);
|
||||
}
|
||||
} else if (engine.armed()) {
|
||||
engine.disarm();
|
||||
model_.trigger_armed().store(false, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
double last_stamp_used = last_stamp_;
|
||||
|
||||
// The latch is handled on the sample it happens on, and with the edge the engine
|
||||
// latched *on* -- after that sample's channels have all been pushed (see the loop).
|
||||
//
|
||||
// It used to set a flag and publish once after the loop, reading the edge back from
|
||||
// the engine. But the loop keeps feeding the engine after a latch, so any further
|
||||
// edge in the rest of the packet -- the hold-off is 10 ms and a packet can be a long
|
||||
// block of samples -- overwrote that edge before it was read. The capture was then
|
||||
// cut around an edge whose post window had not been collected yet, so the event came
|
||||
// out all pre-trigger with the marker and x = 0 at the right-hand end of the data.
|
||||
// Publishing here also means the ring provably holds the post window: the latch
|
||||
// condition is that stream time has passed `edge + post`, and every sample up to
|
||||
// this one has already been pushed.
|
||||
const double span = std::max(1.0e-9, model_.event_span());
|
||||
const double post_s = tc.post_span_s(span);
|
||||
const auto latch = [&](double edge) {
|
||||
model_.publish_event(span - post_s, post_s, edge);
|
||||
model_.trigger_latches().fetch_add(1, std::memory_order_relaxed);
|
||||
model_.trigger_time().store(edge, std::memory_order_relaxed);
|
||||
model_.trigger_time_valid().store(true, std::memory_order_relaxed);
|
||||
// The captured window is reported from the snapshot itself, not from the request:
|
||||
// `want` versus `got` is what distinguishes "the engine did not wait" from "the
|
||||
// cut lost the data", which is otherwise guesswork from the picture alone.
|
||||
auto snap = model_.event();
|
||||
std::fprintf(stderr,
|
||||
"[udpscope] trigger: edge at %.3f s, latched at %.3f s of stream time "
|
||||
"(want [-%.4f..%.4f] got [-%.4f..%.4f] n=%llu, frame recv=%.3f, "
|
||||
"ticks_so_far=%llu, wall=%.3f s)\n",
|
||||
edge, last_stamp_, span - post_s, post_s, snap ? snap->pre_s : 0.0,
|
||||
snap ? snap->post_s : 0.0,
|
||||
static_cast<unsigned long long>(snap ? snap->stamps.size() : 0),
|
||||
frame->recv_time, static_cast<unsigned long long>(tick_),
|
||||
std::chrono::duration<double>(std::chrono::steady_clock::now() - started_)
|
||||
.count());
|
||||
std::fflush(stderr);
|
||||
if (tc.mode == TriggerMode::Single) tc.armed = false;
|
||||
};
|
||||
|
||||
for (std::uint32_t k = 0; k < n; ++k) {
|
||||
const u64 tick = tick_++;
|
||||
const double rel = std::max(stamps_[k] - epoch, last_stamp_);
|
||||
last_stamp_used = rel;
|
||||
last_stamp_ = rel;
|
||||
|
||||
if (tick % g == g - 1) store.time().push(static_cast<f32>(rel));
|
||||
|
||||
// The latch belongs to this sample, but it cannot be published from inside the
|
||||
// channel loop: `publish_event()` clamps the slice to every value ring's
|
||||
// `produced()`, and the channels *after* the trigger channel have not pushed this
|
||||
// sample yet, so the newest sample -- the one the post window was waiting for --
|
||||
// would be clamped away. One sample is invisible in a 20 000-sample capture (it is
|
||||
// the `n=19999` and the `0.9999` instead of `1.0000` in the probe's output), and at
|
||||
// one sample per second it is the entire post side: `got [-0.9997..0.0000]`, which
|
||||
// reads exactly like the "post window is empty" defect.
|
||||
bool latch_here = false;
|
||||
double latch_edge = 0.0;
|
||||
|
||||
for (std::size_t i = 0; i < channels.size(); ++i) {
|
||||
const u64 period = g * part.stride[i];
|
||||
if (tick % period != period - 1) continue;
|
||||
const ChannelInfo& ch = channels[i];
|
||||
// A signal's columns are consecutive samples of the same channel: a scalar
|
||||
// (one element) carries one value per batched cycle, a block signal carries
|
||||
// the cycle's sample at element `k`. Either way element 0 is the channel.
|
||||
const double raw =
|
||||
ch.elements > 1
|
||||
? udps_frame_value(frame, static_cast<std::uint32_t>(ch.signal), 0,
|
||||
std::min<std::uint32_t>(k, ch.elements - 1))
|
||||
: udps_frame_value(frame, static_cast<std::uint32_t>(ch.signal), k, 0);
|
||||
const f32 value = static_cast<f32>(raw * ch.gain + ch.offset);
|
||||
store.values(i).push(value);
|
||||
if (static_cast<int>(i) == trig_channel) {
|
||||
if (engine.process(value, rel)) {
|
||||
latch_here = true;
|
||||
latch_edge = engine.last_trigger_time();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (latch_here) latch(latch_edge);
|
||||
}
|
||||
last_stamp_ = last_stamp_used;
|
||||
|
||||
model_.trigger_armed().store(engine.armed(), std::memory_order_relaxed);
|
||||
model_.set_trigger_pending(engine.pending());
|
||||
static std::int64_t next_engine_log = 0;
|
||||
const std::int64_t now_e = static_cast<std::int64_t>(frame->recv_time * 1.0e6);
|
||||
if (now_e >= next_engine_log) {
|
||||
next_engine_log = now_e + 1000000;
|
||||
// `elapsed/post_want` is the capture's progress in the only unit that matters now
|
||||
// (stream seconds); `accum` is how many samples that window has collected, which
|
||||
// is what says whether a capture is thin because the window is short or because
|
||||
// the stream is slow.
|
||||
std::fprintf(stderr,
|
||||
"[udpscope] trig engine: ch=%d armed=%d pending=%d elapsed=%.4f/%.4f s "
|
||||
"accum=%llu history=%.4f/%.4f s pre_pct=%.0f v=%.4g now_t=%.3f "
|
||||
"holdoff_until=%.3f\n",
|
||||
trig_channel, int(engine.armed()), int(engine.pending()),
|
||||
engine.post_elapsed(), engine.post_seconds(),
|
||||
static_cast<unsigned long long>(engine.post_accum()), engine.history(),
|
||||
engine.pre_seconds(), tc.pretrigger_pct, engine.last_value(),
|
||||
engine.last_t(), engine.holdoff_until());
|
||||
std::fflush(stderr);
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
Reference in new issue
Block a user