19e649aa38
Parse the optional +Recorder block in Initialise (Enabled/AutoStart/ Directory/MaxFileMB/KeepFiles/StagingMB/FlushIntervalSec/MinDiskFreeMB/ Signals, MB->bytes), apply it to each session before Start (static and dynamic sources). Drive recorder disk I/O from the push loop via RecorderFlushTick, and add recStart/recStop/recInfo WS commands plus a recStatus broadcast/snapshot. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1624 lines
59 KiB
C++
1624 lines
59 KiB
C++
/**
|
||
* @file StreamHub.cpp
|
||
* @brief StreamHub top-level orchestrator implementation.
|
||
*/
|
||
|
||
#include "StreamHub.h"
|
||
#include "Environment/Linux/HighResolutionTimer.h"
|
||
#include "LTTB.h"
|
||
#include "AdvancedErrorManagement.h"
|
||
#include "Sleep.h"
|
||
#include "HighResolutionTimer.h"
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
#include <stdlib.h>
|
||
#include <stdarg.h>
|
||
#include <time.h>
|
||
|
||
namespace StreamHub {
|
||
|
||
using MARTe::Sleep;
|
||
|
||
/**
|
||
* @brief printf-append into a heap buffer, growing it on demand.
|
||
* @return false only on encoding error.
|
||
*/
|
||
static bool JsonAppendf(char *&buf, MARTe::uint32 &len, MARTe::uint32 &cap,
|
||
const char *fmt, ...) {
|
||
for (;;) {
|
||
va_list ap;
|
||
va_start(ap, fmt);
|
||
const int wrote = vsnprintf(buf + len, cap - len, fmt, ap);
|
||
va_end(ap);
|
||
if (wrote < 0) { return false; }
|
||
if (static_cast<MARTe::uint32>(wrote) < (cap - len)) {
|
||
len += static_cast<MARTe::uint32>(wrote);
|
||
return true;
|
||
}
|
||
/* Grow and retry */
|
||
MARTe::uint32 newCap = cap * 2u;
|
||
while ((newCap - len) <= static_cast<MARTe::uint32>(wrote)) {
|
||
newCap *= 2u;
|
||
}
|
||
char *nb = new char[newCap];
|
||
memcpy(nb, buf, len);
|
||
delete[] buf;
|
||
buf = nb;
|
||
cap = newCap;
|
||
}
|
||
}
|
||
|
||
/*---------------------------------------------------------------------------*/
|
||
/* Constructor / Destructor */
|
||
/*---------------------------------------------------------------------------*/
|
||
|
||
StreamHub::StreamHub()
|
||
: numSessions_(0u),
|
||
wsPort_(8090u),
|
||
maxPoints_(20000u),
|
||
pushRateHz_(30u),
|
||
maxPushPoints_(50u),
|
||
statsRateHz_(1u),
|
||
ringTemporal_(1000000u),
|
||
ringScalar_(100000u),
|
||
nextSourceId_(1u),
|
||
running_(false),
|
||
tickCount_(0u),
|
||
pendingMaxPointsSet_(false),
|
||
pendingMaxPoints_(0u),
|
||
pushBuf_(static_cast<uint8 *>(0)),
|
||
lttbT_(static_cast<float64 *>(0)),
|
||
lttbV_(static_cast<float64 *>(0)),
|
||
pushT_(static_cast<float64 *>(0)),
|
||
pushV_(static_cast<float64 *>(0)),
|
||
lastTrigState_(kTrigIdle),
|
||
rearmPending_(false),
|
||
rearmAtWallS_(0.0) {
|
||
memset(&recorderCfg_, 0, sizeof(recorderCfg_));
|
||
for (uint32 i = 0u; i < kMaxSessions; i++) {
|
||
sessionActive_[i] = false;
|
||
configBroadcast_[i] = false;
|
||
for (uint32 s = 0u; s < UDPSS_MAX_SIGNALS; s++) {
|
||
pushCursor_[i][s] = 0u;
|
||
}
|
||
/* All session slots share the hub trigger engine. */
|
||
sessions_[i].SetTriggerEngine(&trigger_);
|
||
}
|
||
}
|
||
|
||
StreamHub::~StreamHub() {
|
||
Stop();
|
||
if (pushBuf_ != static_cast<uint8 *>(0)) {
|
||
delete[] pushBuf_;
|
||
pushBuf_ = static_cast<uint8 *>(0);
|
||
}
|
||
if (lttbT_ != static_cast<float64 *>(0)) {
|
||
delete[] lttbT_;
|
||
lttbT_ = static_cast<float64 *>(0);
|
||
}
|
||
if (lttbV_ != static_cast<float64 *>(0)) {
|
||
delete[] lttbV_;
|
||
lttbV_ = static_cast<float64 *>(0);
|
||
}
|
||
if (pushT_ != static_cast<float64 *>(0)) {
|
||
delete[] pushT_;
|
||
pushT_ = static_cast<float64 *>(0);
|
||
}
|
||
if (pushV_ != static_cast<float64 *>(0)) {
|
||
delete[] pushV_;
|
||
pushV_ = static_cast<float64 *>(0);
|
||
}
|
||
}
|
||
|
||
/*---------------------------------------------------------------------------*/
|
||
/* Initialise */
|
||
/*---------------------------------------------------------------------------*/
|
||
|
||
bool StreamHub::Initialise(StructuredDataI &cfg) {
|
||
/* Use uint32 as the read buffer — never bool, which MARTe2 converts from
|
||
* integer values incorrectly (8090 -> false), leading to zero port/points. */
|
||
uint32 tmp = 0u;
|
||
|
||
if (cfg.Read("WSPort", tmp)) { wsPort_ = static_cast<uint16>(tmp); }
|
||
if (cfg.Read("MaxPoints", tmp)) { maxPoints_ = tmp; }
|
||
if (cfg.Read("PushRate", tmp)) { pushRateHz_ = (tmp > 0u) ? tmp : 30u; }
|
||
if (cfg.Read("MaxPushPoints",tmp)) { maxPushPoints_ = (tmp > 0u) ? tmp : 50u; }
|
||
if (cfg.Read("StatsRate", tmp)) { statsRateHz_ = (tmp > 0u) ? tmp : 1u; }
|
||
if (cfg.Read("RingTemporal", tmp)) { ringTemporal_ = (tmp > 0u) ? tmp : 1000000u; }
|
||
if (cfg.Read("RingScalar", tmp)) { ringScalar_ = (tmp > 0u) ? tmp : 100000u; }
|
||
|
||
sourcesFile_ = "streamhub_sources.json";
|
||
StreamString sf;
|
||
if (cfg.Read("SourcesFile", sf)) { sourcesFile_ = sf; }
|
||
|
||
/* Parse +History block (optional).
|
||
* StandardParser stores the '+' prefix in the node name, so we try both. */
|
||
if (cfg.MoveRelative("+History") || cfg.MoveRelative("History")) {
|
||
history_.Initialise(cfg);
|
||
cfg.MoveToAncestor(1u);
|
||
}
|
||
|
||
/* Parse +Recorder block (optional). Defaults: disabled, auto-start when
|
||
* enabled, 256 MiB files, keep 8, 8 MiB staging, 5 s fsync cadence,
|
||
* 500 MiB free-disk guard, all signals. MB sizes are converted to bytes. */
|
||
recorderCfg_.enabled = false;
|
||
recorderCfg_.autoStart = true;
|
||
strncpy(recorderCfg_.directory, "streamhub_rec",
|
||
sizeof(recorderCfg_.directory) - 1u);
|
||
recorderCfg_.maxFileBytes = 256u * 1024u * 1024u;
|
||
recorderCfg_.keepFiles = 8u;
|
||
recorderCfg_.stagingBytes = 8u * 1024u * 1024u;
|
||
recorderCfg_.flushIntervalSec = 5u;
|
||
recorderCfg_.minDiskFreeMB = 500u;
|
||
strncpy(recorderCfg_.signals, "all", sizeof(recorderCfg_.signals) - 1u);
|
||
|
||
if (cfg.MoveRelative("+Recorder") || cfg.MoveRelative("Recorder")) {
|
||
uint32 rtmp = 0u;
|
||
if (cfg.Read("Enabled", rtmp)) { recorderCfg_.enabled = (rtmp != 0u); }
|
||
if (cfg.Read("AutoStart", rtmp)) { recorderCfg_.autoStart = (rtmp != 0u); }
|
||
StreamString rdir;
|
||
if (cfg.Read("Directory", rdir)) {
|
||
strncpy(recorderCfg_.directory, rdir.Buffer(),
|
||
sizeof(recorderCfg_.directory) - 1u);
|
||
}
|
||
if (cfg.Read("MaxFileMB", rtmp) && rtmp > 0u) {
|
||
recorderCfg_.maxFileBytes = rtmp * 1024u * 1024u;
|
||
}
|
||
if (cfg.Read("KeepFiles", rtmp)) { recorderCfg_.keepFiles = rtmp; }
|
||
if (cfg.Read("StagingMB", rtmp) && rtmp > 0u) {
|
||
recorderCfg_.stagingBytes = rtmp * 1024u * 1024u;
|
||
}
|
||
if (cfg.Read("FlushIntervalSec", rtmp) && rtmp > 0u) {
|
||
recorderCfg_.flushIntervalSec = rtmp;
|
||
}
|
||
if (cfg.Read("MinDiskFreeMB", rtmp)) { recorderCfg_.minDiskFreeMB = rtmp; }
|
||
StreamString rsigs;
|
||
if (cfg.Read("Signals", rsigs)) {
|
||
strncpy(recorderCfg_.signals, rsigs.Buffer(),
|
||
sizeof(recorderCfg_.signals) - 1u);
|
||
}
|
||
cfg.MoveToAncestor(1u);
|
||
}
|
||
|
||
/* Allocate scratch buffers */
|
||
pushBuf_ = new uint8[kPushBufSize];
|
||
lttbT_ = new float64[maxPushPoints_];
|
||
lttbV_ = new float64[maxPushPoints_];
|
||
pushT_ = new float64[kPushScratchPts];
|
||
pushV_ = new float64[kPushScratchPts];
|
||
|
||
/* Parse Sources block (unconditionally — not gated on a bool read result) */
|
||
if (cfg.MoveRelative("Sources")) {
|
||
uint32 nChildren = cfg.GetNumberOfChildren();
|
||
for (uint32 i = 0u; i < nChildren && numSessions_ < kMaxSessions; i++) {
|
||
if (!cfg.MoveToChild(i)) { continue; }
|
||
|
||
/* Node name = source id */
|
||
StreamString nodeId;
|
||
const MARTe::char8 *nname = cfg.GetName();
|
||
if (nname != static_cast<const MARTe::char8 *>(0)) {
|
||
nodeId = nname;
|
||
}
|
||
|
||
char label[128] = "";
|
||
char addr[64] = "127.0.0.1";
|
||
uint32 port32 = 44500u;
|
||
char mcGroup[64] = "";
|
||
uint32 dataPort32 = 0u;
|
||
|
||
StreamString lbl;
|
||
if (cfg.Read("Label", lbl)) {
|
||
strncpy(label, lbl.Buffer(), sizeof(label) - 1u);
|
||
}
|
||
StreamString addrStr;
|
||
if (cfg.Read("Addr", addrStr)) {
|
||
strncpy(addr, addrStr.Buffer(), sizeof(addr) - 1u);
|
||
}
|
||
cfg.Read("Port", port32);
|
||
|
||
StreamString mcGrpStr;
|
||
if (cfg.Read("MulticastGroup", mcGrpStr)) {
|
||
strncpy(mcGroup, mcGrpStr.Buffer(), sizeof(mcGroup) - 1u);
|
||
}
|
||
cfg.Read("DataPort", dataPort32);
|
||
|
||
sessions_[numSessions_].SetRingCapacities(ringTemporal_, ringScalar_);
|
||
bool ok = sessions_[numSessions_].Initialise(
|
||
nodeId.Buffer(), label, addr,
|
||
static_cast<uint16>(port32), maxPoints_,
|
||
mcGroup, static_cast<uint16>(dataPort32));
|
||
|
||
if (ok) {
|
||
sessions_[numSessions_].SetRecorderConfig(recorderCfg_);
|
||
ok = sessions_[numSessions_].Start();
|
||
}
|
||
if (ok) {
|
||
sessionActive_[numSessions_] = true;
|
||
numSessions_++;
|
||
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
|
||
"StreamHub: session '%s' started (%s:%u).",
|
||
nodeId.Buffer(), addr, static_cast<uint32>(port32));
|
||
} else {
|
||
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Warning,
|
||
"StreamHub: failed to start session '%s'.", nodeId.Buffer());
|
||
}
|
||
|
||
cfg.MoveToAncestor(1u);
|
||
}
|
||
cfg.MoveToAncestor(1u); /* back to root */
|
||
}
|
||
|
||
/* Start any persisted dynamic sources (Go SourceConfig schema). */
|
||
LoadSourcesFile();
|
||
|
||
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
|
||
"StreamHub: initialised with %u session(s), WSPort=%u, MaxPoints=%u, PushRate=%u Hz.",
|
||
numSessions_, static_cast<uint32>(wsPort_), maxPoints_, pushRateHz_);
|
||
|
||
return true;
|
||
}
|
||
|
||
/*---------------------------------------------------------------------------*/
|
||
/* Run / Stop */
|
||
/*---------------------------------------------------------------------------*/
|
||
|
||
bool StreamHub::Run() {
|
||
if (!wsServer_.Start(wsPort_, this)) {
|
||
REPORT_ERROR_STATIC(MARTe::ErrorManagement::FatalError,
|
||
"StreamHub: failed to start WebSocket server.");
|
||
return false;
|
||
}
|
||
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
|
||
"StreamHub: push loop started at %u Hz.", pushRateHz_);
|
||
|
||
running_ = true;
|
||
tickCount_ = 0u;
|
||
|
||
/* Period in microseconds */
|
||
uint64 periodUs = (pushRateHz_ > 0u) ? (1000000u / pushRateHz_) : 33333u;
|
||
|
||
while (running_) {
|
||
uint64 t0 = MARTe::HighResolutionTimer::Counter();
|
||
|
||
PushData();
|
||
|
||
/* Trigger servicing: capture finalization, auto-rearm, state events */
|
||
{
|
||
struct timespec tsNow;
|
||
(void) clock_gettime(CLOCK_REALTIME, &tsNow);
|
||
const float64 wallNowS = static_cast<float64>(tsNow.tv_sec) +
|
||
static_cast<float64>(tsNow.tv_nsec) * 1.0e-9;
|
||
TriggerTick(wallNowS);
|
||
}
|
||
|
||
/* Stats at statsRateHz_ */
|
||
uint32 statsDivisor = (statsRateHz_ > 0u && pushRateHz_ > 0u)
|
||
? (pushRateHz_ / statsRateHz_) : pushRateHz_;
|
||
if (statsDivisor == 0u) { statsDivisor = 1u; }
|
||
if ((tickCount_ % statsDivisor) == 0u) {
|
||
PushStats();
|
||
}
|
||
|
||
/* History: flush headers at the configured interval, then re-broadcast
|
||
* historyInfo so new clients (or clients that saw empty signal lists
|
||
* before the first data was written) pick up the current state. */
|
||
if (history_.IsEnabled()) {
|
||
uint32 flushIntervalTicks = pushRateHz_ * 5u; /* default 5 s */
|
||
if (pushRateHz_ > 0u) {
|
||
flushIntervalTicks = pushRateHz_ * 5u;
|
||
}
|
||
if (flushIntervalTicks == 0u) { flushIntervalTicks = 1u; }
|
||
if ((tickCount_ % flushIntervalTicks) == 0u) {
|
||
history_.FlushHeaders();
|
||
/* Re-broadcast so clients see updated signal counts and
|
||
* time ranges after the first batch of data is written. */
|
||
uint32 cap2 = 8192u;
|
||
char *hbuf = new char[cap2];
|
||
uint32 hoff = 0u;
|
||
JsonAppendf(hbuf, hoff, cap2, "{\"type\":\"historyInfo\",");
|
||
history_.AppendInfoJSON(hbuf, hoff, cap2);
|
||
JsonAppendf(hbuf, hoff, cap2, "}");
|
||
wsServer_.BroadcastText(hbuf, hoff);
|
||
delete[] hbuf;
|
||
}
|
||
}
|
||
|
||
tickCount_++;
|
||
|
||
/* Sleep for remainder of period */
|
||
uint64 t1 = MARTe::HighResolutionTimer::Counter();
|
||
uint64 freq = MARTe::HighResolutionTimer::Frequency();
|
||
uint64 elapsedUs = ((t1 - t0) * 1000000u) / freq;
|
||
if (periodUs - elapsedUs > 1000) {
|
||
Sleep::MSec(static_cast<uint32>((periodUs - elapsedUs) / 1000u));
|
||
}
|
||
}
|
||
|
||
wsServer_.Stop();
|
||
return true;
|
||
}
|
||
|
||
void StreamHub::Stop() {
|
||
running_ = false;
|
||
}
|
||
|
||
/*---------------------------------------------------------------------------*/
|
||
/* Push helpers */
|
||
/*---------------------------------------------------------------------------*/
|
||
|
||
void StreamHub::PushData() {
|
||
/* Apply a deferred setMaxPoints request on the push thread (the WS read
|
||
* threads must not touch session read state concurrently). */
|
||
if (pendingMaxPointsSet_) {
|
||
pendingMaxPointsSet_ = false;
|
||
maxPoints_ = pendingMaxPoints_;
|
||
for (uint32 i = 0u; i < kMaxSessions; i++) {
|
||
if (sessionActive_[i]) {
|
||
(void) sessions_[i].SetMaxPoints(maxPoints_);
|
||
}
|
||
}
|
||
char resp[128];
|
||
snprintf(resp, sizeof(resp),
|
||
"{\"type\":\"maxPointsUpdated\",\"maxPoints\":%u}", maxPoints_);
|
||
wsServer_.BroadcastText(resp, static_cast<uint32>(strlen(resp)));
|
||
}
|
||
|
||
/* Wall-clock seconds for recorder fsync cadence and file timestamps. */
|
||
uint32 nowSec = 0u;
|
||
{
|
||
struct timespec tsRec;
|
||
(void) clock_gettime(CLOCK_REALTIME, &tsRec);
|
||
nowSec = static_cast<uint32>(tsRec.tv_sec);
|
||
}
|
||
|
||
for (uint32 i = 0u; i < kMaxSessions; i++) {
|
||
if (!sessionActive_[i]) { continue; }
|
||
if (!sessions_[i].IsConfigured()) { continue; }
|
||
|
||
/* First time we see this session become configured: broadcast its signal
|
||
* list to all currently connected WS clients. This handles the timing
|
||
* race where a client connected before the UDPStreamer sent its CONFIG
|
||
* packet, so OnWSClientConnected() could not send it then. */
|
||
if (!configBroadcast_[i]) {
|
||
configBroadcast_[i] = true;
|
||
/* New (or re-)configuration: restart push cursors so the first
|
||
* frame starts from the freshly allocated rings. */
|
||
for (uint32 s = 0u; s < UDPSS_MAX_SIGNALS; s++) {
|
||
pushCursor_[i][s] = 0u;
|
||
}
|
||
BroadcastSources();
|
||
BroadcastConfig(i);
|
||
|
||
/* Open history files for this source */
|
||
if (history_.IsEnabled()) {
|
||
StreamString sid = sessions_[i].GetId();
|
||
history_.OnSourceConfigured(i, sid.Buffer(), sessions_[i]);
|
||
}
|
||
}
|
||
|
||
/* Always serialize (advancing the per-signal cursors), even with no
|
||
* connected clients — avoids a huge backlog burst on first connect. */
|
||
uint32 frameLen = SerializeBinaryFrame(i, pushBuf_, kPushBufSize);
|
||
if (frameLen > 0u) {
|
||
wsServer_.BroadcastBinary(pushBuf_, frameLen);
|
||
}
|
||
|
||
/* Write to disk history (separate read cursors, own decimation) */
|
||
if (history_.IsEnabled()) {
|
||
history_.WriteTick(i, sessions_[i]);
|
||
}
|
||
|
||
/* Binary recorder: all disk I/O happens here on the push thread. */
|
||
if (sessions_[i].IsRecorderEnabled()) {
|
||
sessions_[i].RecorderFlushTick(nowSec);
|
||
}
|
||
}
|
||
}
|
||
|
||
uint32 StreamHub::SerializeBinaryFrame(uint32 sessionIdx,
|
||
uint8 *buf, uint32 bufSize) {
|
||
UDPSourceSession &sess = sessions_[sessionIdx];
|
||
|
||
StreamString sid = sess.GetId();
|
||
const char *id = sid.Buffer();
|
||
uint32 idLen = static_cast<uint32>(strlen(id));
|
||
if (idLen > 255u) { idLen = 255u; }
|
||
|
||
uint32 numSigs = sess.GetNumSignals();
|
||
if (numSigs == 0u) { return 0u; }
|
||
|
||
uint32 offset = 0u;
|
||
|
||
/* Header: [1] version, [1] idLen, [idLen] id, [4] numSignals */
|
||
if (offset + 1u + 1u + idLen + 4u > bufSize) {
|
||
return 0u;
|
||
}
|
||
buf[offset++] = 1u; /* version */
|
||
buf[offset++] = static_cast<uint8>(idLen);
|
||
memcpy(buf + offset, id, idLen);
|
||
offset += idLen;
|
||
|
||
buf[offset++] = static_cast<uint8>( numSigs & 0xFFu);
|
||
buf[offset++] = static_cast<uint8>((numSigs >> 8) & 0xFFu);
|
||
buf[offset++] = static_cast<uint8>((numSigs >> 16) & 0xFFu);
|
||
buf[offset++] = static_cast<uint8>((numSigs >> 24) & 0xFFu);
|
||
|
||
uint32 sigsWritten = 0u;
|
||
|
||
for (uint32 s = 0u; s < numSigs; s++) {
|
||
MARTe::UDPSSignalDescriptor desc;
|
||
if (!sess.GetSignalDescriptor(s, desc)) { continue; }
|
||
|
||
/* Read only the samples written since the last tick (per-signal
|
||
* cursor). Re-reading overlapping windows and re-LTTBing them is
|
||
* what corrupted the traces before. */
|
||
uint32 nRaw = sess.ReadSignalSince(s, pushCursor_[sessionIdx][s],
|
||
pushT_, pushV_, kPushScratchPts);
|
||
if (nRaw == 0u) { continue; }
|
||
|
||
/* LTTB decimation only for temporal (multi-element, sample-timed)
|
||
* signals — Go hub policy. Scalars and PACKET-timed arrays are
|
||
* pushed verbatim (their per-tick batches are small). */
|
||
const uint32 nElems = desc.numRows * ((desc.numCols > 0u) ? desc.numCols : 1u);
|
||
const bool temporal = (nElems > 1u) &&
|
||
(desc.timeMode != MARTe::UDPS_TIMEMODE_PACKET);
|
||
|
||
const float64 *tOut;
|
||
const float64 *vOut;
|
||
uint32 nOut;
|
||
if (temporal && (nRaw > maxPushPoints_)) {
|
||
nOut = LTTBDecimate(pushT_, pushV_, nRaw,
|
||
lttbT_, lttbV_, maxPushPoints_);
|
||
tOut = lttbT_;
|
||
vOut = lttbV_;
|
||
} else {
|
||
nOut = nRaw;
|
||
tOut = pushT_;
|
||
vOut = pushV_;
|
||
}
|
||
|
||
const char *sigName = desc.name;
|
||
uint32 keyLen = static_cast<uint32>(strlen(sigName));
|
||
if (keyLen > 65535u) { keyLen = 65535u; }
|
||
|
||
uint32 frameBytes = 2u + keyLen + 4u + nOut * 8u + nOut * 8u;
|
||
if (offset + frameBytes > bufSize) { break; }
|
||
|
||
/* [2] keyLen, [K] key */
|
||
buf[offset++] = static_cast<uint8>( keyLen & 0xFFu);
|
||
buf[offset++] = static_cast<uint8>((keyLen >> 8) & 0xFFu);
|
||
memcpy(buf + offset, sigName, keyLen);
|
||
offset += keyLen;
|
||
|
||
/* [4] pairCount */
|
||
buf[offset++] = static_cast<uint8>( nOut & 0xFFu);
|
||
buf[offset++] = static_cast<uint8>((nOut >> 8) & 0xFFu);
|
||
buf[offset++] = static_cast<uint8>((nOut >> 16) & 0xFFu);
|
||
buf[offset++] = static_cast<uint8>((nOut >> 24) & 0xFFu);
|
||
|
||
/* [N×8] time array, [N×8] value array (little-endian) */
|
||
memcpy(buf + offset, tOut, nOut * sizeof(float64));
|
||
offset += nOut * 8u;
|
||
memcpy(buf + offset, vOut, nOut * sizeof(float64));
|
||
offset += nOut * 8u;
|
||
|
||
sigsWritten++;
|
||
}
|
||
|
||
if (sigsWritten == 0u) { return 0u; }
|
||
|
||
/* Patch numSignals field with actual written count */
|
||
uint32 numSigsOffset = 2u + idLen;
|
||
buf[numSigsOffset] = static_cast<uint8>( sigsWritten & 0xFFu);
|
||
buf[numSigsOffset + 1u] = static_cast<uint8>((sigsWritten >> 8) & 0xFFu);
|
||
buf[numSigsOffset + 2u] = static_cast<uint8>((sigsWritten >> 16) & 0xFFu);
|
||
buf[numSigsOffset + 3u] = static_cast<uint8>((sigsWritten >> 24) & 0xFFu);
|
||
|
||
return offset;
|
||
}
|
||
|
||
/*---------------------------------------------------------------------------*/
|
||
/* Stats broadcast */
|
||
/*---------------------------------------------------------------------------*/
|
||
|
||
void StreamHub::PushStats() {
|
||
(void) sessionsMutex_.FastLock();
|
||
const uint32 nActive = numSessions_;
|
||
sessionsMutex_.FastUnLock();
|
||
|
||
if (nActive == 0u) { return; }
|
||
|
||
/* Build JSON (Go hub StatInfo field names + "state"):
|
||
* {"type":"stats","sources":{"id":{"totalReceived":N,...,"cycleHist":[...]},...}}
|
||
*/
|
||
uint32 cap = 8192u;
|
||
char *buf = new char[cap];
|
||
uint32 off = 0u;
|
||
|
||
JsonAppendf(buf, off, cap, "{\"type\":\"stats\",\"sources\":{");
|
||
|
||
bool first = true;
|
||
for (uint32 i = 0u; i < kMaxSessions; i++) {
|
||
if (!sessionActive_[i]) { continue; }
|
||
UDPSourceStats st = sessions_[i].GetStats();
|
||
StreamString sid = sessions_[i].GetId();
|
||
|
||
JsonAppendf(buf, off, cap,
|
||
"%s\"%s\":{"
|
||
"\"state\":\"%s\","
|
||
"\"totalReceived\":%llu,"
|
||
"\"totalLost\":%llu,"
|
||
"\"rateHz\":%.6g,"
|
||
"\"rateStdHz\":%.6g,"
|
||
"\"fragsPerCycle\":%.6g,"
|
||
"\"bytesPerCycle\":%.6g,"
|
||
"\"cycleAvgMs\":%.6g,"
|
||
"\"cycleStdMs\":%.6g,"
|
||
"\"cycleMinMs\":%.6g,"
|
||
"\"cycleMaxMs\":%.6g,"
|
||
"\"cycleHistMin\":%.6g,"
|
||
"\"cycleHistMax\":%.6g,"
|
||
"\"cycleHist\":[",
|
||
(first ? "" : ","),
|
||
sid.Buffer(),
|
||
st.state.Buffer(),
|
||
static_cast<unsigned long long>(st.totalReceived),
|
||
static_cast<unsigned long long>(st.totalLost),
|
||
st.rateHz,
|
||
st.rateStdHz,
|
||
st.fragsPerCycle,
|
||
st.bytesPerCycle,
|
||
st.cycleAvgMs,
|
||
st.cycleStdMs,
|
||
st.cycleMinMs,
|
||
st.cycleMaxMs,
|
||
st.cycleHistMin,
|
||
st.cycleHistMax);
|
||
|
||
if (st.histValid) {
|
||
for (uint32 b = 0u; b < UDPS_STAT_HIST_BINS; b++) {
|
||
JsonAppendf(buf, off, cap, "%s%u",
|
||
(b > 0u ? "," : ""), st.cycleHist[b]);
|
||
}
|
||
}
|
||
JsonAppendf(buf, off, cap, "]}");
|
||
first = false;
|
||
}
|
||
|
||
JsonAppendf(buf, off, cap, "}}");
|
||
|
||
wsServer_.BroadcastText(buf, off);
|
||
delete[] buf;
|
||
}
|
||
|
||
/*---------------------------------------------------------------------------*/
|
||
/* Sources / Config broadcast */
|
||
/*---------------------------------------------------------------------------*/
|
||
|
||
void StreamHub::BroadcastSources() {
|
||
static const uint32 kBuf = 4096u;
|
||
char *buf = new char[kBuf];
|
||
uint32 off = 0u;
|
||
|
||
off += static_cast<uint32>(snprintf(buf + off, kBuf - off,
|
||
"{\"type\":\"sources\",\"sources\":["));
|
||
|
||
bool first = true;
|
||
for (uint32 i = 0u; i < kMaxSessions; i++) {
|
||
if (!sessionActive_[i]) { continue; }
|
||
UDPSourceStats st = sessions_[i].GetStats();
|
||
StreamString sid = sessions_[i].GetId();
|
||
StreamString lbl = sessions_[i].GetLabel();
|
||
StreamString adr = sessions_[i].GetAddr();
|
||
uint16 prt = sessions_[i].GetPort();
|
||
|
||
if (off >= kBuf - 256u) { break; }
|
||
/* Go hub shape: addr is the combined "host:port" string. */
|
||
off += static_cast<uint32>(snprintf(buf + off, kBuf - off,
|
||
"%s{\"id\":\"%s\",\"label\":\"%s\","
|
||
"\"addr\":\"%s:%u\",\"state\":\"%s\"}",
|
||
(first ? "" : ","),
|
||
sid.Buffer(), lbl.Buffer(),
|
||
adr.Buffer(), static_cast<uint32>(prt),
|
||
st.state.Buffer()));
|
||
first = false;
|
||
}
|
||
|
||
off += static_cast<uint32>(snprintf(buf + off, kBuf - off, "]}"));
|
||
wsServer_.BroadcastText(buf, off);
|
||
delete[] buf;
|
||
}
|
||
|
||
void StreamHub::BroadcastConfig(uint32 idx) {
|
||
if ((idx >= kMaxSessions) || (!sessionActive_[idx])) { return; }
|
||
UDPSourceSession &sess = sessions_[idx];
|
||
if (!sess.IsConfigured()) { return; }
|
||
|
||
StreamString sid = sess.GetId();
|
||
uint32 nsig = sess.GetNumSignals();
|
||
uint8 pm = sess.GetPublishMode();
|
||
|
||
static const uint32 kBuf = 16384u;
|
||
char *buf = new char[kBuf];
|
||
uint32 off = 0u;
|
||
|
||
off += static_cast<uint32>(snprintf(buf + off, kBuf - off,
|
||
"{\"type\":\"config\",\"sourceId\":\"%s\","
|
||
"\"publishMode\":%u,\"signals\":[",
|
||
sid.Buffer(), static_cast<uint32>(pm)));
|
||
|
||
for (uint32 s = 0u; s < nsig; s++) {
|
||
MARTe::UDPSSignalDescriptor desc;
|
||
if (!sess.GetSignalDescriptor(s, desc)) { continue; }
|
||
|
||
if (off >= kBuf - 512u) { break; }
|
||
/* Field names match the Go hub's UDPSSignalDescriptor JSON tags —
|
||
* required by the SPA (numElements(sig) = numRows*numCols, etc.). */
|
||
off += static_cast<uint32>(snprintf(buf + off, kBuf - off,
|
||
"%s{\"name\":\"%s\","
|
||
"\"typeCode\":%u,"
|
||
"\"quantType\":%u,"
|
||
"\"numDimensions\":%u,"
|
||
"\"numRows\":%u,"
|
||
"\"numCols\":%u,"
|
||
"\"rangeMin\":%.9g,"
|
||
"\"rangeMax\":%.9g,"
|
||
"\"timeMode\":%u,"
|
||
"\"samplingRate\":%.9g,"
|
||
"\"timeSignalIdx\":%u,"
|
||
"\"unit\":\"%s\"}",
|
||
(s > 0u ? "," : ""),
|
||
desc.name,
|
||
static_cast<uint32>(desc.typeCode),
|
||
static_cast<uint32>(desc.quantType),
|
||
static_cast<uint32>(desc.numDimensions),
|
||
desc.numRows,
|
||
desc.numCols,
|
||
desc.rangeMin,
|
||
desc.rangeMax,
|
||
static_cast<uint32>(desc.timeMode),
|
||
desc.samplingRate,
|
||
desc.timeSignalIdx,
|
||
desc.unit));
|
||
}
|
||
|
||
off += static_cast<uint32>(snprintf(buf + off, kBuf - off, "]}"));
|
||
wsServer_.BroadcastText(buf, off);
|
||
delete[] buf;
|
||
}
|
||
|
||
/*---------------------------------------------------------------------------*/
|
||
/* WSCommandCallback */
|
||
/*---------------------------------------------------------------------------*/
|
||
|
||
void StreamHub::OnWSClientConnected() {
|
||
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
|
||
"StreamHub: WebSocket client connected (%u total).",
|
||
wsServer_.ClientCount());
|
||
/* Send current sources list and config to the new client */
|
||
BroadcastSources();
|
||
|
||
for (uint32 i = 0u; i < kMaxSessions; i++) {
|
||
if (sessionActive_[i] && sessions_[i].IsConfigured()) {
|
||
BroadcastConfig(i);
|
||
}
|
||
}
|
||
|
||
/* Let the new client render the current trigger badge/buttons. */
|
||
BroadcastTriggerState();
|
||
|
||
/* Inform the new client about history availability. */
|
||
if (history_.IsEnabled()) {
|
||
/* Broadcast to all (simple; no unicast-on-connect path for broadcast). */
|
||
uint32 cap2 = 8192u;
|
||
char *hbuf = new char[cap2];
|
||
uint32 hoff = 0u;
|
||
JsonAppendf(hbuf, hoff, cap2, "{\"type\":\"historyInfo\",");
|
||
history_.AppendInfoJSON(hbuf, hoff, cap2);
|
||
JsonAppendf(hbuf, hoff, cap2, "}");
|
||
wsServer_.BroadcastText(hbuf, hoff);
|
||
delete[] hbuf;
|
||
}
|
||
|
||
/* Inform the new client about the current MaxPoints setting. */
|
||
{
|
||
char mp[64];
|
||
int n = snprintf(mp, sizeof(mp),
|
||
"{\"type\":\"maxPointsUpdated\",\"maxPoints\":%u}", maxPoints_);
|
||
if (n > 0) { wsServer_.BroadcastText(mp, static_cast<uint32>(n)); }
|
||
}
|
||
}
|
||
|
||
void StreamHub::OnWSClientDisconnected() {
|
||
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
|
||
"StreamHub: WebSocket client disconnected (%u remaining).",
|
||
wsServer_.ClientCount());
|
||
}
|
||
|
||
void StreamHub::OnWSCommand(const char *json, uint32 /*len*/, uint32 slotIdx) {
|
||
char type[64] = "";
|
||
if (!JsonGetString(json, "type", type, sizeof(type))) { return; }
|
||
|
||
if (strcmp(type, "addSource") == 0) { HandleAddSource(json); }
|
||
else if (strcmp(type, "removeSource") == 0) { HandleRemoveSource(json); }
|
||
else if (strcmp(type, "saveSources") == 0) { HandleSaveSources(); }
|
||
else if (strcmp(type, "getSources") == 0) { HandleGetSources(); }
|
||
else if (strcmp(type, "getConfig") == 0) { HandleGetConfig(json); }
|
||
else if (strcmp(type, "getStats") == 0) { HandleGetStats(); }
|
||
else if (strcmp(type, "arm") == 0) { HandleArm(); }
|
||
else if (strcmp(type, "disarm") == 0) { HandleDisarm(); }
|
||
else if (strcmp(type, "rearm") == 0) { HandleRearm(); }
|
||
else if (strcmp(type, "trigStop") == 0) { HandleTrigStop(json); }
|
||
else if (strcmp(type, "setTrigger") == 0) { HandleSetTrigger(json); }
|
||
else if (strcmp(type, "zoom") == 0) { HandleZoom(json, slotIdx); }
|
||
else if (strcmp(type, "historyZoom") == 0) { HandleHistoryZoom(json, slotIdx); }
|
||
else if (strcmp(type, "historyInfo") == 0) { HandleHistoryInfo(slotIdx); }
|
||
else if (strcmp(type, "setMaxPoints") == 0) { HandleSetMaxPoints(json); }
|
||
else if (strcmp(type, "recStart") == 0) { HandleRecStart(json); }
|
||
else if (strcmp(type, "recStop") == 0) { HandleRecStop(json); }
|
||
else if (strcmp(type, "recInfo") == 0) { HandleRecInfo(slotIdx); }
|
||
else if (strcmp(type, "ping") == 0) { HandlePing(slotIdx); }
|
||
}
|
||
|
||
/*---------------------------------------------------------------------------*/
|
||
/* Command handlers */
|
||
/*---------------------------------------------------------------------------*/
|
||
|
||
void StreamHub::HandleAddSource(const char *json) {
|
||
/* SPA shape: {"type":"addSource","label":L,"addr":"host:port",
|
||
* "multicastGroup":G?,"dataPort":N?} — the hub generates ids. */
|
||
char label[128] = "";
|
||
char addr[80] = "";
|
||
char mcGroup[64] = "";
|
||
float64 dataPortF = 0.0;
|
||
|
||
JsonGetString(json, "label", label, sizeof(label));
|
||
JsonGetString(json, "addr", addr, sizeof(addr));
|
||
JsonGetString(json, "multicastGroup", mcGroup, sizeof(mcGroup));
|
||
JsonGetFloat(json, "dataPort", dataPortF);
|
||
|
||
if (AddSourceInternal(label, addr, mcGroup,
|
||
static_cast<uint16>(dataPortF))) {
|
||
BroadcastSources();
|
||
}
|
||
}
|
||
|
||
bool StreamHub::AddSourceInternal(const char *label, const char *addrPort,
|
||
const char *mcGroup, uint16 dataPort) {
|
||
if ((addrPort == static_cast<const char *>(0)) || (addrPort[0] == '\0')) {
|
||
return false;
|
||
}
|
||
|
||
/* Split "host:port" (Go-style combined address). */
|
||
char host[64];
|
||
strncpy(host, addrPort, sizeof(host) - 1u);
|
||
host[sizeof(host) - 1u] = '\0';
|
||
uint32 port = 44500u;
|
||
char *colon = strrchr(host, ':');
|
||
if (colon != static_cast<char *>(0)) {
|
||
port = static_cast<uint32>(strtoul(colon + 1,
|
||
static_cast<char **>(0), 10));
|
||
*colon = '\0';
|
||
}
|
||
|
||
/* Whole add is serialized against other WS-thread add/remove requests.
|
||
* The push thread never takes this mutex: it only observes the
|
||
* sessionActive_[] flag, which is set last (after a successful Start). */
|
||
(void) sessionsMutex_.FastLock();
|
||
|
||
uint32 idx = kMaxSessions;
|
||
for (uint32 i = 0u; i < kMaxSessions; i++) {
|
||
if (!sessionActive_[i]) { idx = i; break; }
|
||
}
|
||
if (idx == kMaxSessions) {
|
||
sessionsMutex_.FastUnLock();
|
||
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Warning,
|
||
"StreamHub: max sessions reached, ignoring addSource.");
|
||
return false;
|
||
}
|
||
|
||
char id[16];
|
||
snprintf(id, sizeof(id), "s%u", nextSourceId_++);
|
||
|
||
/* Label defaults to the address (Go SourceManager.Add). */
|
||
const char *lbl = ((label != static_cast<const char *>(0)) &&
|
||
(label[0] != '\0')) ? label : addrPort;
|
||
|
||
configBroadcast_[idx] = false; /* set true by PushData on first config */
|
||
for (uint32 s = 0u; s < UDPSS_MAX_SIGNALS; s++) {
|
||
pushCursor_[idx][s] = 0u;
|
||
}
|
||
sessions_[idx].SetRingCapacities(ringTemporal_, ringScalar_);
|
||
bool ok = sessions_[idx].Initialise(id, lbl, host,
|
||
static_cast<uint16>(port), maxPoints_,
|
||
mcGroup, dataPort);
|
||
if (ok) {
|
||
sessions_[idx].SetRecorderConfig(recorderCfg_);
|
||
ok = sessions_[idx].Start();
|
||
}
|
||
|
||
if (ok) {
|
||
sessionActive_[idx] = true;
|
||
numSessions_++;
|
||
}
|
||
sessionsMutex_.FastUnLock();
|
||
|
||
if (ok) {
|
||
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
|
||
"StreamHub: added session '%s' (%s:%u).", id, host, port);
|
||
}
|
||
return ok;
|
||
}
|
||
|
||
void StreamHub::LoadSourcesFile() {
|
||
if (sourcesFile_.Size() == 0u) { return; }
|
||
|
||
FILE *f = fopen(sourcesFile_.Buffer(), "rb");
|
||
if (f == static_cast<FILE *>(0)) { return; } /* missing file is fine */
|
||
|
||
(void) fseek(f, 0, SEEK_END);
|
||
const long fsz = ftell(f);
|
||
(void) fseek(f, 0, SEEK_SET);
|
||
if ((fsz <= 0) || (fsz > (1L << 20))) {
|
||
(void) fclose(f);
|
||
return;
|
||
}
|
||
char *data = new char[static_cast<uint32>(fsz) + 1u];
|
||
const MARTe::osulong nRead = fread(data, 1u, static_cast<MARTe::osulong>(fsz), f);
|
||
data[nRead] = '\0';
|
||
(void) fclose(f);
|
||
|
||
/* JSON array of flat objects — iterate over each {...} block. */
|
||
uint32 nLoaded = 0u;
|
||
const char *p = data;
|
||
while ((p = strchr(p, '{')) != static_cast<const char *>(0)) {
|
||
const char *end = strchr(p, '}');
|
||
if (end == static_cast<const char *>(0)) { break; }
|
||
|
||
uint32 objLen = static_cast<uint32>(end - p) + 1u;
|
||
if (objLen > 1023u) { objLen = 1023u; }
|
||
char obj[1024];
|
||
memcpy(obj, p, objLen);
|
||
obj[objLen] = '\0';
|
||
|
||
char label[128] = "";
|
||
char addr[80] = "";
|
||
char mcGroup[64] = "";
|
||
float64 dataPortF = 0.0;
|
||
JsonGetString(obj, "label", label, sizeof(label));
|
||
JsonGetString(obj, "addr", addr, sizeof(addr));
|
||
JsonGetString(obj, "multicastGroup", mcGroup, sizeof(mcGroup));
|
||
JsonGetFloat(obj, "dataPort", dataPortF);
|
||
|
||
if (AddSourceInternal(label, addr, mcGroup,
|
||
static_cast<uint16>(dataPortF))) {
|
||
nLoaded++;
|
||
}
|
||
p = end + 1;
|
||
}
|
||
delete[] data;
|
||
|
||
if (nLoaded > 0u) {
|
||
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
|
||
"StreamHub: loaded %u source(s) from '%s'.",
|
||
nLoaded, sourcesFile_.Buffer());
|
||
}
|
||
}
|
||
|
||
void StreamHub::HandleSaveSources() {
|
||
if (sourcesFile_.Size() == 0u) { return; }
|
||
|
||
FILE *f = fopen(sourcesFile_.Buffer(), "wb");
|
||
if (f == static_cast<FILE *>(0)) {
|
||
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Warning,
|
||
"StreamHub: cannot write sources file '%s'.", sourcesFile_.Buffer());
|
||
return;
|
||
}
|
||
|
||
uint32 nSaved = 0u;
|
||
(void) fprintf(f, "[\n");
|
||
for (uint32 i = 0u; i < kMaxSessions; i++) {
|
||
if (!sessionActive_[i]) { continue; }
|
||
StreamString lbl = sessions_[i].GetLabel();
|
||
StreamString adr = sessions_[i].GetAddr();
|
||
StreamString mcg = sessions_[i].GetMulticastGroup();
|
||
const uint16 prt = sessions_[i].GetPort();
|
||
const uint16 dpt = sessions_[i].GetDataPort();
|
||
|
||
(void) fprintf(f, "%s {\n \"label\": \"%s\",\n \"addr\": \"%s:%u\"",
|
||
(nSaved > 0u) ? ",\n" : "",
|
||
lbl.Buffer(), adr.Buffer(), static_cast<uint32>(prt));
|
||
if (mcg.Size() > 0u) {
|
||
(void) fprintf(f, ",\n \"multicastGroup\": \"%s\"", mcg.Buffer());
|
||
if (dpt > 0u) {
|
||
(void) fprintf(f, ",\n \"dataPort\": %u",
|
||
static_cast<uint32>(dpt));
|
||
}
|
||
}
|
||
(void) fprintf(f, "\n }");
|
||
nSaved++;
|
||
}
|
||
(void) fprintf(f, "\n]\n");
|
||
(void) fclose(f);
|
||
|
||
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
|
||
"StreamHub: saved %u source(s) to '%s'.", nSaved, sourcesFile_.Buffer());
|
||
}
|
||
|
||
void StreamHub::HandleRemoveSource(const char *json) {
|
||
char id[64] = "";
|
||
JsonGetString(json, "id", id, sizeof(id));
|
||
|
||
/* Slots are never compacted (sessions are not copyable): deactivate the
|
||
* slot so all iteration paths skip it, then stop the client thread. The
|
||
* slot is reused by the next AddSourceInternal. */
|
||
(void) sessionsMutex_.FastLock();
|
||
uint32 found = kMaxSessions;
|
||
for (uint32 i = 0u; i < kMaxSessions; i++) {
|
||
if (!sessionActive_[i]) { continue; }
|
||
StreamString sid = sessions_[i].GetId();
|
||
if (strcmp(sid.Buffer(), id) == 0) {
|
||
found = i;
|
||
break;
|
||
}
|
||
}
|
||
if (found < kMaxSessions) {
|
||
sessionActive_[found] = false;
|
||
configBroadcast_[found] = false; /* allow re-broadcast on slot reuse */
|
||
numSessions_--;
|
||
}
|
||
sessionsMutex_.FastUnLock();
|
||
|
||
if (found == kMaxSessions) { return; }
|
||
|
||
sessions_[found].Stop();
|
||
|
||
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
|
||
"StreamHub: removed session '%s'.", id);
|
||
BroadcastSources();
|
||
}
|
||
|
||
void StreamHub::HandleGetSources() {
|
||
BroadcastSources();
|
||
}
|
||
|
||
void StreamHub::HandleGetConfig(const char *json) {
|
||
char id[64] = "";
|
||
JsonGetString(json, "sourceId", id, sizeof(id));
|
||
|
||
for (uint32 i = 0u; i < kMaxSessions; i++) {
|
||
if (!sessionActive_[i]) { continue; }
|
||
StreamString sid = sessions_[i].GetId();
|
||
if (strcmp(sid.Buffer(), id) == 0) {
|
||
BroadcastConfig(i);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
void StreamHub::HandleGetStats() {
|
||
PushStats();
|
||
}
|
||
|
||
void StreamHub::HandleArm() {
|
||
rearmPending_ = false;
|
||
trigger_.Arm();
|
||
BroadcastTriggerState();
|
||
}
|
||
|
||
void StreamHub::HandleDisarm() {
|
||
rearmPending_ = false;
|
||
trigger_.Disarm();
|
||
BroadcastTriggerState();
|
||
}
|
||
|
||
void StreamHub::HandleRearm() {
|
||
/* Single-mode manual rearm (TRIGGERED → ARMED); same as Arm. */
|
||
HandleArm();
|
||
}
|
||
|
||
void StreamHub::HandleTrigStop(const char *json) {
|
||
/* {"type":"trigStop","stopped":bool} — absent "stopped" toggles. */
|
||
bool stopped = !trigger_.GetStopped();
|
||
JsonGetBool(json, "stopped", stopped);
|
||
trigger_.SetStopped(stopped);
|
||
if (stopped) { rearmPending_ = false; }
|
||
BroadcastTriggerState();
|
||
}
|
||
|
||
void StreamHub::HandleSetTrigger(const char *json) {
|
||
/* Web client shape:
|
||
* {"type":"setTrigger","signal":"src:sig[i]","edge":"rising|falling|both",
|
||
* "threshold":F,"windowSec":F,"prePercent":F,"mode":"normal|single"} */
|
||
TriggerConfig cfg = trigger_.GetConfig();
|
||
|
||
char key[160] = "";
|
||
char edge[16] = "";
|
||
char mode[16] = "";
|
||
float64 thr = cfg.threshold;
|
||
float64 winSec = cfg.windowSec;
|
||
float64 prePct = cfg.prePercent;
|
||
|
||
if (JsonGetString(json, "signal", key, sizeof(key))) {
|
||
cfg.signalKey = key;
|
||
}
|
||
if (JsonGetString(json, "edge", edge, sizeof(edge))) {
|
||
if (strcmp(edge, "falling") == 0) { cfg.edge = kEdgeFalling; }
|
||
else if (strcmp(edge, "both") == 0) { cfg.edge = kEdgeBoth; }
|
||
else { cfg.edge = kEdgeRising; }
|
||
}
|
||
if (JsonGetString(json, "mode", mode, sizeof(mode))) {
|
||
cfg.mode = (strcmp(mode, "single") == 0) ? kTrigSingle : kTrigNormal;
|
||
}
|
||
if (JsonGetFloat(json, "threshold", thr)) { cfg.threshold = thr; }
|
||
if (JsonGetFloat(json, "windowSec", winSec)) { cfg.windowSec = winSec; }
|
||
if (JsonGetFloat(json, "prePercent", prePct)) { cfg.prePercent = prePct; }
|
||
|
||
trigger_.SetConfig(cfg);
|
||
BroadcastTriggerState();
|
||
}
|
||
|
||
/*---------------------------------------------------------------------------*/
|
||
/* Trigger servicing (push thread) */
|
||
/*---------------------------------------------------------------------------*/
|
||
|
||
void StreamHub::TriggerTick(float64 wallNowS) {
|
||
/* Capture-margin: wait a little past the post window so the rings have
|
||
* received the last post-trigger samples (web client used 120 ms). */
|
||
static const float64 kCaptureMarginS = 0.15;
|
||
static const float64 kAutoRearmDelayS = 0.2;
|
||
|
||
const TrigState st = trigger_.GetState();
|
||
|
||
if (st == kTrigCollecting) {
|
||
float64 trigTime = 0.0;
|
||
float64 preSec = 0.0;
|
||
float64 postSec = 0.0;
|
||
if (trigger_.GetFiredWindow(trigTime, preSec, postSec) &&
|
||
(wallNowS >= (trigTime + postSec + kCaptureMarginS))) {
|
||
BroadcastTriggerCapture(trigTime, preSec, postSec);
|
||
trigger_.MarkTriggered();
|
||
TriggerConfig cfg = trigger_.GetConfig();
|
||
if ((cfg.mode == kTrigNormal) && !trigger_.GetStopped()) {
|
||
rearmPending_ = true;
|
||
rearmAtWallS_ = wallNowS + kAutoRearmDelayS;
|
||
}
|
||
}
|
||
}
|
||
else if ((st == kTrigTriggered) && rearmPending_ &&
|
||
(wallNowS >= rearmAtWallS_)) {
|
||
rearmPending_ = false;
|
||
if (!trigger_.GetStopped()) {
|
||
trigger_.Arm();
|
||
}
|
||
}
|
||
|
||
const TrigState cur = trigger_.GetState();
|
||
if (cur != lastTrigState_) {
|
||
lastTrigState_ = cur;
|
||
BroadcastTriggerState();
|
||
}
|
||
}
|
||
|
||
void StreamHub::BroadcastTriggerState() {
|
||
const TrigState st = trigger_.GetState();
|
||
TriggerConfig cfg = trigger_.GetConfig();
|
||
const bool stopped = trigger_.GetStopped();
|
||
lastTrigState_ = st;
|
||
|
||
const char *stateStr =
|
||
(st == kTrigArmed) ? "armed" :
|
||
(st == kTrigCollecting) ? "collecting" :
|
||
(st == kTrigTriggered) ? "triggered" : "idle";
|
||
const char *modeStr = (cfg.mode == kTrigSingle) ? "single" : "normal";
|
||
|
||
char buf[256];
|
||
int n;
|
||
float64 trigTime = 0.0;
|
||
float64 preSec = 0.0;
|
||
float64 postSec = 0.0;
|
||
if (((st == kTrigCollecting) || (st == kTrigTriggered)) &&
|
||
trigger_.GetFiredWindow(trigTime, preSec, postSec)) {
|
||
n = snprintf(buf, sizeof(buf),
|
||
"{\"type\":\"triggerState\",\"state\":\"%s\",\"mode\":\"%s\","
|
||
"\"stopped\":%s,\"trigTime\":%.17g}",
|
||
stateStr, modeStr, (stopped ? "true" : "false"), trigTime);
|
||
} else {
|
||
n = snprintf(buf, sizeof(buf),
|
||
"{\"type\":\"triggerState\",\"state\":\"%s\",\"mode\":\"%s\","
|
||
"\"stopped\":%s}",
|
||
stateStr, modeStr, (stopped ? "true" : "false"));
|
||
}
|
||
if (n > 0) {
|
||
wsServer_.BroadcastText(buf, static_cast<uint32>(n));
|
||
}
|
||
}
|
||
|
||
void StreamHub::BroadcastTriggerCapture(float64 trigTime, float64 preSec,
|
||
float64 postSec) {
|
||
const float64 t0 = trigTime - preSec;
|
||
const float64 t1 = trigTime + postSec;
|
||
|
||
/* Read scratch sized for the largest ring; LTTB scratch for the cap. */
|
||
const uint32 scratchCap = (ringTemporal_ > ringScalar_) ? ringTemporal_
|
||
: ringScalar_;
|
||
float64 *tRaw = new float64[scratchCap];
|
||
float64 *vRaw = new float64[scratchCap];
|
||
float64 *tDec = new float64[kTrigCapturePts];
|
||
float64 *vDec = new float64[kTrigCapturePts];
|
||
|
||
uint32 cap = 1u << 20;
|
||
uint8 *buf = new uint8[cap];
|
||
uint32 off = 0u;
|
||
|
||
/* Header: [u8 2][f64 trigTime][f64 preSec][f64 postSec][u32 nSig] */
|
||
buf[off++] = 2u;
|
||
memcpy(buf + off, &trigTime, 8u); off += 8u;
|
||
memcpy(buf + off, &preSec, 8u); off += 8u;
|
||
memcpy(buf + off, &postSec, 8u); off += 8u;
|
||
const uint32 nSigOff = off;
|
||
uint32 nSigWritten = 0u;
|
||
off += 4u;
|
||
|
||
for (uint32 i = 0u; i < kMaxSessions; i++) {
|
||
if (!sessionActive_[i]) { continue; }
|
||
UDPSourceSession &sess = sessions_[i];
|
||
if (!sess.IsConfigured()) { continue; }
|
||
|
||
StreamString sid = sess.GetId();
|
||
const uint32 numSigs = sess.GetNumSignals();
|
||
|
||
for (uint32 s = 0u; s < numSigs; s++) {
|
||
MARTe::UDPSSignalDescriptor desc;
|
||
if (!sess.GetSignalDescriptor(s, desc)) { continue; }
|
||
|
||
const uint32 nRaw = sess.ReadSignalRange(s, t0, t1,
|
||
tRaw, vRaw, scratchCap);
|
||
if (nRaw == 0u) { continue; }
|
||
|
||
const float64 *tOut = tRaw;
|
||
const float64 *vOut = vRaw;
|
||
uint32 nOut = nRaw;
|
||
if (nRaw > kTrigCapturePts) {
|
||
nOut = LTTBDecimate(tRaw, vRaw, nRaw, tDec, vDec,
|
||
kTrigCapturePts);
|
||
tOut = tDec;
|
||
vOut = vDec;
|
||
}
|
||
|
||
char fullKey[192];
|
||
const int kn = snprintf(fullKey, sizeof(fullKey), "%s:%s",
|
||
sid.Buffer(), desc.name);
|
||
if (kn <= 0) { continue; }
|
||
const uint32 keyLen = static_cast<uint32>(kn);
|
||
|
||
const uint32 need = 2u + keyLen + 4u + nOut * 16u;
|
||
if ((off + need) > cap) {
|
||
uint32 newCap = cap * 2u;
|
||
while ((off + need) > newCap) { newCap *= 2u; }
|
||
uint8 *nb = new uint8[newCap];
|
||
memcpy(nb, buf, off);
|
||
delete[] buf;
|
||
buf = nb;
|
||
cap = newCap;
|
||
}
|
||
|
||
buf[off++] = static_cast<uint8>( keyLen & 0xFFu);
|
||
buf[off++] = static_cast<uint8>((keyLen >> 8) & 0xFFu);
|
||
memcpy(buf + off, fullKey, keyLen);
|
||
off += keyLen;
|
||
|
||
buf[off++] = static_cast<uint8>( nOut & 0xFFu);
|
||
buf[off++] = static_cast<uint8>((nOut >> 8) & 0xFFu);
|
||
buf[off++] = static_cast<uint8>((nOut >> 16) & 0xFFu);
|
||
buf[off++] = static_cast<uint8>((nOut >> 24) & 0xFFu);
|
||
|
||
memcpy(buf + off, tOut, nOut * sizeof(float64));
|
||
off += nOut * 8u;
|
||
memcpy(buf + off, vOut, nOut * sizeof(float64));
|
||
off += nOut * 8u;
|
||
|
||
nSigWritten++;
|
||
}
|
||
}
|
||
|
||
/* Patch nSig */
|
||
buf[nSigOff] = static_cast<uint8>( nSigWritten & 0xFFu);
|
||
buf[nSigOff + 1u] = static_cast<uint8>((nSigWritten >> 8) & 0xFFu);
|
||
buf[nSigOff + 2u] = static_cast<uint8>((nSigWritten >> 16) & 0xFFu);
|
||
buf[nSigOff + 3u] = static_cast<uint8>((nSigWritten >> 24) & 0xFFu);
|
||
|
||
wsServer_.BroadcastBinary(buf, off);
|
||
|
||
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
|
||
"StreamHub: trigger capture broadcast (%u signal(s), %u bytes).",
|
||
nSigWritten, off);
|
||
|
||
delete[] buf;
|
||
delete[] tRaw; delete[] vRaw;
|
||
delete[] tDec; delete[] vDec;
|
||
}
|
||
|
||
void StreamHub::HandleZoom(const char *json, uint32 slotIdx) {
|
||
/* Request : {"type":"zoom","reqId":N,"t0":F,"t1":F,"n":N,
|
||
* "signals":"src:sig,src:sig2"}
|
||
* Response: {"type":"zoom","reqId":N,
|
||
* "signals":{"src:sig":{"t":[...],"v":[...]},...}}
|
||
* Sent unicast to the requesting client (Go hub /api/zoom shape). */
|
||
float64 t0 = 0.0, t1 = 0.0;
|
||
float64 nF = 2400.0;
|
||
float64 reqIdF = 0.0;
|
||
|
||
JsonGetFloat(json, "t0", t0);
|
||
JsonGetFloat(json, "t1", t1);
|
||
JsonGetFloat(json, "reqId", reqIdF);
|
||
const bool haveN = JsonGetFloat(json, "n", nF);
|
||
|
||
/* n semantics (Go hub): missing → 2400; ≤0 → no decimation; <10 → 2400 */
|
||
uint32 maxOut = 2400u;
|
||
if (haveN) {
|
||
if (nF <= 0.0) { maxOut = 0u; /* 0 = no decimation */ }
|
||
else if (nF < 10.0) { maxOut = 2400u; }
|
||
else { maxOut = static_cast<uint32>(nF); }
|
||
}
|
||
|
||
/* Comma-separated full keys "src:sig" */
|
||
static const uint32 kKeysBuf = 8192u;
|
||
char *keys = new char[kKeysBuf];
|
||
keys[0] = '\0';
|
||
JsonGetString(json, "signals", keys, kKeysBuf);
|
||
|
||
/* Read scratch sized for the largest possible ring (no double decimation:
|
||
* the whole [t0,t1] slice is read, then LTTB'd once to maxOut). */
|
||
const uint32 scratchCap = (ringTemporal_ > ringScalar_) ? ringTemporal_
|
||
: ringScalar_;
|
||
float64 *tRaw = new float64[scratchCap];
|
||
float64 *vRaw = new float64[scratchCap];
|
||
float64 *tDec = (maxOut > 0u) ? new float64[maxOut] : static_cast<float64 *>(0);
|
||
float64 *vDec = (maxOut > 0u) ? new float64[maxOut] : static_cast<float64 *>(0);
|
||
|
||
uint32 cap = 65536u;
|
||
char *buf = new char[cap];
|
||
uint32 off = 0u;
|
||
|
||
JsonAppendf(buf, off, cap, "{\"type\":\"zoom\",\"reqId\":%u,\"signals\":{",
|
||
static_cast<uint32>(reqIdF));
|
||
|
||
bool first = true;
|
||
char *tok = keys;
|
||
while ((tok != static_cast<char *>(0)) && (*tok != '\0') && (t1 > t0)) {
|
||
char *next = strchr(tok, ',');
|
||
if (next != static_cast<char *>(0)) { *next = '\0'; next++; }
|
||
while (*tok == ' ') { tok++; }
|
||
|
||
char *colon = strchr(tok, ':');
|
||
if (colon == static_cast<char *>(0)) { tok = next; continue; }
|
||
*colon = '\0';
|
||
const char *srcId = tok;
|
||
const char *sigName = colon + 1;
|
||
|
||
for (uint32 i = 0u; i < kMaxSessions; i++) {
|
||
if (!sessionActive_[i]) { continue; }
|
||
StreamString sid = sessions_[i].GetId();
|
||
if (strcmp(sid.Buffer(), srcId) != 0) { continue; }
|
||
|
||
UDPSourceSession &sess = sessions_[i];
|
||
const uint32 numSigs = sess.GetNumSignals();
|
||
for (uint32 s = 0u; s < numSigs; s++) {
|
||
MARTe::UDPSSignalDescriptor desc;
|
||
if (!sess.GetSignalDescriptor(s, desc)) { continue; }
|
||
if (strcmp(desc.name, sigName) != 0) { continue; }
|
||
|
||
const uint32 nRaw = sess.ReadSignalRange(s, t0, t1,
|
||
tRaw, vRaw, scratchCap);
|
||
if (nRaw == 0u) { break; }
|
||
|
||
const float64 *tOut = tRaw;
|
||
const float64 *vOut = vRaw;
|
||
uint32 nOut = nRaw;
|
||
if ((maxOut > 0u) && (nRaw > maxOut)) {
|
||
nOut = LTTBDecimate(tRaw, vRaw, nRaw, tDec, vDec, maxOut);
|
||
tOut = tDec;
|
||
vOut = vDec;
|
||
}
|
||
|
||
/* %.17g keeps full float64 precision for Unix-epoch times;
|
||
* %.9g is plenty for float32-quality values. */
|
||
JsonAppendf(buf, off, cap, "%s\"%s:%s\":{\"t\":[",
|
||
(first ? "" : ","), srcId, sigName);
|
||
first = false;
|
||
for (uint32 p = 0u; p < nOut; p++) {
|
||
JsonAppendf(buf, off, cap, "%s%.17g",
|
||
(p > 0u ? "," : ""), tOut[p]);
|
||
}
|
||
JsonAppendf(buf, off, cap, "],\"v\":[");
|
||
for (uint32 p = 0u; p < nOut; p++) {
|
||
JsonAppendf(buf, off, cap, "%s%.9g",
|
||
(p > 0u ? "," : ""), vOut[p]);
|
||
}
|
||
JsonAppendf(buf, off, cap, "]}");
|
||
break;
|
||
}
|
||
break;
|
||
}
|
||
tok = next;
|
||
}
|
||
|
||
JsonAppendf(buf, off, cap, "}}");
|
||
wsServer_.SendText(slotIdx, buf, off);
|
||
|
||
delete[] buf;
|
||
delete[] keys;
|
||
delete[] tRaw; delete[] vRaw;
|
||
delete[] tDec; delete[] vDec;
|
||
}
|
||
|
||
void StreamHub::HandleHistoryZoom(const char *json, uint32 slotIdx) {
|
||
if (!history_.IsEnabled()) {
|
||
const char *msg = "{\"type\":\"historyZoom\",\"error\":\"history not enabled\"}";
|
||
wsServer_.SendText(slotIdx, msg, static_cast<uint32>(strlen(msg)));
|
||
return;
|
||
}
|
||
|
||
float64 t0 = 0.0, t1 = 0.0;
|
||
float64 nF = 2400.0;
|
||
float64 reqIdF = 0.0;
|
||
|
||
JsonGetFloat(json, "t0", t0);
|
||
JsonGetFloat(json, "t1", t1);
|
||
JsonGetFloat(json, "reqId", reqIdF);
|
||
const bool haveN = JsonGetFloat(json, "n", nF);
|
||
|
||
uint32 maxOut = 2400u;
|
||
if (haveN) {
|
||
if (nF <= 0.0) { maxOut = 0u; }
|
||
else if (nF < 10.0) { maxOut = 2400u; }
|
||
else { maxOut = static_cast<uint32>(nF); }
|
||
}
|
||
|
||
/* Parse comma-separated full keys "src:sig" */
|
||
static const uint32 kKeysBuf = 8192u;
|
||
char *keys = new char[kKeysBuf];
|
||
keys[0] = '\0';
|
||
JsonGetString(json, "signals", keys, kKeysBuf);
|
||
|
||
/* Allocate scratch for the largest possible read */
|
||
const uint32 scratchCap = 2000000u; /* 2M pts should be enough */
|
||
float64 *tRaw = new float64[scratchCap];
|
||
float64 *vRaw = new float64[scratchCap];
|
||
float64 *tDec = (maxOut > 0u) ? new float64[maxOut] : static_cast<float64 *>(0);
|
||
float64 *vDec = (maxOut > 0u) ? new float64[maxOut] : static_cast<float64 *>(0);
|
||
|
||
uint32 cap = 65536u;
|
||
char *buf = new char[cap];
|
||
uint32 off = 0u;
|
||
|
||
JsonAppendf(buf, off, cap, "{\"type\":\"historyZoom\",\"reqId\":%u,\"signals\":{",
|
||
static_cast<uint32>(reqIdF));
|
||
|
||
bool first = true;
|
||
char *tok = keys;
|
||
while ((tok != static_cast<char *>(0)) && (*tok != '\0') && (t1 > t0)) {
|
||
char *next = strchr(tok, ',');
|
||
if (next != static_cast<char *>(0)) { *next = '\0'; next++; }
|
||
while (*tok == ' ') { tok++; }
|
||
|
||
char *colon = strchr(tok, ':');
|
||
if (colon == static_cast<char *>(0)) { tok = next; continue; }
|
||
*colon = '\0';
|
||
const char *srcId = tok;
|
||
const char *sigName = colon + 1;
|
||
|
||
uint32 nRaw = history_.ReadRange(srcId, sigName, t0, t1,
|
||
tRaw, vRaw, scratchCap);
|
||
if (nRaw > 0u) {
|
||
const float64 *tOut = tRaw;
|
||
const float64 *vOut = vRaw;
|
||
uint32 nOut = nRaw;
|
||
if ((maxOut > 0u) && (nRaw > maxOut)) {
|
||
nOut = LTTBDecimate(tRaw, vRaw, nRaw, tDec, vDec, maxOut);
|
||
tOut = tDec;
|
||
vOut = vDec;
|
||
}
|
||
|
||
JsonAppendf(buf, off, cap, "%s\"%s:%s\":{\"t\":[",
|
||
(first ? "" : ","), srcId, sigName);
|
||
first = false;
|
||
for (uint32 p = 0u; p < nOut; p++) {
|
||
JsonAppendf(buf, off, cap, "%s%.17g",
|
||
(p > 0u ? "," : ""), tOut[p]);
|
||
}
|
||
JsonAppendf(buf, off, cap, "],\"v\":[");
|
||
for (uint32 p = 0u; p < nOut; p++) {
|
||
JsonAppendf(buf, off, cap, "%s%.9g",
|
||
(p > 0u ? "," : ""), vOut[p]);
|
||
}
|
||
JsonAppendf(buf, off, cap, "]}");
|
||
}
|
||
tok = next;
|
||
}
|
||
|
||
JsonAppendf(buf, off, cap, "}}");
|
||
wsServer_.SendText(slotIdx, buf, off);
|
||
|
||
delete[] buf;
|
||
delete[] keys;
|
||
delete[] tRaw; delete[] vRaw;
|
||
delete[] tDec; delete[] vDec;
|
||
}
|
||
|
||
void StreamHub::HandleHistoryInfo(uint32 slotIdx) {
|
||
uint32 cap = 8192u;
|
||
char *buf = new char[cap];
|
||
uint32 off = 0u;
|
||
|
||
JsonAppendf(buf, off, cap, "{\"type\":\"historyInfo\",");
|
||
history_.AppendInfoJSON(buf, off, cap);
|
||
JsonAppendf(buf, off, cap, "}");
|
||
|
||
wsServer_.SendText(slotIdx, buf, off);
|
||
delete[] buf;
|
||
}
|
||
|
||
void StreamHub::HandleSetMaxPoints(const char *json) {
|
||
float64 mpF = static_cast<float64>(maxPoints_);
|
||
JsonGetFloat(json, "maxPoints", mpF);
|
||
uint32 newMax = static_cast<uint32>(mpF);
|
||
if (newMax < 2u) { newMax = 2u; }
|
||
|
||
/* Defer to the push loop: the WS read thread must not mutate session
|
||
* read state while PushData is iterating. The push loop applies the
|
||
* value and broadcasts "maxPointsUpdated". */
|
||
pendingMaxPoints_ = newMax;
|
||
pendingMaxPointsSet_ = true;
|
||
}
|
||
|
||
void StreamHub::HandlePing(uint32 slotIdx) {
|
||
const char *msg = "{\"type\":\"pong\"}";
|
||
wsServer_.SendText(slotIdx, msg, static_cast<uint32>(strlen(msg)));
|
||
}
|
||
|
||
/*---------------------------------------------------------------------------*/
|
||
/* Binary recorder commands */
|
||
/*---------------------------------------------------------------------------*/
|
||
|
||
void StreamHub::HandleRecStart(const char *json) {
|
||
char wantId[64] = "";
|
||
const bool filtered = JsonGetString(json, "sourceId", wantId, sizeof(wantId));
|
||
for (uint32 i = 0u; i < kMaxSessions; i++) {
|
||
if (!sessionActive_[i]) { continue; }
|
||
if (!sessions_[i].IsRecorderEnabled()) { continue; }
|
||
if (filtered) {
|
||
StreamString sid = sessions_[i].GetId();
|
||
if (strcmp(sid.Buffer(), wantId) != 0) { continue; }
|
||
}
|
||
sessions_[i].RequestRecArm();
|
||
}
|
||
BroadcastRecStatus();
|
||
}
|
||
|
||
void StreamHub::HandleRecStop(const char *json) {
|
||
char wantId[64] = "";
|
||
const bool filtered = JsonGetString(json, "sourceId", wantId, sizeof(wantId));
|
||
for (uint32 i = 0u; i < kMaxSessions; i++) {
|
||
if (!sessionActive_[i]) { continue; }
|
||
if (!sessions_[i].IsRecorderEnabled()) { continue; }
|
||
if (filtered) {
|
||
StreamString sid = sessions_[i].GetId();
|
||
if (strcmp(sid.Buffer(), wantId) != 0) { continue; }
|
||
}
|
||
sessions_[i].RequestRecDisarm();
|
||
}
|
||
BroadcastRecStatus();
|
||
}
|
||
|
||
void StreamHub::AppendRecStatusJSON(char *&buf, uint32 &off, uint32 &cap) {
|
||
JsonAppendf(buf, off, cap, "{\"type\":\"recStatus\",\"sources\":{");
|
||
bool first = true;
|
||
for (uint32 i = 0u; i < kMaxSessions; i++) {
|
||
if (!sessionActive_[i]) { continue; }
|
||
if (!sessions_[i].IsRecorderEnabled()) { continue; }
|
||
|
||
bool recording = false;
|
||
char file[768] = "";
|
||
uint64 bytesWritten = 0u;
|
||
uint64 rowsWritten = 0u;
|
||
uint64 droppedRows = 0u;
|
||
uint64 freeMB = 0u;
|
||
sessions_[i].GetRecorderInfo(recording, file, sizeof(file),
|
||
bytesWritten, rowsWritten,
|
||
droppedRows, freeMB);
|
||
|
||
StreamString sid = sessions_[i].GetId();
|
||
JsonAppendf(buf, off, cap, "%s\"%s\":{\"recording\":%s,\"file\":\"%s\","
|
||
"\"bytesWritten\":%llu,\"rowsWritten\":%llu,"
|
||
"\"droppedRows\":%llu,\"freeMB\":%llu}",
|
||
first ? "" : ",", sid.Buffer(),
|
||
recording ? "true" : "false", file,
|
||
static_cast<unsigned long long>(bytesWritten),
|
||
static_cast<unsigned long long>(rowsWritten),
|
||
static_cast<unsigned long long>(droppedRows),
|
||
static_cast<unsigned long long>(freeMB));
|
||
first = false;
|
||
}
|
||
JsonAppendf(buf, off, cap, "}}");
|
||
}
|
||
|
||
void StreamHub::HandleRecInfo(uint32 slotIdx) {
|
||
uint32 cap = 4096u;
|
||
char *buf = new char[cap];
|
||
uint32 off = 0u;
|
||
AppendRecStatusJSON(buf, off, cap);
|
||
wsServer_.SendText(slotIdx, buf, off);
|
||
delete[] buf;
|
||
}
|
||
|
||
void StreamHub::BroadcastRecStatus() {
|
||
uint32 cap = 4096u;
|
||
char *buf = new char[cap];
|
||
uint32 off = 0u;
|
||
AppendRecStatusJSON(buf, off, cap);
|
||
wsServer_.BroadcastText(buf, off);
|
||
delete[] buf;
|
||
}
|
||
|
||
/*---------------------------------------------------------------------------*/
|
||
/* Tiny JSON helpers */
|
||
/*---------------------------------------------------------------------------*/
|
||
|
||
bool StreamHub::JsonGetString(const char *json, const char *key,
|
||
char *out, uint32 outSize) {
|
||
/* Look for "key":"value" */
|
||
char pattern[128];
|
||
snprintf(pattern, sizeof(pattern), "\"%s\":\"", key);
|
||
const char *p = strstr(json, pattern);
|
||
if (p == static_cast<const char *>(0)) { return false; }
|
||
p += strlen(pattern);
|
||
|
||
uint32 i = 0u;
|
||
while (*p != '\0' && *p != '"' && i < outSize - 1u) {
|
||
out[i++] = *p++;
|
||
}
|
||
out[i] = '\0';
|
||
return true;
|
||
}
|
||
|
||
bool StreamHub::JsonGetFloat(const char *json, const char *key, float64 &out) {
|
||
char pattern[128];
|
||
snprintf(pattern, sizeof(pattern), "\"%s\":", key);
|
||
const char *p = strstr(json, pattern);
|
||
if (p == static_cast<const char *>(0)) { return false; }
|
||
p += strlen(pattern);
|
||
while (*p == ' ') { p++; }
|
||
if (*p == '\0') { return false; }
|
||
out = strtod(p, static_cast<char **>(0));
|
||
return true;
|
||
}
|
||
|
||
bool StreamHub::JsonGetUint32(const char *json, const char *key, uint32 &out) {
|
||
float64 v = 0.0;
|
||
if (!JsonGetFloat(json, key, v)) { return false; }
|
||
out = static_cast<uint32>(v);
|
||
return true;
|
||
}
|
||
|
||
bool StreamHub::JsonGetBool(const char *json, const char *key, bool &out) {
|
||
char pattern[128];
|
||
snprintf(pattern, sizeof(pattern), "\"%s\":", key);
|
||
const char *p = strstr(json, pattern);
|
||
if (p == static_cast<const char *>(0)) { return false; }
|
||
p += strlen(pattern);
|
||
while (*p == ' ') { p++; }
|
||
if (strncmp(p, "true", 4u) == 0) {
|
||
out = true;
|
||
return true;
|
||
}
|
||
if (strncmp(p, "false", 5u) == 0) {
|
||
out = false;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
} /* namespace StreamHub */
|