This demo showcases how Bonsai's Web UI interacts with a WebSocket server. The demo illustrates how the Web UI establishes a connection, exchanges data with the server, and updates the UI dynamically based on incoming data.
You can use the above textbox to send a message to Bonsai which will be printed to the console!
If you prefer, you can explore the source code directly in the data/webui directory:
Each file contains detailed comments to help you understand the code. Alternatively, continue reading this demo page to understand how the Web UI operates
In the websocket server, there are two key namespaces.
These namespaces are attached to the WebSocket server, allowing event-driven communication.
class WebNamespace(socketio.AsyncNamespace):
async def on_connect(self, sid, environ):
...
async def on_disconnect(self, sid):
...
async def on_web_operator(self, sid, data):
...
class BlenderNamespace(socketio.AsyncNamespace):
async def on_connect(self, sid, environ):
...
async def on_disconnect(self, sid):
...
async def on_demo_data(self, sid, data):
...
Each namespace defines event handlers that are triggered when a connected client emits specific events. For example, the on_demo_data function is invoked when a client emits the demo_data event.
The server also defines routes, such as the /demo URL, which are tied to specific request handlers.
async def demo(request):
with open("templates/demo.html", "r") as f:
template = f.read()
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
return web.Response(text=html_content, content_type="text/html")
app.router.add_get("/demo", demo)
In this example, when the /demo URL is accessed, the server reads the demo.html template, renders it with variables like port and version, and returns the fully rendered HTML to the client's browser.
When the Web UI URL is visited, the server follows these steps:
The SOCKET_PORT variable is injected into the HTML and later used by the JavaScript file to establish a WebSocket connection.
<script>
var SOCKET_PORT = {{port}};
</script>
After the HTML page loads, the browser automatically requests static files, such as CSS and JavaScript, from the static directory specified in the sioserver.py file.
<link rel="stylesheet" href="/static/css/demo.css" id="demo-stylesheet" />
<script defer src="./static/js/demo.js"></script>
The JavaScript file is responsible for establishing and managing the WebSocket connection. It starts by ensuring the DOM is fully loaded before executing any scripts.
let socket;
$(document).ready(function () {
var defaultTheme = "blender"; // Default theme to be applied
var theme = localStorage.getItem("theme") || defaultTheme; // Retrieve the stored theme or use the default
setTheme(theme); // Apply the theme
connectSocket(); // Establish WebSocket connection
});
Upon page load, the script sets the theme (either from local storage or the default) and then calls connectSocket function to initiate the WebSocket connection.
The connectSocket function constructs the WebSocket connection URL using the SOCKET_PORT variable and then initializes the connection.
function connectSocket() {
const url = "ws://localhost:" + SOCKET_PORT + "/web";
socket = io(url);
console.log("socket: ", socket);
// Register socket event handlers
socket.on("blender_connect", handleBlenderConnect);
socket.on("blender_disconnect", handleBlenderDisconnect);
socket.on("theme_data", handleThemeData);
socket.on("demo_data", handleDemoData);
}
The socket variable is used to manage the WebSocket connection within the web namespace. After establishing the connection, the script registers event handlers for specific events emitted by the server.
Event handlers process the data received from the server and manipulate the DOM to reflect the changes on the webpage. For instance, the handleDemoData function processes the demo_data event and updates the demo content on the page.
function handleDemoData(demoData) {
console.log(demoData);
const message = demoData["data"]["demo_message"];
const blenderId = demoData["blenderId"];
const id = "message-" + blenderId;
const messageHeader = $("#" + id);
const messageText = `Bonsai instance with ID: ${blenderId} sent the message: ${message}`;
if (messageHeader[0] === undefined) {
const newMessageElement = $("<h3>", { id: id }).text(messageText);
newMessageElement.prependTo("#message-container");
} else {
messageHeader.text(messageText);
messageHeader.prependTo("#message-container");
}
}
we use the BlenderId, which is a unique identifier given for by the server for each client connedted to it, to differentiate between different Bonsai instances.
This functionality is repeated for other event handlers, such as handleBlenderConnect, handleBlenderDisconnect, and handleThemeData, where specific actions are taken based on the event type and the data provided by the server.
Data is sent to the server in response to DOM events, such as onclick. These events trigger functions that use the socket variable to emit an event to the server, passing the appropriate data for the specific web operator.
function SendMessage() {
const inputMessage = $("#input-message").val();
const msg = {
sourcePage: "demo",
// blenderId: BlenderId,
operator: {
type: "message",
message: inputMessage,
},
};
socket.emit("web_operator", msg);
}
In this example, the blenderId field specifies which Bonsai instance should receive the operator. If blenderId is not set, the operator is broadcast to all instances.
The server forwards this operator to the appropriate Bonsai instance, where it is processed by functions like sio_listen_web_operator and check_operator_queue in tool/web.py.
the sio_listen_web_operator function is automatically called when the event web_operator is emitted to Bonsai. It takes the web operator and attempts to put it in a web operators queue.
@classmethod
async def sio_listen_web_operator(cls, data):
try:
web_operator_queue.put_nowait(data)
except queue.Full:
pass
then the check_operator_queue function is called by a timer that is run every second to retrieve operators from the queue and handle them
@classmethod
def check_operator_queue(cls):
if not bpy.context.scene.WebProperties.is_connected:
with web_operator_queue.mutex:
web_operator_queue.queue.clear()
return None # unregister timer if not connected
while not web_operator_queue.empty():
operator = web_operator_queue.get_nowait()
if not operator:
continue
if operator["sourcePage"] == "csv":
cls.handle_csv_operator(operator["operator"])
elif operator["sourcePage"] == "gantt":
cls.handle_gantt_operator(operator["operator"])
elif operator["sourcePage"] == "drawings":
cls.handle_drawings_operator(operator["operator"])
elif operator["sourcePage"] == "demo":
message = operator["operator"]["message"]
print(f"Message from demo page: {message}")
return 1.0
Here we check the source page of the operator and call the appropriate handler for that page.