← All modules

6.2 Web Robot Control

Draft — not verified

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 useROSBridge composable
  • 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)'
})
ModeBackend requiredrosbridge URL
simulationNoneN/A (client-side physics only)
ros2-virtualDocker or native on same hostws://localhost:9090
ros2-hardwareNative on Pi, laptop connectsws://pi-ip:9090
ros2-pi4bNative on Pi, Pi hosts browserws://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:

  • /odom position display , path trail, velocity HUD
  • /scan laser point cloud overlay in Three.js 3D view
  • /latency_reports live latency sparkline on /manual page
  • /robot_status behaviour mode badge in /ros2-test page

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_vel values 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.

Inspect the rosbridge WebSocket traffic
Open browser DevTools → Network → WS to see the JSON Twist frames described above.
/manual