feat(tcp): TCPStreamer DataSources, in-process TCPStreamService, stream multiplexing and TCPStreamJoin

Add TCPStreamer/TCPStreamerClient (TCP transport behind the UDPSServerI/UDPSClientI
seam), an optional single in-process TCPStreamService serving many streams over
worker threads, multiplexed SUBSCRIBE (comma list, stream index in fragmentIdx),
and TCPStreamJoin, which merges several streams into one sample-and-hold stream
paced by a master. StreamHub takes a comma list of streams over one connection.
Includes GTests, E2E scenarios s58-s73 and docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-11-06 13:41:27 +01:00
1 parent 5562877c99
commit 50d85363a3
79 files changed
+12348 -138

No files matched your search

+203 -9
View File
@@ -92,6 +92,7 @@ StreamHub::StreamHub()
memset(calibration_, 0, sizeof(CalibrationEntry) * kMaxCalibration);
for (uint32 i = 0u; i < kMaxSessions; i++) {
sessionActive_[i] = false;
sessionMux_[i] = -1;
configBroadcast_[i] = false;
capHarvested_[i] = false;
for (uint32 s = 0u; s < UDPSS_MAX_SIGNALS; s++) {
@@ -273,11 +274,33 @@ bool StreamHub::Initialise(StructuredDataI &cfg) {
}
cfg.Read("DataPort", dataPort32);
char transport[8] = "udp";
char streamName[256] = "";
StreamString trStr;
if (cfg.Read("Transport", trStr)) {
strncpy(transport, trStr.Buffer(), sizeof(transport) - 1u);
}
StreamString snStr;
if (cfg.Read("StreamName", snStr)) {
strncpy(streamName, snStr.Buffer(), sizeof(streamName) - 1u);
}
if ((strcmp(transport, "tcp") == 0) && (strchr(streamName, ',') != static_cast<char *>(0))) {
/* Several streams over one connection: slots are allocated by the helper. */
if (!AddMuxSourceInternal(label, addr, port32, addr, streamName, nodeId.Buffer())) {
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Warning,
"StreamHub: failed to start multiplexed source '%s'.", nodeId.Buffer());
}
cfg.MoveToAncestor(1u);
continue;
}
sessions_[numSessions_].SetRingCapacities(ringTemporal_, ringScalar_);
bool ok = sessions_[numSessions_].Initialise(
nodeId.Buffer(), label, addr,
static_cast<uint16>(port32), maxPoints_,
mcGroup, static_cast<uint16>(dataPort32));
mcGroup, static_cast<uint16>(dataPort32),
transport, streamName);
if (ok) {
sessions_[numSessions_].SetRecorderConfig(recorderCfg_);
@@ -1201,15 +1224,20 @@ void StreamHub::HandleAddSource(const char *json) {
JsonGetString(json, "addr", addr, sizeof(addr));
JsonGetString(json, "multicastGroup", mcGroup, sizeof(mcGroup));
JsonGetFloat(json, "dataPort", dataPortF);
char transport[8] = "udp";
char streamName[256] = "";
JsonGetString(json, "transport", transport, sizeof(transport));
JsonGetString(json, "streamName", streamName, sizeof(streamName));
if (AddSourceInternal(label, addr, mcGroup,
static_cast<uint16>(dataPortF))) {
static_cast<uint16>(dataPortF), transport, streamName)) {
BroadcastSources();
}
}
bool StreamHub::AddSourceInternal(const char *label, const char *addrPort,
const char *mcGroup, uint16 dataPort) {
const char *mcGroup, uint16 dataPort,
const char *transport, const char *streamName) {
if ((addrPort == static_cast<const char *>(0)) || (addrPort[0] == '\0')) {
return false;
}
@@ -1226,6 +1254,12 @@ bool StreamHub::AddSourceInternal(const char *label, const char *addrPort,
*colon = '\0';
}
if ((transport != static_cast<const char *>(0)) && (strcmp(transport, "tcp") == 0) &&
(streamName != static_cast<const char *>(0)) &&
(strchr(streamName, ',') != static_cast<const char *>(0))) {
return AddMuxSourceInternal(label, host, port, addrPort, streamName);
}
/* 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). */
@@ -1256,7 +1290,7 @@ bool StreamHub::AddSourceInternal(const char *label, const char *addrPort,
sessions_[idx].SetRingCapacities(ringTemporal_, ringScalar_);
bool ok = sessions_[idx].Initialise(id, lbl, host,
static_cast<uint16>(port), maxPoints_,
mcGroup, dataPort);
mcGroup, dataPort, transport, streamName);
if (ok) {
sessions_[idx].SetRecorderConfig(recorderCfg_);
ok = sessions_[idx].Start();
@@ -1275,14 +1309,151 @@ bool StreamHub::AddSourceInternal(const char *label, const char *addrPort,
return ok;
}
bool StreamHub::SourceIsActive(const char *addrPort) {
bool StreamHub::AddMuxSourceInternal(const char *label, const char *host, uint32 port,
const char *addrPort, const char *streamList,
const char *idPrefix) {
char list[MARTe::UDPS_TCP_MAX_SUBSCRIBE_PAYLOAD + 1u];
strncpy(list, streamList, sizeof(list) - 1u);
list[sizeof(list) - 1u] = '\0';
const char *names[kMaxSessions];
uint32 n = 0u;
char *cur = list;
while (cur != static_cast<char *>(0)) {
char *comma = strchr(cur, ',');
if (comma != static_cast<char *>(0)) { *comma = '\0'; }
if ((cur[0] == '\0') || (n >= kMaxSessions)) {
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Warning,
"StreamHub: bad stream list '%s' (empty name or more than %u streams).",
streamList, kMaxSessions);
return false;
}
names[n++] = cur;
cur = (comma != static_cast<char *>(0)) ? (comma + 1) : static_cast<char *>(0);
}
(void) sessionsMutex_.FastLock();
uint32 slots[kMaxSessions];
uint32 found = 0u;
for (uint32 i = 0u; (i < kMaxSessions) && (found < n); i++) {
if (!sessionActive_[i]) { slots[found++] = i; }
}
uint32 mux = kMaxSessions;
for (uint32 i = 0u; i < kMaxSessions; i++) {
if (muxClients_[i].client == static_cast<MARTe::TCPSClient *>(0)) { mux = i; break; }
}
if ((found < n) || (mux == kMaxSessions)) {
sessionsMutex_.FastUnLock();
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Warning,
"StreamHub: not enough free sessions for %u streams, ignoring addSource.", n);
return false;
}
MARTe::TCPSClient *client = new MARTe::TCPSClient();
ConfigurationDatabase cfg;
(void) cfg.Write("ServerAddr", host);
(void) cfg.Write("Port", port);
(void) cfg.Write("StreamName", streamList);
bool ok = client->Initialise(cfg) && (client->GetNumStreams() == n);
if (!ok) {
delete client;
sessionsMutex_.FastUnLock();
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Warning,
"StreamHub: could not set up the multiplexed receiver for '%s'.", streamList);
return false;
}
muxClients_[mux].client = client;
muxClients_[mux].refs = 0u;
const char *base = ((label != static_cast<const char *>(0)) && (label[0] != '\0')) ? label
: static_cast<const char *>(0);
/* Initialise every session (listeners attached) before any can receive. */
uint32 initialised = 0u;
for (uint32 k = 0u; (k < n) && ok; k++) {
const uint32 idx = slots[k];
char id[16 + 256];
if (idPrefix != static_cast<const char *>(0)) {
snprintf(id, sizeof(id), "%s_%s", idPrefix, names[k]);
} else {
snprintf(id, sizeof(id), "s%u", nextSourceId_++);
}
char lbl[160];
if (base != static_cast<const char *>(0)) {
snprintf(lbl, sizeof(lbl), "%s_%s", base, names[k]);
} else {
snprintf(lbl, sizeof(lbl), "%s", names[k]);
}
configBroadcast_[idx] = false;
for (uint32 s = 0u; s < UDPSS_MAX_SIGNALS; s++) { pushCursor_[idx][s] = 0u; }
sessions_[idx].SetRingCapacities(ringTemporal_, ringScalar_);
ok = sessions_[idx].Initialise(id, lbl, host, static_cast<uint16>(port), maxPoints_,
"", 0u, "tcp", names[k], client->GetStream(k));
if (ok) {
sessionMux_[idx] = static_cast<MARTe::int32>(mux);
muxClients_[mux].refs++;
initialised++;
}
}
for (uint32 k = 0u; (k < initialised) && ok; k++) {
sessions_[slots[k]].SetRecorderConfig(recorderCfg_);
ok = sessions_[slots[k]].Start();
}
if (ok) {
for (uint32 k = 0u; k < n; k++) {
sessionActive_[slots[k]] = true;
numSessions_++;
}
} else {
for (uint32 k = 0u; k < initialised; k++) { ReleaseMuxSlot(slots[k]); }
if (muxClients_[mux].client != static_cast<MARTe::TCPSClient *>(0)) {
delete muxClients_[mux].client; /* no session ever attached */
muxClients_[mux].client = static_cast<MARTe::TCPSClient *>(0);
}
}
sessionsMutex_.FastUnLock();
if (ok) {
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
"StreamHub: added %u multiplexed sessions over one connection (%s).", n, addrPort);
}
return ok;
}
void StreamHub::ReleaseMuxSlot(uint32 slot) {
/* Caller holds sessionsMutex_. */
if (sessionMux_[slot] < 0) { return; }
const uint32 m = static_cast<uint32>(sessionMux_[slot]);
sessions_[slot].DetachClient();
sessionMux_[slot] = -1;
if ((muxClients_[m].refs > 0u) && (--muxClients_[m].refs == 0u)) {
delete muxClients_[m].client;
muxClients_[m].client = static_cast<MARTe::TCPSClient *>(0);
}
}
bool StreamHub::SourceIsActive(const char *addrPort, const char *streamName) {
if (strchr(streamName, ',') != static_cast<const char *>(0)) {
/* A list is active when every stream of it is. */
char list[MARTe::UDPS_TCP_MAX_SUBSCRIBE_PAYLOAD + 1u];
strncpy(list, streamName, sizeof(list) - 1u);
list[sizeof(list) - 1u] = '\0';
bool all = true;
char *cur = list;
while (all && (cur != static_cast<char *>(0))) {
char *comma = strchr(cur, ',');
if (comma != static_cast<char *>(0)) { *comma = '\0'; }
all = SourceIsActive(addrPort, cur);
cur = (comma != static_cast<char *>(0)) ? (comma + 1) : static_cast<char *>(0);
}
return all;
}
for (uint32 i = 0u; i < kMaxSessions; i++) {
if (!sessionActive_[i]) { continue; }
StreamString adr = sessions_[i].GetAddr();
char cur[96];
(void) snprintf(cur, sizeof(cur), "%s:%u", adr.Buffer(),
static_cast<uint32>(sessions_[i].GetPort()));
if (strcmp(cur, addrPort) == 0) { return true; }
StreamString sn = sessions_[i].GetStreamName();
if ((strcmp(cur, addrPort) == 0) && (strcmp(sn.Buffer(), streamName) == 0)) { return true; }
}
return false;
}
@@ -1335,11 +1506,16 @@ bool StreamHub::LoadSourcesFile(bool skipActive, bool clearCalibration) {
(void) JsonGetString(obj, "label", label, sizeof(label));
(void) JsonGetString(obj, "multicastGroup", mcGroup, sizeof(mcGroup));
(void) JsonGetFloat(obj, "dataPort", dataPortF);
if (skipActive && SourceIsActive(addr)) {
char transport[8] = "udp";
char streamName[256] = "";
(void) JsonGetString(obj, "transport", transport, sizeof(transport));
(void) JsonGetString(obj, "streamName", streamName, sizeof(streamName));
if (skipActive && SourceIsActive(addr, streamName)) {
/* Already streaming — leave the live session untouched. */
}
else if (AddSourceInternal(label, addr, mcGroup,
static_cast<uint16>(dataPortF))) {
static_cast<uint16>(dataPortF),
transport, streamName)) {
nLoaded++;
}
}
@@ -1414,6 +1590,14 @@ void StreamHub::HandleSaveSources() {
static_cast<uint32>(dpt));
}
}
StreamString trn = sessions_[i].GetTransport();
if (strcmp(trn.Buffer(), "tcp") == 0) {
(void) fprintf(f, ",\n \"transport\": \"tcp\"");
StreamString sn = sessions_[i].GetStreamName();
if (sn.Size() > 0u) {
(void) fprintf(f, ",\n \"streamName\": \"%s\"", sn.Buffer());
}
}
(void) fprintf(f, "\n }");
nSaved++;
}
@@ -1500,16 +1684,26 @@ void StreamHub::HandleRemoveSource(const char *json) {
break;
}
}
bool shared = false;
if (found < kMaxSessions) {
sessionActive_[found] = false;
configBroadcast_[found] = false; /* allow re-broadcast on slot reuse */
numSessions_--;
if (sessionMux_[found] >= 0) {
/* One stream of a shared receiver: stop it and drop its listener
* under the lock, so the slot cannot be reused underneath. */
sessions_[found].Stop();
ReleaseMuxSlot(found);
shared = true;
}
}
sessionsMutex_.FastUnLock();
if (found == kMaxSessions) { return; }
sessions_[found].Stop();
if (!shared) {
sessions_[found].Stop();
}
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
"StreamHub: removed session '%s'.", id);