Files
udpscope/src/trigger.hpp
T

126 lines
6.3 KiB
C++

// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#pragma once
#include <cstdint>
#include <string>
#include "condition.hpp"
namespace udpscope {
/// 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.
/// What counts as an event on that channel: the level and edge, and for the qualified
/// shapes the width, band or timeout as well. The engine does not interpret it -- it
/// hands it to an `EventDetector`.
EventCondition condition{};
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
/// trigger moves to a different channel.
///
/// `channel_name` is that channel's name, and it -- not `cfg.channel` -- is what
/// identifies it here. The index is not stable: every rebuild of the channel list
/// renumbers it (enabling a signal that sorts before the trigger's own is enough),
/// and the model re-resolves the trigger by name afterwards. Keying the reset on the
/// index therefore made any such rebuild abandon an open capture *and set no
/// hold-off*, so the next edge inside the post window became t0 -- which is the
/// "re-triggers mid-capture and ignores the hold-off" defect. Choosing a genuinely
/// different source still resets, because the name changes with it.
///
/// `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, const std::string& channel_name,
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 detector_.last_value(); }
bool have_last() const noexcept { return detector_.have_last(); }
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_{};
std::string channel_name_; ///< the trigger channel's stable identity (see `configure`).
bool armed_ = false;
/// What an event *is*. The engine owns when one may open a capture; this owns whether
/// the sample is one at all.
EventDetector detector_{};
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