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:
1 parent
5562877c99
commit
50d85363a3
79 files changed
+12348
-138
No files matched your search
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user