refactor: what counts as an event moves into its own evaluator, so a search can ask the same question the trigger asks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
178cf54ef6
commit
b3fd9ae00f
6 files changed
+187
-48
No files matched your search
@@ -105,6 +105,7 @@ add_executable(udpscope
|
|||||||
src/config.cpp
|
src/config.cpp
|
||||||
src/channel.cpp
|
src/channel.cpp
|
||||||
src/model.cpp
|
src/model.cpp
|
||||||
|
src/condition.cpp
|
||||||
src/trigger.cpp
|
src/trigger.cpp
|
||||||
src/acquisition.cpp
|
src/acquisition.cpp
|
||||||
src/plot_host.cpp
|
src/plot_host.cpp
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||||
|
|
||||||
|
#include "condition.hpp"
|
||||||
|
|
||||||
|
namespace udpscope {
|
||||||
|
|
||||||
|
void EventDetector::configure(const EventCondition& c) noexcept {
|
||||||
|
if (c == cond_) return;
|
||||||
|
cond_ = c;
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventDetector::reset() noexcept {
|
||||||
|
have_prev_ = false;
|
||||||
|
prev_ = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
double EventDetector::feed(double value, double t) noexcept {
|
||||||
|
if (!have_prev_) {
|
||||||
|
prev_ = value;
|
||||||
|
have_prev_ = true;
|
||||||
|
return kNoEvent;
|
||||||
|
}
|
||||||
|
double event = kNoEvent;
|
||||||
|
switch (cond_.type) {
|
||||||
|
case TriggerType::Edge:
|
||||||
|
event = feed_edge(value, t);
|
||||||
|
break;
|
||||||
|
case TriggerType::PulseWidth:
|
||||||
|
case TriggerType::Window:
|
||||||
|
case TriggerType::Dropout:
|
||||||
|
break; // implemented in the tasks that add their panel rows
|
||||||
|
}
|
||||||
|
// After the handler: every crossing test above reads `prev_` as the previous sample.
|
||||||
|
prev_ = value;
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
double EventDetector::feed_edge(double value, double t) noexcept {
|
||||||
|
const bool up = crossed_up(value);
|
||||||
|
const bool down = crossed_down(value);
|
||||||
|
const bool fired = (cond_.edge == TriggerEdge::Rising && up) ||
|
||||||
|
(cond_.edge == TriggerEdge::Falling && down) ||
|
||||||
|
(cond_.edge == TriggerEdge::Both && (up || down));
|
||||||
|
return fired ? t : kNoEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace udpscope
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
namespace udpscope {
|
||||||
|
|
||||||
|
/// Which way through a level counts. Shared by every condition type: for `Window` it is
|
||||||
|
/// the direction through the *band* (into it, out of it, or either), and for `Dropout` it
|
||||||
|
/// selects which crossings count as the signal still being alive.
|
||||||
|
///
|
||||||
|
/// It lives here rather than in `trigger.hpp` because the buffer search needs the
|
||||||
|
/// evaluator without the engine's arming state.
|
||||||
|
enum class TriggerEdge { Rising, Falling, Both };
|
||||||
|
|
||||||
|
/// What shape in the signal counts as an event.
|
||||||
|
enum class TriggerType { Edge, PulseWidth, Window, Dropout };
|
||||||
|
|
||||||
|
/// How a measured pulse width is compared with the condition's bounds. `Less` is the
|
||||||
|
/// glitch trigger: "a pulse shorter than it should be".
|
||||||
|
enum class WidthCmp { Less, Greater, Within, Outside };
|
||||||
|
|
||||||
|
/// Everything that decides whether a sample is an event. No arming, no capture window:
|
||||||
|
/// those belong to `TriggerConfig`.
|
||||||
|
struct EventCondition {
|
||||||
|
TriggerType type = TriggerType::Edge;
|
||||||
|
double level = 0.0;
|
||||||
|
double level2 = 0.0; ///< Window: the band's other edge. Ignored otherwise.
|
||||||
|
TriggerEdge edge = TriggerEdge::Rising;
|
||||||
|
WidthCmp cmp = WidthCmp::Greater; ///< PulseWidth only.
|
||||||
|
double width_s = 1.0e-3; ///< PulseWidth only.
|
||||||
|
double width2_s = 2.0e-3; ///< PulseWidth, Within/Outside only.
|
||||||
|
double timeout_s = 1.0e-3; ///< Dropout only.
|
||||||
|
|
||||||
|
bool operator==(const EventCondition& o) const noexcept {
|
||||||
|
return type == o.type && level == o.level && level2 == o.level2 && edge == o.edge &&
|
||||||
|
cmp == o.cmp && width_s == o.width_s && width2_s == o.width2_s &&
|
||||||
|
timeout_s == o.timeout_s;
|
||||||
|
}
|
||||||
|
bool operator!=(const EventCondition& o) const noexcept { return !(*this == o); }
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The event condition's state machine: one sample in, an event time or nothing out.
|
||||||
|
///
|
||||||
|
/// It holds no arming state and no capture state -- that is `TriggerEngine`'s job -- which
|
||||||
|
/// is what lets the buffer search run the *same* evaluator over retained samples and get
|
||||||
|
/// the answers the live trigger would have given.
|
||||||
|
///
|
||||||
|
/// Every condition but `Edge` is sequential: a pulse's width is only known at its closing
|
||||||
|
/// edge, and a dropout is a measured absence. Callers must therefore feed it **every**
|
||||||
|
/// sample in stream order, including samples they intend to ignore; a gap is a pulse it can
|
||||||
|
/// never close.
|
||||||
|
class EventDetector {
|
||||||
|
public:
|
||||||
|
/// What `feed()` returns when the sample is not an event.
|
||||||
|
static constexpr double kNoEvent = std::numeric_limits<double>::quiet_NaN();
|
||||||
|
|
||||||
|
/// Re-applying the same condition is a no-op, deliberately: the producer calls this on
|
||||||
|
/// every packet, and resetting there would restart the machine before any pulse could
|
||||||
|
/// close. A genuinely different condition resets, because its state means nothing.
|
||||||
|
void configure(const EventCondition& c) noexcept;
|
||||||
|
void reset() noexcept;
|
||||||
|
|
||||||
|
/// One sample, in stream order. Returns the event's own stream time, or `kNoEvent`.
|
||||||
|
///
|
||||||
|
/// There is no finite-value check: a NaN sample is benign here (every comparison against
|
||||||
|
/// it is false, so it cannot fire), and rejecting one would leave `prev_` at the last good
|
||||||
|
/// value and let the sample *after* the NaN fire a crossing that today cannot happen.
|
||||||
|
double feed(double value, double t) noexcept;
|
||||||
|
|
||||||
|
double last_value() const noexcept { return prev_; }
|
||||||
|
bool have_last() const noexcept { return have_prev_; }
|
||||||
|
const EventCondition& condition() const noexcept { return cond_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool crossed_up(double v) const noexcept {
|
||||||
|
return prev_ < cond_.level && v >= cond_.level;
|
||||||
|
}
|
||||||
|
bool crossed_down(double v) const noexcept {
|
||||||
|
return prev_ > cond_.level && v <= cond_.level;
|
||||||
|
}
|
||||||
|
double feed_edge(double value, double t) noexcept;
|
||||||
|
|
||||||
|
EventCondition cond_{};
|
||||||
|
bool have_prev_ = false;
|
||||||
|
double prev_ = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace udpscope
|
||||||
+28
-33
@@ -4,6 +4,7 @@
|
|||||||
#include "trigger.hpp"
|
#include "trigger.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
namespace udpscope {
|
namespace udpscope {
|
||||||
|
|
||||||
@@ -20,8 +21,10 @@ void TriggerEngine::configure(const TriggerConfig& cfg, const std::string& chann
|
|||||||
post_seconds_ = post_seconds > 0.0 ? post_seconds : 0.0;
|
post_seconds_ = post_seconds > 0.0 ? post_seconds : 0.0;
|
||||||
pre_seconds_ = pre_seconds > 0.0 ? pre_seconds : 0.0;
|
pre_seconds_ = pre_seconds > 0.0 ? pre_seconds : 0.0;
|
||||||
oldest_s_ = oldest_s;
|
oldest_s_ = oldest_s;
|
||||||
|
// A no-op unless the condition actually changed: this runs on every packet.
|
||||||
|
detector_.configure(cfg_.condition);
|
||||||
if (channel_changed) {
|
if (channel_changed) {
|
||||||
have_prev_ = false;
|
detector_.reset();
|
||||||
pending_ = false;
|
pending_ = false;
|
||||||
post_accum_count_ = 0;
|
post_accum_count_ = 0;
|
||||||
}
|
}
|
||||||
@@ -32,23 +35,21 @@ void TriggerEngine::arm(double now_s) noexcept {
|
|||||||
pending_ = false;
|
pending_ = false;
|
||||||
post_accum_count_ = 0.0;
|
post_accum_count_ = 0.0;
|
||||||
holdoff_until_s_ = now_s;
|
holdoff_until_s_ = now_s;
|
||||||
have_prev_ = false;
|
detector_.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TriggerEngine::process(double value, double now_s) noexcept {
|
bool TriggerEngine::process(double value, double now_s) noexcept {
|
||||||
if (!cfg_.enabled || !armed_ || cfg_.channel < 0) return false;
|
if (!cfg_.enabled || !armed_ || cfg_.channel < 0) return false;
|
||||||
|
|
||||||
// Hold-off: settled, waiting for the next edge.
|
// The detector is fed **first and unconditionally**, on every sample the engine sees --
|
||||||
if (!pending_ && now_s < holdoff_until_s_) {
|
// including during the hold-off and during an open capture, where its answer is thrown
|
||||||
prev_ = value;
|
// away. It is a sequential machine: a pulse whose opening edge fell in a skipped stretch
|
||||||
have_prev_ = true;
|
// can never be closed, and the symptom would be a width condition that silently stops
|
||||||
last_t_s_ = now_s;
|
// firing after the first hold-off.
|
||||||
return false;
|
const double event_t = detector_.feed(value, now_s);
|
||||||
}
|
|
||||||
|
|
||||||
if (!have_prev_) {
|
// Hold-off: settled, waiting for the next event.
|
||||||
prev_ = value;
|
if (!pending_ && now_s < holdoff_until_s_) {
|
||||||
have_prev_ = true;
|
|
||||||
last_t_s_ = now_s;
|
last_t_s_ = now_s;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -60,7 +61,6 @@ bool TriggerEngine::process(double value, double now_s) noexcept {
|
|||||||
// past it. Time, not a sample count: `now_s` is the same clock the x axis reads,
|
// past it. Time, not a sample count: `now_s` is the same clock the x axis reads,
|
||||||
// so the capture covers exactly the requested span whatever the cadence is.
|
// so the capture covers exactly the requested span whatever the cadence is.
|
||||||
++post_accum_count_;
|
++post_accum_count_;
|
||||||
prev_ = value;
|
|
||||||
if (now_s - pending_start_s_ >= post_seconds_) {
|
if (now_s - pending_start_s_ >= post_seconds_) {
|
||||||
pending_ = false;
|
pending_ = false;
|
||||||
post_accum_count_ = 0;
|
post_accum_count_ = 0;
|
||||||
@@ -74,33 +74,28 @@ bool TriggerEngine::process(double value, double now_s) noexcept {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Edge detection against the level.
|
if (!std::isfinite(event_t)) return false;
|
||||||
const bool crossed_up = prev_ < cfg_.level && value >= cfg_.level;
|
|
||||||
const bool crossed_down = prev_ > cfg_.level && value <= cfg_.level;
|
|
||||||
const bool fired = (cfg_.edge == TriggerEdge::Rising && crossed_up) ||
|
|
||||||
(cfg_.edge == TriggerEdge::Falling && crossed_down) ||
|
|
||||||
(cfg_.edge == TriggerEdge::Both && (crossed_up || crossed_down));
|
|
||||||
prev_ = value;
|
|
||||||
if (!fired) return false;
|
|
||||||
|
|
||||||
// The pre-trigger window has to already be behind this edge, or the capture opens
|
// The pre-trigger window has to already be behind the event, or the capture opens on a
|
||||||
// on a half-filled pre window and the event is short on its left side -- the plot
|
// half-filled pre window and the event is short on its left side -- the plot then shows a
|
||||||
// then shows a window that is correctly sized but only partly drawn. The buffer
|
// window that is correctly sized but only partly drawn. The buffer fills whether or not the
|
||||||
// fills whether or not the trigger is armed, so this normally passes on the first
|
// trigger is armed, so this normally passes on the first event; it only bites just after a
|
||||||
// edge; it only bites just after a rebuild (a CONFIG, or enabling the signal),
|
// rebuild (a CONFIG, or enabling the signal), when the history genuinely is not there yet.
|
||||||
// when the history genuinely is not there yet.
|
//
|
||||||
if (now_s - oldest_s_ < pre_seconds_) return false;
|
// Measured from the *event's own* time, not from `now_s`: for a qualified condition the two
|
||||||
|
// differ (a pulse is reported at its closing edge), and the pre window belongs to the event.
|
||||||
|
if (event_t - oldest_s_ < pre_seconds_) return false;
|
||||||
|
|
||||||
// The edge's own time, which is what a trigger marker marks: the latch itself
|
// The event's own time, which is what a trigger marker marks: the latch itself may happen
|
||||||
// may happen a post-window later.
|
// a post-window later.
|
||||||
last_trigger_s_ = now_s;
|
last_trigger_s_ = event_t;
|
||||||
|
|
||||||
// The edge always opens a capture; there is no "latch on the edge itself" case.
|
// The event always opens a capture; there is no "latch on the event itself" case.
|
||||||
// A zero-length post window latches on the very next sample, which is the least a
|
// A zero-length post window latches on the very next sample, which is the least a
|
||||||
// capture can hold, and every longer window is just more samples -- no conversion
|
// capture can hold, and every longer window is just more samples -- no conversion
|
||||||
// from seconds to a sample count, so no conversion to get wrong.
|
// from seconds to a sample count, so no conversion to get wrong.
|
||||||
pending_ = true;
|
pending_ = true;
|
||||||
pending_start_s_ = now_s;
|
pending_start_s_ = event_t;
|
||||||
post_accum_count_ = 0;
|
post_accum_count_ = 0;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-9
@@ -6,10 +6,9 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace udpscope {
|
#include "condition.hpp"
|
||||||
|
|
||||||
/// Which way the trigger signal crosses the level.
|
namespace udpscope {
|
||||||
enum class TriggerEdge { Rising, Falling, Both };
|
|
||||||
|
|
||||||
/// How an armed trigger re-arms after it fired.
|
/// How an armed trigger re-arms after it fired.
|
||||||
enum class TriggerMode { Normal, Single };
|
enum class TriggerMode { Normal, Single };
|
||||||
@@ -24,8 +23,10 @@ struct TriggerConfig {
|
|||||||
bool enabled = false; ///< trigger mode on/off (toolbar).
|
bool enabled = false; ///< trigger mode on/off (toolbar).
|
||||||
bool armed = false; ///< arm/disarm button.
|
bool armed = false; ///< arm/disarm button.
|
||||||
int channel = -1; ///< index into ScopeModel::channels(), -1 = none.
|
int channel = -1; ///< index into ScopeModel::channels(), -1 = none.
|
||||||
double level = 0.0;
|
/// What counts as an event on that channel: the level and edge, and for the qualified
|
||||||
TriggerEdge edge = TriggerEdge::Rising;
|
/// 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 pretrigger_pct = 50.0; ///< share of the buffer shown before the event.
|
||||||
double holdoff_s = 0.010; ///< default from the spec.
|
double holdoff_s = 0.010; ///< default from the spec.
|
||||||
TriggerMode mode = TriggerMode::Normal;
|
TriggerMode mode = TriggerMode::Normal;
|
||||||
@@ -92,8 +93,8 @@ class TriggerEngine {
|
|||||||
/// pre-trigger window an edge needs before it may open a capture.
|
/// pre-trigger window an edge needs before it may open a capture.
|
||||||
double history() const noexcept { return last_t_s_ - oldest_s_; }
|
double history() const noexcept { return last_t_s_ - oldest_s_; }
|
||||||
double pre_seconds() const noexcept { return pre_seconds_; }
|
double pre_seconds() const noexcept { return pre_seconds_; }
|
||||||
double last_value() const noexcept { return prev_; }
|
double last_value() const noexcept { return detector_.last_value(); }
|
||||||
bool have_last() const noexcept { return have_prev_; }
|
bool have_last() const noexcept { return detector_.have_last(); }
|
||||||
double last_t() const noexcept { return last_t_s_; }
|
double last_t() const noexcept { return last_t_s_; }
|
||||||
double holdoff_until() const noexcept { return holdoff_until_s_; }
|
double holdoff_until() const noexcept { return holdoff_until_s_; }
|
||||||
|
|
||||||
@@ -107,8 +108,9 @@ class TriggerEngine {
|
|||||||
TriggerConfig cfg_{};
|
TriggerConfig cfg_{};
|
||||||
std::string channel_name_; ///< the trigger channel's stable identity (see `configure`).
|
std::string channel_name_; ///< the trigger channel's stable identity (see `configure`).
|
||||||
bool armed_ = false;
|
bool armed_ = false;
|
||||||
bool have_prev_ = false;
|
/// What an event *is*. The engine owns when one may open a capture; this owns whether
|
||||||
double prev_ = 0.0;
|
/// the sample is one at all.
|
||||||
|
EventDetector detector_{};
|
||||||
bool pending_ = false; ///< an edge was seen; collecting the post window
|
bool pending_ = false; ///< an edge was seen; collecting the post window
|
||||||
double pending_start_s_ = 0.0;
|
double pending_start_s_ = 0.0;
|
||||||
double holdoff_until_s_ = 0.0;
|
double holdoff_until_s_ = 0.0;
|
||||||
|
|||||||
+7
-6
@@ -1609,10 +1609,10 @@ void publish_trigger_fields(App* app) {
|
|||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(app->model->mutex());
|
std::lock_guard<std::mutex> lock(app->model->mutex());
|
||||||
TriggerConfig& tc = app->model->trigger();
|
TriggerConfig& tc = app->model->trigger();
|
||||||
tc.level = level;
|
tc.condition.level = level;
|
||||||
tc.pretrigger_pct = pre;
|
tc.pretrigger_pct = pre;
|
||||||
tc.holdoff_s = holdoff;
|
tc.holdoff_s = holdoff;
|
||||||
tc.edge = edge == 0 ? TriggerEdge::Rising
|
tc.condition.edge = edge == 0 ? TriggerEdge::Rising
|
||||||
: edge == 1 ? TriggerEdge::Falling
|
: edge == 1 ? TriggerEdge::Falling
|
||||||
: TriggerEdge::Both;
|
: TriggerEdge::Both;
|
||||||
tc.mode = mode == 0 ? TriggerMode::Normal : TriggerMode::Single;
|
tc.mode = mode == 0 ? TriggerMode::Normal : TriggerMode::Single;
|
||||||
@@ -2050,7 +2050,7 @@ void rebuild_trigger_signals(App& app) {
|
|||||||
if (std::isfinite(sig.range_min) && std::isfinite(sig.range_max) &&
|
if (std::isfinite(sig.range_min) && std::isfinite(sig.range_max) &&
|
||||||
sig.range_max > sig.range_min) {
|
sig.range_max > sig.range_min) {
|
||||||
const double mid = 0.5 * (sig.range_min + sig.range_max);
|
const double mid = 0.5 * (sig.range_min + sig.range_max);
|
||||||
tc.level = mid;
|
tc.condition.level = mid;
|
||||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_level_spin), mid);
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_level_spin), mid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6344,7 +6344,7 @@ gboolean tick(gpointer user) {
|
|||||||
span_s = model.partition().span_s;
|
span_s = model.partition().span_s;
|
||||||
time_stride = model.partition().time_stride;
|
time_stride = model.partition().time_stride;
|
||||||
const int chan = model.trigger().channel;
|
const int chan = model.trigger().channel;
|
||||||
trigger_level = model.trigger().level;
|
trigger_level = model.trigger().condition.level;
|
||||||
if (chan >= 0 && chan < static_cast<int>(model.channel_count())) {
|
if (chan >= 0 && chan < static_cast<int>(model.channel_count())) {
|
||||||
trigger_name = model.channels()[static_cast<std::size_t>(chan)].name;
|
trigger_name = model.channels()[static_cast<std::size_t>(chan)].name;
|
||||||
}
|
}
|
||||||
@@ -8270,7 +8270,8 @@ gboolean trigrenum_probe(gpointer user) {
|
|||||||
"holdoff=%.4f level=%.4g edge=%d mode=%d enabled=%d armed=%d ch=%d "
|
"holdoff=%.4f level=%.4g edge=%d mode=%d enabled=%d armed=%d ch=%d "
|
||||||
"channels=%zu\n",
|
"channels=%zu\n",
|
||||||
a->rows->time_window(), a->model->event_span(), tc.pretrigger_pct,
|
a->rows->time_window(), a->model->event_span(), tc.pretrigger_pct,
|
||||||
tc.post_span_s(a->model->event_span()), tc.holdoff_s, tc.level, int(tc.edge),
|
tc.post_span_s(a->model->event_span()), tc.holdoff_s, tc.condition.level,
|
||||||
|
int(tc.condition.edge),
|
||||||
int(tc.mode), int(tc.enabled), int(tc.armed), tc.channel,
|
int(tc.mode), int(tc.enabled), int(tc.armed), tc.channel,
|
||||||
a->model->channel_count());
|
a->model->channel_count());
|
||||||
}
|
}
|
||||||
@@ -8295,7 +8296,7 @@ gboolean fields_probe(gpointer user) {
|
|||||||
std::lock_guard<std::mutex> lock(a->model->mutex());
|
std::lock_guard<std::mutex> lock(a->model->mutex());
|
||||||
std::fprintf(stderr, "[fields-probe] mode=%u edge=%u published mode=%d edge=%d capturing=%d\n",
|
std::fprintf(stderr, "[fields-probe] mode=%u edge=%u published mode=%d edge=%d capturing=%d\n",
|
||||||
gtk_drop_down_get_selected(mode), gtk_drop_down_get_selected(edge),
|
gtk_drop_down_get_selected(mode), gtk_drop_down_get_selected(edge),
|
||||||
int(a->model->trigger().mode), int(a->model->trigger().edge),
|
int(a->model->trigger().mode), int(a->model->trigger().condition.edge),
|
||||||
int(a->model->trigger_pending()));
|
int(a->model->trigger_pending()));
|
||||||
std::fflush(stderr);
|
std::fflush(stderr);
|
||||||
return G_SOURCE_REMOVE;
|
return G_SOURCE_REMOVE;
|
||||||
|
|||||||
Reference in new issue
Block a user