Add Auto publishing mode to UDPStreamer; optimise WebUI hub

UDPStreamer:
- Add PublishingMode = "Strict" | "Auto" config parameter
- Add MinRefreshRate (Hz) for Auto mode; uses HRT phase-locked tick
  counting to rate-limit sends without accumulation buffers
- Fix high-frequency integration test configs to use newline-separated
  key-value pairs (MARTe2 StandardParser does not treat ';' as delimiter)
- Add 3 new unit tests (AutoMode_Valid, AutoMode_MissingRefreshRate,
  UnknownPublishingMode); all 33 tests passing

WebUI hub:
- Skip ring-buffer LTTB writes when zoom has not been accessed in 10 s,
  reducing idle CPU usage
- Use unsafe float64→bytes reinterpretation to eliminate per-element
  encoding overhead in the hot broadcast path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino Ferrari
2026-05-26 22:29:40 +02:00
parent b465dd680c
commit f85ab8652c
8 changed files with 549 additions and 127 deletions
@@ -101,20 +101,23 @@ UDPStreamer::UDPStreamer() :
MemoryDataSourceI(),
EmbeddedServiceMethodBinderI(),
executor(*this) {
port = UDPS_DEFAULT_PORT;
maxPayloadSize = UDPS_DEFAULT_MAX_PAYLOAD;
cpuMask = 0xFFFFFFFFu;
stackSize = THREADS_DEFAULT_STACKSIZE;
numSigs = 0u;
signalInfos = NULL_PTR(UDPStreamerSignalInfo *);
readyBuffer = NULL_PTR(uint8 *);
scratchBuffer = NULL_PTR(uint8 *);
wireBuffer = NULL_PTR(uint8 *);
totalSrcBytes = 0u;
totalWireBytes = 0u;
syncTimestamp = 0u;
clientConnected = false;
packetCounter = 0u;
port = UDPS_DEFAULT_PORT;
maxPayloadSize = UDPS_DEFAULT_MAX_PAYLOAD;
cpuMask = 0xFFFFFFFFu;
stackSize = THREADS_DEFAULT_STACKSIZE;
publishMode = UDPStreamerPublishStrict;
minRefreshRate = 0.0;
flushPeriodTicks = 0u;
numSigs = 0u;
signalInfos = NULL_PTR(UDPStreamerSignalInfo *);
readyBuffer = NULL_PTR(uint8 *);
scratchBuffer = NULL_PTR(uint8 *);
wireBuffer = NULL_PTR(uint8 *);
totalSrcBytes = 0u;
totalWireBytes = 0u;
syncTimestamp = 0u;
clientConnected = false;
packetCounter = 0u;
if (!dataSem.Create()) {
REPORT_ERROR(ErrorManagement::FatalError, "Could not create EventSem.");
@@ -218,6 +221,40 @@ bool UDPStreamer::Initialise(StructuredDataI &data) {
}
}
if (ok) {
StreamString publishStr = "";
(void) data.Read("PublishingMode", publishStr);
if ((publishStr.Size() == 0u) || (publishStr == "Strict")) {
publishMode = UDPStreamerPublishStrict;
}
else if (publishStr == "Auto") {
publishMode = UDPStreamerPublishAuto;
}
else {
REPORT_ERROR(ErrorManagement::ParametersError,
"Unknown PublishingMode '%s'. Allowed: Strict|Auto.",
publishStr.Buffer());
ok = false;
}
}
if (ok && (publishMode == UDPStreamerPublishAuto)) {
if (!data.Read("MinRefreshRate", minRefreshRate) || (minRefreshRate <= 0.0)) {
REPORT_ERROR(ErrorManagement::ParametersError,
"MinRefreshRate > 0 is required when PublishingMode = Auto.");
ok = false;
}
else {
/* Pre-compute the HRT tick count for one flush interval */
float64 hrtFreq = static_cast<float64>(HighResolutionTimer::Frequency());
flushPeriodTicks = static_cast<uint64>(hrtFreq / minRefreshRate);
REPORT_ERROR(ErrorManagement::Information,
"Auto mode: MinRefreshRate=%.1f Hz, flushPeriodTicks=%llu.",
minRefreshRate,
static_cast<unsigned long long>(flushPeriodTicks));
}
}
return ok;
}
@@ -611,10 +648,17 @@ 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();
REPORT_ERROR(ErrorManagement::Information,
"UDPStreamer background thread started (port %u).",
static_cast<uint32>(port));
"UDPStreamer background thread started (port %u, mode %s).",
static_cast<uint32>(port),
(publishMode == UDPStreamerPublishAuto) ? "Auto" : "Strict");
}
if (info.GetStage() == ExecutionInfo::MainStage) {
@@ -646,21 +690,44 @@ ErrorManagement::ErrorType UDPStreamer::Execute(ExecutionInfo &info) {
}
if (dataReady && clientConnected) {
/* Copy readyBuffer → scratchBuffer under brief spinlock */
uint64 ts = 0u;
bufMutex.FastLock(TTInfiniteWait);
(void) MemoryOperationsHelper::Copy(scratchBuffer, readyBuffer, totalSrcBytes);
ts = syncTimestamp;
bufMutex.FastUnLock();
/* 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;
}
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;
}
}
}
/* Serialize signal data into wireBuffer */
QuantizeAndSerialize(scratchBuffer, ts);
if (shouldSend) {
/* Copy readyBuffer → scratchBuffer under brief spinlock */
uint64 ts = 0u;
bufMutex.FastLock(TTInfiniteWait);
(void) MemoryOperationsHelper::Copy(scratchBuffer, readyBuffer, totalSrcBytes);
ts = syncTimestamp;
bufMutex.FastUnLock();
/* Send (fragmented if needed) */
packetCounter++;
if (!SendFragmented(UDPS_TYPE_DATA, packetCounter, wireBuffer, totalWireBytes)) {
REPORT_ERROR(ErrorManagement::Warning,
"Failed to send DATA packet (counter=%u).", packetCounter);
/* Serialize signal data into wireBuffer */
QuantizeAndSerialize(scratchBuffer, ts);
/* Send (fragmented if needed) */
packetCounter++;
if (!SendFragmented(UDPS_TYPE_DATA, packetCounter, wireBuffer, totalWireBytes)) {
REPORT_ERROR(ErrorManagement::Warning,
"Failed to send DATA packet (counter=%u).", packetCounter);
}
}
}
}