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

+49 -7
View File
@@ -103,10 +103,23 @@ def _streamer_sig(sig):
return f"{sig['name']} = {{ {' '.join(parts)} }}"
SERVICE_NAME = "Svc"
def _service_port(scenario):
"""Port of the shared TCPStreamService (first source's port)."""
return scenario["sources"][0]["udp_port"]
def _streamer_block(src, scenario):
s = src["signals"]
parts = [f"Port = {src['udp_port']}",
f"MaxPayloadSize = {scenario['max_payload']}",
net = scenario["network"]
if net == "tcp_service":
# Port, MaxClients, ... belong to the service; the streamer only joins it.
parts = [f'Service = "{SERVICE_NAME}"', f'StreamName = "{S.stream_name(scenario, src)}"']
else:
parts = [f"Port = {src['udp_port']}"]
parts += [f"MaxPayloadSize = {scenario['max_payload']}",
f'PublishingMode = "{scenario["publishing"]}"']
if scenario["publishing"] == "Accumulate":
parts.append(f"MinRefreshRate = {scenario['min_refresh_hz']}")
@@ -118,7 +131,8 @@ def _streamer_block(src, scenario):
iface = src.get("interface") or _mcast_interface_ip(src["multicast_group"])
parts.append(f'Interface = "{iface}"')
sigs = " ".join(_streamer_sig(sig) for sig in s)
return (f" +Streamer_{src['id']} = {{ Class = UDPStreamer "
cls = "UDPStreamer" if net in ("unicast", "multicast") else "TCPStreamer"
return (f" +Streamer_{src['id']} = {{ Class = {cls} "
f"{' '.join(parts)} Signals = {{ {sigs} }} }}")
@@ -202,7 +216,19 @@ def write_marte_cfg(scenario, path, input_bin, tap_bin=None):
data_block = "\n".join(datas)
thread_list = " ".join(thread_funcs)
cfg = f"""$ChainE2E = {{
service_block = ""
if scenario["network"] == "tcp_service":
service_block = (f"+{SERVICE_NAME} = {{ Class = TCPStreamService "
f"Port = {_service_port(scenario)} Workers = 2 "
f"MaxClients = 16 MaxStreams = 16 }}\n")
hub = scenario.get("hub")
if hub and hub["mode"] == "join":
names = ",".join(S.stream_name(scenario, x) for x in scenario["sources"])
service_block += (f"+Join = {{ Class = TCPStreamJoin Service = \"{SERVICE_NAME}\" "
f"StreamName = \"{hub['name']}\" Sources = \"{names}\" "
f"Master = \"{hub['master']}\" }}\n")
cfg = f"""{service_block}$ChainE2E = {{
Class = RealTimeApplication
+Functions = {{
Class = ReferenceContainer
@@ -227,10 +253,26 @@ def write_marte_cfg(scenario, path, input_bin, tap_bin=None):
def write_hub_cfg(scenario, path):
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
src_blocks = []
for src in scenario["sources"]:
hub = scenario.get("hub")
if hub and hub["mode"] == "mux":
names = ",".join(S.stream_name(scenario, x) for x in scenario["sources"])
src_blocks.append(
f' {hub["node"]} = {{ Label = "chain" Addr = "127.0.0.1" '
f'Port = {_service_port(scenario)} Transport = "tcp" StreamName = "{names}" }}')
elif hub and hub["mode"] == "join":
src_blocks.append(
f' {hub["name"]} = {{ Label = "chain {hub["name"]}" Addr = "127.0.0.1" '
f'Port = {_service_port(scenario)} Transport = "tcp" StreamName = "{hub["name"]}" }}')
for src in ([] if hub else scenario["sources"]):
net = scenario["network"]
port = _service_port(scenario) if net == "tcp_service" else src["udp_port"]
parts = [f'Label = "chain {src["id"]}"', 'Addr = "127.0.0.1"',
f"Port = {src['udp_port']}"]
if scenario["network"] == "multicast":
f"Port = {port}"]
if net in ("tcp", "tcp_service"):
parts.append('Transport = "tcp"')
if net == "tcp_service":
parts.append(f'StreamName = "{src["id"]}"')
if net == "multicast":
parts.append(f'MulticastGroup = "{src["multicast_group"]}"')
parts.append(f"DataPort = {src['data_port']}")
src_blocks.append(f" {src['id']} = {{ {' '.join(parts)} }}")