Skip to Content

← TCP Socket Between Python and Node-RED on a Raspberry PLC

Two-axis solar trackersRaspberry PLC 21TCP socketCommunication

TCP Socket Between Python and Node-RED on a Raspberry PLC — full example

Link a Python control process to a Node-RED dashboard with local TCP sockets on a Raspberry PLC 21. Encoder streaming plus start, stop, exit commands.

Complete, runnable program for the Raspberry PLC 21 (tcp-socket-node-red.py): wiring header, requirements and integration notes included.

Download the full project pack — freeThis example + the related ones + bill of materials

Read-only preview.

#!/usr/bin/env python3
# -----------------------------------------------------------------------------
# COMPLETE EXAMPLE — Local TCP socket between the Python script and Node-RED
#
# Device:    Raspberry PLC 21 (Industrial Shields)
# Based on:  dual-axis solar tracker project
#
# Requirements:
#   Python standard library only (socket, threading).
#   In Node-RED: a "tcp request/in" node on localhost:8181 (data) and a
#   "tcp out" node on localhost:8182 (commands).
#
# Logic:
#   The control process opens two TCP servers on localhost:
#     - 8181 (data):     streams encoder positions to the dashboard with an
#                        axis prefix: "e1234\n" elevation, "a567\n" azimuth.
#     - 8182 (commands): accepts "start", "stop" and "exit" lines sent from
#                        the Node-RED dashboard buttons.
#   Each server runs in its own thread; the data thread publishes the latest
#   reading every second. Here the encoders are simulated; on the real
#   tracker the position comes from the position_value SDO (see
#   canopen-encoder-inclinometer-reading.py).
# -----------------------------------------------------------------------------

import socket
import threading
import time

HOST      = '127.0.0.1'   # localhost only: Node-RED runs on the same PLC
DATA_PORT = 8181
CMD_PORT  = 8182

# State shared between threads
tracking_active = threading.Event()
shutdown        = threading.Event()


def read_elevation_encoder():
    """Stub: on the real tracker, encoder.sdo['position_value'].raw."""
    return int(time.time()) % 750          # simulation 0..749 (0..75.0 deg)


def read_azimuth_encoder():
    """Stub: on the real tracker, encoder.sdo['position_value'].raw."""
    return 600 + int(time.time()) % 2400   # simulation 600..2999


def data_server():
    """Accepts one dashboard connection and streams positions every 1 s."""
    srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    srv.bind((HOST, DATA_PORT))
    srv.listen(1)
    print(f"Data server on {HOST}:{DATA_PORT}")
    while not shutdown.is_set():
        conn, addr = srv.accept()
        print(f"Dashboard connected (data) from {addr}")
        try:
            while not shutdown.is_set():
                pos_e = read_elevation_encoder()
                pos_a = read_azimuth_encoder()
                # One-character prefix so Node-RED can split the axes
                conn.sendall(("e" + str(pos_e) + "\n").encode())  # elevation
                conn.sendall(("a" + str(pos_a) + "\n").encode())  # azimuth
                time.sleep(1)
        except (BrokenPipeError, ConnectionResetError):
            print("Dashboard disconnected (data); waiting for reconnection")
        finally:
            conn.close()
    srv.close()


def command_server():
    """Receives command lines from the dashboard: start / stop / exit."""
    srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    srv.bind((HOST, CMD_PORT))
    srv.listen(1)
    print(f"Command server on {HOST}:{CMD_PORT}")
    while not shutdown.is_set():
        conn, addr = srv.accept()
        print(f"Dashboard connected (commands) from {addr}")
        try:
            buffer = b""
            while not shutdown.is_set():
                data = conn.recv(64)
                if not data:
                    break                   # the client closed
                buffer += data
                while b"\n" in buffer:
                    line, buffer = buffer.split(b"\n", 1)
                    process_command(line.decode().strip().lower())
        except ConnectionResetError:
            pass
        finally:
            conn.close()
    srv.close()


def process_command(cmd):
    """Executes the command received from the dashboard buttons."""
    if cmd == "start":
        tracking_active.set()
        print("CMD start: tracking enabled")
    elif cmd == "stop":
        tracking_active.clear()
        print("CMD stop: tracking stopped (motors halted)")
    elif cmd == "exit":
        print("CMD exit: shutting down control process")
        shutdown.set()
    elif cmd:
        print(f"Unknown CMD ignored: '{cmd}'")


if __name__ == '__main__':
    threads = [threading.Thread(target=data_server, daemon=True),
               threading.Thread(target=command_server, daemon=True)]
    for t in threads:
        t.start()
    try:
        while not shutdown.is_set():
            time.sleep(0.5)               # the real control loop would go here
    except KeyboardInterrupt:
        shutdown.set()
    print("Control process finished")
Download the full project pack — freeThis example + the related ones + bill of materials