TriggerEngine::CheckSample returned early in every state but ARMED, so an edge arriving while a capture was being collected or handed out was dropped, and the automatic rearm then waited for a FRESH edge. The engine was therefore blind from its own trigger point until the capture had been harvested — a post-window — and for the holdoff on top of that. On a sparse pulse train that rounds the capture spacing up to a whole pulse period: at the default 1 s window the blind stretch is 1 s, so a 1 Hz train was caught at 0.5 Hz and a wider window lost whole multiples. The comparator now keeps running through COLLECTING and TRIGGERED and remembers the first edge at or past trigTime + max(postSec, holdoffSec). The holdoff guards against re-triggering on the ringing of the same event and is measured from the trigger point, so it overlaps the post-window rather than adding to it. Rearm() fires on the remembered edge; it also keeps the tracked level, so the first sample after it has a real predecessor instead of being spent seeding one. Arm() stays the operator's arm and discards the held edge — they asked for the next event, not one already been and gone — and SetConfig() and Disarm() drop it too, since it was never judged against the new window. This is the same defect and the same remedy already validated in the Go hub (wshub/trigger.go, trigger_sporadic_test.go); the C++ hub had been left with the original semantics. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
239 lines
7.3 KiB
C++
239 lines
7.3 KiB
C++
/**
|
|
* @file TriggerEngine.cpp
|
|
* @brief Hub-side trigger FSM implementation (web client semantics).
|
|
*/
|
|
|
|
#include "TriggerEngine.h"
|
|
#include "AdvancedErrorManagement.h"
|
|
|
|
namespace StreamHub {
|
|
|
|
TriggerEngine::TriggerEngine()
|
|
: epoch_(0u),
|
|
state_(kTrigIdle),
|
|
stopped_(false),
|
|
prevValue_(0.0),
|
|
prevValid_(false),
|
|
lastTime_(0.0),
|
|
lastTimeValid_(false),
|
|
trigTime_(0.0),
|
|
firedPreSec_(0.0),
|
|
firedPostSec_(0.0),
|
|
firedValid_(false),
|
|
pendingTime_(0.0),
|
|
pendingValid_(false) {
|
|
}
|
|
|
|
void TriggerEngine::LatchWindowLocked(float64 t) {
|
|
state_ = kTrigCollecting;
|
|
trigTime_ = t;
|
|
firedPreSec_ = config_.windowSec * config_.prePercent / 100.0;
|
|
firedPostSec_ = config_.windowSec - firedPreSec_;
|
|
firedValid_ = true;
|
|
}
|
|
|
|
void TriggerEngine::SetConfig(const TriggerConfig &cfg) {
|
|
(void) mutex_.FastLock();
|
|
config_ = cfg;
|
|
/* Clamp to web UI bounds */
|
|
if (config_.windowSec < 1.0e-4) { config_.windowSec = 1.0e-4; }
|
|
/* 60 s where the Go hub allows 600. Deliberate: these rings are
|
|
* fixed-capacity and store every sample, so a window they cannot hold is
|
|
* harvested truncated and silently decimated to kTrigCapturePts. The Go
|
|
* hub stores min/max pairs instead once a window outgrows its budget, so
|
|
* there a long window costs resolution rather than coverage. */
|
|
if (config_.windowSec > 60.0) { config_.windowSec = 60.0; }
|
|
if (config_.prePercent < 0.0) { config_.prePercent = 0.0; }
|
|
if (config_.prePercent > 100.0) { config_.prePercent = 100.0; }
|
|
if (config_.holdoffSec < 0.0) { config_.holdoffSec = 0.0; }
|
|
if (config_.holdoffSec > 60.0) { config_.holdoffSec = 60.0; }
|
|
epoch_++;
|
|
prevValid_ = false;
|
|
prevValue_ = 0.0;
|
|
/* An edge held over from the old configuration would be latched against the
|
|
* new window, which it was never judged against. */
|
|
pendingValid_ = false;
|
|
mutex_.FastUnLock();
|
|
}
|
|
|
|
TriggerConfig TriggerEngine::GetConfig() const {
|
|
(void) mutex_.FastLock();
|
|
TriggerConfig ret = config_;
|
|
mutex_.FastUnLock();
|
|
return ret;
|
|
}
|
|
|
|
uint32 TriggerEngine::GetConfigEpoch() const {
|
|
(void) mutex_.FastLock();
|
|
uint32 ret = epoch_;
|
|
mutex_.FastUnLock();
|
|
return ret;
|
|
}
|
|
|
|
void TriggerEngine::Arm() {
|
|
(void) mutex_.FastLock();
|
|
state_ = kTrigArmed;
|
|
prevValid_ = false;
|
|
prevValue_ = 0.0;
|
|
pendingValid_ = false;
|
|
mutex_.FastUnLock();
|
|
}
|
|
|
|
void TriggerEngine::Rearm() {
|
|
(void) mutex_.FastLock();
|
|
if (pendingValid_) {
|
|
const float64 t = pendingTime_;
|
|
pendingValid_ = false;
|
|
LatchWindowLocked(t);
|
|
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
|
|
"TriggerEngine: rearmed onto the edge held at t=%.6f "
|
|
"(pre=%.4fs post=%.4fs)",
|
|
t, firedPreSec_, firedPostSec_);
|
|
}
|
|
else {
|
|
/* prevValue_/prevValid_ are deliberately kept: the comparator ran right
|
|
* through the dead time, so the next sample has a real predecessor. */
|
|
state_ = kTrigArmed;
|
|
}
|
|
mutex_.FastUnLock();
|
|
}
|
|
|
|
void TriggerEngine::Disarm() {
|
|
(void) mutex_.FastLock();
|
|
state_ = kTrigIdle;
|
|
stopped_ = false;
|
|
prevValid_ = false;
|
|
prevValue_ = 0.0;
|
|
firedValid_ = false;
|
|
pendingValid_ = false;
|
|
mutex_.FastUnLock();
|
|
}
|
|
|
|
void TriggerEngine::SetStopped(bool stopped) {
|
|
(void) mutex_.FastLock();
|
|
stopped_ = stopped;
|
|
mutex_.FastUnLock();
|
|
}
|
|
|
|
bool TriggerEngine::GetStopped() const {
|
|
(void) mutex_.FastLock();
|
|
bool ret = stopped_;
|
|
mutex_.FastUnLock();
|
|
return ret;
|
|
}
|
|
|
|
void TriggerEngine::CheckSample(float64 t, float64 v) {
|
|
(void) mutex_.FastLock();
|
|
|
|
/* Track the newest watched timestamp in every state so Force() has a
|
|
* reference time to latch the capture window around. */
|
|
lastTime_ = t;
|
|
lastTimeValid_ = true;
|
|
|
|
/* A capture in flight does not stop the comparator; it only changes what an
|
|
* edge does. See pendingTime_. */
|
|
const bool inFlight = (state_ == kTrigCollecting) || (state_ == kTrigTriggered);
|
|
if ((state_ != kTrigArmed) && !inFlight) {
|
|
mutex_.FastUnLock();
|
|
return;
|
|
}
|
|
if (!prevValid_) {
|
|
prevValue_ = v;
|
|
prevValid_ = true;
|
|
mutex_.FastUnLock();
|
|
return;
|
|
}
|
|
|
|
const float64 thr = config_.threshold;
|
|
const bool up = (prevValue_ < thr) && (v >= thr);
|
|
const bool down = (prevValue_ > thr) && (v <= thr);
|
|
prevValue_ = v;
|
|
|
|
bool fired = false;
|
|
switch (config_.edge) {
|
|
case kEdgeRising: fired = up; break;
|
|
case kEdgeFalling: fired = down; break;
|
|
case kEdgeBoth: fired = (up || down); break;
|
|
default: break;
|
|
}
|
|
|
|
if (fired) {
|
|
if (!inFlight) {
|
|
/* Latch the window at fire time so later config edits do not
|
|
* affect this capture (web client snap._preS/_postS). */
|
|
LatchWindowLocked(t);
|
|
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
|
|
"TriggerEngine: fired at t=%.6f (pre=%.4fs post=%.4fs)",
|
|
t, firedPreSec_, firedPostSec_);
|
|
}
|
|
else if (!pendingValid_ && firedValid_) {
|
|
/* The earliest trigger point a new capture may take. The one in
|
|
* flight owns everything up to the end of its own post-window, and
|
|
* the holdoff — a guard against re-triggering on the ringing of the
|
|
* SAME event — is measured from its trigger point too, so the two
|
|
* overlap rather than add.
|
|
*
|
|
* Keep only the FIRST qualifying edge: a later one would deliver
|
|
* the same capture a pulse further on and skip the one between. */
|
|
float64 guard = firedPostSec_;
|
|
if (config_.holdoffSec > guard) {
|
|
guard = config_.holdoffSec;
|
|
}
|
|
if (t >= (trigTime_ + guard)) {
|
|
pendingTime_ = t;
|
|
pendingValid_ = true;
|
|
}
|
|
}
|
|
else {
|
|
/* Already holding an edge, or no window latched to measure against. */
|
|
}
|
|
}
|
|
|
|
mutex_.FastUnLock();
|
|
}
|
|
|
|
bool TriggerEngine::Force() {
|
|
(void) mutex_.FastLock();
|
|
|
|
bool ok = lastTimeValid_ && (state_ != kTrigCollecting);
|
|
if (ok) {
|
|
LatchWindowLocked(lastTime_);
|
|
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
|
|
"TriggerEngine: forced at t=%.6f (pre=%.4fs post=%.4fs)",
|
|
trigTime_, firedPreSec_, firedPostSec_);
|
|
}
|
|
|
|
mutex_.FastUnLock();
|
|
return ok;
|
|
}
|
|
|
|
TrigState TriggerEngine::GetState() const {
|
|
(void) mutex_.FastLock();
|
|
TrigState ret = state_;
|
|
mutex_.FastUnLock();
|
|
return ret;
|
|
}
|
|
|
|
bool TriggerEngine::GetFiredWindow(float64 &trigTime, float64 &preSec,
|
|
float64 &postSec) const {
|
|
(void) mutex_.FastLock();
|
|
const bool ok = firedValid_;
|
|
if (ok) {
|
|
trigTime = trigTime_;
|
|
preSec = firedPreSec_;
|
|
postSec = firedPostSec_;
|
|
}
|
|
mutex_.FastUnLock();
|
|
return ok;
|
|
}
|
|
|
|
void TriggerEngine::MarkTriggered() {
|
|
(void) mutex_.FastLock();
|
|
if (state_ == kTrigCollecting) {
|
|
state_ = kTrigTriggered;
|
|
}
|
|
mutex_.FastUnLock();
|
|
}
|
|
|
|
} /* namespace StreamHub */
|