Add TCP control + UDP multicast mode to UDPStreamer

DataSource (C++):
- New optional config: MulticastGroup (e.g. "239.0.0.1") and DataPort
  (default Port+1); when set, enables multicast mode; absent = unicast
- Control plane: BasicTCPSocket listener on Port; CONNECT received over
  TCP triggers HandleTCPConnect() which sends CONFIG via the same TCP
  connection
- Data plane: BasicUDPSocket aimed at MulticastGroup:DataPort; all DATA
  fragments sent via dataSocket.Write() so any joined client receives them
- useMulticast is the single branch point; every unicast code path is
  unchanged when MulticastGroup is absent
- New methods: HandleTCPConnect(), IsMulticast()
- 5 new unit tests (ports 44710-44729); all 38 tests passing

WebUI hub (Go):
- SourceConfig gains MulticastGroup and DataPort fields (JSON, optional)
- UDPClient gains multicastGroup/dataPort; Run() routes to new
  runMulticastSession() when multicastGroup is non-empty
- runMulticastSession(): TCP dial for CONNECT/CONFIG, net.ListenMulticastUDP
  for DATA, background goroutine watches TCP for DISCONNECT/close
- All existing sm.Add() call sites updated (unicast callers pass "", 0)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino Ferrari
2026-05-26 22:56:27 +02:00
parent f85ab8652c
commit b15e637f14
9 changed files with 864 additions and 58 deletions
@@ -31,6 +31,7 @@
/* Project header includes */
/*---------------------------------------------------------------------------*/
#include "AdvancedErrorManagement.h"
#include "BasicTCPSocket.h"
#include "BasicUDPSocket.h"
#include "ConfigurationDatabase.h"
#include "GAM.h"
@@ -1547,3 +1548,288 @@ bool UDPStreamerTest::TestHighFrequency_DataIntegrity() {
ObjectRegistryDatabase::Instance()->Purge();
return ok;
}
/* ============================================================
* Multicast mode tests (ports 44710-44729)
* ============================================================ */
bool UDPStreamerTest::TestInitialise_MulticastMode_Valid() {
using namespace MARTe;
UDPStreamer ds;
ConfigurationDatabase cdb;
cdb.Write("Port", 44710u);
cdb.Write("MulticastGroup", "239.0.0.1");
cdb.Write("DataPort", 44711u);
cdb.CreateRelative("Signals");
cdb.MoveToRoot();
bool ok = ds.Initialise(cdb);
ok &= ds.IsMulticast();
ok &= (ds.GetPort() == 44710u);
return ok;
}
bool UDPStreamerTest::TestInitialise_MulticastMode_DefaultDataPort() {
using namespace MARTe;
UDPStreamer ds;
ConfigurationDatabase cdb;
cdb.Write("Port", 44712u);
cdb.Write("MulticastGroup", "239.0.0.1");
/* DataPort intentionally omitted: should default to 44713 */
cdb.CreateRelative("Signals");
cdb.MoveToRoot();
bool ok = ds.Initialise(cdb);
ok &= ds.IsMulticast();
return ok;
}
bool UDPStreamerTest::TestInitialise_MulticastMode_InvalidDataPort() {
using namespace MARTe;
UDPStreamer ds;
ConfigurationDatabase cdb;
cdb.Write("Port", 44714u);
cdb.Write("MulticastGroup", "239.0.0.1");
cdb.Write("DataPort", 44714u); /* same as Port — must be rejected */
cdb.CreateRelative("Signals");
cdb.MoveToRoot();
return !ds.Initialise(cdb);
}
bool UDPStreamerTest::TestPrepareNextState_Multicast() {
using namespace MARTe;
static const char8 *const cfg =
"+Test = {\n"
" Class = RealTimeApplication\n"
" +Functions = {\n"
" Class = ReferenceContainer\n"
" +Writer = {\n"
" Class = UDPStreamerTestOutputGAM\n"
" OutputSignals = {\n"
" Counter = {\n"
" DataSource = Streamer\n"
" Type = uint32\n"
" }\n"
" }\n"
" }\n"
" }\n"
" +Data = {\n"
" Class = ReferenceContainer\n"
" +Streamer = {\n"
" Class = UDPStreamer\n"
" Port = 44716\n"
" MulticastGroup = \"239.0.0.1\"\n"
" DataPort = 44717\n"
" MaxPayloadSize = 1400\n"
" Signals = {\n"
" Counter = {\n"
" Type = uint32\n"
" }\n"
" }\n"
" }\n"
" +Timings = {\n"
" Class = TimingDataSource\n"
" }\n"
" }\n"
" +States = {\n"
" Class = ReferenceContainer\n"
" +State1 = {\n"
" Class = RealTimeState\n"
" +Threads = {\n"
" Class = ReferenceContainer\n"
" +Thread1 = {\n"
" Class = RealTimeThread\n"
" Functions = { Writer }\n"
" }\n"
" }\n"
" }\n"
" }\n"
" +Scheduler = {\n"
" Class = GAMScheduler\n"
" TimingDataSource = Timings\n"
" }\n"
"}\n";
ReferenceT<RealTimeApplication> app = LoadApplication(cfg);
bool ok = app.IsValid();
if (ok) {
ok = (app->PrepareNextState("State1") == ErrorManagement::NoError);
}
Sleep::MSec(50u);
ReferenceT<UDPStreamer> ds = ObjectRegistryDatabase::Instance()->Find("Test.Data.Streamer");
if (ok) {
ok = ds.IsValid();
}
if (ok) {
ok = ds->IsMulticast();
}
if (ok) {
ok = !ds->IsClientConnected(); /* no client connected yet */
}
ObjectRegistryDatabase::Instance()->Purge();
return ok;
}
bool UDPStreamerTest::TestExecute_MulticastConnectDataDisconnect() {
using namespace MARTe;
static const char8 *const cfg =
"+Test = {\n"
" Class = RealTimeApplication\n"
" +Functions = {\n"
" Class = ReferenceContainer\n"
" +Writer = {\n"
" Class = UDPStreamerTestOutputGAM\n"
" OutputSignals = {\n"
" Counter = {\n"
" DataSource = Streamer\n"
" Type = uint32\n"
" }\n"
" }\n"
" }\n"
" }\n"
" +Data = {\n"
" Class = ReferenceContainer\n"
" +Streamer = {\n"
" Class = UDPStreamer\n"
" Port = 44720\n"
" MulticastGroup = \"239.0.0.1\"\n"
" DataPort = 44721\n"
" MaxPayloadSize = 1400\n"
" Signals = {\n"
" Counter = {\n"
" Type = uint32\n"
" }\n"
" }\n"
" }\n"
" +Timings = {\n"
" Class = TimingDataSource\n"
" }\n"
" }\n"
" +States = {\n"
" Class = ReferenceContainer\n"
" +State1 = {\n"
" Class = RealTimeState\n"
" +Threads = {\n"
" Class = ReferenceContainer\n"
" +Thread1 = {\n"
" Class = RealTimeThread\n"
" Functions = { Writer }\n"
" }\n"
" }\n"
" }\n"
" }\n"
" +Scheduler = {\n"
" Class = GAMScheduler\n"
" TimingDataSource = Timings\n"
" }\n"
"}\n";
ReferenceT<RealTimeApplication> app = LoadApplication(cfg);
bool ok = app.IsValid();
if (ok) {
ok = (app->PrepareNextState("State1") == ErrorManagement::NoError);
}
Sleep::MSec(100u);
/* Step 1: TCP CONNECT to control port 44720 */
BasicTCPSocket clientTCP;
if (ok) {
ok = clientTCP.Open();
}
if (ok) {
ok = clientTCP.Connect("127.0.0.1", 44720u, TimeoutType(2000u));
}
/* Send CONNECT packet over TCP */
if (ok) {
UDPSPacketHeader connectHdr;
connectHdr.magic = UDPS_MAGIC;
connectHdr.type = UDPS_TYPE_CONNECT;
connectHdr.counter = 0u;
connectHdr.fragmentIdx = 0u;
connectHdr.totalFragments = 1u;
connectHdr.payloadBytes = 0u;
uint32 sendSz = static_cast<uint32>(sizeof(UDPSPacketHeader));
ok = clientTCP.Write(reinterpret_cast<const char8 *>(&connectHdr), sendSz);
}
/* Step 2: Read CONFIG from TCP (header then payload) */
if (ok) {
uint8 cfgHdrBuf[sizeof(UDPSPacketHeader)];
uint32 recvSz = static_cast<uint32>(sizeof(UDPSPacketHeader));
ok = clientTCP.Read(reinterpret_cast<char8 *>(cfgHdrBuf), recvSz, TimeoutType(2000u));
if (ok) {
const UDPSPacketHeader *cfgHdr =
reinterpret_cast<const UDPSPacketHeader *>(cfgHdrBuf);
ok = (cfgHdr->magic == UDPS_MAGIC) && (cfgHdr->type == UDPS_TYPE_CONFIG);
if (ok && (cfgHdr->payloadBytes > 0u)) {
/* Drain the payload so the TCP stream is clean */
HeapI *heap = GlobalObjectsDatabase::Instance()->GetStandardHeap();
uint8 *payload = reinterpret_cast<uint8 *>(
heap->Malloc(cfgHdr->payloadBytes));
uint32 plSz = cfgHdr->payloadBytes;
(void) clientTCP.Read(reinterpret_cast<char8 *>(payload), plSz,
TimeoutType(2000u));
heap->Free(reinterpret_cast<void *&>(payload));
}
}
}
/* Step 3: Join multicast group 239.0.0.1 on data port 44721 */
BasicUDPSocket mcastReader;
if (ok) {
ok = mcastReader.Open();
}
if (ok) {
ok = mcastReader.Listen(44721u);
}
if (ok) {
ok = mcastReader.Join("239.0.0.1");
}
/* Step 4: Trigger Synchronise() to generate a DATA packet */
if (ok) {
ReferenceT<UDPStreamer> ds =
ObjectRegistryDatabase::Instance()->Find("Test.Data.Streamer");
ok = ds.IsValid() && ds->IsClientConnected();
if (ok) {
ok = ds->Synchronise();
}
}
Sleep::MSec(100u);
/* Step 5: Receive at least one DATA fragment from the multicast group */
if (ok) {
uint8 dataBuf[2048u];
uint32 dataSz = static_cast<uint32>(sizeof(dataBuf));
bool received = mcastReader.Read(reinterpret_cast<char8 *>(dataBuf), dataSz,
TimeoutType(500u));
ok = received;
if (ok && (dataSz >= static_cast<uint32>(sizeof(UDPSPacketHeader)))) {
const UDPSPacketHeader *dHdr =
reinterpret_cast<const UDPSPacketHeader *>(dataBuf);
ok = (dHdr->magic == UDPS_MAGIC) && (dHdr->type == UDPS_TYPE_DATA);
}
}
/* Step 6: Send DISCONNECT via TCP */
if (clientTCP.IsValid()) {
UDPSPacketHeader disPkt;
disPkt.magic = UDPS_MAGIC;
disPkt.type = UDPS_TYPE_DISCONNECT;
disPkt.counter = 0u;
disPkt.fragmentIdx = 0u;
disPkt.totalFragments = 1u;
disPkt.payloadBytes = 0u;
uint32 disSz = static_cast<uint32>(sizeof(UDPSPacketHeader));
(void) clientTCP.Write(reinterpret_cast<const char8 *>(&disPkt), disSz);
(void) clientTCP.Close();
}
(void) mcastReader.Close();
Sleep::MSec(50u);
ObjectRegistryDatabase::Instance()->Purge();
return ok;
}