# Websocket (Pro)

{% hint style="info" %}
This feature was added in realvirtual **6.3** (Professional).
{% endhint %}

{% hint style="info" %}
This interface requires the **REALVIRTUAL\_JSON** compiler define. See [Newtonsoft JSON](https://doc.realvirtual.io/advanced-topics/newtonsoft-json) for setup instructions.
{% endhint %}

## Overview

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](https://doc.realvirtual.io/components-and-scripts/interfaces/custom-interfaces) 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

## Setup

### Adding the Interface

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

<figure><img src="https://260262196-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FpYxFg97YnJX96UzNNTSd%2Fuploads%2Fgit-blob-1e666a5ec80a550dccf285b87770d66be8b30969%2Fwebsocket-realtime-inspector.png?alt=media" alt=""><figcaption><p>WebSocket Realtime Interface Inspector</p></figcaption></figure>

### Server Mode

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)&#x20;

### Client Mode

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

### Signal Import

#### Client Mode Import (Edit Mode)

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.

#### Server Mode Import (Play Mode)

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.

## Properties

### Configuration

| Property                         | Description                                                                 |
| -------------------------------- | --------------------------------------------------------------------------- |
| **Update Cycle Ms**              | Communication loop interval in milliseconds (default: 10)                   |
| **Only Transmit Changed Inputs** | Only send signals whose values changed since last cycle                     |
| **Auto Reconnect**               | Automatically reconnect on connection loss                                  |
| **Reconnect Interval Seconds**   | Seconds between reconnection attempts                                       |
| **Max Reconnect Attempts**       | Maximum number of reconnection attempts before giving up (`-1` = unlimited) |
| **Debug Mode**                   | Enable detailed logging for troubleshooting                                 |

### Connection Settings

| Property            | Description                                              |
| ------------------- | -------------------------------------------------------- |
| **Is Server**       | `true` = WebSocket server, `false` = WebSocket client    |
| **Address**         | IP address to bind (server) or connect to (client)       |
| **Port**            | TCP 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 Play** | Automatically connect when entering play mode            |

### Status

| Property              | Description                                         |
| --------------------- | --------------------------------------------------- |
| **Connected Clients** | (Server only) Number of currently connected clients |

### Signal Import

| Property                     | Description                                                                  |
| ---------------------------- | ---------------------------------------------------------------------------- |
| **Default Signal Direction** | Direction for imported signals when no pattern matches (`Input` or `Output`) |
| **Use Pattern Matching**     | Enable name-based direction detection                                        |
| **Input Patterns**           | Name substrings that identify signals Unity writes (e.g. `"Unity"`)          |
| **Output Patterns**          | Name substrings that identify signals the PLC writes                         |

## Use Cases

* **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

***

## Wire Protocol v2 Reference

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.

### Message Envelope

Every message has a `type` field that determines its purpose:

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

### Message Types

#### `init` - Client Identification

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

```json
{
  "type": "init",
  "version": 2,
  "name": "MyClient"
}
```

| Field     | Type   | Description                |
| --------- | ------ | -------------------------- |
| `type`    | string | Always `"init"`            |
| `version` | int    | Protocol version (`2`)     |
| `name`    | string | Client identification name |

#### `data` - Cyclic Signal Exchange

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

```json
{
  "type": "data",
  "signals": {
    "Sensor1": true,
    "DriveSpeed": 150.5,
    "MotorOn": false,
    "Counter": 42,
    "Status": "Running"
  }
}
```

| Field     | Type   | Description                                                                                                                          |
| --------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------ |
| `type`    | string | Always `"data"`                                                                                                                      |
| `signals` | object | Map of signal name to current value. Values are native JSON types: `true`/`false` for bool, numbers for int/float, strings for text. |

#### `import_request` - Signal Discovery Request

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

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

#### `import_answer` - Signal Catalog Response

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

```json
{
  "type": "import_answer",
  "version": 2,
  "signals": {
    "Sensor1": true,
    "DriveSpeed": 0.0,
    "Counter": 0
  },
  "signalTypes": {
    "Sensor1": "PLCInputBool",
    "DriveSpeed": "PLCInputFloat",
    "Counter": "PLCOutputInt"
  }
}
```

| Field         | Type   | Description                                                                                                                                                                                                                                              |
| ------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `signals`     | object | Signal name to current value                                                                                                                                                                                                                             |
| `signalTypes` | object | Signal 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)

#### `subscribe` - Filter Server Output

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.

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

#### `snapshot` - Immediate Full State

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

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

#### `config_get` - Read Server Configuration

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

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

#### `config_answer` - Configuration Response

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

```json
{
  "type": "config_answer",
  "version": 2,
  "config": {
    "publishIntervalMs": 5,
    "loggingLevel": "Information",
    "browsePaths": [
      { "prefix": "plc/app/Application/sym", "direction": "input", "recursive": true }
    ]
  }
}
```

| Field    | Type   | Description                                                                                           |
| -------- | ------ | ----------------------------------------------------------------------------------------------------- |
| `config` | object | Application-specific key-value configuration map. The structure depends on the server implementation. |

#### `config_set` - Write Server Configuration

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

```json
{
  "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

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

```json
{
  "type": "config_result",
  "version": 2,
  "success": true,
  "message": "Configuration saved and applied"
}
```

| Field     | Type    | Description                                               |
| --------- | ------- | --------------------------------------------------------- |
| `success` | boolean | `true` if the configuration was applied, `false` on error |
| `message` | string  | Human-readable result message                             |

{% hint style="info" %}
The `config` messages are optional and application-specific. They are used by the ctrlX bridge to remotely configure publish intervals, logging levels, and Data Layer browse paths. If you implement your own server, you can define any key-value structure for the `config` field or ignore these messages entirely.
{% endhint %}

### Signal Value Types

| realvirtual Type                   | JSON Type | Example                 |
| ---------------------------------- | --------- | ----------------------- |
| `PLCInputBool` / `PLCOutputBool`   | boolean   | `true`, `false`         |
| `PLCInputInt` / `PLCOutputInt`     | integer   | `42`, `-1`, `0`         |
| `PLCInputFloat` / `PLCOutputFloat` | number    | `3.14`, `0.0`, `-100.5` |
| `PLCInputText` / `PLCOutputText`   | string    | `"Running"`, `""`       |

***

## Implementing the Other Side

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.

### Connection Sequence

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
```

### Example: Python Client

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

```python
import asyncio
import json
import websockets

async def main():
    uri = "ws://127.0.0.1:8080"

    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())
```

### Example: Python Server

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

```python
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", 8080):
        print("WebSocket server running on ws://0.0.0.0:8080")
        await asyncio.Future()  # run forever

asyncio.run(main())
```

### Example: C# Client (.NET)

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

```csharp
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)

```javascript
const ws = new WebSocket("ws://127.0.0.1:8080");

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;
    }
};
```

### Direction Mapping Guide

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 default | Neutral 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.

### Tips for Implementers

* **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 remains open.
* **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.

## Troubleshooting

**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**

## See Also

* [Custom Interfaces (FastInterface)](https://doc.realvirtual.io/components-and-scripts/interfaces/custom-interfaces) - How to build your own interfaces
* [Signal Manager](https://doc.realvirtual.io/components-and-scripts/interfaces/interface-tools/signal-manager) - Managing PLC signals
* [Interfaces Overview](https://doc.realvirtual.io/components-and-scripts/interfaces) - All available interfaces
