/** * @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; /** * @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 { + { 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); /* ---- 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 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); /* ---- 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 from sourcesFile_ (JSON array of * {"label","addr","multicastGroup","dataPort"}) and start them. */ void LoadSourcesFile(); /* ---- 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_; /* 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") /* 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_ */