Theoretical Background: Web-Based Robot Control and Monitoring
Module 6 Theory: Web-Based Robot Control and Monitoring
Learning Objectives
- Understand the rosbridge WebSocket protocol and
useROSBridgecomposable - Analyse the four connection modes in
useRobotConnection - Understand the Nuxt 3 server-side rosbridge proxy pattern
- Study real-time data visualisation and the joystick velocity-mapping model
- Connect the web control layer to the autonomous behaviour (6.1), analytics (6.3), and deployment (6.4) modules
6.2.1 Web-Based Robotics Architecture
The ArbiterROS web interface eliminates platform-specific software requirements:
any device with a modern browser can control and monitor the robot. The
useROSBridge composable abstracts the rosbridge WebSocket connection,
while useRobotConnection unifies four distinct connection modes behind a
single API.
Communication Stack:
Browser (Vue 3)
useRobotConnection
useROSBridge <--> rosbridge_websocket :9090
useMobileRobotWithROS2 <--> /cmd_vel, /odom, /scan
Nuxt server (/proxy/rosbridge) <--> rosbridge_websocket :9090
(server-side proxy for CORS)
6.2.2 ROSBridge and roslibjs
rosbridge_websocket translates ROS 2 pub/sub, service calls, and action
goals into JSON messages transported over a WebSocket connection. The roslibjs
client library provides a JavaScript API that mirrors ROS concepts.
Connecting to rosbridge (useROSBridge.ts):
// useROSBridge.ts
export interface ROSConnectionConfig {
url: string // ws://host:9090
autoReconnect: boolean
reconnectInterval: number // ms
maxReconnectAttempts: number
}
const connect = (config: ROSConnectionConfig) => {
ros = new ROSLIB.Ros({ url: config.url })
ros.on('connection', () => {
connectionStatus.value = 'connected'
isConnected.value = true
})
ros.on('error', (err) => {
connectionStatus.value = 'error'
if (config.autoReconnect) _scheduleReconnect(config)
})
ros.on('close', () => {
connectionStatus.value = 'disconnected'
isConnected.value = false
})
}
Topic Publication and Subscription:
// useROSBridge.ts
const publishTopic = <T>(
topicConfig: { name: string; messageType: string },
message: T,
throttleMs = 0
) => {
const topic = _getOrCreateTopic(topicConfig)
topic.publish(new ROSLIB.Message(message as object))
}
const subscribeTopic = <T>(
topicConfig: { name: string; messageType: string },
callback: (msg: T) => void
) => {
const topic = _getOrCreateTopic(topicConfig)
topic.subscribe(callback)
return () => topic.unsubscribe(callback) // returns unsubscribe function
}
6.2.3 Connection Modes
useRobotConnection defines four named modes. Switching mode is atomic —
the composable disconnects the previous bridge before establishing a new connection:
// useRobotConnection.ts
export type ConnectionMode =
'simulation' | 'ros2-virtual' | 'ros2-hardware' | 'ros2-pi4b'
const statusLabel = computed(() => {
if (mode.value === 'simulation') return 'Simulation (no ROS2)'
if (!bridge.isConnected.value) return 'Disconnected'
if (mode.value === 'ros2-pi4b') return 'Raspberry Pi 4B'
if (robotInfo.value?.type === 'hardware') return 'Physical Robot'
return 'Virtual Robot (ROS2)'
})
| Mode | Backend required | rosbridge URL |
|---|---|---|
simulation | None | N/A (client-side physics only) |
ros2-virtual | Docker or native on same host | ws://localhost:9090 |
ros2-hardware | Native on Pi, laptop connects | ws://pi-ip:9090 |
ros2-pi4b | Native on Pi, Pi hosts browser | ws://localhost:9090 |
The /robot_info topic carries a JSON string identifying the backend type
(virtual vs hardware) and its capabilities. This allows the UI to
conditionally show hardware-specific controls:
// Published by hardware_robot.py on startup:
{
"type": "hardware",
"wheel_base": 0.20,
"max_linear_vel": 0.4,
"capabilities": ["encoders", "pigpio", "rplidar"]
}
6.2.4 Nuxt Server-Side Rosbridge Proxy
When the frontend is hosted at a public URL (e.g. arbiter.txio.live) but
the rosbridge is on localhost of the deployment machine, browser CORS
restrictions prevent a direct WebSocket connection. ArbiterROS solves this with a
Nuxt 3 server route that proxies the WebSocket connection server-side:
// frontend/server/routes/proxy/rosbridge.ts
export default defineEventHandler(async (event) => {
// Proxy WebSocket upgrade requests to local rosbridge
const targetUrl = process.env.ROSBRIDGE_URL || 'ws://localhost:9090'
await proxyRequest(event, targetUrl)
})
The nuxt.config.ts exposes this at /proxy/rosbridge, so the
browser connects to wss://arbiter.txio.live/ proxy/rosbridge (TLS-encrypted)
and the Nuxt server maintains the internal ws://localhost:9090
connection.
6.2.5 Manual Control: Joystick Velocity Mapping
The /manual page implements a virtual joystick that maps 2D
pointer/touch position to differential-drive velocity commands.
Velocity Mapping Model:
where are normalised joystick coordinates. The negation in Equation (2) converts rightward joystick deflection (positive ) to clockwise robot rotation (negative ), matching the natural driving convention.
// Joystick handler — /manual page
const onJoystickMove = (x: number, y: number) => {
// x,y in [-1, 1]
const cmd: Twist = {
linear: { x: y * maxLinear.value, y: 0, z: 0 },
angular: { x: 0, y: 0, z: -x * maxAngular.value }
}
// Publish at up to 20 Hz (rate-limited)
publishTopic('/cmd_vel', 'geometry_msgs/Twist', cmd)
}
// On joystick release — stop immediately
const onJoystickRelease = () => {
publishTopic('/cmd_vel', 'geometry_msgs/Twist', ZERO_TWIST)
}
Rate Limiting:
Manual commands are rate-limited to 20 Hz. Publishing faster does not improve control feel because the robot’s mechanical response time (motor inertia + encoder update rate) is slower than 50 Hz, and excessive messages create WebSocket backpressure.
6.2.6 Real-Time Data Visualisation
Telemetry Topics:
/odomposition display , path trail, velocity HUD/scanlaser point cloud overlay in Three.js 3D view/latency_reportslive latency sparkline on/manualpage/robot_statusbehaviour mode badge in/ros2-testpage
Odometry Processing (coordinate mapping):
// Quaternion to Euler (yaw only) — used in all ROS2 composables
const quat2yaw = (q: Quaternion): number =>
Math.atan2(
2.0 * (q.w * q.z + q.x * q.y),
1.0 - 2.0 * (q.y * q.y + q.z * q.z)
)
// Coordinate mapping to Three.js (from CLAUDE.md design rule):
// physics.x -> THREE.x (no negation)
// physics.y -> THREE.z (no negation)
// theta -> mesh.rotation.y = -theta (negate ONLY theta)
6.2.7 ROS 2 Test Bench
The /ros2-test page (ros2Robot.ts) provides a developer-facing
interface for direct topic inspection:
- Publish arbitrary
/cmd_velvalues with sliders - Subscribe and display raw message payloads from any topic
- Trigger ROS 2 services directly from the browser
- View live connection health and last-received timestamps per topic
Integration: Theory to Practice
The web control layer is the primary human interface that connects every other
Chapter 6 module. The web dashboard switches the autonomous behaviour node (6.1) via
/behavior_mode topic messages. It reads analytics data (6.3) from
useAnalyticsEngine to display live performance cards. When deployed via
Docker Compose (6.4), the Nuxt 3 server and rosbridge containers share an internal
Docker network, and the server-side proxy routes browser WebSocket traffic through
to rosbridge without CORS issues.
Theoretical Design Choices
Why WebSocket over HTTP polling? HTTP polling introduces latency equal to half the polling interval and wastes bandwidth with empty responses when no new data is available. WebSocket provides true push-based delivery where messages arrive at the browser the instant they are published in ROS 2, with latency limited only by network propagation time and 2–6 bytes of framing overhead versus hundreds of bytes for HTTP headers.
Why server-side proxy instead of direct browser-to-rosbridge? Two reasons:
(1) TLS — browsers require wss:// for secure contexts; the Nuxt proxy
handles TLS termination via the nginx-proxy upstream, and the internal
ws://localhost:9090 connection does not need certificates.
(2) CORS — rosbridge does not implement the WebSocket CORS handshake that modern
browsers require; the Nuxt proxy runs on the same origin as the frontend, so the
browser treats it as a same-origin connection.
Why joystick uses velocity mode, not position mode? In position mode, releasing the joystick returns it to centre, which would require the robot to reverse to a home position — unintuitive and potentially dangerous. Velocity mode maps joystick deflection to rate of motion; releasing the joystick publishes a zero-velocity command and the robot stops immediately. This matches the mental model of a physical joystick-controlled vehicle.
Why 20 Hz command rate for manual control? The virtual robot physics
engine and the hardware ROS 2 node both process /cmd_vel at 20 Hz. Commands
at higher rates are redundant and create WebSocket queue backpressure. The
10–20 ms round-trip for a local Docker deployment means the robot tracks joystick
input with imperceptible delay at this rate.