Live monitor via WebSocket

How it works

Your application (robot controller, game AI, simulation) opens a WebSocket connection to the editor and sends NDJSON-formatted events as nodes tick. The editor receives these events and updates the visualization instantly — nodes flash green on success, red on failure, and blue while running.

Use cases
  • Debug autonomous robots during field testing
  • Monitor game AI behavior during playtesting
  • Validate tree logic in simulation before deployment
  • Share live demos with remote team members

Message format

Each message is a single line of NDJSON (Newline-Delimited JSON). The editor expects messages in this format:

Node status update

{"type":"node_status","timestamp":1721145600000,"node_id":"Wait_1","status":"RUNNING","tick_count":42}
Field Type Description
type string Always "node_status" for status updates
timestamp number Unix timestamp in milliseconds
node_id string Unique identifier matching the node in the editor
status string One of: RUNNING, SUCCESS, FAILURE
tick_count number Optional: current tick number for correlation

Tree reset signal

Send this when the tree restarts (e.g., after a failure or manual reset):

{"type":"tree_reset","timestamp":1721145600000}

Custom metadata (optional)

You can attach arbitrary metadata to help with debugging:

{"type":"metadata","timestamp":1721145600000,"key":"battery_level","value":85.3,"unit":"percent"}

Server-side implementation

C++ (with Boost.Beast)

#include <boost/beast.hpp>
#include <nlohmann/json.hpp>

using json = nlohmann::json;
namespace beast = boost::beast;
namespace http = beast::http;
namespace websocket = beast::websocket;

class BehaviorTreeWebSocket {
public:
    void send_node_status(const std::string& node_id, 
                          const std::string& status,
                          int tick_count) {
        json msg;
        msg["type"] = "node_status";
        msg["timestamp"] = current_timestamp_ms();
        msg["node_id"] = node_id;
        msg["status"] = status;
        msg["tick_count"] = tick_count;
        
        ws_.write(beast::buffer(msg.dump()));
    }
    
private:
    websocket::stream<beast::tcp_stream> ws_;
};

Python (with websockets library)

import asyncio
import json
import time
import websockets

async def stream_tree_events(websocket):
    """Send behavior tree events to connected editor."""
    
    # Example: simulate a simple tree execution
    events = [
        {"node_id": "Sequence_1", "status": "RUNNING"},
        {"node_id": "IsEnable", "status": "SUCCESS"},
        {"node_id": "Wait_1", "status": "RUNNING"},
        {"node_id": "Wait_1", "status": "SUCCESS"},
        {"node_id": "Sequence_1", "status": "SUCCESS"},
    ]
    
    for event in events:
        msg = {
            "type": "node_status",
            "timestamp": int(time.time() * 1000),
            **event,
            "tick_count": len(events)
        }
        await websocket.send(json.dumps(msg))
        await asyncio.sleep(0.5)  # Simulate execution delay

async def main():
    async with websockets.serve(stream_tree_events, "localhost", 8765):
        print("WebSocket server running on ws://localhost:8765")
        await asyncio.Future()  # Run forever

if __name__ == "__main__":
    asyncio.run(main())

Node.js (with ws library)

const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8765 });

wss.on('connection', (ws) => {
    console.log('Editor connected');
    
    // Simulate tree execution
    const events = [
        { node_id: 'Sequence_1', status: 'RUNNING' },
        { node_id: 'IsEnable', status: 'SUCCESS' },
        { node_id: 'Wait_1', status: 'RUNNING' },
        { node_id: 'Wait_1', status: 'SUCCESS' },
        { node_id: 'Sequence_1', status: 'SUCCESS' },
    ];
    
    let index = 0;
    const interval = setInterval(() => {
        if (index >= events.length) {
            clearInterval(interval);
            return;
        }
        
        const msg = {
            type: 'node_status',
            timestamp: Date.now(),
            ...events[index],
            tick_count: events.length
        };
        
        ws.send(JSON.stringify(msg));
        index++;
    }, 500);
    
    ws.on('close', () => {
        console.log('Editor disconnected');
        clearInterval(interval);
    });
});

console.log('WebSocket server running on ws://localhost:8765');

Connecting from the editor

Step 1: Open the WebSocket panel

Click the 📡 WebSocket button in the top toolbar. A drawer will slide in from the right side of the screen.

Step 2: Enter the WebSocket URL

Type your server's WebSocket URL in the input field. Common formats:

Step 3: Connect

Click Connect. The button will change to Disconnect when connected. Status indicators:

Step 4: Load the matching tree

For the visualization to work correctly, the tree in the editor must match the tree running on your server. Either:

Real-time visualization

As events arrive, nodes in the canvas update their appearance:

The most recent event is highlighted, and previous states fade out. This creates a visual trail showing the execution path through the tree.

Advanced features

Event playback controls

The WebSocket panel includes playback controls similar to the NDJSON replay feature:

Event buffering

The editor buffers the last 1000 events in memory. This allows you to:

Filtering events

Use the filter input to show only specific nodes or statuses:

Security considerations

Development (ws://)

For local testing, unencrypted WebSocket (ws://) is fine. Keep your server bound to localhost or your local network to prevent external access.

Production (wss://)

Always use encrypted WebSocket (wss://) in production:

Authentication (recommended)

For production deployments, implement authentication:

// Client connects with token
const ws = new WebSocket('wss://your-server.com/ws', {
    headers: {
        'Authorization': 'Bearer YOUR_API_TOKEN'
    }
});

// Server validates token
wss.on('connection', (ws, req) => {
    const token = req.headers['authorization'];
    if (!validateToken(token)) {
        ws.close(1008, 'Invalid token');
        return;
    }
    // ... proceed with connection
});

Troubleshooting

Connection refused

Events not updating visualization

High latency or dropped messages

Browser blocks WebSocket connection

Performance tip

For high-frequency trees (>100 ticks/sec), consider throttling events on the server side. Send status updates only when a node's state changes, rather than every tick. This reduces bandwidth and keeps the visualization readable.

Integration examples

ROS 2 integration

Publish tree events as ROS 2 topics, then bridge to WebSocket:

# Python ROS 2 node
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
import json
import websockets

class TreeEventBridge(Node):
    def __init__(self):
        super().__init__('tree_event_bridge')
        self.subscription = self.create_subscription(
            String,
            '/behavior_tree/events',
            self.forward_to_websocket,
            10
        )
    
    async def forward_to_websocket(self, msg):
        event = json.loads(msg.data)
        async with websockets.connect('ws://localhost:8765') as ws:
            await ws.send(json.dumps(event))

Unity integration

Use Unity's WebSocket package to stream from game AI:

using System.Collections;
using UnityEngine;
using NativeWebSocket;

public class TreeEventSender : MonoBehaviour
{
    private WebSocket websocket;
    
    async void Start()
    {
        websocket = new WebSocket("ws://localhost:8765");
        await websocket.Connect();
    }
    
    public void SendNodeStatus(string nodeId, string status)
    {
        var msg = new {
            type = "node_status",
            timestamp = System.DateTimeOffset.Now.ToUnixTimeMilliseconds(),
            node_id = nodeId,
            status = status
        };
        websocket.SendText(JsonUtility.ToJson(msg));
    }
    
    async void OnApplicationQuit()
    {
        await websocket.Close();
    }
}

Related resources

← Log replay Custom nodes →