SoftControl
📚 Tutorials

UDP Works but TCP Doesn't: Message Framing in AV Control

SoftControl Team2026-08-2511 min read
TCPUDPframingAV controlintegration

The Symptom

You are integrating a third-party system — a PLC, a touch panel, a scheduling server, another vendor's control system — with your AV control software. The integrator tests over UDP and everything works. They switch to TCP because they want a connection they can monitor, and the exact same command string does nothing.

No error. No rejection. The connection is established and stays up. The bytes leave the sender. Nothing happens at the receiver.

This is almost never a network problem, and almost always a framing problem.

Why: UDP Has Message Boundaries, TCP Does Not

This is the whole explanation, and it is worth stating precisely because the consequence is not obvious.

UDP is datagram-based. One send() produces one datagram, and the receiver's one recv() returns exactly that datagram. The message boundary is preserved by the protocol itself. If you send PLAY, the receiver gets PLAY as one unit. Framing is free.

TCP is a byte stream. The protocol guarantees that bytes arrive in order and without loss — and guarantees nothing at all about where one message ends and the next begins. Your PLAY may arrive as PLAY, or as PL then AY, or glued to the next command as PLAYSTOP.

So a TCP receiver must decide for itself where each command ends. It needs a framing rule. The near-universal choice is a line terminator — read until you see \n.

And that is exactly where it breaks: a great many third-party tools, PLCs and network debugging utilities send commands with no terminator at all, and keep the connection open. The receiver, waiting for a newline that will never arrive, holds the command in its buffer forever.

The command is not lost. It is not rejected. It is sitting in a buffer, waiting. Which is why nothing happens and nothing errors.

Three-Way Framing

Newline framing alone is not enough for real-world integration. A robust TCP receiver needs three rules together. This is how SoftControl's external interface server does it:

1. Newline framing

The normal case. A newline arrives, everything before it is one command.

2. Idle framing — the one that fixes the stuck buffer

If no new bytes arrive for 300 ms, treat whatever is in the buffer as a complete command.

The value matters, and 300 ms is chosen deliberately: TCP segments belonging to a single message arrive on a LAN typically less than 10 ms apart. A 300 ms threshold is far above that gap, so it will not chop a partially-received message into two commands, while still being fast enough that a human pressing a button does not perceive the delay.

Too short — say 20 ms — and you risk splitting one command into two on a congested network. Too long and the system feels sluggish. 300 ms sits comfortably between the two.

3. Disconnect framing

Some clients send a command and immediately close the connection, with no terminator at all. If the receiver only frames on newline and idle, that last command can be discarded when the socket closes.

So the close handler must also flush the buffer.

A trap in implementing rule 3

This one is worth calling out because it is invisible in code review. In Dart — and the same shape exists in other event-driven runtimes — attaching the close handler after the fact:

final sub = client.listen(onData);
sub.onDone(myFlushLogic);     // WRONG

replaces any existing done-handler rather than adding to it. If the framing logic was registered in the listen call, this silently removes it:

client.listen(
  onData,
  onError: ...,
  onDone: myFlushLogic,       // CORRECT — passed as a named argument
);

The symptom of getting this wrong is precise: commands that end with a newline work fine, commands without one are silently dropped — which looks exactly like a client-side problem and sends you debugging the wrong end of the connection.

Bound the buffer

A client that streams data with no terminator and never disconnects will grow your buffer indefinitely. Cap it — SoftControl uses 8192 characters — and discard beyond that. Past that size it is not a command, it is a misconfigured client or garbage, and the only real risk is running the machine out of memory.

The Other Reason TCP Fails: You Are Talking to an HTTP Server

There is a second, entirely different cause worth knowing.

Modern AV devices increasingly expose an HTTP API rather than a raw TCP socket. If you point a raw TCP command at port 80 or 8080 and send a JSON payload, the server waits for HTTP headers that never come, and eventually times out or closes.

The signature is distinctive: a TCP timeout or EOF when sending a JSON-looking payload. SoftControl detects this pattern specifically and surfaces a hint suggesting http_post instead of raw tcp — because the underlying fix is not to adjust framing, it is to use the right protocol driver.

If your payload starts with { and TCP times out, check whether the device wants HTTP.

Diagnostic Sequence

1. Does it work over UDP?
If yes, the command content and the device logic are proven correct. The problem is framing, not the command.

2. Does adding a trailing newline fix it?
This confirms the receiver frames on newline and your sender was not terminating. Either configure the sender to append one, or fix the receiver to support idle framing.

3. Does the command arrive after you close the connection?
That is disconnect framing working and idle framing missing — or an onDone handler that got replaced, per the trap above.

4. Do two commands sent quickly arrive glued together?
Classic missing framing. TCP concatenated them because there was no boundary.

5. Does it time out with a JSON payload?
Suspect an HTTP endpoint, not a raw socket.

6. Does the first command work and later ones fail?
The first was flushed by something (idle or close), and subsequent ones are accumulating in a buffer that never drains.

Practical Advice for Integration Specs

When you publish a control interface for other vendors to integrate against, document the framing explicitly. "Send PLAY to TCP port 8819" is not a complete specification. State:

  • whether a terminator is required, and which one

  • whether the connection should be short-lived or persistent

  • what happens with no terminator

  • the maximum command length

And support all three framing rules on the receiving side anyway, because the integrator on the other end will not read your document carefully, and their PLC may not be able to append a newline even if they wanted to.

Being permissive at the receiver costs you fifty lines of code once. Being strict costs you a phone call on every integration, forever.

With SoftControl

SoftControl's external interface accepts commands over UDP (default 8818) and TCP (default 8819), and the TCP path implements all three framing rules described above — newline, 300 ms idle, and disconnect — so a third-party system that appends nothing still works.

The two channels also track their running state independently: if one fails to bind, the other still serves, and the failure reason is surfaced rather than swallowed. That matters because a port conflict on one protocol should not silently take down the other.

For choosing between transports in the first place, see AV control protocols explained; for the command content itself, see command format: terminators, ASCII vs HEX and checksums.

Try SoftControl Now

Free download, no registration required, starts with a 30-day trial

Download FreeView Features

Comments

Comments are reviewed manually before they appear. Abusive content will be removed.

    No comments yet — be the first to share your thoughts