Skip to content

WebSocket Realtime Interface

The WebSocket Realtime Interface provides high-performance, bidirectional signal exchange over WebSocket connections. It operates as both server and client, enabling connectivity between two Unity instances, external applications, PLCs, or any system that supports WebSocket communication.

Built on the FastInterface framework, it offers thread-safe background communication, automatic reconnection, change detection, and low-latency signal transport.

Key capabilities:

  • Server and Client mode in a single component
  • Flat JSON wire protocol (v2) for easy integration from any language
  • Signal import from remote side with automatic direction inversion
  • SSL/TLS support (wss://) for secure connections through reverse proxies
  • Pattern-based signal direction detection for external systems without PLC-style type prefixes
  • Subscribe filtering so clients only receive the signals they need
  1. Drag the WebsocketRealtimeInterface prefab into your scene, or add the component to an existing GameObject
  2. The interface appears as a child of your realvirtual root object
  3. Create PLC signal objects (PLCInputBool, PLCOutputFloat, etc.) as children of the interface

WebSocket Realtime Interface Inspector

Set Is Server to true (in the Connection Settings foldout). The server listens on the configured address and port and broadcasts signal data to all connected clients.

  • Address: IP to bind to (127.0.0.1 for localhost, 0.0.0.0 for all interfaces)
  • Port: TCP port (default: 1234)

Set Is Server to false. The client connects to a remote WebSocket server.

  • Address: IP address of the server
  • Port: TCP port of the server
  • Use SSL: Enable for wss:// connections (e.g. through a reverse proxy)
  • Path: Optional URL path (e.g. /my-bridge/ws for reverse proxy routing)
  • Client Name: Identification sent during handshake

In client mode, you can import signals from the remote server without manually creating them:

  1. Make sure the remote server is running
  2. Click Import Signals in the Inspector (works in edit mode — no play mode required)
  3. The interface connects, requests the signal catalog, creates local mirror signals with inverted directions, and disconnects

You can also use Test Connection to verify connectivity without importing.

In server mode, you can import signals from connected clients during play mode:

  1. Start the simulation so the server is running and clients are connected
  2. Click Import Signals from Clients in the Inspector
  3. The server broadcasts an import request to all connected clients and creates local mirror signals with inverted directions

This is useful for discovering which signals a remote client provides without manually defining them.

PropertyDescription
Update Cycle MsCommunication loop interval in milliseconds (default: 10)
Only Transmit Changed InputsOnly send signals whose values changed since last cycle
Auto ReconnectAutomatically reconnect on connection loss
Reconnect Interval SecondsSeconds between reconnection attempts
Max Reconnect AttemptsMaximum number of reconnection attempts before giving up (-1 = unlimited)
Debug ModeEnable detailed logging for troubleshooting
PropertyDescription
Is Servertrue = WebSocket server, false = WebSocket client
AddressIP address to bind (server) or connect to (client)
PortTCP port for communication (default: 1234)
Use SSL(Client only) Use wss:// for encrypted connections
Path(Client only) Optional URL path appended after host:port
Client Name(Client only) Name sent during init handshake
Connect On PlayAutomatically connect when entering play mode
PropertyDescription
Connected Clients(Server only) Number of currently connected clients
PropertyDescription
Default Signal DirectionDirection for imported signals when no pattern matches (Input or Output)
Use Pattern MatchingEnable name-based direction detection
Input PatternsName substrings that identify signals Unity writes (e.g. "Unity")
Output PatternsName substrings that identify signals the PLC writes
  • Unity-to-Unity distributed simulation with signal mirroring
  • External application integration (web dashboards, custom HMIs, test frameworks)
  • PLC bridge connections (e.g. via a bridge application running on ctrlX CORE or other edge devices)
  • WebGL builds where TCP-based protocols are not available

The WebSocket Realtime Interface uses a flat JSON protocol. All messages are UTF-8 encoded JSON objects sent as WebSocket text frames. There is no double-encoding - all fields are native JSON types.

The examples on this page use port 1234, the default of this interface. realvirtual CONNECT serves the same protocol on port 5100 at the path /ws, but behaves differently as a server in a few points: it sends import_answer and a full data message right after connect without a request, ignores subscribe (every client receives every change), never sends snapshot, does not apply config_set, needs an API key for clients on other machines, and closes connections that send nothing for 90 seconds. See the CONNECT REST and WebSocket API.

Every message has a type field that determines its purpose:

{
"type": "<message_type>",
"version": 2,
...
}

Sent by the client immediately after the WebSocket connection is established.

{
"type": "init",
"version": 2,
"name": "MyClient"
}
FieldTypeDescription
typestringAlways "init"
versionintProtocol version (2)
namestringClient identification name

Sent cyclically in both directions. Contains signal names and their current values.

{
"type": "data",
"signals": {
"Sensor1": true,
"DriveSpeed": 150.5,
"MotorOn": false,
"Counter": 42,
"Status": "Running"
}
}
FieldTypeDescription
typestringAlways "data"
signalsobjectMap of signal name to current value. Values are native JSON types: true/false for bool, numbers for int/float, strings for text.

Sent from one side to request the full signal catalog from the other side.

{
"type": "import_request",
"version": 2
}

Response to import_request. Contains all signal names, their current values, and their PLC type strings.

{
"type": "import_answer",
"version": 2,
"signals": {
"Sensor1": true,
"DriveSpeed": 0.0,
"Counter": 0
},
"signalTypes": {
"Sensor1": "PLCInputBool",
"DriveSpeed": "PLCInputFloat",
"Counter": "PLCOutputInt"
}
}
FieldTypeDescription
signalsobjectSignal name to current value
signalTypesobjectSignal name to type string. Recognized types: PLCInputBool, PLCOutputBool, PLCInputInt, PLCOutputInt, PLCInputFloat, PLCOutputFloat, PLCInputText, PLCOutputText. Neutral types without prefix (e.g. Bool, Float) are also accepted.

Direction inversion on import: When the receiving side imports signals, the direction is inverted:

  • Remote PLCInputBool (remote reads from PLC) becomes local PLCOutputBool (local writes to Unity)
  • Remote PLCOutputBool (remote writes from PLC) becomes local PLCInputBool (local reads from Unity)

Sent by the client to tell the server which signals it wants to receive. The server then only sends matching signals in data messages. If no subscribe message is sent, the server sends all signals.

{
"type": "subscribe",
"version": 2,
"subscribe": ["Sensor1", "DriveSpeed", "Counter"]
}

Sent by the server after receiving a subscribe message. Contains the current values of all subscribed signals.

{
"type": "snapshot",
"signals": {
"Sensor1": true,
"DriveSpeed": 0.0,
"Counter": 0
}
}

Sent by the client to request the current configuration from the server (e.g. a bridge application).

{
"type": "config_get",
"version": 2
}

Sent by the server in response to config_get. Contains the current configuration as a key-value map.

{
"type": "config_answer",
"version": 2,
"config": {
"publishIntervalMs": 5,
"loggingLevel": "Information",
"browsePaths": [
{ "prefix": "plc/app/Application/sym", "direction": "input", "recursive": true }
]
}
}
FieldTypeDescription
configobjectApplication-specific key-value configuration map. The structure depends on the server implementation.

Sent by the client to update configuration values on the server. The server should persist and apply the new configuration.

{
"type": "config_set",
"version": 2,
"config": {
"publishIntervalMs": 10,
"loggingLevel": "Debug",
"browsePaths": [
{ "prefix": "plc/app/Application/sym", "direction": "input", "recursive": true }
]
}
}

config_result - Configuration Update Result

Section titled “config_result - Configuration Update Result”

Sent by the server in response to config_set. Reports whether the configuration was applied successfully.

{
"type": "config_result",
"version": 2,
"success": true,
"message": "Configuration saved and applied"
}
FieldTypeDescription
successbooleantrue if the configuration was applied, false on error
messagestringHuman-readable result message
realvirtual TypeJSON TypeExample
PLCInputBool / PLCOutputBoolbooleantrue, false
PLCInputInt / PLCOutputIntinteger42, -1, 0
PLCInputFloat / PLCOutputFloatnumber3.14, 0.0, -100.5
PLCInputText / PLCOutputTextstring"Running", ""

This section describes how to build your own WebSocket application that communicates with the realvirtual WebSocket Realtime Interface. You can use any programming language that supports WebSocket connections.

When connecting to a realvirtual server:

1. Open WebSocket connection to ws://address:port
2. Send "init" message with your client name
3. (Optional) Send "import_request" to discover available signals
4. (Optional) Send "subscribe" to filter which signals you receive
5. Start cyclic exchange of "data" messages

When running your own server that realvirtual connects to as client:

1. Start WebSocket server on your address:port
2. Receive "init" message from realvirtual (client identifies itself)
3. (Optional) Receive "subscribe" message listing signals the client wants
4. (Optional) Send "snapshot" with current values of subscribed signals
5. Start cyclic exchange of "data" messages

A minimal Python client that connects to a realvirtual server, imports signals, and exchanges data:

import asyncio
import json
import websockets
async def main():
uri = "ws://127.0.0.1:1234"
async with websockets.connect(uri) as ws:
# 1. Send init
await ws.send(json.dumps({
"type": "init",
"version": 2,
"name": "PythonClient"
}))
print("Connected and init sent")
# 2. Request signal catalog
await ws.send(json.dumps({
"type": "import_request",
"version": 2
}))
# 3. Wait for import_answer
while True:
raw = await ws.recv()
msg = json.loads(raw)
if msg.get("type") == "import_answer":
print(f"Available signals: {list(msg['signalTypes'].keys())}")
break
# 4. Subscribe to specific signals (optional)
await ws.send(json.dumps({
"type": "subscribe",
"version": 2,
"subscribe": ["Sensor1", "DriveSpeed"]
}))
# 5. Cyclic data exchange
while True:
# Receive data from realvirtual
raw = await asyncio.wait_for(ws.recv(), timeout=5.0)
msg = json.loads(raw)
if msg.get("type") in ("data", "snapshot"):
signals = msg.get("signals", {})
print(f"Received: {signals}")
# Send data back to realvirtual
await ws.send(json.dumps({
"type": "data",
"signals": {
"MyOutput": True,
"SetSpeed": 100.5
}
}))
asyncio.run(main())

A server that realvirtual connects to as a client. This is useful when your application is the “host” and realvirtual is a connected client:

import asyncio
import json
import websockets
# Signal state
signals = {
"PLC_Running": True,
"PLC_Speed": 0.0,
"PLC_Counter": 0
}
signal_types = {
"PLC_Running": "PLCOutputBool",
"PLC_Speed": "PLCOutputFloat",
"PLC_Counter": "PLCOutputInt"
}
async def handle_client(ws):
client_name = "unknown"
subscribed_signals = None # None = send all
async for raw in ws:
msg = json.loads(raw)
msg_type = msg.get("type")
if msg_type == "init":
client_name = msg.get("name", "unknown")
print(f"Client connected: {client_name}")
elif msg_type == "import_request":
# Send signal catalog
await ws.send(json.dumps({
"type": "import_answer",
"version": 2,
"signals": signals,
"signalTypes": signal_types
}))
elif msg_type == "subscribe":
subscribed_signals = msg.get("subscribe", [])
# Send snapshot of subscribed signals
snapshot = {k: v for k, v in signals.items()
if k in subscribed_signals}
await ws.send(json.dumps({
"type": "snapshot",
"signals": snapshot
}))
elif msg_type == "data":
# Receive values from realvirtual
received = msg.get("signals", {})
print(f"From {client_name}: {received}")
# Process received values in your application...
# Send updated values back
to_send = signals
if subscribed_signals is not None:
to_send = {k: v for k, v in signals.items()
if k in subscribed_signals}
await ws.send(json.dumps({
"type": "data",
"signals": to_send
}))
async def main():
async with websockets.serve(handle_client, "0.0.0.0", 1234):
print("WebSocket server running on ws://0.0.0.0:1234")
await asyncio.Future() # run forever
asyncio.run(main())

For .NET applications (e.g. a bridge application on a PLC or edge device):

using System;
using System.Collections.Generic;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
public class RealvirtualWebSocketClient
{
private ClientWebSocket ws;
private readonly string uri;
private readonly string clientName;
public RealvirtualWebSocketClient(string uri, string clientName = "DotNetClient")
{
this.uri = uri;
this.clientName = clientName;
}
public async Task ConnectAsync(CancellationToken ct = default)
{
ws = new ClientWebSocket();
await ws.ConnectAsync(new Uri(uri), ct);
// Send init
await SendAsync(new
{
type = "init",
version = 2,
name = clientName
}, ct);
}
public async Task<Dictionary<string, JsonElement>> ReceiveDataAsync(
CancellationToken ct = default)
{
var buffer = new byte[8192];
var result = await ws.ReceiveAsync(buffer, ct);
var json = Encoding.UTF8.GetString(buffer, 0, result.Count);
var msg = JsonSerializer.Deserialize<JsonElement>(json);
var msgType = msg.GetProperty("type").GetString();
if (msgType == "data" || msgType == "snapshot")
{
var signals = new Dictionary<string, JsonElement>();
foreach (var prop in msg.GetProperty("signals").EnumerateObject())
signals[prop.Name] = prop.Value;
return signals;
}
return null;
}
public async Task SendDataAsync(Dictionary<string, object> signals,
CancellationToken ct = default)
{
await SendAsync(new { type = "data", signals }, ct);
}
public async Task RequestImportAsync(CancellationToken ct = default)
{
await SendAsync(new { type = "import_request", version = 2 }, ct);
}
private async Task SendAsync(object message, CancellationToken ct)
{
var json = JsonSerializer.Serialize(message);
var bytes = Encoding.UTF8.GetBytes(json);
await ws.SendAsync(bytes, WebSocketMessageType.Text, true, ct);
}
}

Example: JavaScript / TypeScript (Browser or Node.js)

Section titled “Example: JavaScript / TypeScript (Browser or Node.js)”
const ws = new WebSocket("ws://127.0.0.1:1234");
ws.onopen = () => {
// Send init
ws.send(JSON.stringify({
type: "init",
version: 2,
name: "WebClient"
}));
// Request signal catalog
ws.send(JSON.stringify({
type: "import_request",
version: 2
}));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
switch (msg.type) {
case "import_answer":
console.log("Available signals:", Object.keys(msg.signalTypes));
// Subscribe to specific signals
ws.send(JSON.stringify({
type: "subscribe",
version: 2,
subscribe: ["Sensor1", "DriveSpeed"]
}));
break;
case "data":
case "snapshot":
console.log("Received signals:", msg.signals);
// Send data back
ws.send(JSON.stringify({
type: "data",
signals: {
"MyOutput": true,
"SetSpeed": 100.5
}
}));
break;
}
};

When implementing the other side, understanding signal direction is critical:

Your application sends…realvirtual creates…Meaning
signalTypes: {"Speed": "PLCOutputFloat"}Local PLCInputFloat named “Speed”Your app writes Speed, Unity reads it
signalTypes: {"Enable": "PLCInputBool"}Local PLCOutputBool named “Enable”Your app reads Enable, Unity writes it
signalTypes: {"Value": "Float"}Direction from pattern matching or defaultNeutral type - no direction prefix

Rule of thumb: If your application writes a value, declare it as PLCOutput. If your application reads a value from Unity, declare it as PLCInput. realvirtual inverts the direction on import.

  • JSON only - All messages are JSON text frames. Binary frames are not used.
  • No heartbeat required - The cyclic data messages serve as implicit heartbeat. If you stop sending data, the connection to a Unity server remains open. realvirtual CONNECT is stricter: it closes a connection after 90 seconds without an incoming message, so send init every 25 seconds when talking to CONNECT.
  • Partial updates - data messages do not need to contain all signals. You can send only the signals that changed.
  • Type coercion - realvirtual will attempt to convert JSON number types. Sending 1 for a float signal or 1.0 for an int signal works.
  • Signal names - Use consistent names between both sides. Names are case-sensitive and matched exactly.
  • SSL/TLS - For production deployments through reverse proxies (nginx, traefik), configure useSSL=true and set the path to match your proxy route.

Interface does not connect:

  • Check that the server is running and reachable on the configured address and port
  • For client mode, verify the address, port, SSL, and path settings
  • Use the Test Connection button to verify connectivity
  • Enable Debug Mode for detailed logging

Signals not updating:

  • Ensure signals are child GameObjects of the interface
  • Check signal directions (Input = Unity writes, Output = Unity reads)
  • Verify that signal names match exactly between both sides
  • Check the data messages being sent contain the expected signal names

Import creates wrong directions:

  • Review your signalTypes in the import_answer - PLCInput and PLCOutput are inverted on import
  • For neutral types, configure Input Patterns and Output Patterns or set the Default Signal Direction