Files
MARTe-Integrated-Components/Source/Applications/StreamHub/StreamHub.h
T
Martino FerrariandClaude Sonnet 4.6 42f5a726af fix: three StreamHub C++ defects from final code review
Finding 1 (HandleReloadConfig data loss): move ClearCalibration inside
LoadSourcesFile so the table is wiped only after a successful fread.
Adds a clearCalibration bool parameter (default false); the reload path
passes true, the startup path passes false.

Finding 2 (JSON injection via unit/source/signal): add JsonEscape()
static helper (escapes \", \\, \n \r \t, and \u00XX for other control
chars). Applied at all three emission sites: BroadcastCalibration,
HandleSaveSources, and BroadcastSources (label). Teach JsonGetString to
unescape the same set on read, so values round-trip correctly.

Finding 3 (%.17g verbosity): add ShortFloat() static helper that tries
%.15g then %.16g then %.17g, stopping at the first precision whose
strtod() output compares equal to the original. Applied at both float
emission sites. 0.1 now prints as "0.1", not "0.10000000000000001".

Minor: fix two inaccurate comments in StreamHub.h — the CalibrationEntry
rationale (not a 133 MB / address-limit issue; the real reason is no
per-entry heap churn, STL-free, trivially copyable) and "chars" to "bytes"
for the unit cap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-17 07:50:01 +02:00

319 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file StreamHub.h
* @brief Top-level orchestrator: N UDP sources, M WebSocket clients, push loop, trigger.
*
* StreamHub owns:
* - Up to kMaxSessions UDPSourceSessions (one per MARTe2 UDPStreamer source)
* - WSServer (multi-client WebSocket server)
* - TriggerEngine (continuous / armed / triggered FSM)
*
* The push loop runs at pushRateHz Hz and:
* 1. For each configured session, reads ring buffers, applies LTTB decimation,
* serializes a binary data frame, broadcasts to all WS clients.
* 2. At statsRateHz intervals, serializes a JSON stats frame and broadcasts.
*
* Commands are received as JSON text frames from any connected WS client via the
* WSCommandCallback interface.
*/
#ifndef STREAMHUB_H_
#define STREAMHUB_H_
#include "CompilerTypes.h"
#include "FastPollingMutexSem.h"
#include "StreamString.h"
#include "StructuredDataI.h"
#include "Threads.h"
#include "UDPSourceSession.h"
#include "WSServer.h"
#include "TriggerEngine.h"
#include "HistoryWriter.h"
namespace StreamHub {
using MARTe::uint8;
using MARTe::uint16;
using MARTe::uint32;
using MARTe::uint64;
using MARTe::float64;
using MARTe::StreamString;
using MARTe::FastPollingMutexSem;
using MARTe::StructuredDataI;
/** Maximum number of simultaneously connected UDPStreamer sources. */
static const uint32 kMaxSessions = 32u;
/** Maximum number of stored per-signal calibration entries. */
static const uint32 kMaxCalibration = 256u;
/** Maximum length of a calibration unit override (mirrors the Go maxUnitLen). */
static const uint32 kMaxUnitLen = 16u;
/**
* @brief One per-signal affine calibration: y = raw*scale + offset.
*
* Keyed by the source LABEL (not the runtime "sN" id, which is assigned in
* add-order and would rebind if the source list were reordered) and by the
* BASE signal name (no "[i]" suffix: one entry covers a whole array signal).
*
* Fixed-size char arrays are used deliberately: they avoid per-entry heap
* churn (no StreamString allocation per calibration slot), keep the type free
* of STL, and make a CalibrationEntry snapshot trivially copyable under the
* calibration mutex lock.
*/
struct CalibrationEntry {
char source[128]; ///< Source label
char signal[128]; ///< Base signal name (no "[i]" suffix)
char unit[17]; ///< Unit override (max kMaxUnitLen bytes + NUL)
MARTe::float64 scale;
MARTe::float64 offset;
};
/**
* @brief Top-level StreamHub orchestrator.
*
* Usage:
* StreamHub hub;
* hub.Initialise(cfg); // parse WSPort, MaxPoints, PushRate, Sources
* hub.Run(); // blocking push loop; returns on Stop()
*/
class StreamHub : public WSCommandCallback {
public:
StreamHub();
virtual ~StreamHub();
/**
* @brief Configure the hub from a MARTe2 StructuredDataI.
*
* Expected keys:
* WSPort (uint32, default 8090)
* MaxPoints (uint32, default 20000) — ring buffer capacity per signal
* PushRate (uint32, default 30) — push loop rate in Hz
* MaxPushPoints (uint32, default 500) — LTTB threshold for live push
* StatsRate (uint32, default 1) — stats broadcast rate in Hz
* +Sources { +<id> { Label=...; Addr=...; Port=... } }
*
* @return true on success.
*/
bool Initialise(StructuredDataI &cfg);
/**
* @brief Start the WebSocket server and run the push loop (blocking).
* Returns when Stop() is called (e.g. from SIGINT handler).
*/
bool Run();
/**
* @brief Signal the push loop to exit. Thread-safe.
*/
void Stop();
/* ---- WSCommandCallback (called from WS client read threads) ---------- */
virtual void OnWSCommand(const char *json, uint32 len, uint32 slotIdx);
virtual void OnWSClientConnected();
virtual void OnWSClientDisconnected();
private:
/* ---- Push loop helpers ----------------------------------------------- */
/** One push tick: for each session emit a binary frame; handle trigger. */
void PushData();
/** Serialize one binary data frame for session idx into pushBuf_. */
uint32 SerializeBinaryFrame(uint32 sessionIdx, uint8 *buf, uint32 bufSize);
/** Broadcast JSON stats for all sessions. */
void PushStats();
/** Broadcast {"type":"sources"} listing. */
void BroadcastSources();
/** Broadcast {"type":"config","sourceId":...} for one session. */
void BroadcastConfig(uint32 sessionIdx);
/** Broadcast {"type":"calibration","cal":[...]} to all clients. */
void BroadcastCalibration();
/** Broadcast {"type":"configSaved"|"configReloaded","ok":...} to all clients. */
void BroadcastConfigAck(const char *msgType, bool ok, const char *errText);
/* ---- Trigger (push loop side) ----------------------------------------- */
/**
* @brief Per-tick trigger servicing: finalize captures, auto-rearm,
* broadcast state transitions.
*/
void TriggerTick(float64 wallNowS);
/** Broadcast {"type":"triggerState",...} reflecting the current FSM. */
void BroadcastTriggerState();
/**
* @brief Build and broadcast the version=2 binary capture frame:
* [u8 2][f64 trigTime][f64 preSec][f64 postSec][u32 nSig]
* {[u16 keyLen][fullKey][u32 N][t f64×N][v f64×N]}
*/
void BroadcastTriggerCapture(float64 trigTime, float64 preSec,
float64 postSec);
/* ---- Command handlers (called from OnWSCommand) ---------------------- */
void HandleAddSource(const char *json);
void HandleRemoveSource(const char *json);
void HandleSaveSources();
void HandleGetSources();
void HandleGetConfig(const char *json);
void HandleGetStats();
void HandleArm();
void HandleDisarm();
void HandleRearm();
void HandleTrigStop(const char *json);
void HandleSetTrigger(const char *json);
void HandleForceTrigger();
void HandleZoom(const char *json, uint32 slotIdx);
void HandleHistoryZoom(const char *json, uint32 slotIdx);
void HandleHistoryInfo(uint32 slotIdx);
void HandleSetMaxPoints(const char *json);
void HandlePing(uint32 slotIdx);
void HandleSetCalibration(const char *json);
void HandleReloadConfig();
/* ---- Binary recorder commands --------------------------------------- */
/** Arm recording on all enabled sessions (optional "sourceId" filter). */
void HandleRecStart(const char *json);
/** Disarm recording on all enabled sessions (optional "sourceId" filter). */
void HandleRecStop(const char *json);
/** Unicast a {"type":"recStatus",...} snapshot to one client. */
void HandleRecInfo(uint32 slotIdx);
/** Broadcast a {"type":"recStatus",...} snapshot to all clients. */
void BroadcastRecStatus();
/** Append the full recStatus message body into a growable buffer. */
void AppendRecStatusJSON(char *&buf, uint32 &off, uint32 &cap);
/* ---- Sources persistence (Go SourceConfig schema) --------------------- */
/**
* @brief Start a new dynamic session from "host:port" + options.
* Generates the session id ("s1", "s2", ...).
* @return true if the session was started.
*/
bool AddSourceInternal(const char *label, const char *addrPort,
const char *mcGroup, uint16 dataPort);
/**
* @brief Load sources and calibration from sourcesFile_ (a flat JSON array
* of {"label","addr","multicastGroup","dataPort"} source blocks and
* {"source","signal","scale","offset","unit"} calibration blocks).
* @param skipActive when true, a source whose "host:port" is already
* streaming is left alone instead of being started a second time.
* @param clearCalibration when true, the calibration table is cleared
* after a successful fread (never before), so a transient I/O failure
* does not silently wipe user calibration data.
* @return true if the file was read.
*/
bool LoadSourcesFile(bool skipActive, bool clearCalibration = false);
/** @return true if a session for this "host:port" is already active. */
bool SourceIsActive(const char *addrPort);
/**
* @brief Store or replace one calibration entry. An identity entry
* (scale 1, offset 0, empty unit) removes any stored one instead.
* @return true if the entry was valid (and therefore stored or removed).
*/
bool SetCalibrationEntry(const char *source, const char *signal,
MARTe::float64 scale, MARTe::float64 offset,
const char *unit);
/** Drop every calibration entry (used by reload, which replaces wholesale). */
void ClearCalibration();
/* ---- Tiny JSON helpers ----------------------------------------------- */
/**
* @brief Extract a string field from flat JSON: "key":"value".
* @return true if found; value written into out (up to outSize-1 chars).
*/
static bool JsonGetString(const char *json, const char *key,
char *out, uint32 outSize);
/**
* @brief Extract a numeric field from flat JSON: "key":number.
* @return true if found.
*/
static bool JsonGetFloat(const char *json, const char *key, float64 &out);
static bool JsonGetUint32(const char *json, const char *key, uint32 &out);
static bool JsonGetBool(const char *json, const char *key, bool &out);
/* ---- Members --------------------------------------------------------- */
UDPSourceSession sessions_[kMaxSessions];
uint32 numSessions_; ///< Count of active sessions (not an upper slot bound)
bool sessionActive_[kMaxSessions]; ///< Per-slot in-use flag (slots are not compacted on removal)
bool configBroadcast_[kMaxSessions]; ///< True once BroadcastConfig was sent for this session
FastPollingMutexSem sessionsMutex_; ///< Serializes add/remove (WS threads); push thread reads flags lock-free
WSServer wsServer_;
TriggerEngine trigger_;
HistoryWriter history_;
/* Recorder configuration (parsed once, applied to each session). */
RecorderConfig recorderCfg_;
/* Configuration */
uint16 wsPort_;
uint32 maxPoints_;
uint32 pushRateHz_;
uint32 maxPushPoints_;
uint32 statsRateHz_;
uint32 ringTemporal_; ///< Ring capacity for multi-element (waveform) signals
uint32 ringScalar_; ///< Ring capacity for scalar signals
StreamString sourcesFile_; ///< Persistent dynamic source list (JSON)
uint32 nextSourceId_; ///< Counter for generated session ids ("sN")
CalibrationEntry *calibration_; ///< Heap-allocated array[kMaxCalibration]
uint32 numCalibration_;
FastPollingMutexSem calibrationMutex_; ///< Serializes calibration reads/writes
/* Push loop state */
volatile bool running_;
uint32 tickCount_; ///< incremented each push tick
/* Deferred setMaxPoints (applied by the push loop, not the WS thread) */
volatile bool pendingMaxPointsSet_;
uint32 pendingMaxPoints_;
/* Per-push scratch buffer (~8 MiB): one binary frame at a time */
static const uint32 kPushBufSize = 8u * 1024u * 1024u;
uint8 *pushBuf_;
/* Decimated output scratch (LTTB): maxPushPoints × 2 arrays per signal */
float64 *lttbT_;
float64 *lttbV_;
/* Ring-buffer read scratch for the push loop (preallocated in Initialise) */
static const uint32 kPushScratchPts = 262144u; ///< Max new pts/signal/tick
float64 *pushT_;
float64 *pushV_;
/* Per-(session, signal) monotonic read cursors for ReadSignalSince.
* Only new samples since the last tick are pushed (fixes re-LTTB
* corruption of overlapping windows). */
uint64 pushCursor_[kMaxSessions][UDPSS_MAX_SIGNALS];
/* Trigger servicing state (push thread only) */
static const uint32 kTrigCapturePts = 20000u; ///< LTTB cap per captured signal
TrigState lastTrigState_; ///< Last broadcast FSM state
bool rearmPending_; ///< Normal-mode auto-rearm scheduled
float64 rearmAtWallS_; ///< Wall time of the scheduled auto-rearm
};
} /* namespace StreamHub */
#endif /* STREAMHUB_H_ */