Live monitor via WebSocket
Watch your behavior tree execute in real-time on a remote robot, game server, or simulation. The WebSocket monitor streams node status updates (RUNNING, SUCCESS, FAILURE) as they happen, letting you debug live systems without stopping execution or parsing log files.
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.
- 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:
ws://localhost:8765— local developmentwss://your-server.com/ws— production with TLSws://192.168.1.100:9000— local network robot
Step 3: Connect
Click Connect. The button will change to Disconnect when connected. Status indicators:
- 🟢 Green — Connected and receiving events
- 🟡 Yellow — Connecting...
- 🔴 Red — Disconnected or connection failed
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:
- Import the same JSON/XML file that your server loaded
- Reconstruct the tree manually with matching node names
-
Use consistent naming conventions (e.g.,
Wait_1,Sequence_Patrol)
Real-time visualization
As events arrive, nodes in the canvas update their appearance:
- Blue border — Node is RUNNING (currently executing)
- Green background — Node returned SUCCESS
- Red background — Node returned FAILURE
- Gray — Node hasn't been ticked yet
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:
- ⏸ Pause — Freeze the visualization at current state
- ⏮ Step back — Show previous event (if buffered)
- ⏭ Step forward — Skip to next event
- Speed slider — Control event processing rate
Event buffering
The editor buffers the last 1000 events in memory. This allows you to:
- Step backward through recent history
- Analyze execution patterns after the fact
- Export the buffer as an NDJSON file for later analysis
Filtering events
Use the filter input to show only specific nodes or statuses:
Wait— Show only Wait nodesFAILURE— Show only failuresSequence_1 FAILURE— Combined filter
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:
- Protects against eavesdropping on node status data
- Prevents man-in-the-middle attacks
- Required by modern browsers for cross-origin connections
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
- Verify the WebSocket server is running
- Check firewall settings allow the port
- Ensure you're using the correct protocol (ws vs wss)
Events not updating visualization
- Confirm node IDs in messages match node names in the editor
- Check browser console for JSON parse errors
- Verify message format matches the schema above
High latency or dropped messages
- Reduce event frequency (batch multiple ticks into one message)
- Check network bandwidth between server and client
- Consider compressing messages with gzip
Browser blocks WebSocket connection
- Mixed content error: Ensure both page and WebSocket use HTTPS/WSS
- CORS issues: Configure server to allow your domain
- Extension interference: Try incognito mode
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
- Replay NDJSON execution logs — offline analysis of recorded runs
- Define custom node types — create domain-specific nodes
- Reactive control patterns — design principles for responsive trees