WebUI: per-signal vscale toolbar, active-signal highlighting, zoom fix
- Per-signal, per-plot vertical scale state (sigVScale keyed by plotId:signalKey) so the same signal in two plots has fully independent vscale config - Active signal redrawn on top of all series with 2× line width for clear visual identification; badge click toggles selection and opens/closes the embedded vscale toolbar (click same badge again to deselect) - Vscale configurator moved from floating popup to a slim toolbar strip anchored inside the plot card, with an × close button - Trigger dropdown shows one entry per array signal with [0…N-1] label; opening it shows an index-picker dialog to choose the element - Zoom resampling: when server returns no data for a zoomed range, fall back to the local circular buffer instead of returning empty arrays Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -127,6 +127,18 @@ UDPStreamer::UDPStreamer() :
|
||||
syncTimestamp = 0u;
|
||||
clientConnected = false;
|
||||
packetCounter = 0u;
|
||||
maxBatchCount = 0u;
|
||||
singleCycleWireBytes = 0u;
|
||||
fixedWireBytes = 0u;
|
||||
lastPublishTs = 0u;
|
||||
accumBuffer = NULL_PTR(uint8 *);
|
||||
accumTimestamps = NULL_PTR(uint64 *);
|
||||
accumFill = 0u;
|
||||
readyTimestamps = NULL_PTR(uint64 *);
|
||||
scratchTimestamps = NULL_PTR(uint64 *);
|
||||
readyFill = 0u;
|
||||
decimateRatio = 1u;
|
||||
decimateCounter = 0u;
|
||||
|
||||
if (!dataSem.Create()) {
|
||||
REPORT_ERROR(ErrorManagement::FatalError, "Could not create EventSem.");
|
||||
@@ -157,6 +169,23 @@ UDPStreamer::~UDPStreamer() {
|
||||
(void) clientSocket.Close();
|
||||
}
|
||||
|
||||
HeapI *heapAccum = GlobalObjectsDatabase::Instance()->GetStandardHeap();
|
||||
if (accumBuffer != NULL_PTR(uint8 *)) {
|
||||
heapAccum->Free(reinterpret_cast<void *&>(accumBuffer));
|
||||
}
|
||||
if (accumTimestamps != NULL_PTR(uint64 *)) {
|
||||
delete[] accumTimestamps;
|
||||
accumTimestamps = NULL_PTR(uint64 *);
|
||||
}
|
||||
if (readyTimestamps != NULL_PTR(uint64 *)) {
|
||||
delete[] readyTimestamps;
|
||||
readyTimestamps = NULL_PTR(uint64 *);
|
||||
}
|
||||
if (scratchTimestamps != NULL_PTR(uint64 *)) {
|
||||
delete[] scratchTimestamps;
|
||||
scratchTimestamps = NULL_PTR(uint64 *);
|
||||
}
|
||||
|
||||
/* Multicast-mode cleanup */
|
||||
if (tcpClient != NULL_PTR(BasicTCPSocket *)) {
|
||||
(void) tcpClient->Close();
|
||||
@@ -249,34 +278,59 @@ bool UDPStreamer::Initialise(StructuredDataI &data) {
|
||||
if ((publishStr.Size() == 0u) || (publishStr == "Strict")) {
|
||||
publishMode = UDPStreamerPublishStrict;
|
||||
}
|
||||
else if (publishStr == "Auto") {
|
||||
publishMode = UDPStreamerPublishAuto;
|
||||
else if (publishStr == "Accumulate") {
|
||||
publishMode = UDPStreamerPublishAccumulate;
|
||||
}
|
||||
else if (publishStr == "Decimate") {
|
||||
publishMode = UDPStreamerPublishDecimate;
|
||||
}
|
||||
else {
|
||||
REPORT_ERROR(ErrorManagement::ParametersError,
|
||||
"Unknown PublishingMode '%s'. Allowed: Strict|Auto.",
|
||||
"Unknown PublishingMode '%s'. Allowed: Strict|Accumulate|Decimate.",
|
||||
publishStr.Buffer());
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (ok && (publishMode == UDPStreamerPublishAuto)) {
|
||||
if (ok && (publishMode == UDPStreamerPublishAccumulate)) {
|
||||
/* MinRefreshRate controls the time-based flush: flush when
|
||||
* (now - lastPublishTs) >= flushPeriodTicks, or when adding one more
|
||||
* sample would overflow MaxPayloadSize. Whichever fires first. */
|
||||
if (!data.Read("MinRefreshRate", minRefreshRate) || (minRefreshRate <= 0.0)) {
|
||||
REPORT_ERROR(ErrorManagement::ParametersError,
|
||||
"MinRefreshRate > 0 is required when PublishingMode = Auto.");
|
||||
"MinRefreshRate > 0 is required when PublishingMode = Accumulate.");
|
||||
ok = false;
|
||||
}
|
||||
else {
|
||||
/* Pre-compute the HRT tick count for one flush interval */
|
||||
float64 hrtFreq = static_cast<float64>(HighResolutionTimer::Frequency());
|
||||
float64 hrtFreq = static_cast<float64>(HighResolutionTimer::Frequency());
|
||||
flushPeriodTicks = static_cast<uint64>(hrtFreq / minRefreshRate);
|
||||
REPORT_ERROR(ErrorManagement::Information,
|
||||
"Auto mode: MinRefreshRate=%.1f Hz, flushPeriodTicks=%llu.",
|
||||
"Accumulate mode: MinRefreshRate=%.1f Hz, flushPeriodTicks=%llu.",
|
||||
minRefreshRate,
|
||||
static_cast<unsigned long long>(flushPeriodTicks));
|
||||
}
|
||||
}
|
||||
|
||||
if (ok && (publishMode == UDPStreamerPublishDecimate)) {
|
||||
/* Ratio: send 1 packet every Ratio Synchronise() calls. */
|
||||
uint32 ratio = 0u;
|
||||
if (!data.Read("Ratio", ratio) || (ratio == 0u)) {
|
||||
REPORT_ERROR(ErrorManagement::ParametersError,
|
||||
"Ratio >= 1 is required when PublishingMode = Decimate.");
|
||||
ok = false;
|
||||
}
|
||||
else {
|
||||
decimateRatio = ratio;
|
||||
if (decimateRatio == 1u) {
|
||||
REPORT_ERROR(ErrorManagement::Warning,
|
||||
"Decimate mode with Ratio=1 is equivalent to Strict mode.");
|
||||
}
|
||||
REPORT_ERROR(ErrorManagement::Information,
|
||||
"Decimate mode: Ratio=%u (1 packet per %u RT cycle(s)).",
|
||||
decimateRatio, decimateRatio);
|
||||
}
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
StreamString mcastStr = "";
|
||||
(void) data.Read("MulticastGroup", mcastStr);
|
||||
@@ -537,6 +591,9 @@ bool UDPStreamer::SetConfiguredDatabase(StructuredDataI &data) {
|
||||
|
||||
/* --- Pass 4: validate time-signal dimensions and compute wire sizes --- */
|
||||
for (uint32 i = 0u; i < numSigs && ok; i++) {
|
||||
/* Initialise accumulated flag: false until pass 5 may flip it */
|
||||
signalInfos[i].accumulated = false;
|
||||
|
||||
/* Compute wire byte size per element */
|
||||
uint32 elemWireBytes = 0u;
|
||||
switch (signalInfos[i].quantType) {
|
||||
@@ -586,6 +643,99 @@ bool UDPStreamer::SetConfiguredDatabase(StructuredDataI &data) {
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Pass 5: Accumulate mode setup ---
|
||||
*
|
||||
* Scalars (numElements == 1) are tagged accumulated = true and auto-assigned
|
||||
* a FullArray time reference if a primary time signal exists. numCols / numRows
|
||||
* are left at 1 — the actual per-packet element count is determined at runtime
|
||||
* and transmitted as a 4-byte numSamples field in the DATA payload header.
|
||||
*
|
||||
* Compute singleCycleWireBytes (accumulated signals) and fixedWireBytes
|
||||
* (non-accumulated arrays that travel once per packet from the most-recent slot).
|
||||
* Override totalWireBytes to the maximum possible DATA payload for wireBuffer
|
||||
* allocation: 12 + maxBatchCount × singleCycleWireBytes + fixedWireBytes.
|
||||
*/
|
||||
if (ok && (publishMode == UDPStreamerPublishAccumulate)) {
|
||||
|
||||
/* Find primary time signal: prefer Unit="us"/"ns", fall back to first integer scalar */
|
||||
uint32 primaryTsIdx = UDPS_NO_TIME_SIGNAL;
|
||||
for (uint32 i = 0u; i < numSigs && (primaryTsIdx == UDPS_NO_TIME_SIGNAL); i++) {
|
||||
if (signalInfos[i].numElements == 1u) {
|
||||
if ((signalInfos[i].unit == "us") || (signalInfos[i].unit == "ns")) {
|
||||
primaryTsIdx = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (primaryTsIdx == UDPS_NO_TIME_SIGNAL) {
|
||||
for (uint32 i = 0u; i < numSigs && (primaryTsIdx == UDPS_NO_TIME_SIGNAL); i++) {
|
||||
if (signalInfos[i].numElements == 1u) {
|
||||
TypeDescriptor td = signalInfos[i].type;
|
||||
if ((td == UnsignedInteger32Bit) || (td == UnsignedInteger64Bit) ||
|
||||
(td == SignedInteger32Bit) || (td == SignedInteger64Bit)) {
|
||||
primaryTsIdx = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (primaryTsIdx != UDPS_NO_TIME_SIGNAL) {
|
||||
REPORT_ERROR(ErrorManagement::Information,
|
||||
"Accumulate: primary time signal '%s' (idx=%u).",
|
||||
signalInfos[primaryTsIdx].name.Buffer(), primaryTsIdx);
|
||||
}
|
||||
|
||||
/* Partition signals into accumulated (scalars) and fixed (arrays).
|
||||
* Auto-assign FullArray time mode for scalars that had PacketTime. */
|
||||
singleCycleWireBytes = 0u;
|
||||
fixedWireBytes = 0u;
|
||||
for (uint32 i = 0u; i < numSigs; i++) {
|
||||
if (signalInfos[i].numElements == 1u) {
|
||||
signalInfos[i].accumulated = true;
|
||||
singleCycleWireBytes += signalInfos[i].wireByteSize; /* = srcByteSize for 1 elem */
|
||||
/* Auto-assign time reference for non-primary, non-time scalars */
|
||||
if ((i != primaryTsIdx) && (primaryTsIdx != UDPS_NO_TIME_SIGNAL) &&
|
||||
(signalInfos[i].timeMode == UDPStreamerTimePacket)) {
|
||||
signalInfos[i].timeMode = UDPStreamerTimeFullArray;
|
||||
signalInfos[i].timeSignalIdx = primaryTsIdx;
|
||||
}
|
||||
}
|
||||
else {
|
||||
/* Non-scalar: not accumulated; wire size already computed in pass 4 */
|
||||
fixedWireBytes += signalInfos[i].wireByteSize;
|
||||
}
|
||||
}
|
||||
|
||||
if (singleCycleWireBytes == 0u) {
|
||||
REPORT_ERROR(ErrorManagement::ParametersError,
|
||||
"Accumulate mode: no scalar signals found to accumulate.");
|
||||
ok = false;
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
/* DATA payload: [8 HRT][4 numSamples][numSamples × singleCycle][fixed] */
|
||||
static const uint32 ACCUM_HEADER = UDPS_TIMESTAMP_BYTES + 4u; /* 12 bytes */
|
||||
if ((ACCUM_HEADER + singleCycleWireBytes + fixedWireBytes) > maxPayloadSize) {
|
||||
REPORT_ERROR(ErrorManagement::ParametersError,
|
||||
"Accumulate mode: even a single sample (%u B) exceeds "
|
||||
"MaxPayloadSize (%u B).",
|
||||
ACCUM_HEADER + singleCycleWireBytes + fixedWireBytes,
|
||||
maxPayloadSize);
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
static const uint32 ACCUM_HEADER = UDPS_TIMESTAMP_BYTES + 4u;
|
||||
maxBatchCount = (maxPayloadSize - ACCUM_HEADER - fixedWireBytes) / singleCycleWireBytes;
|
||||
/* Override totalWireBytes: size of the largest possible DATA payload */
|
||||
totalWireBytes = ACCUM_HEADER + maxBatchCount * singleCycleWireBytes + fixedWireBytes;
|
||||
REPORT_ERROR(ErrorManagement::Information,
|
||||
"Accumulate mode: singleCycleWireBytes=%u, fixedWireBytes=%u, "
|
||||
"maxBatchCount=%u, maxPayloadSize=%u, totalWireBytes=%u.",
|
||||
singleCycleWireBytes, fixedWireBytes,
|
||||
maxBatchCount, maxPayloadSize, totalWireBytes);
|
||||
}
|
||||
}
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
@@ -602,21 +752,25 @@ bool UDPStreamer::AllocateMemory() {
|
||||
|
||||
HeapI *heap = GlobalObjectsDatabase::Instance()->GetStandardHeap();
|
||||
|
||||
/* In Accumulate mode, readyBuffer / scratchBuffer hold maxBatchCount consecutive
|
||||
* snapshots instead of a single one. */
|
||||
uint32 readyBufSize = (maxBatchCount > 0u) ? (maxBatchCount * totalSrcBytes) : totalSrcBytes;
|
||||
|
||||
/* readyBuffer: copy of signal memory shared with background thread */
|
||||
readyBuffer = reinterpret_cast<uint8 *>(heap->Malloc(totalSrcBytes));
|
||||
readyBuffer = reinterpret_cast<uint8 *>(heap->Malloc(readyBufSize));
|
||||
if (readyBuffer == NULL_PTR(uint8 *)) {
|
||||
REPORT_ERROR(ErrorManagement::FatalError, "Could not allocate readyBuffer.");
|
||||
return false;
|
||||
}
|
||||
(void) MemoryOperationsHelper::Set(readyBuffer, 0, totalSrcBytes);
|
||||
(void) MemoryOperationsHelper::Set(readyBuffer, 0, readyBufSize);
|
||||
|
||||
/* scratchBuffer: background-thread-private copy for serialization */
|
||||
scratchBuffer = reinterpret_cast<uint8 *>(heap->Malloc(totalSrcBytes));
|
||||
scratchBuffer = reinterpret_cast<uint8 *>(heap->Malloc(readyBufSize));
|
||||
if (scratchBuffer == NULL_PTR(uint8 *)) {
|
||||
REPORT_ERROR(ErrorManagement::FatalError, "Could not allocate scratchBuffer.");
|
||||
return false;
|
||||
}
|
||||
(void) MemoryOperationsHelper::Set(scratchBuffer, 0, totalSrcBytes);
|
||||
(void) MemoryOperationsHelper::Set(scratchBuffer, 0, readyBufSize);
|
||||
|
||||
/* wireBuffer: serialized/quantized payload for transmission */
|
||||
wireBuffer = reinterpret_cast<uint8 *>(heap->Malloc(totalWireBytes));
|
||||
@@ -635,6 +789,44 @@ bool UDPStreamer::AllocateMemory() {
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Accumulate-mode extra buffers --- */
|
||||
if (maxBatchCount > 0u) {
|
||||
/* Linear fill buffer: RT thread writes one snapshot per slot (0..maxBatchCount-1) */
|
||||
uint32 accumBufSize = maxBatchCount * totalSrcBytes;
|
||||
accumBuffer = reinterpret_cast<uint8 *>(heap->Malloc(accumBufSize));
|
||||
if (accumBuffer == NULL_PTR(uint8 *)) {
|
||||
REPORT_ERROR(ErrorManagement::FatalError, "Could not allocate accumBuffer.");
|
||||
return false;
|
||||
}
|
||||
(void) MemoryOperationsHelper::Set(accumBuffer, 0, accumBufSize);
|
||||
|
||||
/* Per-slot HRT timestamp arrays */
|
||||
accumTimestamps = new uint64[maxBatchCount];
|
||||
readyTimestamps = new uint64[maxBatchCount];
|
||||
scratchTimestamps = new uint64[maxBatchCount];
|
||||
if ((accumTimestamps == NULL_PTR(uint64 *)) ||
|
||||
(readyTimestamps == NULL_PTR(uint64 *)) ||
|
||||
(scratchTimestamps == NULL_PTR(uint64 *))) {
|
||||
REPORT_ERROR(ErrorManagement::FatalError,
|
||||
"Could not allocate timestamp arrays.");
|
||||
return false;
|
||||
}
|
||||
uint32 tsBytes = maxBatchCount * static_cast<uint32>(sizeof(uint64));
|
||||
(void) MemoryOperationsHelper::Set(
|
||||
reinterpret_cast<uint8 *>(accumTimestamps), 0, tsBytes);
|
||||
(void) MemoryOperationsHelper::Set(
|
||||
reinterpret_cast<uint8 *>(readyTimestamps), 0, tsBytes);
|
||||
(void) MemoryOperationsHelper::Set(
|
||||
reinterpret_cast<uint8 *>(scratchTimestamps), 0, tsBytes);
|
||||
|
||||
accumFill = 0u;
|
||||
readyFill = 0u;
|
||||
|
||||
REPORT_ERROR(ErrorManagement::Information,
|
||||
"Accumulate buffers: maxBatchCount=%u, accumBufSize=%u B, readyBufSize=%u B.",
|
||||
maxBatchCount, accumBufSize, readyBufSize);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -707,6 +899,14 @@ bool UDPStreamer::PrepareNextState(const char8 *const currentStateName,
|
||||
}
|
||||
}
|
||||
|
||||
/* Initialise the flush timestamp so the first Accumulate flush is deferred
|
||||
* until MinRefreshRate elapses (not immediately on the first Synchronise). */
|
||||
if (ok && (publishMode == UDPStreamerPublishAccumulate)) {
|
||||
lastPublishTs = HighResolutionTimer::Counter();
|
||||
accumFill = 0u;
|
||||
readyFill = 0u;
|
||||
}
|
||||
|
||||
/* Start the background thread (idempotent; shared by both modes) */
|
||||
if (ok && (executor.GetStatus() == EmbeddedThreadI::OffState)) {
|
||||
executor.SetName(GetName());
|
||||
@@ -724,17 +924,76 @@ bool UDPStreamer::PrepareNextState(const char8 *const currentStateName,
|
||||
}
|
||||
|
||||
bool UDPStreamer::Synchronise() {
|
||||
/* Capture timestamp as early as possible */
|
||||
/* Capture HRT timestamp as early as possible. */
|
||||
uint64 ts = HighResolutionTimer::Counter();
|
||||
|
||||
/* RT-safe copy of signal memory → readyBuffer */
|
||||
bufMutex.FastLock(TTInfiniteWait);
|
||||
(void) MemoryOperationsHelper::Copy(readyBuffer, memory, totalSrcBytes);
|
||||
syncTimestamp = ts;
|
||||
bufMutex.FastUnLock();
|
||||
if (publishMode == UDPStreamerPublishAccumulate) {
|
||||
/* --- Accumulate path ---
|
||||
*
|
||||
* Append this snapshot to the linear accumulation buffer, then check
|
||||
* the two flush conditions (from the user spec):
|
||||
*
|
||||
* (a) size: accumulate_size + next_sample_size >= MaxPayloadSize
|
||||
* (adding one more would overflow the UDP datagram)
|
||||
* (b) time: expected_next_cycle_time - lastPublishTs >= flushPeriodTicks
|
||||
* approximated as: ts - lastPublishTs >= flushPeriodTicks
|
||||
*
|
||||
* When either fires, the completed batch is promoted to readyBuffer /
|
||||
* readyTimestamps and dataSem is posted. The background thread sends
|
||||
* the ready batch without any additional timer check. */
|
||||
bufMutex.FastLock(TTInfiniteWait);
|
||||
uint8 *slot = accumBuffer + (accumFill * totalSrcBytes);
|
||||
(void) MemoryOperationsHelper::Copy(slot, memory, totalSrcBytes);
|
||||
accumTimestamps[accumFill] = ts;
|
||||
accumFill++;
|
||||
uint32 filled = accumFill;
|
||||
bufMutex.FastUnLock();
|
||||
|
||||
/* Wake the background sender thread */
|
||||
(void) dataSem.Post();
|
||||
/* Check flush conditions (volatile read of lastPublishTs is safe on x86). */
|
||||
static const uint32 ACCUM_HEADER = UDPS_TIMESTAMP_BYTES + 4u; /* 12 bytes */
|
||||
uint32 curPayload = ACCUM_HEADER + filled * singleCycleWireBytes + fixedWireBytes;
|
||||
uint32 nextPayload = curPayload + singleCycleWireBytes;
|
||||
bool sizeCondition = (nextPayload >= maxPayloadSize);
|
||||
bool timeCondition = ((ts - lastPublishTs) >= flushPeriodTicks);
|
||||
|
||||
if (sizeCondition || timeCondition) {
|
||||
bufMutex.FastLock(TTInfiniteWait);
|
||||
(void) MemoryOperationsHelper::Copy(
|
||||
readyBuffer, accumBuffer, filled * totalSrcBytes);
|
||||
(void) MemoryOperationsHelper::Copy(
|
||||
reinterpret_cast<uint8 *>(readyTimestamps),
|
||||
reinterpret_cast<const uint8 *>(accumTimestamps),
|
||||
filled * static_cast<uint32>(sizeof(uint64)));
|
||||
readyFill = filled;
|
||||
accumFill = 0u;
|
||||
bufMutex.FastUnLock();
|
||||
|
||||
/* Reset the time-based deadline (volatile write). */
|
||||
lastPublishTs = ts;
|
||||
(void) dataSem.Post();
|
||||
}
|
||||
}
|
||||
else if (publishMode == UDPStreamerPublishDecimate) {
|
||||
/* --- Decimate path ---
|
||||
* Post dataSem only every decimateRatio calls. */
|
||||
decimateCounter++;
|
||||
if (decimateCounter >= decimateRatio) {
|
||||
decimateCounter = 0u;
|
||||
bufMutex.FastLock(TTInfiniteWait);
|
||||
(void) MemoryOperationsHelper::Copy(readyBuffer, memory, totalSrcBytes);
|
||||
syncTimestamp = ts;
|
||||
bufMutex.FastUnLock();
|
||||
(void) dataSem.Post();
|
||||
}
|
||||
}
|
||||
else {
|
||||
/* --- Strict path: post every call --- */
|
||||
bufMutex.FastLock(TTInfiniteWait);
|
||||
(void) MemoryOperationsHelper::Copy(readyBuffer, memory, totalSrcBytes);
|
||||
syncTimestamp = ts;
|
||||
bufMutex.FastUnLock();
|
||||
(void) dataSem.Post();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -742,17 +1001,13 @@ bool UDPStreamer::Synchronise() {
|
||||
ErrorManagement::ErrorType UDPStreamer::Execute(ExecutionInfo &info) {
|
||||
ErrorManagement::ErrorType ret = ErrorManagement::NoError;
|
||||
|
||||
/* nextFlushTick: HRT counter target for the next Auto-mode flush.
|
||||
* Declared static so it persists across Execute() calls (the framework
|
||||
* calls Execute() in a tight loop for the MainStage). */
|
||||
static uint64 nextFlushTick = 0u;
|
||||
|
||||
if (info.GetStage() == ExecutionInfo::StartupStage) {
|
||||
nextFlushTick = HighResolutionTimer::Counter();
|
||||
const char8 *modeStr = "Strict";
|
||||
if (publishMode == UDPStreamerPublishAccumulate) { modeStr = "Accumulate"; }
|
||||
else if (publishMode == UDPStreamerPublishDecimate) { modeStr = "Decimate"; }
|
||||
REPORT_ERROR(ErrorManagement::Information,
|
||||
"UDPStreamer background thread started (port %u, mode %s).",
|
||||
static_cast<uint32>(port),
|
||||
(publishMode == UDPStreamerPublishAuto) ? "Auto" : "Strict");
|
||||
static_cast<uint32>(port), modeStr);
|
||||
}
|
||||
|
||||
if (info.GetStage() == ExecutionInfo::MainStage) {
|
||||
@@ -856,43 +1111,54 @@ ErrorManagement::ErrorType UDPStreamer::Execute(ExecutionInfo &info) {
|
||||
}
|
||||
|
||||
if (dataReady && clientConnected) {
|
||||
/* In Auto mode, only flush when the HRT flush interval has elapsed.
|
||||
* This reduces send syscalls and network load by (rtHz / minRefreshRate)x
|
||||
* without any changes to the RT thread or the wire protocol.
|
||||
* The most-recent signal values (captured in readyBuffer) are used. */
|
||||
bool shouldSend = true;
|
||||
if (publishMode == UDPStreamerPublishAuto) {
|
||||
uint64 now = HighResolutionTimer::Counter();
|
||||
if (now < nextFlushTick) {
|
||||
shouldSend = false;
|
||||
/* Synchronise() already gates posting dataSem to the correct rate
|
||||
* (size/time for Accumulate, every-Nth for Decimate, every call for
|
||||
* Strict). Execute() just sends whatever is in the ready buffers. */
|
||||
if (publishMode == UDPStreamerPublishAccumulate) {
|
||||
/* --- Accumulate batch send --- */
|
||||
uint32 fill = 0u;
|
||||
bufMutex.FastLock(TTInfiniteWait);
|
||||
fill = readyFill;
|
||||
if (fill > 0u) {
|
||||
(void) MemoryOperationsHelper::Copy(
|
||||
scratchBuffer, readyBuffer, fill * totalSrcBytes);
|
||||
(void) MemoryOperationsHelper::Copy(
|
||||
reinterpret_cast<uint8 *>(scratchTimestamps),
|
||||
reinterpret_cast<const uint8 *>(readyTimestamps),
|
||||
fill * static_cast<uint32>(sizeof(uint64)));
|
||||
}
|
||||
else {
|
||||
/* Advance deadline by one full period (keeps phase-locked). */
|
||||
nextFlushTick += flushPeriodTicks;
|
||||
/* Guard against clock drift: if we're already more than one
|
||||
* period behind, reset to avoid a burst of back-to-back sends. */
|
||||
if (nextFlushTick < now) {
|
||||
nextFlushTick = now + flushPeriodTicks;
|
||||
bufMutex.FastUnLock();
|
||||
|
||||
if (fill > 0u) {
|
||||
SerializeAccumulated(scratchBuffer, scratchTimestamps, fill);
|
||||
uint32 sendBytes = UDPS_TIMESTAMP_BYTES + 4u +
|
||||
fill * singleCycleWireBytes + fixedWireBytes;
|
||||
packetCounter++;
|
||||
if (!SendFragmented(UDPS_TYPE_DATA, packetCounter,
|
||||
wireBuffer, sendBytes)) {
|
||||
REPORT_ERROR(ErrorManagement::Warning,
|
||||
"Failed to send Accumulate DATA packet (counter=%u).",
|
||||
packetCounter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldSend) {
|
||||
/* Copy readyBuffer → scratchBuffer under brief spinlock */
|
||||
else {
|
||||
/* --- Single-snapshot send (Strict or Decimate) --- */
|
||||
uint64 ts = 0u;
|
||||
bufMutex.FastLock(TTInfiniteWait);
|
||||
(void) MemoryOperationsHelper::Copy(scratchBuffer, readyBuffer, totalSrcBytes);
|
||||
(void) MemoryOperationsHelper::Copy(
|
||||
scratchBuffer, readyBuffer, totalSrcBytes);
|
||||
ts = syncTimestamp;
|
||||
bufMutex.FastUnLock();
|
||||
|
||||
/* Serialize signal data into wireBuffer */
|
||||
QuantizeAndSerialize(scratchBuffer, ts);
|
||||
|
||||
/* Send (fragmented if needed) */
|
||||
packetCounter++;
|
||||
if (!SendFragmented(UDPS_TYPE_DATA, packetCounter, wireBuffer, totalWireBytes)) {
|
||||
if (!SendFragmented(UDPS_TYPE_DATA, packetCounter,
|
||||
wireBuffer, totalWireBytes)) {
|
||||
REPORT_ERROR(ErrorManagement::Warning,
|
||||
"Failed to send DATA packet (counter=%u).", packetCounter);
|
||||
"Failed to send DATA packet (counter=%u).",
|
||||
packetCounter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -919,6 +1185,152 @@ ErrorManagement::ErrorType UDPStreamer::Execute(ExecutionInfo &info) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
void UDPStreamer::SerializeAccumulated(const uint8 *src,
|
||||
const uint64 *timestamps,
|
||||
uint32 numSamples) {
|
||||
/* Wire layout (Accumulate mode DATA payload):
|
||||
* [8 bytes] : HRT of slot 0 (oldest sample)
|
||||
* [4 bytes] : numSamples (uint32, little-endian)
|
||||
* for each signal:
|
||||
* if accumulated : numSamples elements (one per slot)
|
||||
* if non-accumulated (array): one copy from the most-recent slot
|
||||
*/
|
||||
uint8 *dst = wireBuffer;
|
||||
|
||||
/* 8-byte packet-level HRT timestamp = timestamp of the first (oldest) sample */
|
||||
(void) MemoryOperationsHelper::Copy(dst, ×tamps[0u], UDPS_TIMESTAMP_BYTES);
|
||||
dst += UDPS_TIMESTAMP_BYTES;
|
||||
|
||||
/* 4-byte sample count */
|
||||
(void) MemoryOperationsHelper::Copy(dst, &numSamples, 4u);
|
||||
dst += 4u;
|
||||
|
||||
for (uint32 i = 0u; i < numSigs; i++) {
|
||||
if (signalInfos[i].accumulated) {
|
||||
/* Scalar: pack one value from each slot in order */
|
||||
uint32 elemSrcBytes = signalInfos[i].srcByteSize; /* bytes for one element */
|
||||
|
||||
for (uint32 k = 0u; k < numSamples; k++) {
|
||||
const uint8 *slotSrc = src + (k * totalSrcBytes) + signalInfos[i].bufferOffset;
|
||||
|
||||
if (signalInfos[i].quantType == UDPStreamerQuantNone) {
|
||||
(void) MemoryOperationsHelper::Copy(dst, slotSrc, elemSrcBytes);
|
||||
dst += elemSrcBytes;
|
||||
}
|
||||
else {
|
||||
float64 rawVal = 0.0;
|
||||
if (signalInfos[i].type == Float32Bit) {
|
||||
float32 f32 = 0.0f;
|
||||
(void) MemoryOperationsHelper::Copy(&f32, slotSrc, 4u);
|
||||
rawVal = static_cast<float64>(f32);
|
||||
}
|
||||
else {
|
||||
(void) MemoryOperationsHelper::Copy(&rawVal, slotSrc, 8u);
|
||||
}
|
||||
float64 rMin = signalInfos[i].rangeMin;
|
||||
float64 rRange = signalInfos[i].rangeMax - rMin;
|
||||
if (rRange == 0.0) { rRange = 1.0; }
|
||||
float64 norm = (rawVal - rMin) / rRange;
|
||||
if (norm < 0.0) { norm = 0.0; }
|
||||
if (norm > 1.0) { norm = 1.0; }
|
||||
switch (signalInfos[i].quantType) {
|
||||
case UDPStreamerQuantUint8: {
|
||||
uint8 q = static_cast<uint8>(norm * 255.0);
|
||||
*dst = q; dst += 1u;
|
||||
break;
|
||||
}
|
||||
case UDPStreamerQuantInt8: {
|
||||
int8 q = static_cast<int8>((norm * 254.0) - 127.0);
|
||||
(void) MemoryOperationsHelper::Copy(dst, &q, 1u);
|
||||
dst += 1u;
|
||||
break;
|
||||
}
|
||||
case UDPStreamerQuantUint16: {
|
||||
uint16 q = static_cast<uint16>(norm * 65535.0);
|
||||
(void) MemoryOperationsHelper::Copy(dst, &q, 2u);
|
||||
dst += 2u;
|
||||
break;
|
||||
}
|
||||
case UDPStreamerQuantInt16: {
|
||||
int16 q = static_cast<int16>((norm * 65534.0) - 32767.0);
|
||||
(void) MemoryOperationsHelper::Copy(dst, &q, 2u);
|
||||
dst += 2u;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
(void) MemoryOperationsHelper::Copy(dst, slotSrc, elemSrcBytes);
|
||||
dst += elemSrcBytes;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
/* Non-accumulated array: send from the most-recent slot */
|
||||
const uint8 *slotSrc = src + ((numSamples - 1u) * totalSrcBytes) +
|
||||
signalInfos[i].bufferOffset;
|
||||
|
||||
if (signalInfos[i].quantType == UDPStreamerQuantNone) {
|
||||
(void) MemoryOperationsHelper::Copy(dst, slotSrc, signalInfos[i].srcByteSize);
|
||||
dst += signalInfos[i].srcByteSize;
|
||||
}
|
||||
else {
|
||||
float64 rMin = signalInfos[i].rangeMin;
|
||||
float64 rRange = signalInfos[i].rangeMax - rMin;
|
||||
if (rRange == 0.0) { rRange = 1.0; }
|
||||
bool isSrcFloat32 = (signalInfos[i].type == Float32Bit);
|
||||
uint32 nelems = signalInfos[i].numElements;
|
||||
const uint8 *s = slotSrc;
|
||||
|
||||
for (uint32 e = 0u; e < nelems; e++) {
|
||||
float64 rawVal = 0.0;
|
||||
if (isSrcFloat32) {
|
||||
float32 f32 = 0.0f;
|
||||
(void) MemoryOperationsHelper::Copy(&f32, s, 4u);
|
||||
rawVal = static_cast<float64>(f32);
|
||||
s += 4u;
|
||||
}
|
||||
else {
|
||||
(void) MemoryOperationsHelper::Copy(&rawVal, s, 8u);
|
||||
s += 8u;
|
||||
}
|
||||
float64 norm = (rawVal - rMin) / rRange;
|
||||
if (norm < 0.0) { norm = 0.0; }
|
||||
if (norm > 1.0) { norm = 1.0; }
|
||||
switch (signalInfos[i].quantType) {
|
||||
case UDPStreamerQuantUint8: {
|
||||
uint8 q = static_cast<uint8>(norm * 255.0);
|
||||
*dst = q; dst += 1u;
|
||||
break;
|
||||
}
|
||||
case UDPStreamerQuantInt8: {
|
||||
int8 q = static_cast<int8>((norm * 254.0) - 127.0);
|
||||
(void) MemoryOperationsHelper::Copy(dst, &q, 1u);
|
||||
dst += 1u;
|
||||
break;
|
||||
}
|
||||
case UDPStreamerQuantUint16: {
|
||||
uint16 q = static_cast<uint16>(norm * 65535.0);
|
||||
(void) MemoryOperationsHelper::Copy(dst, &q, 2u);
|
||||
dst += 2u;
|
||||
break;
|
||||
}
|
||||
case UDPStreamerQuantInt16: {
|
||||
int16 q = static_cast<int16>((norm * 65534.0) - 32767.0);
|
||||
(void) MemoryOperationsHelper::Copy(dst, &q, 2u);
|
||||
dst += 2u;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UDPStreamer::HandleClientCommand(const uint8 *buf, uint32 size) {
|
||||
if (size < static_cast<uint32>(sizeof(UDPSPacketHeader))) {
|
||||
return;
|
||||
@@ -956,7 +1368,7 @@ void UDPStreamer::HandleClientCommand(const uint8 *buf, uint32 size) {
|
||||
static_cast<uint32>(src.GetPort()));
|
||||
|
||||
/* Send CONFIG packet */
|
||||
uint32 configBufSize = 4u + (numSigs * UDPS_SIGNAL_DESC_SIZE) + 32u;
|
||||
uint32 configBufSize = 4u + (numSigs * UDPS_SIGNAL_DESC_SIZE) + 32u + 1u;
|
||||
HeapI *heap = GlobalObjectsDatabase::Instance()->GetStandardHeap();
|
||||
uint8 *cfgBuf = reinterpret_cast<uint8 *>(heap->Malloc(configBufSize));
|
||||
if (cfgBuf != NULL_PTR(uint8 *)) {
|
||||
@@ -1082,6 +1494,13 @@ bool UDPStreamer::BuildConfigPayload(uint8 *buf,
|
||||
payloadSize += UDPS_SIGNAL_DESC_SIZE;
|
||||
}
|
||||
|
||||
/* 1 byte: publishing mode (so clients can parse DATA payloads correctly) */
|
||||
if ((payloadSize + 1u) > bufSize) {
|
||||
return false;
|
||||
}
|
||||
buf[payloadSize] = static_cast<uint8>(publishMode);
|
||||
payloadSize += 1u;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,13 +50,17 @@ namespace MARTe {
|
||||
/**
|
||||
* @brief Publishing mode for the background sender thread.
|
||||
*
|
||||
* - Strict: send one UDP packet on every Synchronise() call (legacy behaviour).
|
||||
* - Auto: buffer successive ticks and only flush when the HRT-based flush
|
||||
* interval has elapsed. Controlled by MinRefreshRate (Hz).
|
||||
* - Strict: send one packet every Synchronise() call (default).
|
||||
* - Accumulate: buffer N successive snapshots and flush either when the next
|
||||
* snapshot would exceed MaxPayloadSize or when 1/MinRefreshRate
|
||||
* has elapsed. Scalars expand to arrays; the first scalar with
|
||||
* Unit="us"/"ns" is auto-promoted as the FullArray time reference.
|
||||
* - Decimate: send one packet every Ratio Synchronise() calls.
|
||||
*/
|
||||
typedef enum {
|
||||
UDPStreamerPublishStrict = 0u, /**< Send on every RT cycle (default) */
|
||||
UDPStreamerPublishAuto = 1u /**< Rate-limited: flush at MinRefreshRate Hz */
|
||||
UDPStreamerPublishStrict = 0u, /**< Send on every RT cycle (default) */
|
||||
UDPStreamerPublishAccumulate = 1u, /**< Accumulate until size/time limit; then flush */
|
||||
UDPStreamerPublishDecimate = 2u /**< Send 1 packet every Ratio cycles */
|
||||
} UDPStreamerPublishMode;
|
||||
|
||||
/**
|
||||
@@ -100,6 +104,7 @@ struct UDPStreamerSignalInfo {
|
||||
uint32 srcByteSize; /**< Bytes in MARTe2 memory */
|
||||
uint32 wireByteSize; /**< Bytes on the wire (may differ when quantized) */
|
||||
uint32 bufferOffset; /**< Byte offset in the flat MemoryDataSourceI memory buffer */
|
||||
bool accumulated; /**< True when this scalar was expanded to flushCount elements in Auto accumulation mode */
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -145,59 +150,134 @@ static const uint8 UDPS_TYPECODE_FLOAT64 = 9u;
|
||||
static const uint8 UDPS_TYPECODE_UNKNOWN = 255u;
|
||||
|
||||
/**
|
||||
* @brief A DataSource that streams MARTe2 signals to a single UDP client.
|
||||
* @brief A DataSource that streams MARTe2 signals to UDP clients.
|
||||
*
|
||||
* @details This output DataSource accepts signals from GAMs and forwards them
|
||||
* asynchronously to a connected UDP client. A dedicated background thread handles
|
||||
* all network I/O so that the real-time thread is only blocked for a fast spinlock
|
||||
* asynchronously over the network. A dedicated background thread handles all
|
||||
* network I/O so that the real-time thread is only blocked by a fast spinlock
|
||||
* and a memcpy during Synchronise().
|
||||
*
|
||||
* Protocol:
|
||||
* - Client sends CONNECT → server replies with CONFIG describing all signals.
|
||||
* - Server sends DATA packets (fragmented if needed) on every RT cycle.
|
||||
* - Client sends ACK packets (optional, for monitoring loss).
|
||||
* - Client sends DISCONNECT to terminate the session.
|
||||
* Two operating modes are selected by the presence or absence of MulticastGroup:
|
||||
*
|
||||
* Signal configuration syntax:
|
||||
* @par Unicast mode (default — no MulticastGroup)
|
||||
* The server opens a single UDP socket on Port. The client initiates the session
|
||||
* by sending a CONNECT packet to that port. The server replies with a CONFIG
|
||||
* packet on the same socket and subsequently sends DATA packets directly to the
|
||||
* client's address. Any number of clients may connect sequentially (one at a
|
||||
* time); a new CONNECT evicts the previous client.
|
||||
*
|
||||
* @par Multicast mode (MulticastGroup specified)
|
||||
* The server opens a TCP listener on Port for control traffic and a UDP socket
|
||||
* aimed at MulticastGroup:DataPort for data traffic. The client:
|
||||
* 1. Connects to Port via TCP and sends a CONNECT packet.
|
||||
* 2. Receives the CONFIG packet over TCP.
|
||||
* 3. Joins the multicast group (MulticastGroup:DataPort) to receive DATA packets.
|
||||
* Multiple clients may receive data simultaneously by joining the same group.
|
||||
* A new TCP CONNECT evicts the previous client from the control channel; the
|
||||
* multicast data stream continues regardless.
|
||||
*
|
||||
* @par Packet protocol (both modes)
|
||||
* - Client → Server: CONNECT (initiates session), DISCONNECT (terminates session),
|
||||
* ACK (optional, for packet-loss monitoring).
|
||||
* - Server → Client: CONFIG (signal metadata), DATA (signal values, possibly
|
||||
* fragmented into multiple datagrams if payload exceeds MaxPayloadSize).
|
||||
*
|
||||
* @par Top-level configuration parameters
|
||||
* | Parameter | Type | Default | Description |
|
||||
* |-----------------|---------|---------|-------------|
|
||||
* | Port | uint16 | 44500 | TCP control port (multicast) or UDP server port (unicast). Values ≤ 1024 produce a warning. |
|
||||
* | MulticastGroup | string | *(absent)* | **Enables multicast mode.** IPv4 multicast address, e.g. `"239.0.0.1"`. Must be in 224.0.0.0/4. Absent or empty = unicast. |
|
||||
* | DataPort | uint16 | Port+1 | UDP port for multicast DATA datagrams. Ignored in unicast mode. Must be non-zero and differ from Port. |
|
||||
* | MaxPayloadSize | uint32 | 1400 | Maximum bytes of signal payload per UDP datagram (excluding the 17-byte header). Larger signals are fragmented. |
|
||||
* | PublishingMode | string | Strict | `Strict`: send one packet every Synchronise() call. `Auto`: rate-limited; flush only when MinRefreshRate interval has elapsed. |
|
||||
* | MinRefreshRate | float64 | — | Required when PublishingMode = Auto. Flush frequency in Hz (e.g. 120.0). |
|
||||
* | MaxBatchSize | uint32 | 1 | Optional when PublishingMode = Auto. Number of RT cycles to accumulate before flushing one packet. Scalar signals are expanded to arrays of MaxBatchSize elements; the first scalar with Unit="us" or "ns" is auto-promoted as the per-sample FullArray timestamp reference for all other scalars. When omitted or 1, the most-recent single value is sent at MinRefreshRate. |
|
||||
* | CPUMask | uint32 | 0xFFFFFFFF | CPU affinity bitmask for the background thread. |
|
||||
* | StackSize | uint32 | (MARTe2 default) | Stack size in bytes for the background thread. |
|
||||
*
|
||||
* @par Per-signal configuration parameters
|
||||
* | Parameter | Type | Default | Description |
|
||||
* |------------------|---------|-------------|-------------|
|
||||
* | Type | string | — | MARTe2 type name (uint8, int16, float32, float64, …). Mandatory. |
|
||||
* | NumberOfDimensions | uint8 | 0 | 0 = scalar, 1 = array, 2 = matrix. |
|
||||
* | NumberOfElements | uint32 | 1 | Total element count (rows × cols for matrices). |
|
||||
* | Unit | string | "" | Physical unit label, e.g. `"Pa"` or `"m/s"`. Transmitted in CONFIG for display purposes. |
|
||||
* | RangeMin | float64 | 0.0 | Minimum of the physical range. Required when QuantizedType is not `none`. |
|
||||
* | RangeMax | float64 | 1.0 | Maximum of the physical range. Required when QuantizedType is not `none`. |
|
||||
* | QuantizedType | string | none | Wire quantization for float signals: `none` \| `uint8` \| `int8` \| `uint16` \| `int16`. Reduces wire bandwidth at the cost of precision. Only valid for float32/float64 signals. |
|
||||
* | TimeMode | string | PacketTime | How the signal's time axis is encoded (see below). |
|
||||
* | TimeSignal | string | — | Name of the signal that carries timestamps. Required when TimeMode ≠ PacketTime. |
|
||||
* | SamplingRate | float64 | — | Signal sampling rate in Hz. Required when TimeMode = FirstSample or LastSample. |
|
||||
*
|
||||
* @par TimeMode values
|
||||
* | Value | Description |
|
||||
* |--------------|-------------|
|
||||
* | PacketTime | Uses the HRT counter captured at Synchronise() time as the single packet timestamp. No dedicated time signal needed. |
|
||||
* | FullArray | TimeSignal has the same NumberOfElements as this signal; one timestamp per element. |
|
||||
* | FirstSample | TimeSignal is a scalar = timestamp of the first element; subsequent elements are spaced by 1/SamplingRate. |
|
||||
* | LastSample | TimeSignal is a scalar = timestamp of the last element; elements are spaced backwards by 1/SamplingRate. |
|
||||
*
|
||||
* @par Example — unicast mode
|
||||
* <pre>
|
||||
* +UDPStreamer1 = {
|
||||
* Class = UDPStreamer
|
||||
* Port = 44500 // Optional (default 44500)
|
||||
* MaxPayloadSize = 1400 // Optional (default 1400); max payload bytes per UDP datagram
|
||||
* CPUMask = 0x2 // Optional, affinity for background thread
|
||||
* StackSize = 1048576 // Optional, stack size for background thread
|
||||
* PublishingMode = "Auto" // Optional: "Strict" (default) | "Auto"
|
||||
* MinRefreshRate = 120 // Optional (Hz); only used when PublishingMode = "Auto"
|
||||
* +Streamer = {
|
||||
* Class = UDPStreamer
|
||||
* Port = 44500
|
||||
* MaxPayloadSize = 1400
|
||||
* PublishingMode = "Strict"
|
||||
* Signals = {
|
||||
* Time = {
|
||||
* Type = uint64
|
||||
* }
|
||||
* Pressure = {
|
||||
* Type = float32
|
||||
* NumberOfDimensions = 1
|
||||
* NumberOfElements = 100
|
||||
* Unit = "Pa" // Optional
|
||||
* RangeMin = 0.0 // Optional (required for quantization)
|
||||
* RangeMax = 1000000.0 // Optional (required for quantization)
|
||||
* QuantizedType = uint16 // Optional: none|uint8|int8|uint16|int16
|
||||
* TimeMode = LastSample // Optional: PacketTime|FullArray|FirstSample|LastSample
|
||||
* TimeSignal = Time // Required when TimeMode != PacketTime
|
||||
* SamplingRate = 10000.0 // Required when TimeMode = FirstSample or LastSample
|
||||
* Type = float32
|
||||
* NumberOfDimensions = 1
|
||||
* NumberOfElements = 100
|
||||
* Unit = "Pa"
|
||||
* RangeMin = 0.0
|
||||
* RangeMax = 1000000.0
|
||||
* QuantizedType = uint16
|
||||
* TimeMode = LastSample
|
||||
* TimeSignal = Time
|
||||
* SamplingRate = 10000.0
|
||||
* }
|
||||
* Temperature = {
|
||||
* Type = float64
|
||||
* Unit = "K"
|
||||
* Type = float64
|
||||
* Unit = "degC"
|
||||
* TimeMode = PacketTime
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* Notes:
|
||||
* - QuantizedType is only valid for float32/float64 signals.
|
||||
* - TimeMode = PacketTime uses the HRT counter captured in Synchronise().
|
||||
* - TimeMode = FullArray requires TimeSignal to have the same NumberOfElements.
|
||||
* - TimeMode = FirstSample/LastSample requires a scalar TimeSignal and SamplingRate.
|
||||
* @par Example — multicast mode
|
||||
* <pre>
|
||||
* +Streamer = {
|
||||
* Class = UDPStreamer
|
||||
* Port = 44500 // TCP control port
|
||||
* MulticastGroup = "239.0.0.1" // Enables multicast mode
|
||||
* DataPort = 44501 // UDP data port (default: Port+1)
|
||||
* MaxPayloadSize = 1400
|
||||
* PublishingMode = "Auto"
|
||||
* MinRefreshRate = 60
|
||||
* Signals = {
|
||||
* Time = {
|
||||
* Type = uint64
|
||||
* }
|
||||
* Voltage = {
|
||||
* Type = float32
|
||||
* NumberOfDimensions = 1
|
||||
* NumberOfElements = 1000
|
||||
* Unit = "V"
|
||||
* RangeMin = -10.0
|
||||
* RangeMax = 10.0
|
||||
* QuantizedType = int16
|
||||
* TimeMode = FirstSample
|
||||
* TimeSignal = Time
|
||||
* SamplingRate = 100000.0
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
class UDPStreamer : public MemoryDataSourceI, public EmbeddedServiceMethodBinderI {
|
||||
public:
|
||||
@@ -214,8 +294,11 @@ public:
|
||||
virtual ~UDPStreamer();
|
||||
|
||||
/**
|
||||
* @brief Parses top-level configuration parameters (Port, MaxPayloadSize, CPUMask, StackSize).
|
||||
* @return true if all mandatory parameters are valid.
|
||||
* @brief Parses top-level configuration parameters.
|
||||
* @details Reads Port, MaxPayloadSize, CPUMask, StackSize, PublishingMode,
|
||||
* MinRefreshRate, MulticastGroup, and DataPort from the configuration node.
|
||||
* Sets useMulticast and dataPort when MulticastGroup is present and valid.
|
||||
* @return true if all mandatory parameters are valid and consistent.
|
||||
*/
|
||||
virtual bool Initialise(StructuredDataI &data);
|
||||
|
||||
@@ -312,6 +395,14 @@ private:
|
||||
*/
|
||||
void HandleTCPConnect(const uint8 *buf, uint32 size);
|
||||
|
||||
/**
|
||||
* @brief Serializes an accumulated batch into wireBuffer.
|
||||
* @param src Flat buffer [numSamples × totalSrcBytes], slot 0 = oldest.
|
||||
* @param timestamps HRT counters per slot; timestamps[0] is the packet timestamp.
|
||||
* @param numSamples Actual number of filled slots to serialize.
|
||||
*/
|
||||
void SerializeAccumulated(const uint8 *src, const uint64 *timestamps, uint32 numSamples);
|
||||
|
||||
/**
|
||||
* @brief Maps a MARTe2 TypeDescriptor to the UDPS_TYPECODE_* constants.
|
||||
*/
|
||||
@@ -325,6 +416,20 @@ private:
|
||||
UDPStreamerPublishMode publishMode; /**< Strict or Auto publishing mode */
|
||||
float64 minRefreshRate; /**< Minimum flush rate (Hz) for Auto mode */
|
||||
uint64 flushPeriodTicks; /**< HRT ticks per flush interval (computed from minRefreshRate) */
|
||||
/* Accumulate mode — dynamic batch parameters */
|
||||
uint32 maxBatchCount; /**< Max snapshots that fit in MaxPayloadSize (Accumulate) */
|
||||
uint32 singleCycleWireBytes; /**< Wire bytes for all accumulated signals per snapshot */
|
||||
uint32 fixedWireBytes; /**< Wire bytes for non-accumulated signals (arrays, once per packet) */
|
||||
volatile uint64 lastPublishTs; /**< HRT counter of last successful flush (Accumulate mode) */
|
||||
uint8 *accumBuffer; /**< Heap: [maxBatchCount × totalSrcBytes] linear fill */
|
||||
uint64 *accumTimestamps; /**< Heap: [maxBatchCount] HRT counter per snapshot */
|
||||
uint32 accumFill; /**< Slots filled in accumBuffer (0..maxBatchCount) */
|
||||
uint64 *readyTimestamps; /**< Heap: [maxBatchCount] HRT for completed ready batch */
|
||||
uint64 *scratchTimestamps; /**< Heap: [maxBatchCount] background-thread local copy */
|
||||
uint32 readyFill; /**< Snapshot count in the ready batch */
|
||||
/* Decimate mode */
|
||||
uint32 decimateRatio; /**< Send 1 packet every decimateRatio Synchronise() calls */
|
||||
uint32 decimateCounter; /**< Current decimate cycle counter */
|
||||
|
||||
/* Signal metadata */
|
||||
uint32 numSigs; /**< Number of signals */
|
||||
|
||||
Reference in New Issue
Block a user