ROS Integration for Custom View Panes
This guide assumes basic knowledge of HTML and JavaScript, and that you have already created a custom view pane.
For preliminary reading, please refer to:
Custom View Panes can subscribe and publish to ROS topics from inside their iframe. The iframe imports a small client library (iframe-ros-client.js) and calls subscribe / publish on an IframeROSClient instance; the parent frame routes each call over MoveIt Pro's WebSocket bridge to the live ROS graph. The iframe never opens its own WebSocket; all traffic flows through the parent over postMessage, so the pane works the same whether MoveIt Pro is on the same machine or accessed over a remote network.
Use this to build interactive dashboards that watch sensor data, send commands, and monitor robot state in real time.
IframeROSClient for Custom View PanesFor panes loaded inside MoveIt Pro, IframeROSClient is the supported migration path. It uses the parent-frame web bridge and avoids the rosbridge JSON protocol break on port 3201; panes that use this default path keep receiving data without starting the compatibility sidecar.
Enable ROS for a View Pane
Two things are required:
- Set
enable_web_bridge: trueon the pane's entry iniframeViewports(infrontend_settings.yaml) or via the in-app modal. Without this flag the pane is sandboxed andIframeROSClientcalls will be no-ops. - Include the
iframe-ros-client.jslibrary in your HTML file and initialize the client.
The iframe-ros-client.js file is automatically generated and served by MoveIt Pro. You don't need to download or manually include this file in your project - it's available at /iframe-ros-client.js when your custom view pane iframe is loaded from the MoveIt Pro web server.
Add the following script tag to your HTML file's <head> or before the closing </body> tag:
<script src="/iframe-ros-client.js"></script>
If your pane is served from a different host or port, such as http://localhost:8731/my-custom-pane.html, the root-relative path /iframe-ros-client.js resolves against that pane server, not MoveIt Pro. In that setup, either serve a matching copy of iframe-ros-client.js with your pane files or load it from the same origin where the MoveIt Pro web UI is open. For example, if the UI is loaded from http://localhost, use:
<script src="http://localhost/iframe-ros-client.js"></script>
The iframe can still use new IframeROSClient() and the parent-frame postMessage path after the script loads.
Initialize the ROS Client
Create an instance of the IframeROSClient:
<script>
// Initialize the ROS client. All ROS traffic flows through the parent
// frame's WebSocket bridge connection via postMessage.
const ros = new IframeROSClient();
</script>
useDirectConnection: true no longer works as of MoveIt Pro 9.4The IframeROSClient({ useDirectConnection: true }) option still compiles, but it opens a roslibjs connection that speaks the rosbridge JSON protocol. The default MoveIt Pro web bridge on port 3201 uses a binary CDR wire format instead, so a direct rosbridge client can appear connected while subscriptions produce no data. Existing panes that set this option must switch to the default parent-frame postMessage path shown above.
Prefer IframeROSClient for panes embedded in MoveIt Pro. If an existing pane or dashboard cannot use that parent-frame API and must open its own rosbridge JSON WebSocket, start MoveIt Pro with moveit_pro run --enable-rosbridge and point that client at the sidecar port, 3204 by default. See Legacy Websocket Compatibility for details and the --rosbridge-port override.
Subscribe to ROS Topics
To receive messages from ROS topics, use the subscribe method:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Joint States Monitor</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
background-color: #1a1a1a;
color: #ffffff;
}
#joint-states {
background-color: #2a2a2a;
padding: 15px;
border-radius: 5px;
margin-top: 20px;
font-family: monospace;
white-space: pre-wrap;
}
</style>
</head>
<body>
<h1>Joint States Monitor</h1>
<div id="joint-states">Waiting for joint states...</div>
<script src="/iframe-ros-client.js"></script>
<script>
const ros = new IframeROSClient();
const displayElement = document.getElementById('joint-states');
// Subscribe to joint states topic
ros.subscribe(
'/joint_states',
'sensor_msgs/msg/JointState',
(message) => {
// Update the display with received joint states
const jointNames = message.name || [];
const positions = message.position || [];
let displayText = 'Joint States:\n\n';
for (let i = 0; i < jointNames.length; i++) {
displayText += `${jointNames[i]}: ${positions[i]?.toFixed(4) || 'N/A'}\n`;
}
displayElement.textContent = displayText;
}
);
// Clean up on page unload
window.addEventListener('beforeunload', () => {
ros.unsubscribe('/joint_states', 'sensor_msgs/msg/JointState');
ros.disconnect();
});
</script>
</body>
</html>
This example subscribes to the /joint_states topic and displays the joint names and positions in real-time.
Publish to ROS Topics
To send messages to ROS topics, use the publish method:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Velocity Controller</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
background-color: #1a1a1a;
color: #ffffff;
}
button {
background-color: #4a9eff;
color: white;
border: none;
padding: 10px 20px;
margin: 5px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background-color: #3a8eef;
}
button:active {
background-color: #2a7edf;
}
</style>
</head>
<body>
<h1>Velocity Controller</h1>
<p>Click buttons to send velocity commands:</p>
<button id="forward">Forward</button>
<button id="backward">Backward</button>
<button id="left">Left</button>
<button id="right">Right</button>
<button id="stop">Stop</button>
<script src="/iframe-ros-client.js"></script>
<script>
const ros = new IframeROSClient();
function publishVelocity(linearX, angularZ) {
const message = {
linear: { x: linearX, y: 0.0, z: 0.0 },
angular: { x: 0.0, y: 0.0, z: angularZ }
};
ros.publish('/cmd_vel', 'geometry_msgs/msg/Twist', message);
}
document.getElementById('forward').addEventListener('click', () => {
publishVelocity(0.5, 0.0);
});
document.getElementById('backward').addEventListener('click', () => {
publishVelocity(-0.5, 0.0);
});
document.getElementById('left').addEventListener('click', () => {
publishVelocity(0.0, 0.5);
});
document.getElementById('right').addEventListener('click', () => {
publishVelocity(0.0, -0.5);
});
document.getElementById('stop').addEventListener('click', () => {
publishVelocity(0.0, 0.0);
});
// Clean up on page unload
window.addEventListener('beforeunload', () => {
ros.disconnect();
});
</script>
</body>
</html>
This example creates a simple velocity controller that publishes geometry_msgs/msg/Twist messages to the /cmd_vel topic.
Complete Interactive Example
Here's a complete example that combines subscription and publishing, with error handling and resource cleanup:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive Robot Dashboard</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
background-color: #1a1a1a;
color: #ffffff;
}
.container {
display: flex;
gap: 20px;
}
.panel {
flex: 1;
background-color: #2a2a2a;
padding: 15px;
border-radius: 5px;
}
button {
background-color: #4a9eff;
color: white;
border: none;
padding: 10px 20px;
margin: 5px;
border-radius: 5px;
cursor: pointer;
}
button:hover {
background-color: #3a8eef;
}
#status {
margin-top: 10px;
padding: 10px;
border-radius: 5px;
background-color: #3a3a3a;
}
.error {
background-color: #8b0000;
}
.success {
background-color: #006400;
}
</style>
</head>
<body>
<h1>Interactive Robot Dashboard</h1>
<div class="container">
<div class="panel">
<h2>Joint States</h2>
<div id="joint-states">Waiting for data...</div>
</div>
<div class="panel">
<h2>Control</h2>
<button id="start">Start</button>
<button id="stop">Stop</button>
<div id="status"></div>
</div>
</div>
<script src="/iframe-ros-client.js"></script>
<script>
const ros = new IframeROSClient();
const jointStatesElement = document.getElementById('joint-states');
const statusElement = document.getElementById('status');
let isSubscribed = false;
function updateStatus(message, isError = false) {
statusElement.textContent = message;
statusElement.className = isError ? 'error' : 'success';
}
function subscribeToJointStates() {
if (isSubscribed) {
return;
}
try {
ros.subscribe(
'/joint_states',
'sensor_msgs/msg/JointState',
(message) => {
const jointNames = message.name || [];
const positions = message.position || [];
let displayText = '';
for (let i = 0; i < jointNames.length; i++) {
displayText += `${jointNames[i]}: ${positions[i]?.toFixed(4) || 'N/A'}\n`;
}
jointStatesElement.textContent = displayText || 'No joint data';
}
);
isSubscribed = true;
updateStatus('Subscribed to /joint_states');
} catch (error) {
updateStatus(`Error subscribing: ${error.message}`, true);
}
}
function unsubscribeFromJointStates() {
if (!isSubscribed) {
return;
}
try {
ros.unsubscribe('/joint_states', 'sensor_msgs/msg/JointState');
isSubscribed = false;
jointStatesElement.textContent = 'Unsubscribed';
updateStatus('Unsubscribed from /joint_states');
} catch (error) {
updateStatus(`Error unsubscribing: ${error.message}`, true);
}
}
document.getElementById('start').addEventListener('click', subscribeToJointStates);
document.getElementById('stop').addEventListener('click', unsubscribeFromJointStates);
// Clean up on page unload
window.addEventListener('beforeunload', () => {
if (isSubscribed) {
ros.unsubscribe('/joint_states', 'sensor_msgs/msg/JointState');
}
ros.disconnect();
});
// Initial subscription
subscribeToJointStates();
</script>
</body>
</html>
This example demonstrates:
- Subscribing to ROS topics with error handling
- Displaying received data in real-time
- Proper cleanup when the page unloads
- User interface controls for managing subscriptions
API Reference
The IframeROSClient class provides the following methods:
subscribe(topic, messageType, callback)
Subscribe to a ROS topic and receive messages via a callback function.
- topic (string): The ROS topic name (e.g.,
/joint_states) - messageType (string): The ROS message type (e.g.,
sensor_msgs/msg/JointState) - callback (function): Function called with each received message
ros.subscribe('/joint_states', 'sensor_msgs/msg/JointState', (message) => {
console.log('Received:', message);
});
unsubscribe(topic, messageType)
Unsubscribe from a ROS topic.
- topic (string): The ROS topic name
- messageType (string): The ROS message type
ros.unsubscribe('/joint_states', 'sensor_msgs/msg/JointState');
publish(topic, messageType, message)
Publish a message to a ROS topic.
- topic (string): The ROS topic name (e.g.,
/cmd_vel) - messageType (string): The ROS message type (e.g.,
geometry_msgs/msg/Twist) - message (object): The message data (must match the message type structure)
ros.publish('/cmd_vel', 'geometry_msgs/msg/Twist', {
linear: { x: 0.5, y: 0.0, z: 0.0 },
angular: { x: 0.0, y: 0.0, z: 0.0 }
});
disconnect()
Disconnect from ROS and clean up all subscriptions.
ros.disconnect();
Security Considerations
The postMessage API used by IframeROSClient includes the following security features:
- Origin Validation: The API validates message origins to prevent spoofing
- Sandbox Permissions: Iframes run with restricted permissions
- Secure Communication: All communication between iframe and parent uses postMessage with origin validation
When using ROS integration in a Custom View Pane:
- Be cautious when enabling
enable_web_bridge, as it allows your iframe to interact with the ROS system - Validate and sanitize any data received from ROS topics before displaying it
- Only subscribe to topics you trust
- Be careful when publishing commands that could affect robot behavior
Best Practices
Error Handling
Always include error handling for ROS operations:
try {
ros.subscribe('/topic', 'message_type', (msg) => {
// Handle message
});
} catch (error) {
console.error('Subscription failed:', error);
// Show user-friendly error message
}
Resource Cleanup
Always clean up subscriptions and disconnect when your page unloads:
window.addEventListener('beforeunload', () => {
ros.unsubscribe('/topic', 'message_type');
ros.disconnect();
});
Performance
- Unsubscribe from topics when you no longer need them
- Avoid subscribing to high-frequency topics unnecessarily
- Use throttling or debouncing for UI updates based on ROS messages
Conclusion
This guide has covered how to integrate custom view panes with ROS topics:
- Including and initializing the
IframeROSClientlibrary - Subscribing to ROS topics to receive data
- Publishing messages to ROS topics
- Error handling and resource cleanup
- Best practices for performance and security
You should now be able to create interactive custom view panes that integrate seamlessly with your ROS system.
For more advanced ROS integration patterns, see the Programmatic SDKs overview documentation.