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>
192 lines
6.9 KiB
C++
192 lines
6.9 KiB
C++
/**
|
|
* @file TriggerEngine.h
|
|
* @brief Hub-side trigger FSM with web-oscilloscope semantics.
|
|
*
|
|
* Mirrors the web client's trigger logic (Client/udpstreamer/static/app.js):
|
|
*
|
|
* IDLE --Arm()--> ARMED
|
|
* ARMED --edge fires--> COLLECTING (trigTime + pre/post window latched)
|
|
* COLLECTING -capture done-> TRIGGERED (StreamHub broadcasts the capture)
|
|
* TRIGGERED --Arm()--> ARMED (auto after ~200 ms in normal mode,
|
|
* manual "rearm" in single mode)
|
|
* any --Disarm()--> IDLE
|
|
*
|
|
* Configuration is the web client's: full signal key ("src:sig" or
|
|
* "src:sig[i]"), edge rising/falling/both, threshold, capture window length
|
|
* windowSec, pre-trigger percentage, and acquisition mode normal/single with
|
|
* a "stopped" flag pausing auto-rearm.
|
|
*
|
|
* CheckSample() is called from the UDPSClient receive thread for every decoded
|
|
* sample of the configured signal. All state is protected by a
|
|
* FastPollingMutexSem so the StreamHub push thread can poll it safely.
|
|
*/
|
|
|
|
#ifndef STREAMHUB_TRIGGER_ENGINE_H_
|
|
#define STREAMHUB_TRIGGER_ENGINE_H_
|
|
|
|
#include "CompilerTypes.h"
|
|
#include "FastPollingMutexSem.h"
|
|
#include "StreamString.h"
|
|
|
|
namespace StreamHub {
|
|
|
|
using MARTe::uint32;
|
|
using MARTe::float64;
|
|
using MARTe::StreamString;
|
|
using MARTe::FastPollingMutexSem;
|
|
|
|
/** Trigger FSM states (badge mapping: idle/armed/collecting/triggered). */
|
|
enum TrigState {
|
|
kTrigIdle = 0,
|
|
kTrigArmed = 1,
|
|
kTrigCollecting = 2,
|
|
kTrigTriggered = 3
|
|
};
|
|
|
|
/** Edge selection. */
|
|
enum TrigEdge {
|
|
kEdgeRising = 0,
|
|
kEdgeFalling = 1,
|
|
kEdgeBoth = 2
|
|
};
|
|
|
|
/** Acquisition mode. */
|
|
enum TrigAcqMode {
|
|
kTrigNormal = 0, ///< Auto-rearm after each capture (unless stopped)
|
|
kTrigSingle = 1 ///< Stay TRIGGERED until an explicit rearm
|
|
};
|
|
|
|
/** @brief Trigger configuration (web client semantics). */
|
|
struct TriggerConfig {
|
|
TriggerConfig();
|
|
StreamString signalKey; ///< Full key: "src:sig" or "src:sig[i]"
|
|
TrigEdge edge; ///< Rising / falling / both
|
|
float64 threshold; ///< Trigger threshold (physical units)
|
|
float64 windowSec; ///< Capture window length [1e-4 .. 60] s
|
|
float64 prePercent; ///< Pre-trigger part of the window [0 .. 100] %
|
|
TrigAcqMode mode; ///< Normal (auto-rearm) or single
|
|
float64 holdoffSec; ///< Rearm delay after a capture [0 .. 60] s
|
|
};
|
|
|
|
/**
|
|
* @brief Hub-side trigger FSM.
|
|
*/
|
|
class TriggerEngine {
|
|
public:
|
|
|
|
TriggerEngine();
|
|
|
|
/** @brief Replace the configuration (bumps the config epoch). */
|
|
void SetConfig(const TriggerConfig &cfg);
|
|
|
|
/** @return Copy of the current configuration. */
|
|
TriggerConfig GetConfig() const;
|
|
|
|
/**
|
|
* @return Monotonic configuration epoch. UDPSourceSession caches the
|
|
* resolved (signal idx, element idx) per epoch and re-resolves on change.
|
|
*/
|
|
uint32 GetConfigEpoch() const;
|
|
|
|
/**
|
|
* @brief Arm: any state → ARMED (resets edge detection).
|
|
* This is the user's own arm, so it discards any edge remembered during the
|
|
* previous capture: the user asked for the next event, not for one that has
|
|
* already been and gone.
|
|
*/
|
|
void Arm();
|
|
|
|
/**
|
|
* @brief The automatic arm at the end of a capture (normal mode).
|
|
* Unlike Arm() it honours an edge seen while the capture was being
|
|
* collected, firing on it at once rather than waiting for the next one, and
|
|
* it keeps the tracked level so the first sample afterwards is compared
|
|
* against its real predecessor. TRIGGERED → COLLECTING when an edge was
|
|
* remembered, otherwise → ARMED.
|
|
*/
|
|
void Rearm();
|
|
|
|
/** @brief Disarm: any state → IDLE; clears the stopped flag. */
|
|
void Disarm();
|
|
|
|
/** @brief Set/clear the stopped flag (pauses normal-mode auto-rearm). */
|
|
void SetStopped(bool stopped);
|
|
|
|
/** @return Current stopped flag. */
|
|
bool GetStopped() const;
|
|
|
|
/**
|
|
* @brief Edge-detect one decoded sample of the configured signal.
|
|
* Receive-thread context. In ARMED state a matching edge latches trigTime
|
|
* and the pre/post window and moves to COLLECTING. While a capture is in
|
|
* flight (COLLECTING/TRIGGERED) the comparator keeps running and the first
|
|
* edge clear of that capture is remembered for the next Rearm().
|
|
*/
|
|
void CheckSample(float64 t, float64 v);
|
|
|
|
/**
|
|
* @brief Fire the trigger unconditionally at the most recent sample time of
|
|
* the watched signal, latching the pre/post window exactly as CheckSample
|
|
* does. Any state except COLLECTING → COLLECTING.
|
|
* @return false when no sample has been seen yet, or a capture is already
|
|
* being collected.
|
|
*/
|
|
bool Force();
|
|
|
|
/** @return Current FSM state. */
|
|
TrigState GetState() const;
|
|
|
|
/**
|
|
* @brief Get the latched capture window of the last fired trigger.
|
|
* @return false if no trigger has fired since the last Disarm().
|
|
*/
|
|
bool GetFiredWindow(float64 &trigTime, float64 &preSec,
|
|
float64 &postSec) const;
|
|
|
|
/** @brief COLLECTING → TRIGGERED (push thread, after the capture frame). */
|
|
void MarkTriggered();
|
|
|
|
private:
|
|
mutable FastPollingMutexSem mutex_;
|
|
TriggerConfig config_;
|
|
uint32 epoch_;
|
|
TrigState state_;
|
|
bool stopped_;
|
|
float64 prevValue_; ///< Last sample (edge detection)
|
|
bool prevValid_; ///< First-sample guard in ARMED state
|
|
float64 lastTime_; ///< Timestamp of the newest watched sample
|
|
bool lastTimeValid_;///< true once a watched sample has been seen
|
|
float64 trigTime_; ///< Latched trigger time (Unix s)
|
|
float64 firedPreSec_; ///< Window pre-part latched at fire time
|
|
float64 firedPostSec_; ///< Window post-part latched at fire time
|
|
bool firedValid_; ///< true after a fire, until Disarm()
|
|
/**
|
|
* The edge to fire on as soon as the FSM rearms, in sample time, recorded
|
|
* while a capture is still being collected or handed out. Without it the
|
|
* trigger is deaf from its own trigger point until the capture has been
|
|
* harvested and the holdoff has run, and then waits for a fresh edge, which
|
|
* on a sparse pulse train rounds the capture spacing up to a whole pulse
|
|
* period. Remembering the edge instead makes the blind stretch exactly the
|
|
* guard interval it has to be, since the capture is built from the edge's
|
|
* own timestamp and the rings still hold everything around it.
|
|
*/
|
|
float64 pendingTime_;
|
|
bool pendingValid_;
|
|
|
|
/** @brief Freeze the pre/post split at fire time; caller holds the mutex. */
|
|
void LatchWindowLocked(float64 t);
|
|
};
|
|
|
|
inline TriggerConfig::TriggerConfig()
|
|
: edge(kEdgeRising),
|
|
threshold(0.0),
|
|
windowSec(1.0),
|
|
prePercent(20.0),
|
|
mode(kTrigNormal),
|
|
holdoffSec(0.2) {
|
|
}
|
|
|
|
} /* namespace StreamHub */
|
|
|
|
#endif /* STREAMHUB_TRIGGER_ENGINE_H_ */
|