Configuring Node-RED TCP/IP with Siemens S7 OUC Blocks

David Krause13 min read
SiemensTIA PortalTutorial / How-to
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Configuring Node-RED TCP/IP with Siemens S7 Open User Communication (OUC)

Siemens S7-1200/1500 controllers expose Open User Communication (OUC) as a set of standard library function blocks: TCON, TDISCON, TSEND, TRCV, plus the combined TSEND_C and TRCV_C. These blocks run on ISO-on-TCP (RFC1006), TCP, or UDP and are designed to interoperate with any socket-capable peer. Node-RED's built-in tcp-in and tcp-out nodes, found in the standard node-red-node-tcp package, present a clean socket surface, so the two can be paired in a matter of minutes — provided the data format, byte order, and TCON parameters on the PLC side are correct.

This article gives a working configuration for Node-RED ↔ S7-1500 TCP traffic, explains how to format integer, boolean, real, and string payloads so that they decode cleanly on both ends, and walks through the most common field failure: persistent STATUS = 16#7006 on TRCV_C with no incoming data.

1. Overview of the OUC-to-Node-RED Path

Open User Communication runs on the S7's industrial Ethernet interface and is configured per connection through a TCON parameter block. Each connection requires a unique ID from 1 to 4095 (S7-1500 connection resource), a partner IP/port, and a connection mode. Node-RED, in turn, opens a TCP socket against the PLC's IP and the port declared on the OUC side.

Direction S7 Side Block Node-RED Node Trigger
PLC → Node-RED TSEND_C (FB 1588) tcp-in REQ rising edge / job trigger
Node-RED → PLC TRCV_C (FB 1589) tcp-out CONT = 1, data received on socket
Connection owner TCON (FB 1500) / TSEND_C / TRCV_C Socket open by node Passive or active open

Unlike the S7 communication model used by S7Comm/ISO-on-TCP servers such as Snap7, OUC uses raw TCP with no Siemens header. The PLC is therefore agnostic of Node-RED — anything that can write bytes to a socket is a valid peer.

2. Prerequisites

  1. Siemens S7-1200 (FW 4.0 or later) or S7-1500 with Ethernet interface.
  2. SIMATIC TIA Portal V15.1 or later installed; Open User Communication blocks are part of the standard library shipped with every TIA Portal install.
  3. Node-RED 2.x or 3.x running on a host reachable from the PLC subnet.
  4. node-red-node-tcp package installed: npm install node-red-node-tcp (or use the palette manager inside Node-RED).
  5. Static IP addresses on both peers (DHCP reservations are acceptable for production but complicate commissioning).
  6. Port 2000..2010, 5000, or any unprivileged port above 1024 — the OUC blocks do not require privileged ports, but they must not collide with HMI, PN, or OPC UA on the same CPU.
Wiring note: On S7-1500, the OUC port number is the TCP port the PLC listens on for that specific connection. On S7-1200, only ports configured through Connection resources → TCP connections in the device properties are valid; the CPU will reject any other port at compile time.

3. PLC Hardware Configuration in TIA Portal

Open the device view of the S7 CPU and switch to Properties → General → PROFINET interface [X1] → Ethernet addresses. Configure:

  • IP address: e.g. 192.168.0.10
  • Subnet mask: 255.255.255.0
  • Router (optional): gateway address if Node-RED is on another subnet

Under Connection resources, verify the available OUC slots. The S7-1511 supports up to 64 connection resources, the S7-1515 up to 128, and the S7-1518 up to 320. Each OUC instance consumes one. Set aside IDs 1..16 for OUC; reserve 200+ for HMI, 16x for S7 connections, and so on, to avoid collision with other system services.

4. TCON Parameter Block

Each OUC connection requires a TCON_Param data block (DB of type TCON_Param or a UDT with the structure shown below). The block can be created manually, by using the TCON_IP_v4 / TCON_IP_RFC system data types, or implicitly when a TSEND_C/TRCV_C instance is inserted (TIA generates the parameter DB for you).

Field Type Value (example) Notes
InterfaceId HW_ANY 64 (Word) 64 = PN interface X1, 65 = X2
ID CONN_OUC 1 Unique connection ID, 1..4095
ConnectionType BYTE 16#0B (TCP) or 16#0C (ISO-on-TCP) Use 0x0B for raw TCP with Node-RED
ActiveEstablished BOOL TRUE PLC is active (opens socket to Node-RED)
RemoteAddress ARRAY[1..4] OF BYTE [192,168,0,50] Node-RED host IP
RemotePort UINT 1880 TCP port Node-RED listens on
LocalPort UINT 2000 PLC-side listen port (only used if active = FALSE)
LocalTsapId ARRAY[1..16] OF BYTE n/a for TCP Only relevant for ISO-on-TCP (RFC1006)

When the ActiveEstablished flag is TRUE, the PLC will open the socket to Node-RED. If FALSE, Node-RED must connect to the PLC; this is the more common pattern because it allows Node-RED to reconnect automatically after a flow deploy.

5. TSEND_C Implementation (PLC → Node-RED)

Drop a TSEND_C instance (FB 1588) into a cyclic OB (typically OB1). The block signature on S7-1500 is:

// SCL excerpt of TSEND_C call
"instTSEND_C"(REQ   := bSendTrig,
              CONT  := TRUE,                // keep connection open
              DATA  := "dbPayload".txBuf,   // VARIANT pointing to source data
              LEN   := UINT#20,             // bytes to send (max 8192)
              DONE  => bSendDone,
              BUSY  => bSendBusy,
              ERROR => bSendError,
              STATUS=> wSendStatus);

Set CONT = TRUE so the TCP connection stays established across multiple send jobs. REQ is a rising-edge trigger; on each pulse, exactly LEN bytes are copied from DATA into the send buffer.

5.1 Receive indicator

For monitoring, evaluate STATUS against the TCON/TSEND status codes (see section 10). A successful cycle moves through 16#7002 (job running) and settles on 16#0000 with DONE set for one cycle.

6. TRCV_C Implementation (Node-RED → PLC)

Mirror the setup with a TRCV_C instance (FB 1589):

// SCL excerpt of TRCV_C call
"instTRCV_C"(EN_R   := TRUE,                // permanently enabled
              DATA   := "dbPayload".rxBuf,  // destination buffer
              LEN    := UINT#0,             // 0 = accept whatever arrives
              RCVD_LEN=> wBytesReceived,
              BUSY   => bRcvBusy,
              ERROR  => bRcvError,
              STATUS => wRcvStatus);

Setting LEN = 0 tells the block to accept any length between 1 and the buffer size. The actual byte count of the last successful reception is returned in RCVD_LEN.

Important: After every configuration change to TCON_Param, the PLC should be restarted (STOP/RUN or full power-cycle). Re-loading the project alone does not tear down the existing TCP connection — the old socket remains in CLOSE_WAIT, and the new parameters are never applied.

7. Node-RED TCP-Out Node (Sending to PLC)

The tcp-out node from node-red-node-tcp is the only standard node that opens a TCP socket from Node-RED. Configure it as follows:

  1. Drag a tcp out node onto the canvas.
  2. Type: msg.payload
  3. Host: 192.168.0.10 (the PLC's IP)
  4. Port: 2000 (matches the PLC's local port if ActiveEstablished=FALSE, or any port if the PLC is the active opener)
  5. Return: leave unchecked unless you need an end-of-frame delimiter
  6. Newline: leave blank — TCP is a stream protocol, frame boundaries are your problem

For booleans, a function node can serialize a single byte:

msg.payload = Buffer.from([msg.payload ? 1 : 0]);
return msg;

8. Node-RED TCP-In Node (Receiving from PLC)

The tcp-in node listens for incoming connections. It will accept the connection opened by the PLC when ActiveEstablished=TRUE in TCON_Param.

  1. Drag a tcp in node.
  2. Type: Listen on port
  3. Port: 1880
  4. Output: stream of Buffer (best for binary payloads) or stream of String (best for ASCII)
  5. Close connection: unchecked, so the socket stays open across flow deploys

To decode an INT16 (S7 INT is 16-bit, big-endian) arriving in msg.payload:

// Convert a 2-byte Buffer to a signed 16-bit integer (S7 INT)
const buf = Buffer.isBuffer(msg.payload) ? msg.payload : Buffer.from(msg.payload);
if (buf.length < 2) { node.warn("short frame"); return null; }
const value = buf.readInt16BE(0);
msg.payload = value;
return msg;

9. Data Type Formatting Reference

The most common complaint on the OUC ↔ Node-RED path is "INTs are displayed strangely in the PLC, Bools work". The root cause is almost always byte order. S7 stores integers in big-endian (MSB first); most microcontrollers and Node.js's writeInt16LE default to little-endian. Always serialize as big-endian.

S7 Type Bytes Node-RED Encode Node-RED Decode S7 Buffer Slice
BOOL 1 Buffer.from([v ? 1 : 0]) buf[0] !== 0 AT in DB: Array[0..0] of BYTE then AT view to BOOL
INT 2 b.writeInt16BE(v, 0) b.readInt16BE(0) AT view Array[0..1] of BYTE → INT
DINT 4 b.writeInt32BE(v, 0) b.readInt32BE(0) AT view Array[0..3] of BYTE → DINT
REAL 4 b.writeFloatBE(v, 0) b.readFloatBE(0) AT view Array[0..3] of BYTE → REAL
STRING 2+N length-prefixed, see below length-prefixed, see below DB field of type STRING[n]

9.1 Sending a STRING from Node-RED to the PLC

S7 STRING is laid out as [max_len, cur_len, char0, char1, ...]. The first byte holds the declared maximum length, the second the actual current length, and the payload follows. The PLC ignores the first byte on receive — the second byte is overwritten with the real length. To pack a string in Node-RED:

function packS7String(s) {
  const maxLen = 254;            // matches DB definition
  const body   = Buffer.from(s, 'ascii');
  const head   = Buffer.from([maxLen, body.length]);
  return Buffer.concat([head, body, Buffer.alloc(maxLen - body.length)]);
}
msg.payload = packS7String("HELLO");
return msg;

On the PLC side, declare a DB field rxString : STRING[254]; directly inside the receive buffer. The AT-overlay trick is not necessary for strings because the S7 compiler will already lay out the two header bytes correctly.

9.2 Reading a STRING from the PLC

When the PLC sends a STRING, the first two bytes are the same header. In Node-RED:

const b = Buffer.isBuffer(msg.payload) ? msg.payload : Buffer.from(msg.payload);
const curLen = b.readUInt8(1);
msg.payload = b.slice(2, 2 + curLen).toString('ascii');
return msg;

10. Diagnosing Error 7006 (STATUS_TCP_NO_DATA / CONNECTION_ESTABLISHING)

16#7006 is the most common status value reported by users investigating OUC for the first time. It appears in two distinct contexts and means different things:

Context Meaning Action
STATUS of TRCV_C No new data received — this is not an error, the receive job is simply idle. None. Wait for a frame or trigger a test send from the peer.
STATUS of TSEND_C / TCON Connection establishment in progress. Verify partner reachable, port open, firewall rule, ActiveEstablished flag.

On S7-1500 the full set of OUC status codes is:

Hex Meaning
16#0000 Job completed without error
16#7000 No job active
16#7001 Connection establishing
16#7002 Job running / data being transferred
16#7003 Connection terminating
16#7004 Connection terminated
16#7006 No new data (TRCV) / connection establishing (TCON)
16#80C4 Temporary communications error — partner not reachable
16#8181 Same as 0x80C4 in some FW versions
16#80A7 TCP connection refused by partner

10.1 What "I always receive code 7006, but data never arrives" really means

This symptom is reported when TRCV_C is polled and the user is interpreting the status word directly. Because 7006 is the idle status, the absence of data is the expected behavior, not a failure. Two checks confirm a healthy socket:

  1. Open a Wireshark capture on the Node-RED host. Filter tcp.port == 2000. You should see the SYN/SYN-ACK/ACK handshake followed by heartbeat traffic when CONT=TRUE.
  2. From Node-RED, send a known payload (e.g. Buffer.from([0x01, 0x02])) over tcp-out and watch RCVD_LEN and STATUS in the PLC's watch table.

If the handshake completes and the PLC still shows 16#7006 with RCVD_LEN = 0, the receive job is wired but the data is being dropped on the Node-RED side. The most frequent cause is mismatched port — the tcp-out node is sending to a different port than the one TCON_Param listens on.

11. Commissioning and Verification Procedure

  1. Compile and download the TIA project. After download, perform a STOP → RUN transition so the connection resource table is rebuilt.
  2. Open the Watch table and force REQ = TRUE on the TSEND_C instance. Confirm STATUS = 16#0000 and DONE = TRUE after one cycle.
  3. In Node-RED, wire a debug node to the tcp-in output. The bytes sent in step 2 must appear as a Buffer.
  4. Use a function node to decode the buffer per section 9 and wire a second debug to confirm the parsed value.
  5. Send a value back from Node-RED with the tcp-out node. In the watch table, RCVD_LEN should equal the payload length and the destination buffer should be populated.
  6. Disable and re-enable the connection on the Node-RED side to verify auto-reconnect behavior. The PLC will report 16#7003 → 16#7001 → 16#7002 as the new connection is established.

12. Troubleshooting Matrix

Symptom Probable Cause Resolution
PLC reports 16#80C4 on TSEND_C Partner unreachable (firewall, wrong IP, wrong port) Check route, ping from PLC, verify port on Node-RED host
PLC reports 16#80A7 TCP connection refused — nothing is listening on the partner port Start the Node-RED listener before commissioning; check tcp-in port
No error, no data on TRCV_C Node-RED sending to a different port than TCON_Param Reconcile RemotePort/LocalPort on both sides
INT values appear swapped or negative Little-endian vs. big-endian mismatch Use readInt16BE / writeInt16BE in Node-RED
STRING looks corrupted in the PLC Header bytes missing or wrong length encoding Pack with the 2-byte S7 STRING header (max + cur length)
TRCV_C 16#7006 persists after deploying Node-RED PLC still holds the old socket STOP/RUN the PLC, or cycle power, after every config change
Connection drops every few minutes Node-RED's tcp-in is set to "close on idle" Uncheck the close-on-idle option on tcp-in and tcp-out
Truncated frames on Node-RED tcp-in tcp-in configured for string mode but binary data contains NUL Switch output to "stream of Buffer" and decode manually
DONE never sets on TSEND_C LEN is larger than the source data size Match LEN to the actual byte length of the source VARIANT

13. Performance and Resource Notes

  • OUC blocks are processed in the cyclic OB; on S7-1500 they run in priority class 1 by default. Latency on a 1 ms cycle is typically 1..3 ms per send/receive job.
  • Maximum frame size is 8192 bytes per TSEND/TRCV call on S7-1500. Larger payloads must be segmented manually in SCL.
  • Each OUC instance consumes one connection resource. The S7-1511 supports 64 total, shared with HMI, S7 routing, OPC UA, and WebAPI. Plan accordingly.
  • For high-rate telemetry, prefer ISO-on-TCP (ConnectionType = 0x0C) — the RFC1006 header makes frame boundaries explicit and prevents accidental concatenation in the receive buffer.
  • On the Node-RED side, tcp-in creates one Node.js socket per peer. Deploys do not close existing sockets unless the node is removed, which is why the connection persists across flow changes.

14. Suggested Reference Path

For deeper protocol details, the primary reference is the SIMATIC S7-1500 S7-1500/S7-1500T Motion Control - Function Manual on the Siemens Industry Online Support portal. The OUC blocks are documented in the SIMATIC S7-1500 / ET 200MP - Communication function manual, which lists every status code and the data block structure. The Node-RED side is covered in the Node-RED user guide - network nodes page, with the TCP node reference in the node-red-node-tcp flow library entry. The standard TCP/IP behavior is specified in IETF RFC 793 - Transmission Control Protocol and the Siemens-specific framing in RFC 1006 - ISO Transport on top of TCP.

FAQ

What does error 7006 mean on TRCV_C or TSEND_C?

On TRCV_C, status 16#7006 means "no new data received" — it is an idle state, not a fault. On TSEND_C or TCON, the same value means the TCP connection is still being established. Treat 7006 as informational and check RCVD_LEN or the connection state separately.

Why are my INT values wrong but BOOLs are correct in the PLC?

Byte order. S7 INTs are 16-bit big-endian, while Node.js's default is little-endian. Use readInt16BE and writeInt16BE in your function nodes so the high byte is sent first.

How do I send a STRING from Node-RED to the PLC?

Pack the string with the 2-byte S7 STRING header — a max-length byte (e.g. 254) and a current-length byte, followed by the ASCII payload padded out to the max length. On the PLC side, declare a STRING[254] field inside the receive buffer DB; the header is handled automatically by the compiler.

Do I need to restart the PLC after every TCON change?

Yes. Re-loading the project does not tear down an existing TCP connection — the old socket stays in CLOSE_WAIT and the new parameters never take effect. Either perform a STOP/RUN transition or cycle power after modifying TCON_Param.

What port should I use for OUC from Node-RED?

Any unprivileged port above 1024 that is not used by another PLC service. Common choices are 2000, 2001, 5000, and 1880. The port chosen in TCON_Param must match the port on the Node-RED tcp-in or tcp-out node exactly, including which side is the active opener.

Back to blog