Compare commits
2
Commits
fbae7d712c
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5562877c99 | ||
|
|
092fd3c775 |
+11
-4
@@ -117,9 +117,16 @@ Sent when the signal set changes or a client connects:
|
|||||||
```
|
```
|
||||||
[uint32 numSigs]
|
[uint32 numSigs]
|
||||||
numSigs × UDPSSignalDescriptor (136 bytes each, packed)
|
numSigs × UDPSSignalDescriptor (136 bytes each, packed)
|
||||||
[uint8 publishMode] 0=Strict/Decimate, 1=Accumulate
|
[uint8 publishMode] 0=Strict, 1=Accumulate, 2=Decimate
|
||||||
|
[uint64 hrtFrequency] producer's HRT ticks per second; 0 = unknown
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Everything after the descriptors is an optional trailer: a receiver accepts a
|
||||||
|
payload that stops early and ignores bytes it does not know. `hrtFrequency` is
|
||||||
|
what lets a receiver on another host turn the raw counter in DATA into seconds
|
||||||
|
— without it the only option is the receiver's own timer, which agrees with the
|
||||||
|
producer only when the two share a machine.
|
||||||
|
|
||||||
### DATA Payload (Strict / Decimate modes)
|
### DATA Payload (Strict / Decimate modes)
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -130,9 +137,9 @@ per-signal data in CONFIG order (quantised or raw, no inter-signal padding)
|
|||||||
### DATA Payload (Accumulate mode)
|
### DATA Payload (Accumulate mode)
|
||||||
|
|
||||||
```
|
```
|
||||||
[uint64 HRT timestamp]
|
[uint64 HRT timestamp of the first slot in the batch]
|
||||||
[uint32 numSamples]
|
[uint32 numSamples] RT cycles accumulated into this packet
|
||||||
for each signal: if scalar → numSamples elements; else → NumElements once
|
for each signal, in CONFIG order: numSamples × NumElements values
|
||||||
```
|
```
|
||||||
|
|
||||||
### Quantization / Dequantization
|
### Quantization / Dequantization
|
||||||
|
|||||||
@@ -23,15 +23,22 @@
|
|||||||
* [uint32 numSigs]
|
* [uint32 numSigs]
|
||||||
* numSigs × UDPSSignalDescriptor (136 bytes each, packed)
|
* numSigs × UDPSSignalDescriptor (136 bytes each, packed)
|
||||||
* [uint8 publishMode] (PublishModeStrict / Accumulate / Decimate)
|
* [uint8 publishMode] (PublishModeStrict / Accumulate / Decimate)
|
||||||
|
* [uint64 hrtFrequency] ticks per second of the producer's HRT
|
||||||
|
*
|
||||||
|
* Everything after the descriptors is an optional trailer: a receiver must
|
||||||
|
* accept a payload that stops early and must ignore bytes it does not know.
|
||||||
|
* publishMode defaults to Strict when absent, hrtFrequency to
|
||||||
|
* UDPS_HRT_FREQUENCY_UNKNOWN.
|
||||||
*
|
*
|
||||||
* DATA payload (Strict / Decimate):
|
* DATA payload (Strict / Decimate):
|
||||||
* [uint64 HRT timestamp]
|
* [uint64 HRT timestamp]
|
||||||
* per-signal data in CONFIG order (quantised or raw, no padding)
|
* per-signal data in CONFIG order (quantised or raw, no padding)
|
||||||
*
|
*
|
||||||
* DATA payload (Accumulate):
|
* DATA payload (Accumulate):
|
||||||
* [uint64 HRT timestamp]
|
* [uint64 HRT timestamp of the first slot in the batch]
|
||||||
* [uint32 numSamples]
|
* [uint32 numSamples] RT cycles accumulated into this packet
|
||||||
* for each signal: if scalar → numSamples elements; else → NumElements once
|
* for each signal, in CONFIG order: numSamples × NumElements values
|
||||||
|
* (signal-major, one full snapshot per accumulated cycle)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef UDPS_PROTOCOL_H_
|
#ifndef UDPS_PROTOCOL_H_
|
||||||
@@ -123,6 +130,20 @@ static const uint8 UDPS_PUBLISH_STRICT = 0u; ///< One packet per Synchronise
|
|||||||
static const uint8 UDPS_PUBLISH_ACCUMULATE = 1u; ///< Variable batch; flush on size or time
|
static const uint8 UDPS_PUBLISH_ACCUMULATE = 1u; ///< Variable batch; flush on size or time
|
||||||
static const uint8 UDPS_PUBLISH_DECIMATE = 2u; ///< One packet per Ratio calls
|
static const uint8 UDPS_PUBLISH_DECIMATE = 2u; ///< One packet per Ratio calls
|
||||||
|
|
||||||
|
/*---------------------------------------------------------------------------*/
|
||||||
|
/* HRT frequency (CONFIG trailing uint64) */
|
||||||
|
/*---------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sentinel for a CONFIG that carries no HRT frequency, either because the
|
||||||
|
* trailer is absent (producer older than this field) or because the producer
|
||||||
|
* could not determine it. DATA timestamps are raw ticks of the producer's
|
||||||
|
* high-resolution timer, so without this a receiver on another host has no
|
||||||
|
* way to turn them into seconds and can only fall back to its own timer's
|
||||||
|
* frequency — which is right only while the two happen to agree.
|
||||||
|
*/
|
||||||
|
static const uint64 UDPS_HRT_FREQUENCY_UNKNOWN = 0u;
|
||||||
|
|
||||||
/*---------------------------------------------------------------------------*/
|
/*---------------------------------------------------------------------------*/
|
||||||
/* CONFIG payload — per-signal descriptor */
|
/* CONFIG payload — per-signal descriptor */
|
||||||
/*---------------------------------------------------------------------------*/
|
/*---------------------------------------------------------------------------*/
|
||||||
|
|||||||
+23
-1
@@ -84,8 +84,28 @@ Offset Size Type Field
|
|||||||
0xFFFFFFFF = PacketTime (no reference)
|
0xFFFFFFFF = PacketTime (no reference)
|
||||||
104 32 char[32] unit null-terminated physical unit string
|
104 32 char[32] unit null-terminated physical unit string
|
||||||
── (total per signal: 136 bytes) ────────────────────────────
|
── (total per signal: 136 bytes) ────────────────────────────
|
||||||
|
── trailer, immediately after the last descriptor ───────────
|
||||||
|
0 1 uint8 publishMode 0 = Strict, 1 = Accumulate, 2 = Decimate
|
||||||
|
1 8 uint64 hrtFrequency producer's HRT ticks per second;
|
||||||
|
0 = unknown
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### CONFIG trailer
|
||||||
|
|
||||||
|
Everything after the descriptors is a trailer that grew field by field, so a
|
||||||
|
receiver must accept a payload that stops early and must ignore bytes it does
|
||||||
|
not recognise. An absent `publishMode` means Strict; an absent or zero
|
||||||
|
`hrtFrequency` means the producer did not publish its tick rate.
|
||||||
|
|
||||||
|
`hrtFrequency` is what makes DATA timestamps interpretable off-box. DATA
|
||||||
|
carries the raw value of the producer's high-resolution counter, and on x86
|
||||||
|
that counter runs at the TSC frequency — a different number on every model. A
|
||||||
|
receiver that divides by its own timer's frequency instead is right only while
|
||||||
|
producer and consumer sit on the same host; anywhere else every batch is laid
|
||||||
|
out over the wrong span of time. Fall back to the local frequency only when the
|
||||||
|
field is missing, and reject implausible values (nothing below 1 kHz is a
|
||||||
|
high-resolution timer).
|
||||||
|
|
||||||
### Type Codes
|
### Type Codes
|
||||||
|
|
||||||
| Code | C type | Bytes/element |
|
| Code | C type | Bytes/element |
|
||||||
@@ -129,7 +149,9 @@ After reassembly, the DATA payload layout is:
|
|||||||
```
|
```
|
||||||
Offset Size Type Field
|
Offset Size Type Field
|
||||||
────── ──── ────── ────────────────────────────────────────────────────
|
────── ──── ────── ────────────────────────────────────────────────────
|
||||||
0 8 uint64 hrtTimestamp hardware reference timer count at Synchronise()
|
0 8 uint64 hrtTimestamp producer's high-resolution counter at
|
||||||
|
Synchronise(); divide by the CONFIG
|
||||||
|
hrtFrequency to get seconds
|
||||||
── for each signal (in config order) ────────────────────────────────────
|
── for each signal (in config order) ────────────────────────────────────
|
||||||
varies N×sz — signal data N = numRows×numCols, sz = element size
|
varies N×sz — signal data N = numRows×numCols, sz = element size
|
||||||
(wire size if quantized, raw size otherwise)
|
(wire size if quantized, raw size otherwise)
|
||||||
|
|||||||
@@ -107,12 +107,31 @@ Hub-side, web-client semantics (`setTrigger` fields in
|
|||||||
|
|
||||||
```
|
```
|
||||||
IDLE --arm--> ARMED --edge crossing--> COLLECTING --every source past trigTime+postSec+0.15s--> TRIGGERED
|
IDLE --arm--> ARMED --edge crossing--> COLLECTING --every source past trigTime+postSec+0.15s--> TRIGGERED
|
||||||
TRIGGERED --rearm (single) / auto ~200ms (normal, unless stopped)--> ARMED
|
TRIGGERED --rearm (single) / auto after holdoffSec (normal, unless stopped)--> ARMED
|
||||||
|
└─ or straight to COLLECTING on a held edge
|
||||||
any --disarm--> IDLE
|
any --disarm--> IDLE
|
||||||
```
|
```
|
||||||
|
|
||||||
`UDPSourceSession` calls `TriggerEngine::CheckSample` for every decoded sample
|
`UDPSourceSession` calls `TriggerEngine::CheckSample` for every decoded sample
|
||||||
of the configured signal (signal index cached per config epoch). Each source is
|
of the configured signal (signal index cached per config epoch).
|
||||||
|
|
||||||
|
The comparator keeps running through COLLECTING and TRIGGERED. It cannot fire
|
||||||
|
there — the capture in flight owns that stretch — but it remembers the first
|
||||||
|
edge at or past `trigTime + max(postSec, holdoffSec)`, and `Rearm()` fires on
|
||||||
|
that remembered edge instead of waiting for a fresh one. Without this the engine
|
||||||
|
is deaf from its own trigger point until the capture has been harvested and the
|
||||||
|
holdoff has run, which on a sparse pulse train rounds the capture spacing up to
|
||||||
|
a whole pulse period: at a 1 s window a 1 Hz train was caught at 0.5 Hz, and a
|
||||||
|
wider window lost whole multiples. The capture is built from the edge's own
|
||||||
|
timestamp out of rings that still hold everything around it, so honouring it
|
||||||
|
costs nothing.
|
||||||
|
|
||||||
|
`Arm()` and `Rearm()` differ only in this: `Arm()` is the operator's own arm and
|
||||||
|
discards the held edge (they asked for the next event), while `Rearm()` is the
|
||||||
|
automatic end-of-capture arm and consumes it. `Rearm()` also keeps the tracked
|
||||||
|
level, so the first sample after it is compared against its real predecessor
|
||||||
|
rather than being spent seeding one. `SetConfig()` and `Disarm()` drop the held
|
||||||
|
edge as well — it was never judged against the new window. Each source is
|
||||||
read `[trigTime−preSec, trigTime+postSec]`, LTTB-capped to 20 000 pts/signal and
|
read `[trigTime−preSec, trigTime+postSec]`, LTTB-capped to 20 000 pts/signal and
|
||||||
appended to a binary **version 2** capture frame; every FSM transition
|
appended to a binary **version 2** capture frame; every FSM transition
|
||||||
broadcasts a `triggerState` event.
|
broadcasts a `triggerState` event.
|
||||||
|
|||||||
@@ -1736,7 +1736,11 @@ void StreamHub::TriggerTick(float64 wallNowS) {
|
|||||||
(wallNowS >= rearmAtWallS_)) {
|
(wallNowS >= rearmAtWallS_)) {
|
||||||
rearmPending_ = false;
|
rearmPending_ = false;
|
||||||
if (!trigger_.GetStopped()) {
|
if (!trigger_.GetStopped()) {
|
||||||
trigger_.Arm();
|
/* Rearm, not Arm: an edge that arrived while this capture was being
|
||||||
|
* collected is fired on at once instead of being thrown away, which
|
||||||
|
* is what kept sparse pulse trains from being caught at their own
|
||||||
|
* rate. */
|
||||||
|
trigger_.Rearm();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,17 @@ TriggerEngine::TriggerEngine()
|
|||||||
trigTime_(0.0),
|
trigTime_(0.0),
|
||||||
firedPreSec_(0.0),
|
firedPreSec_(0.0),
|
||||||
firedPostSec_(0.0),
|
firedPostSec_(0.0),
|
||||||
firedValid_(false) {
|
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 TriggerEngine::SetConfig(const TriggerConfig &cfg) {
|
||||||
@@ -40,6 +50,9 @@ void TriggerEngine::SetConfig(const TriggerConfig &cfg) {
|
|||||||
epoch_++;
|
epoch_++;
|
||||||
prevValid_ = false;
|
prevValid_ = false;
|
||||||
prevValue_ = 0.0;
|
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();
|
mutex_.FastUnLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +75,26 @@ void TriggerEngine::Arm() {
|
|||||||
state_ = kTrigArmed;
|
state_ = kTrigArmed;
|
||||||
prevValid_ = false;
|
prevValid_ = false;
|
||||||
prevValue_ = 0.0;
|
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();
|
mutex_.FastUnLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +105,7 @@ void TriggerEngine::Disarm() {
|
|||||||
prevValid_ = false;
|
prevValid_ = false;
|
||||||
prevValue_ = 0.0;
|
prevValue_ = 0.0;
|
||||||
firedValid_ = false;
|
firedValid_ = false;
|
||||||
|
pendingValid_ = false;
|
||||||
mutex_.FastUnLock();
|
mutex_.FastUnLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +130,10 @@ void TriggerEngine::CheckSample(float64 t, float64 v) {
|
|||||||
lastTime_ = t;
|
lastTime_ = t;
|
||||||
lastTimeValid_ = true;
|
lastTimeValid_ = true;
|
||||||
|
|
||||||
if (state_ != kTrigArmed) {
|
/* 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();
|
mutex_.FastUnLock();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -121,17 +158,36 @@ void TriggerEngine::CheckSample(float64 t, float64 v) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (fired) {
|
if (fired) {
|
||||||
state_ = kTrigCollecting;
|
if (!inFlight) {
|
||||||
trigTime_ = t;
|
|
||||||
/* Latch the window at fire time so later config edits do not
|
/* Latch the window at fire time so later config edits do not
|
||||||
* affect this capture (web client snap._preS/_postS). */
|
* affect this capture (web client snap._preS/_postS). */
|
||||||
firedPreSec_ = config_.windowSec * config_.prePercent / 100.0;
|
LatchWindowLocked(t);
|
||||||
firedPostSec_ = config_.windowSec - firedPreSec_;
|
|
||||||
firedValid_ = true;
|
|
||||||
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
|
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
|
||||||
"TriggerEngine: fired at t=%.6f (pre=%.4fs post=%.4fs)",
|
"TriggerEngine: fired at t=%.6f (pre=%.4fs post=%.4fs)",
|
||||||
t, firedPreSec_, firedPostSec_);
|
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();
|
mutex_.FastUnLock();
|
||||||
}
|
}
|
||||||
@@ -141,11 +197,7 @@ bool TriggerEngine::Force() {
|
|||||||
|
|
||||||
bool ok = lastTimeValid_ && (state_ != kTrigCollecting);
|
bool ok = lastTimeValid_ && (state_ != kTrigCollecting);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
state_ = kTrigCollecting;
|
LatchWindowLocked(lastTime_);
|
||||||
trigTime_ = lastTime_;
|
|
||||||
firedPreSec_ = config_.windowSec * config_.prePercent / 100.0;
|
|
||||||
firedPostSec_ = config_.windowSec - firedPreSec_;
|
|
||||||
firedValid_ = true;
|
|
||||||
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
|
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
|
||||||
"TriggerEngine: forced at t=%.6f (pre=%.4fs post=%.4fs)",
|
"TriggerEngine: forced at t=%.6f (pre=%.4fs post=%.4fs)",
|
||||||
trigTime_, firedPreSec_, firedPostSec_);
|
trigTime_, firedPreSec_, firedPostSec_);
|
||||||
|
|||||||
@@ -88,9 +88,24 @@ public:
|
|||||||
*/
|
*/
|
||||||
uint32 GetConfigEpoch() const;
|
uint32 GetConfigEpoch() const;
|
||||||
|
|
||||||
/** @brief Arm: any state → ARMED (resets edge detection). */
|
/**
|
||||||
|
* @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();
|
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. */
|
/** @brief Disarm: any state → IDLE; clears the stopped flag. */
|
||||||
void Disarm();
|
void Disarm();
|
||||||
|
|
||||||
@@ -102,8 +117,10 @@ public:
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Edge-detect one decoded sample of the configured signal.
|
* @brief Edge-detect one decoded sample of the configured signal.
|
||||||
* Receive-thread context. Only acts in ARMED state; on a matching edge
|
* Receive-thread context. In ARMED state a matching edge latches trigTime
|
||||||
* latches trigTime and the pre/post window and moves to COLLECTING.
|
* 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);
|
void CheckSample(float64 t, float64 v);
|
||||||
|
|
||||||
@@ -143,6 +160,21 @@ private:
|
|||||||
float64 firedPreSec_; ///< Window pre-part latched at fire time
|
float64 firedPreSec_; ///< Window pre-part latched at fire time
|
||||||
float64 firedPostSec_; ///< Window post-part latched at fire time
|
float64 firedPostSec_; ///< Window post-part latched at fire time
|
||||||
bool firedValid_; ///< true after a fire, until Disarm()
|
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()
|
inline TriggerConfig::TriggerConfig()
|
||||||
|
|||||||
@@ -228,6 +228,9 @@ void UDPSourceSession::ParseConfigPayload(const uint8 *payload, uint32 size) {
|
|||||||
sigDescs_[i].unit[sizeof(sigDescs_[i].unit) - 1u] = '\0';
|
sigDescs_[i].unit[sizeof(sigDescs_[i].unit) - 1u] = '\0';
|
||||||
}
|
}
|
||||||
publishMode_ = payload[4u + numSigs * UDPS_SIGNAL_DESC_SIZE];
|
publishMode_ = payload[4u + numSigs * UDPS_SIGNAL_DESC_SIZE];
|
||||||
|
hrtFreq_ = UDPSConfigHrtFrequency(
|
||||||
|
payload, size, numSigs,
|
||||||
|
static_cast<float64>(MARTe::HighResolutionTimer::Frequency()));
|
||||||
numSignals_ = numSigs;
|
numSignals_ = numSigs;
|
||||||
configured_ = true;
|
configured_ = true;
|
||||||
|
|
||||||
@@ -276,8 +279,9 @@ void UDPSourceSession::ParseConfigPayload(const uint8 *payload, uint32 size) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
|
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
|
||||||
"UDPSourceSession[%s]: CONFIG received — %u signals.",
|
"UDPSourceSession[%s]: CONFIG received — %u signals, "
|
||||||
id_.Buffer(), numSigs);
|
"producer HRT %.0f Hz.",
|
||||||
|
id_.Buffer(), numSigs, hrtFreq_);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UDPSourceSession::AllocateRingBuffers() {
|
void UDPSourceSession::AllocateRingBuffers() {
|
||||||
@@ -572,9 +576,9 @@ void UDPSourceSession::ParseDataPayload(const uint8 *payload, uint32 size,
|
|||||||
* immune to this because it is sampled at acquisition.
|
* immune to this because it is sampled at acquisition.
|
||||||
*
|
*
|
||||||
* hrtTimestamp is the HRT counter of sample 0; hrtFreq_ (the
|
* hrtTimestamp is the HRT counter of sample 0; hrtFreq_ (the
|
||||||
* local HRT frequency, identical to the sender on the same
|
* producer's tick rate, taken from the CONFIG trailer) converts
|
||||||
* host) converts it to seconds, then a one-time calibration
|
* it to seconds, then a one-time calibration maps the sender
|
||||||
* maps the sender clock onto wall-clock. */
|
* clock onto wall-clock. */
|
||||||
const float64 hrt0Sec = static_cast<float64>(hrtTimestamp) /
|
const float64 hrt0Sec = static_cast<float64>(hrtTimestamp) /
|
||||||
hrtFreq_;
|
hrtFreq_;
|
||||||
if ((!timeSigCalibValid_[s]) ||
|
if ((!timeSigCalibValid_[s]) ||
|
||||||
|
|||||||
@@ -102,6 +102,41 @@ inline float64 UDPSEstimateAccumDt(const float64 gap, const uint32 prevN,
|
|||||||
return dtEMA;
|
return dtEMA;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Pick the tick rate to divide a producer's DATA timestamps by.
|
||||||
|
*
|
||||||
|
* DATA packets carry the raw value of the producer's high-resolution counter,
|
||||||
|
* which is meaningless without the rate it runs at. The rate is published in
|
||||||
|
* the CONFIG trailer, after the descriptors and the publish-mode byte. When it
|
||||||
|
* is missing — an older producer, or one that could not determine it — the
|
||||||
|
* only remaining option is this host's own timer, which is right only while
|
||||||
|
* the two machines agree; on x86 that is the TSC frequency, so it is a
|
||||||
|
* different number on every model.
|
||||||
|
*
|
||||||
|
* @param payload Reassembled CONFIG payload.
|
||||||
|
* @param size Bytes in @p payload.
|
||||||
|
* @param numSigs Signal count already read from the payload, capped to what
|
||||||
|
* the receiver will store.
|
||||||
|
* @param localFreq This host's HRT frequency, used as the fallback.
|
||||||
|
* @return Ticks per second to convert DATA timestamps with; never 0.
|
||||||
|
*/
|
||||||
|
inline float64 UDPSConfigHrtFrequency(const uint8 *payload, const uint32 size,
|
||||||
|
const uint32 numSigs,
|
||||||
|
const float64 localFreq) {
|
||||||
|
const uint32 offset = 4u + (numSigs * MARTe::UDPS_SIGNAL_DESC_SIZE) + 1u;
|
||||||
|
if ((payload != NULL_PTR(const uint8 *)) && (size >= (offset + 8u))) {
|
||||||
|
uint64 wireFreq = 0u;
|
||||||
|
memcpy(&wireFreq, payload + offset, 8u);
|
||||||
|
/* Anything below 1 kHz is not a high-resolution timer; the field is
|
||||||
|
* either absent, unset, or the payload was mis-parsed, and adopting it
|
||||||
|
* would stretch every timestamp far enough to make the trace useless. */
|
||||||
|
if (wireFreq >= 1000u) {
|
||||||
|
return static_cast<float64>(wireFreq);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return localFreq;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief One connected UDPStreamer source.
|
* @brief One connected UDPStreamer source.
|
||||||
*
|
*
|
||||||
@@ -463,10 +498,11 @@ private:
|
|||||||
float64 lastPktWallS_[UDPSS_MAX_SIGNALS];
|
float64 lastPktWallS_[UDPSS_MAX_SIGNALS];
|
||||||
bool lastPktWallValid_[UDPSS_MAX_SIGNALS];
|
bool lastPktWallValid_[UDPSS_MAX_SIGNALS];
|
||||||
|
|
||||||
/* Accumulated-scalar timing: HRT counter frequency (local == sender on the
|
/* Accumulated-scalar timing: the producer's HRT counter frequency and the
|
||||||
* same host) and the previous packet's sample count, used to reconstruct
|
* previous packet's sample count, used to reconstruct per-sample timestamps
|
||||||
* per-sample timestamps from the embedded sender HRT instead of the (UDP
|
* from the embedded sender HRT instead of the (UDP burst-sensitive) packet
|
||||||
* burst-sensitive) packet arrival time. */
|
* arrival time. Seeded from this host's timer and replaced by the rate the
|
||||||
|
* producer publishes in CONFIG; see UDPSConfigHrtFrequency. */
|
||||||
float64 hrtFreq_;
|
float64 hrtFreq_;
|
||||||
uint32 accScalarPrevN_[UDPSS_MAX_SIGNALS];
|
uint32 accScalarPrevN_[UDPSS_MAX_SIGNALS];
|
||||||
|
|
||||||
|
|||||||
@@ -810,7 +810,9 @@ bool UDPStreamer::PrepareNextState(const char8 *const currentStateName,
|
|||||||
* receives it immediately. The config is static for the lifetime of this
|
* receives it immediately. The config is static for the lifetime of this
|
||||||
* state. */
|
* state. */
|
||||||
if (ok) {
|
if (ok) {
|
||||||
uint32 configBufSize = 4u + (numSigs * UDPS_SIGNAL_DESC_SIZE) + 32u + 1u;
|
/* numSigs + descriptors + publishMode + hrtFrequency (+ slack). */
|
||||||
|
uint32 configBufSize =
|
||||||
|
4u + (numSigs * UDPS_SIGNAL_DESC_SIZE) + 1u + 8u + 32u;
|
||||||
HeapI *heap = GlobalObjectsDatabase::Instance()->GetStandardHeap();
|
HeapI *heap = GlobalObjectsDatabase::Instance()->GetStandardHeap();
|
||||||
uint8 *cfgBuf = reinterpret_cast<uint8 *>(heap->Malloc(configBufSize));
|
uint8 *cfgBuf = reinterpret_cast<uint8 *>(heap->Malloc(configBufSize));
|
||||||
if (cfgBuf != NULL_PTR(uint8 *)) {
|
if (cfgBuf != NULL_PTR(uint8 *)) {
|
||||||
@@ -1244,6 +1246,16 @@ bool UDPStreamer::BuildConfigPayload(uint8 *buf, uint32 bufSize,
|
|||||||
buf[payloadSize] = static_cast<uint8>(publishMode);
|
buf[payloadSize] = static_cast<uint8>(publishMode);
|
||||||
payloadSize += 1u;
|
payloadSize += 1u;
|
||||||
|
|
||||||
|
/* 8 bytes: this host's HRT tick rate. DATA packets carry raw counter
|
||||||
|
* values, so a receiver on another machine cannot turn them into seconds
|
||||||
|
* without it. */
|
||||||
|
if ((payloadSize + 8u) > bufSize) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
uint64 hrtFrequency = HighResolutionTimer::Frequency();
|
||||||
|
(void)MemoryOperationsHelper::Copy(buf + payloadSize, &hrtFrequency, 8u);
|
||||||
|
payloadSize += 8u;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -674,6 +674,14 @@ bool DebugService::SendUDPSConfig() {
|
|||||||
payloadOffset++;
|
payloadOffset++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write this host's HRT tick rate: DATA packets carry raw counter values,
|
||||||
|
// which a receiver on another machine cannot convert to seconds without it.
|
||||||
|
if ((payloadOffset + 8u) <= CFG_BUF_SIZE) {
|
||||||
|
uint64 hrtFrequency = HighResolutionTimer::Frequency();
|
||||||
|
memcpy(payload + payloadOffset, &hrtFrequency, 8u);
|
||||||
|
payloadOffset += 8u;
|
||||||
|
}
|
||||||
|
|
||||||
udpsNumSlots = newNumSlots;
|
udpsNumSlots = newNumSlots;
|
||||||
|
|
||||||
mutex.FastUnLock();
|
mutex.FastUnLock();
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
/**
|
||||||
|
* @file ConfigHrtFreqGTest.cpp
|
||||||
|
* @brief Tests UDPSConfigHrtFrequency, which picks the tick rate used to turn
|
||||||
|
* a producer's DATA timestamps into seconds.
|
||||||
|
*
|
||||||
|
* DATA packets carry the raw value of the producer's high-resolution counter.
|
||||||
|
* Until the rate was published in CONFIG the hub divided by its own timer's
|
||||||
|
* frequency, which is only right while the producer runs on the same host —
|
||||||
|
* on x86 that number is the TSC frequency and differs from model to model, so
|
||||||
|
* off-box every accumulated batch was laid out over the wrong span of time.
|
||||||
|
*
|
||||||
|
* The field is a trailer, so these tests pin both directions: a payload that
|
||||||
|
* carries it must be believed, and one that stops early — an older producer —
|
||||||
|
* must still decode against the local fallback rather than against zero.
|
||||||
|
*
|
||||||
|
* @copyright Copyright 2015 F4E | European Joint Undertaking for ITER and
|
||||||
|
* the Development of Fusion Energy ('Fusion for Energy').
|
||||||
|
* Licensed under the EUPL, Version 1.1 or - as soon they will be approved
|
||||||
|
* by the European Commission - subsequent versions of the EUPL (the "Licence")
|
||||||
|
* You may not use this work except in compliance with the Licence.
|
||||||
|
* You may obtain a copy of the Licence at: http://ec.europa.eu/idabc/eupl
|
||||||
|
*
|
||||||
|
* @warning Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the Licence is distributed on an "AS IS"
|
||||||
|
* basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the Licence permissions and limitations under the Licence.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "UDPSourceSession.h"
|
||||||
|
|
||||||
|
using MARTe::uint8;
|
||||||
|
using MARTe::uint32;
|
||||||
|
using MARTe::uint64;
|
||||||
|
using MARTe::float64;
|
||||||
|
using MARTe::UDPS_SIGNAL_DESC_SIZE;
|
||||||
|
using StreamHub::UDPSConfigHrtFrequency;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
/** This hub's own timer rate, i.e. what the code must fall back to. */
|
||||||
|
const float64 kLocalFreq = 1.0e9;
|
||||||
|
|
||||||
|
/** A plausible producer rate that is deliberately not kLocalFreq. */
|
||||||
|
const uint64 kWireFreq = 2400000000ULL;
|
||||||
|
|
||||||
|
const uint32 kNumSigs = 2u;
|
||||||
|
|
||||||
|
/** Offset of the CONFIG trailer that follows the publish-mode byte. */
|
||||||
|
uint32 TrailerOffset(uint32 numSigs) {
|
||||||
|
return 4u + (numSigs * UDPS_SIGNAL_DESC_SIZE) + 1u;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a CONFIG payload for kNumSigs signals.
|
||||||
|
* @param withFreq Append the 8-byte HRT frequency trailer.
|
||||||
|
* @param freq Value to append when @p withFreq.
|
||||||
|
* @param[out] size Bytes written.
|
||||||
|
*/
|
||||||
|
const uint8 *BuildConfig(bool withFreq, uint64 freq, uint32 &size) {
|
||||||
|
static uint8 buf[4u + (kNumSigs * UDPS_SIGNAL_DESC_SIZE) + 1u + 8u];
|
||||||
|
(void) memset(buf, 0, sizeof(buf));
|
||||||
|
(void) memcpy(buf, &kNumSigs, 4u);
|
||||||
|
size = TrailerOffset(kNumSigs);
|
||||||
|
if (withFreq) {
|
||||||
|
(void) memcpy(buf + size, &freq, 8u);
|
||||||
|
size += 8u;
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
/* The whole point of the field: a producer that publishes its rate is believed
|
||||||
|
* even when the hub's own timer runs at a different one. */
|
||||||
|
TEST(ConfigHrtFreqGTest, AdoptsThePublishedRate) {
|
||||||
|
uint32 size = 0u;
|
||||||
|
const uint8 *cfg = BuildConfig(true, kWireFreq, size);
|
||||||
|
|
||||||
|
EXPECT_DOUBLE_EQ(static_cast<float64>(kWireFreq),
|
||||||
|
UDPSConfigHrtFrequency(cfg, size, kNumSigs, kLocalFreq));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A producer older than the field stops after the publish-mode byte. Reading
|
||||||
|
* past it would take whatever follows in the receive buffer as a frequency. */
|
||||||
|
TEST(ConfigHrtFreqGTest, FallsBackWhenTheTrailerIsAbsent) {
|
||||||
|
uint32 size = 0u;
|
||||||
|
const uint8 *cfg = BuildConfig(false, 0u, size);
|
||||||
|
|
||||||
|
EXPECT_DOUBLE_EQ(kLocalFreq,
|
||||||
|
UDPSConfigHrtFrequency(cfg, size, kNumSigs, kLocalFreq));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A trailer cut short mid-field is not a frequency either; taking the bytes
|
||||||
|
* that are there would assemble one out of whatever the rest of the buffer
|
||||||
|
* holds. */
|
||||||
|
TEST(ConfigHrtFreqGTest, FallsBackOnATruncatedTrailer) {
|
||||||
|
uint32 size = 0u;
|
||||||
|
const uint8 *cfg = BuildConfig(true, kWireFreq, size);
|
||||||
|
|
||||||
|
EXPECT_DOUBLE_EQ(kLocalFreq,
|
||||||
|
UDPSConfigHrtFrequency(cfg, size - 1u, kNumSigs,
|
||||||
|
kLocalFreq));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Zero is the protocol's "I do not know my own rate". Dividing by it yields
|
||||||
|
* infinities that propagate into every timestamp. */
|
||||||
|
TEST(ConfigHrtFreqGTest, FallsBackOnTheUnknownSentinel) {
|
||||||
|
uint32 size = 0u;
|
||||||
|
const uint8 *cfg = BuildConfig(true, MARTe::UDPS_HRT_FREQUENCY_UNKNOWN,
|
||||||
|
size);
|
||||||
|
|
||||||
|
EXPECT_DOUBLE_EQ(kLocalFreq,
|
||||||
|
UDPSConfigHrtFrequency(cfg, size, kNumSigs, kLocalFreq));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No high-resolution timer ticks slower than 1 kHz, so a value that low means
|
||||||
|
* the payload was misread. Adopting it would stretch a millisecond batch
|
||||||
|
* across whole seconds. */
|
||||||
|
TEST(ConfigHrtFreqGTest, FallsBackOnAnImplausiblyLowRate) {
|
||||||
|
uint32 size = 0u;
|
||||||
|
const uint8 *cfg = BuildConfig(true, 999u, size);
|
||||||
|
|
||||||
|
EXPECT_DOUBLE_EQ(kLocalFreq,
|
||||||
|
UDPSConfigHrtFrequency(cfg, size, kNumSigs, kLocalFreq));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The trailer sits after the descriptors, so its offset moves with the signal
|
||||||
|
* count; a fixed offset would read descriptor bytes on any other config. */
|
||||||
|
TEST(ConfigHrtFreqGTest, LocatesTheTrailerAfterTheDescriptors) {
|
||||||
|
const uint32 numSigs = 7u;
|
||||||
|
const uint32 size = TrailerOffset(numSigs) + 8u;
|
||||||
|
uint8 buf[4u + (7u * UDPS_SIGNAL_DESC_SIZE) + 1u + 8u];
|
||||||
|
/* Fill the descriptor area with a byte pattern that would decode as a
|
||||||
|
* plausible frequency if the offset were wrong. */
|
||||||
|
(void) memset(buf, 0x11, sizeof(buf));
|
||||||
|
(void) memcpy(buf, &numSigs, 4u);
|
||||||
|
(void) memcpy(buf + TrailerOffset(numSigs), &kWireFreq, 8u);
|
||||||
|
|
||||||
|
EXPECT_DOUBLE_EQ(static_cast<float64>(kWireFreq),
|
||||||
|
UDPSConfigHrtFrequency(buf, size, numSigs, kLocalFreq));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A null payload must not be dereferenced: CONFIG arrives from the network. */
|
||||||
|
TEST(ConfigHrtFreqGTest, FallsBackOnANullPayload) {
|
||||||
|
EXPECT_DOUBLE_EQ(kLocalFreq,
|
||||||
|
UDPSConfigHrtFrequency(NULL_PTR(const uint8 *), 1024u,
|
||||||
|
kNumSigs, kLocalFreq));
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
#
|
#
|
||||||
#############################################################
|
#############################################################
|
||||||
|
|
||||||
OBJSX = TriggerEngineSrc.x BinaryRecorderSrc.x SignalRingBufferGTest.x TriggerEngineGTest.x LTTBGTest.x BinaryRecorderGTest.x BoundsCheckTest.x WSServerBufferTest.x AccumDtGTest.x
|
OBJSX = TriggerEngineSrc.x BinaryRecorderSrc.x SignalRingBufferGTest.x TriggerEngineGTest.x LTTBGTest.x BinaryRecorderGTest.x BoundsCheckTest.x WSServerBufferTest.x AccumDtGTest.x ConfigHrtFreqGTest.x
|
||||||
|
|
||||||
PACKAGE=Applications
|
PACKAGE=Applications
|
||||||
ROOT_DIR=../../..
|
ROOT_DIR=../../..
|
||||||
|
|||||||
@@ -196,6 +196,183 @@ TEST(TriggerEngineGTest, TestRearmResetsEdgeDetection) {
|
|||||||
EXPECT_DOUBLE_EQ(4.0, tt);
|
EXPECT_DOUBLE_EQ(4.0, tt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* An edge that arrives while a capture is still being collected, or while it is
|
||||||
|
* being handed out, used to be dropped on the floor: CheckSample returned early
|
||||||
|
* in every state but ARMED, and the automatic rearm then waited for a FRESH
|
||||||
|
* edge. The engine is therefore deaf from its own trigger point until the
|
||||||
|
* capture has been harvested — a post-window — and then for the holdoff on top.
|
||||||
|
*
|
||||||
|
* 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. Remembering the
|
||||||
|
* edge costs nothing, because the capture is built from the edge's own
|
||||||
|
* timestamp out of a ring that still holds everything around it. */
|
||||||
|
TEST(TriggerEngineGTest, TestEdgeDuringCaptureFiresOnRearm) {
|
||||||
|
TriggerEngine eng;
|
||||||
|
eng.SetConfig(MakeConfig(kEdgeRising, 0.5, 1.0, 20.0)); /* post = 0.8 */
|
||||||
|
eng.Arm();
|
||||||
|
eng.CheckSample(1.0, 0.0);
|
||||||
|
eng.CheckSample(1.1, 1.0);
|
||||||
|
ASSERT_EQ(kTrigCollecting, eng.GetState());
|
||||||
|
|
||||||
|
/* A second pulse, clear of the capture in flight (1.1 + 0.8 = 1.9). */
|
||||||
|
eng.CheckSample(2.4, 0.0);
|
||||||
|
eng.CheckSample(2.5, 1.0);
|
||||||
|
|
||||||
|
eng.MarkTriggered();
|
||||||
|
eng.Rearm();
|
||||||
|
EXPECT_EQ(kTrigCollecting, eng.GetState());
|
||||||
|
|
||||||
|
float64 tt, pre, post;
|
||||||
|
ASSERT_TRUE(eng.GetFiredWindow(tt, pre, post));
|
||||||
|
EXPECT_DOUBLE_EQ(2.5, tt); /* the remembered edge, not the rearm instant */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The remembered edge must not be one the capture in flight already covers, nor
|
||||||
|
* one inside the holdoff — that guard exists to stop the ringing of a single
|
||||||
|
* event re-triggering on itself, and it is measured from the trigger point, so
|
||||||
|
* the two overlap rather than add. */
|
||||||
|
TEST(TriggerEngineGTest, TestEdgeInsideOwnCaptureIsNotRemembered) {
|
||||||
|
TriggerEngine eng;
|
||||||
|
eng.SetConfig(MakeConfig(kEdgeRising, 0.5, 1.0, 20.0)); /* post = 0.8 */
|
||||||
|
eng.Arm();
|
||||||
|
eng.CheckSample(1.0, 0.0);
|
||||||
|
eng.CheckSample(1.1, 1.0);
|
||||||
|
ASSERT_EQ(kTrigCollecting, eng.GetState());
|
||||||
|
|
||||||
|
/* Inside 1.1 + max(0.8, 0.2 holdoff) = 1.9: the capture owns this stretch. */
|
||||||
|
eng.CheckSample(1.4, 0.0);
|
||||||
|
eng.CheckSample(1.5, 1.0);
|
||||||
|
|
||||||
|
eng.MarkTriggered();
|
||||||
|
eng.Rearm();
|
||||||
|
EXPECT_EQ(kTrigArmed, eng.GetState());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A holdoff longer than the post-window is what decides the guard interval. */
|
||||||
|
TEST(TriggerEngineGTest, TestHoldoffOutlastingPostWindowGovernsRearm) {
|
||||||
|
TriggerEngine eng;
|
||||||
|
TriggerConfig cfg = MakeConfig(kEdgeRising, 0.5, 1.0, 80.0); /* post = 0.2 */
|
||||||
|
cfg.holdoffSec = 2.0;
|
||||||
|
eng.SetConfig(cfg);
|
||||||
|
eng.Arm();
|
||||||
|
eng.CheckSample(1.0, 0.0);
|
||||||
|
eng.CheckSample(1.1, 1.0);
|
||||||
|
ASSERT_EQ(kTrigCollecting, eng.GetState());
|
||||||
|
|
||||||
|
/* Past the post-window but inside the holdoff (1.1 + 2.0 = 3.1): ignored. */
|
||||||
|
eng.CheckSample(1.9, 0.0);
|
||||||
|
eng.CheckSample(2.0, 1.0);
|
||||||
|
/* Clear of it: remembered. */
|
||||||
|
eng.CheckSample(3.4, 0.0);
|
||||||
|
eng.CheckSample(3.5, 1.0);
|
||||||
|
|
||||||
|
eng.MarkTriggered();
|
||||||
|
eng.Rearm();
|
||||||
|
ASSERT_EQ(kTrigCollecting, eng.GetState());
|
||||||
|
float64 tt, pre, post;
|
||||||
|
ASSERT_TRUE(eng.GetFiredWindow(tt, pre, post));
|
||||||
|
EXPECT_DOUBLE_EQ(3.5, tt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Only the first qualifying edge is worth keeping; a later one would deliver
|
||||||
|
* the same capture a pulse further on and skip the one in between. */
|
||||||
|
TEST(TriggerEngineGTest, TestFirstQualifyingEdgeWins) {
|
||||||
|
TriggerEngine eng;
|
||||||
|
eng.SetConfig(MakeConfig(kEdgeRising, 0.5, 1.0, 20.0));
|
||||||
|
eng.Arm();
|
||||||
|
eng.CheckSample(1.0, 0.0);
|
||||||
|
eng.CheckSample(1.1, 1.0);
|
||||||
|
ASSERT_EQ(kTrigCollecting, eng.GetState());
|
||||||
|
|
||||||
|
eng.CheckSample(2.4, 0.0);
|
||||||
|
eng.CheckSample(2.5, 1.0); /* first past 1.9 */
|
||||||
|
eng.CheckSample(3.4, 0.0);
|
||||||
|
eng.CheckSample(3.5, 1.0); /* later, must not displace it */
|
||||||
|
|
||||||
|
eng.MarkTriggered();
|
||||||
|
eng.Rearm();
|
||||||
|
float64 tt, pre, post;
|
||||||
|
ASSERT_TRUE(eng.GetFiredWindow(tt, pre, post));
|
||||||
|
EXPECT_DOUBLE_EQ(2.5, tt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Arm() is the user's own arm: it asks for the next event, not for one that has
|
||||||
|
* already been and gone, so it drops anything remembered. */
|
||||||
|
TEST(TriggerEngineGTest, TestUserArmDiscardsRememberedEdge) {
|
||||||
|
TriggerEngine eng;
|
||||||
|
eng.SetConfig(MakeConfig(kEdgeRising, 0.5, 1.0, 20.0));
|
||||||
|
eng.Arm();
|
||||||
|
eng.CheckSample(1.0, 0.0);
|
||||||
|
eng.CheckSample(1.1, 1.0);
|
||||||
|
eng.CheckSample(2.4, 0.0);
|
||||||
|
eng.CheckSample(2.5, 1.0);
|
||||||
|
eng.MarkTriggered();
|
||||||
|
|
||||||
|
eng.Arm();
|
||||||
|
EXPECT_EQ(kTrigArmed, eng.GetState());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reconfiguring drops it too: the edge would be latched against a window it was
|
||||||
|
* never judged against. */
|
||||||
|
TEST(TriggerEngineGTest, TestSetConfigDiscardsRememberedEdge) {
|
||||||
|
TriggerEngine eng;
|
||||||
|
eng.SetConfig(MakeConfig(kEdgeRising, 0.5, 1.0, 20.0));
|
||||||
|
eng.Arm();
|
||||||
|
eng.CheckSample(1.0, 0.0);
|
||||||
|
eng.CheckSample(1.1, 1.0);
|
||||||
|
eng.CheckSample(2.4, 0.0);
|
||||||
|
eng.CheckSample(2.5, 1.0);
|
||||||
|
eng.MarkTriggered();
|
||||||
|
|
||||||
|
eng.SetConfig(MakeConfig(kEdgeRising, 0.5, 2.0, 20.0));
|
||||||
|
eng.Rearm();
|
||||||
|
EXPECT_EQ(kTrigArmed, eng.GetState());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The comparator keeps running through the dead time, so the first sample after
|
||||||
|
* an automatic rearm is measured against its real predecessor rather than being
|
||||||
|
* spent seeding one. A rearm landing mid-pulse would otherwise miss that
|
||||||
|
* pulse's edge as well as the ones it slept through. */
|
||||||
|
TEST(TriggerEngineGTest, TestRearmKeepsTrackingTheLevel) {
|
||||||
|
TriggerEngine eng;
|
||||||
|
eng.SetConfig(MakeConfig(kEdgeRising, 0.5, 1.0, 20.0));
|
||||||
|
eng.Arm();
|
||||||
|
eng.CheckSample(1.0, 0.0);
|
||||||
|
eng.CheckSample(1.1, 1.0);
|
||||||
|
ASSERT_EQ(kTrigCollecting, eng.GetState());
|
||||||
|
|
||||||
|
/* Falls back low during the capture: no rising edge, nothing remembered,
|
||||||
|
* but the level is now known to be 0. */
|
||||||
|
eng.CheckSample(2.0, 0.0);
|
||||||
|
eng.MarkTriggered();
|
||||||
|
eng.Rearm();
|
||||||
|
ASSERT_EQ(kTrigArmed, eng.GetState());
|
||||||
|
|
||||||
|
eng.CheckSample(2.1, 1.0); /* 0.0 → 1.0 across 0.5, on the very first sample */
|
||||||
|
EXPECT_EQ(kTrigCollecting, eng.GetState());
|
||||||
|
float64 tt, pre, post;
|
||||||
|
ASSERT_TRUE(eng.GetFiredWindow(tt, pre, post));
|
||||||
|
EXPECT_DOUBLE_EQ(2.1, tt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Idle is genuinely deaf: nothing is tracked and nothing is remembered, so a
|
||||||
|
* disarmed hub cannot fire the moment it is armed again. */
|
||||||
|
TEST(TriggerEngineGTest, TestDisarmDiscardsRememberedEdge) {
|
||||||
|
TriggerEngine eng;
|
||||||
|
eng.SetConfig(MakeConfig(kEdgeRising, 0.5, 1.0, 20.0));
|
||||||
|
eng.Arm();
|
||||||
|
eng.CheckSample(1.0, 0.0);
|
||||||
|
eng.CheckSample(1.1, 1.0);
|
||||||
|
eng.CheckSample(2.4, 0.0);
|
||||||
|
eng.CheckSample(2.5, 1.0);
|
||||||
|
eng.MarkTriggered();
|
||||||
|
|
||||||
|
eng.Disarm();
|
||||||
|
eng.Rearm();
|
||||||
|
EXPECT_EQ(kTrigArmed, eng.GetState());
|
||||||
|
}
|
||||||
|
|
||||||
TEST(TriggerEngineGTest, TestStoppedFlag) {
|
TEST(TriggerEngineGTest, TestStoppedFlag) {
|
||||||
TriggerEngine eng;
|
TriggerEngine eng;
|
||||||
EXPECT_FALSE(eng.GetStopped());
|
EXPECT_FALSE(eng.GetStopped());
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
#include "ConfigurationDatabase.h"
|
#include "ConfigurationDatabase.h"
|
||||||
#include "GAM.h"
|
#include "GAM.h"
|
||||||
#include "GAMScheduler.h"
|
#include "GAMScheduler.h"
|
||||||
|
#include "HighResolutionTimer.h"
|
||||||
#include "MemoryOperationsHelper.h"
|
#include "MemoryOperationsHelper.h"
|
||||||
#include "ObjectRegistryDatabase.h"
|
#include "ObjectRegistryDatabase.h"
|
||||||
#include "RealTimeApplication.h"
|
#include "RealTimeApplication.h"
|
||||||
@@ -907,6 +908,23 @@ bool UDPStreamerTest::TestExecute_ConnectDataDisconnect() {
|
|||||||
reinterpret_cast<const UDPSPacketHeader *>(recvBuf);
|
reinterpret_cast<const UDPSPacketHeader *>(recvBuf);
|
||||||
ok &= (hdr->magic == UDPS_MAGIC);
|
ok &= (hdr->magic == UDPS_MAGIC);
|
||||||
ok &= (hdr->type == UDPS_TYPE_CONFIG);
|
ok &= (hdr->type == UDPS_TYPE_CONFIG);
|
||||||
|
|
||||||
|
/* The CONFIG trailer must carry this host's HRT tick rate: DATA
|
||||||
|
* packets timestamp with the raw counter, so a receiver on another
|
||||||
|
* machine has nothing to convert it with otherwise. */
|
||||||
|
const uint8 *payload = recvBuf + UDPS_HEADER_SIZE;
|
||||||
|
uint32 numSigs = 0u;
|
||||||
|
if (ok && (hdr->payloadBytes >= 4u)) {
|
||||||
|
(void) memcpy(&numSigs, payload, 4u);
|
||||||
|
}
|
||||||
|
const uint32 freqOff =
|
||||||
|
4u + (numSigs * UDPS_SIGNAL_DESC_SIZE) + 1u;
|
||||||
|
ok &= (hdr->payloadBytes >= (freqOff + 8u));
|
||||||
|
if (ok) {
|
||||||
|
uint64 wireFreq = 0u;
|
||||||
|
(void) memcpy(&wireFreq, payload + freqOff, 8u);
|
||||||
|
ok &= (wireFreq == HighResolutionTimer::Frequency());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user