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 through the MoveIt Pro Web 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.
The desktop app supports ROS access for both Runtime package panes (/api/packages/<package>/web/<file>) and separate HTTPS panes. In a browser deployment, Runtime package panes are display-only. A browser pane that needs ROS must run on a separate HTTPS port on the application hostname.
Use this to build interactive dashboards that watch sensor data, send commands, and monitor robot state in real time.
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 parent does not register the pane with the Web bridge. - Include the
iframe-ros-client.jslibrary in your HTML file and initialize the client.
MoveIt Pro serves iframe-ros-client.js at /iframe-ros-client.js for Runtime
package panes in the desktop app.
Add the following script tag to your HTML file's <head> or before the closing </body> tag:
<script src="/iframe-ros-client.js"></script>
For an absolute pane such as https://robot.example:8731/my-custom-pane.html,
/iframe-ros-client.js resolves against that pane server. The desktop installer
does not currently export this client as a separate SDK download. Use a
backend-local package pane for the supported ROS workflow unless the separately
hosted application already includes a version-matched copy of the client.
Absolute panes remain supported without ROS access. Do not load the client from
the privileged MoveIt Pro app origin.
Initialize the ROS Client
Create an instance of IframeROSClient. Its constructor installs the one trusted
parent-message listener before subscriptions are added. The client records
subscriptions until the parent sends its URL-free ros_bridge_ready message,
then replays them; it repeats that replay after each later ready message, such
as an iframe reload.
<script>
// Initialize the ROS client. All ROS traffic flows through the parent
// frame's Web bridge connection via postMessage.
const ros = new IframeROSClient();
</script>
Custom panes must use IframeROSClient through the parent-frame Web bridge. Existing rosbridge clients must migrate to a supported interface; see Migrating rosbridge Clients.
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 parent accepts messages only from the registered iframe window and its exact approved origin.
- Per-load capability: A new unguessable nonce is issued after each iframe load; stale messages are rejected.
- Credential isolation: The pane receives neither the reusable frontend key nor the parent's WebSocket connection details.
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.