Files
MARTe-Integrated-Components/Docs/TCPStreamer.md
T
Martino FerrariandClaude Opus 4.6 50d85363a3 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>
2026-11-06 13:41:27 +01:00

9.9 KiB

TCPStreamer / TCPStreamerClient

TCP counterparts of UDPStreamer / UDPStreamerClient. Same signals, quantisation, time modes, publishing modes and UDPS wire format; only the transport differs. Use them when you need lossless, ordered delivery to a few consumers and do not need multicast.

How it works

  • TCPStreamer derives from UDPStreamer; TCPStreamerClient from UDPStreamerClient. They install a TCPSServer / TCPSClient through the UDPSServerI / UDPSClientI interfaces (UDPSTransportI.h), so the RT path (Synchronise() = spinlock + memcpy) and serialisation are shared.
  • Frames are UDPS frames (17-byte header + payload) on the byte stream, with totalFragments always 1. The server sends CONFIG on accept; there is no CONNECT / ACK / keepalive.
  • Each client has its own bounded queue (ClientQueueDepth DATA frames plus CONFIG slots). If a client cannot keep up, only its own oldest DATA frames are dropped (counted by GetClientDrops(), logged at most once per second). A client with no progress for ClientStallTimeout seconds is disconnected. The RT thread and other clients are never affected.
  • A new CONFIG (reconfiguration) discards the client's stale queued DATA.
  • The client reconnects automatically after any disconnect or protocol error (bad magic, oversize length, fragmented frame).

TCPStreamer configuration

All UDPStreamer keys apply (see UDPStreamer.md) except the UDP-only MulticastGroup, Interface, DataPort, ClientTimeout, which are rejected.

Key Default Meaning
Port 44500 TCP listen port (0 = ephemeral, see GetBoundPort())
MaxPayloadSize 60000 Frame budget; sizes Accumulate batches. Frames up to 1 MiB are sent
MaxClients 16 Simultaneous clients (1..1024); extra connections are closed
ClientQueueDepth 8 DATA frames queued per client (1..4096)
ClientStallTimeout 5 Seconds without progress before disconnect; 0 disables
SendBufferSize 0 SO_SNDBUF for client sockets; 0 keeps the OS default

Accessors: GetBoundPort(), GetClientCount(), GetClientDrops(), GetStallEvictions(), plus the inherited GetDroppedPublications().

+Streamer = {
    Class = TCPStreamer
    Port = 44500
    ClientQueueDepth = 16
    Signals = {
        Time  = { Type = uint64 }
        Value = { Type = float32 }
    }
}

TCPStreamerClient configuration

Key Default Meaning
ServerAddress 127.0.0.1 Server IPv4 address or host name
Port 44500 Server TCP port
SilenceTimeout 0 Seconds without any byte before reconnecting; 0 disables
ReconnectDelay 2 Seconds between connection attempts
CPUMask, StackSize default Receiver thread

MulticastGroup, DataPort, Interface, KeepAliveInterval are rejected. Accessors: IsConnected(), GetConnectionCount(), GetProtocolErrors().

The client delivers frames as received; it does no gap or stale-packet handling.

Service mode: one in-process server, many streams

TCPStreamService is a root-level object that owns the listening socket and a pool of worker threads. Any number of TCPStreamer DataSources (each on its own real-time thread) publish into it; clients choose a stream by name.

+Svc = { Class = TCPStreamService Port = 44500 Workers = 2 }
...
+Fast = { Class = TCPStreamer Service = "Svc" StreamName = "fast" Signals = { ... } }
+Slow = { Class = TCPStreamer Service = "Svc" StreamName = "slow" Signals = { ... } }
+Rx   = { Class = TCPStreamerClient Port = 44500 StreamName = "fast" Signals = { ... } }
  • Wire protocol: plain UDPS over TCP plus one client-to-server frame, SUBSCRIBE (type 5, payload = stream name, 1..255 bytes, or a comma separated list, see "Joining streams"). The service answers with that stream's CONFIG and then its DATA frames. A connection that does not subscribe within HandshakeTimeout, or names an unknown stream, is closed (the client reconnects).
  • Threads: one acceptor plus Workers I/O threads; each connection lives on one worker. Producers only copy frames into per-connection bounded queues (drop-oldest) and wake the worker through a self-pipe, so a slow client never delays a producer or another client. Lock order: topology, stream, connection.
  • A stream registers at Start() (looked up with ObjectRegistryDatabase::Find(Service)) and unregisters on stop.
  • StreamName defaults to the DataSource name (max 255 chars). In service mode Port, MaxClients, ClientQueueDepth, ClientStallTimeout and SendBufferSize belong to the service and are rejected on the streamer.
  • TCPStreamerClient / TCPSClient send SUBSCRIBE after every (re)connect when StreamName is set. A plain TCPSServer ignores it, so the same client works against both.

TCPStreamService keys:

Key Default Meaning
Port 44500 TCP listen port
Workers 2 I/O threads (1..32)
MaxClients 64 Total simultaneous clients (1..1024)
MaxStreams 32 Streams that can register (1..256)
ClientQueueDepth 8 DATA frames queued per client (1..4096)
ClientStallTimeout 5 Seconds without progress before eviction; 0 disables
HandshakeTimeout 5 Seconds a new connection has to SUBSCRIBE (>= 1)
SendBufferSize 0 SO_SNDBUF; 0 keeps the OS default
CPUMask, StackSize default Worker threads

Joining streams

Two independent ways to take several service streams through one cheaper path.

Multiplexed connection

SUBSCRIBE may carry a comma separated list ("fast,slow"; at most 64 names, 4096 payload bytes, no duplicates, no , in a stream name). One socket then carries every listed stream, each keeping its own CONFIG, rate and timeline:

  • The index of a stream is its position in the list; every server frame carries it in fragmentIdx (totalFragments stays 1). A single name is index 0, so existing clients are unchanged. One unknown name closes the connection.
  • Queues are accounted per stream: ClientQueueDepth DATA frames per stream, drop-oldest within the same stream only, so a fast stream never evicts a slow stream's frames. Queued frames are written with one sendmsg (up to 16).
  • TCPSClient accepts StreamName = "a,b,c", demultiplexes by fragmentIdx (SetStreamListener(i, ...)) and tracks the counter gap per stream. GetStream(i) returns a UDPSClientI facade whose Start()/Stop() are reference counted on the shared connection. TCPStreamerClient is single-stream and rejects a comma.
  • GetClientCount() of a service counts (connection, stream) subscriptions.

Merged stream: TCPStreamJoin

+Svc  = { Class = TCPStreamService Port = 44500 }
+Fast = { Class = TCPStreamer Service = "Svc" StreamName = "fast" ... }
+Slow = { Class = TCPStreamer Service = "Svc" StreamName = "slow" ... }
+Join = { Class = TCPStreamJoin Service = "Svc" StreamName = "all"
          Sources = "fast,slow" Master = "fast" }

A root-level object, declared after the service, that publishes a new stream all holding the signals of several streams of the same service:

  • CONFIG: source descriptors concatenated in Sources order, named <stream>_<signal> (at most 63 characters), timeSignalIdx rebased, publish mode Strict, one HRT frequency (sources with different known frequencies are refused; an unknown one matches any).
  • DATA: one frame per Master frame, [master timestamp][source0 body][source1 body]..., a body being that source's last DATA payload without its 8-byte timestamp (sample-and-hold). Nothing is emitted until every source has delivered one frame. A held value is stamped with the master's time unless the source has a time signal. No thread is added: the frame is built on the master producer's thread.
  • Only Strict and Decimate sources can be merged; a source whose body size changes without a new CONFIG is ignored; the merged frame must fit MaxPayloadSize (default 60000).
  • The merged stream is registered while every source is registered, removed as soon as one leaves (its subscribers are dropped and reconnect) and rebuilt when a source re-registers or changes its CONFIG. A source with a join attached keeps publishing even with no socket subscriber.
  • Keys: Service (required), StreamName (default object name), Sources (required, 1..16), Master (default first source), MaxPayloadSize.

Design: docs/superpowers/specs/2026-11-06-tcp-stream-join-design.md.

StreamHub

A StreamHub source can consume over TCP: in the config Sources block (and in addSource / the SourcesFile) set Transport = "tcp" (default "udp") and, for a service, StreamName = "<stream>". Addr is then the TCP server address. The hub uses TCPSClient, which reports the UDPS counter gap so the accumulate-mode timing estimator tolerates dropped frames.

StreamName = "a,b,c" takes several streams over one multiplexed connection and creates one session per stream, labelled <Label>_<stream> (just the stream name without a label). Session ids are <node>_<stream> for a config Sources node and s<N> for addSource.

Tests

  • GTest: Test/GTest/TCPSServerGTest.cpp, TCPSClientGTest.cpp, TCPStreamerGTest.cpp (helpers in TCPTestUtil.h) and TCPStreamServiceGTest.cpp (queue, protocol, multiplexing, joins, multi-stream/worker, stalls).
  • E2E (Test/E2E/suite/scenarios.py): s58_direct_tcp, s59_direct_tcp_service (FileReader to FileWriter round trip) and chain scenarios s60-s65 (plain TCP: scalar, full array, accumulate, decimate/quantise, multi-source, high rate), s66-s71 (service: same features plus 3- and 6-stream fan-in), s72 (hub on one multiplexed connection) and s73 (merged stream). Scenario configs are generated by gen_cfg.py (network = "tcp" / "tcp_service").

Design

docs/superpowers/specs/2026-11-05-tcp-streamer-design.md (section 7 covers the single-service design implemented by TCPStreamService).