Initial commit

This commit is contained in:
Martino Ferrari committed 2026-10-01 15:42:48 +02:00
commit 8059b90de7
29 files changed
+7893

No files matched your search

+112
View File
@@ -0,0 +1,112 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#pragma once
#include <cstdint>
namespace udpscope {
/// Which way the trigger signal crosses the level.
enum class TriggerEdge { Rising, Falling, Both };
/// How an armed trigger re-arms after it fired.
enum class TriggerMode { Normal, Single };
/// The trigger panel's configuration (spec item 2.2).
///
/// The trigger names a *signal* (an index into `ScopeModel::signals()`), not a
/// channel: the panel lists every signal the stream describes, whether or not it
/// is being acquired, and picking one is what arms the trigger on it. The
/// acquisition resolves the signal to the channel carrying it.
struct TriggerConfig {
bool enabled = false; ///< trigger mode on/off (toolbar).
bool armed = false; ///< arm/disarm button.
int channel = -1; ///< index into ScopeModel::channels(), -1 = none.
double level = 0.0;
TriggerEdge edge = TriggerEdge::Rising;
double pretrigger_pct = 50.0; ///< share of the buffer shown before the event.
double holdoff_s = 0.010; ///< default from the spec.
TriggerMode mode = TriggerMode::Normal;
/// Seconds of post-trigger data to collect before latching the event, given
/// the buffer's span.
double post_span_s(double buffer_span_s) const;
};
/// The producer-side trigger state machine. Fed one sample at a time in stream
/// order; it says when the event buffer should be latched.
class TriggerEngine {
public:
/// Configures the channel and the post-trigger window. Resets the state when the
/// channel changes.
///
/// `post_seconds` is the post-trigger window in **stream seconds**, the same clock
/// `process()` is fed and the same clock the x axis reads, so the capture covers
/// exactly that much axis by construction.
///
/// It used to be a count of samples, converted from seconds by a measured estimate
/// of the channel's cadence. That estimate is what made the window wrong: when it
/// collapsed (any stream whose time signal advances in large steps drove it to the
/// 1 S/s floor) the count came out as 1, the engine latched on the edge sample
/// itself, and the event had no post-trigger data at all. Seconds need no estimate.
///
/// `pre_seconds` is the pre-trigger window, and `oldest_s` the stream time of the
/// oldest sample the buffer still holds. Together they gate the edge: a capture is
/// only allowed once the history reaches back a full `pre_seconds` before it, so an
/// event cannot latch onto a half-filled pre window. `oldest_s` has to be the real
/// buffer's, not a timer since arming -- the ring keeps filling with the trigger
/// off, so the history is usually already there and waiting would be wrong.
void configure(const TriggerConfig& cfg, double post_seconds, double pre_seconds,
double oldest_s);
/// Disarms without forgetting the configuration.
void disarm() noexcept { armed_ = false; pending_ = false; }
void arm(double now_s) noexcept;
/// Feeds one sample of the trigger channel. `now_s` is the sample's timestamp
/// in stream seconds. Returns true when the event has just been latched.
bool process(double value, double now_s) noexcept;
bool pending() const noexcept { return pending_; }
bool armed() const noexcept { return armed_; }
/// Diagnostics for the capture state machine: how many samples have landed in
/// the post-trigger window, the window it is filling (in stream seconds), how
/// much of that has elapsed, and the last value fed.
std::uint64_t post_accum() const noexcept { return post_accum_count_; }
double post_seconds() const noexcept { return post_seconds_; }
double post_elapsed() const noexcept {
return pending_ ? last_t_s_ - pending_start_s_ : 0.0;
}
/// How much history the buffer holds behind the newest sample, against the
/// pre-trigger window an edge needs before it may open a capture.
double history() const noexcept { return last_t_s_ - oldest_s_; }
double pre_seconds() const noexcept { return pre_seconds_; }
double last_value() const noexcept { return prev_; }
bool have_last() const noexcept { return have_prev_; }
double last_t() const noexcept { return last_t_s_; }
double holdoff_until() const noexcept { return holdoff_until_s_; }
/// Stream time of the edge that armed the event now on screen. Valid once
/// `process()` has fired; the view is centred on this instant (pre-trigger share
/// before it, post-trigger share after), so it is the x a trigger marker belongs
/// at. Unchanged while a later event has not fired yet.
double last_trigger_time() const noexcept { return last_trigger_s_; }
private:
TriggerConfig cfg_{};
bool armed_ = false;
bool have_prev_ = false;
double prev_ = 0.0;
bool pending_ = false; ///< an edge was seen; collecting the post window
double pending_start_s_ = 0.0;
double holdoff_until_s_ = 0.0;
std::uint64_t post_accum_count_ = 0;
double post_seconds_ = 0.0; ///< post-trigger window, in stream seconds.
double pre_seconds_ = 0.0; ///< pre-trigger window an edge needs behind it.
double oldest_s_ = 0.0; ///< stream time of the oldest sample still buffered.
double last_t_s_ = 0.0;
double last_trigger_s_ = 0.0;
};
} // namespace udpscope