// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 Martino Ferrari #pragma once #include "config.hpp" #include "model.hpp" #include "trigger.hpp" #include #include #include #include #include #include namespace udpscope { /// Owns the UDPStreamer client and the producer thread that feeds the model's /// acquisition buffer. Everything it touches from the thread is serialised on /// `ScopeModel::mutex()`, so a UI-side structural change cannot race a frame. class Acquisition { public: Acquisition(ScopeModel& model, const Config& cfg); ~Acquisition(); Acquisition(const Acquisition&) = delete; Acquisition& operator=(const Acquisition&) = delete; /// Opens the client (no socket until the first poll) and starts the thread. /// Throws std::runtime_error when the client cannot be created. void start(); /// Stops the thread and destroys the client. Idempotent. void stop(); void set_paused(bool paused) noexcept { paused_.store(paused, std::memory_order_relaxed); } bool paused() const noexcept { return paused_.load(std::memory_order_relaxed); } private: void run(); void on_config(const udps_signal_t* signals, std::uint32_t count, std::uint8_t publish_mode); void on_data(const udps_frame_t* frame); void on_event(int event, const char* detail); 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); /// 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 /// malformed). std::uint32_t build_stamps(const udps_frame_t* frame, int time_signal); ScopeModel& model_; Config cfg_; udps_client* client_ = nullptr; std::thread thread_; std::atomic running_{false}; std::atomic paused_{false}; TriggerEngine engine_; std::uint64_t tick_ = 0; ///< base ticks consumed since the epoch std::uint64_t last_tick_ = ~std::uint64_t{0}; double last_stamp_ = 0.0; ///< last relative time written (monotonic guard) double last_recv_ = 0.0; bool have_recv_ = false; /// Last accepted stream-time stamp: a "time signal" whose values do not advance /// across packets (a producer that restarts them per packet, or a mislabelled /// signal) would freeze the whole time base -- the trigger's hold-off clock /// included -- so the stream source is dropped in favour of arrival time. double last_stream_anchor_ = 0.0; bool have_stream_anchor_ = false; /// Arrival-clock origin: the first packet's receive time, so arrival stamps are /// small seconds (the time ring stores f32, which cannot hold Unix epochs). double arrival_epoch_ = 0.0; bool have_arrival_epoch_ = false; std::vector stamps_; /// The decimation filter's state: a running sum per channel (and one for the shared /// clock) over the group of base samples that becomes one stored sample. A boxcar /// average, which is what turns decimation into a *resampling* rather than a stride: /// its first null sits at the stored rate, so what would fold back is attenuated /// instead of reappearing as a false low frequency. Cleared whenever the partition /// changes, since the group length is `time_stride`. std::vector value_acc_; std::vector value_acc_n_; double time_acc_ = 0.0; std::uint64_t time_acc_n_ = 0; /// The partition the accumulators were sized for, so a re-plan resets them rather than /// averaging across the boundary. std::uint64_t acc_stride_ = 0; std::size_t acc_channels_ = 0; std::chrono::steady_clock::time_point started_{}; }; } // namespace udpscope