feat: rosbridge WebSocket server for web UI (port 9090)

Adds rosbridge_suite to the Jetson stack so the browser dashboard can
subscribe to ROS2 topics via roslibjs over ws://jetson:9090.

docker-compose.yml
  New service: saltybot-rosbridge
  - Runs saltybot_bringup/launch/rosbridge.launch.py
  - network_mode: host → port 9090 directly reachable on Jetson LAN
  - Depends on saltybot-ros2, stm32-bridge, csi-cameras

saltybot_bringup/launch/rosbridge.launch.py
  - rosbridge_websocket node (port 9090, params from rosbridge_params.yaml)
  - 4× image_transport/republish nodes: compress CSI camera streams
    /camera/<name>/image_raw → /camera/<name>/image_raw/compressed (JPEG 75%)

saltybot_bringup/config/rosbridge_params.yaml
  Whitelisted topics:
    /map  /scan  /tf  /tf_static
    /saltybot/imu  /saltybot/balance_state
    /cmd_vel
    /person/*
    /camera/*/image_raw/compressed
  max_message_size: 10 MB (OccupancyGrid headroom)

saltybot_bringup/SENSORS.md
  Added rosbridge connection section with roslibjs snippet,
  topic reference table, bandwidth estimates, and throttle_rate tips.

saltybot_bringup/package.xml
  Added exec_depend: rosbridge_server, image_transport,
  image_transport_plugins (all already installed in Docker image).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sl-firmware 2026-02-28 23:16:17 -05:00 committed by Sebastien Vayrette
parent 3d2d317431
commit 6420e07487
14 changed files with 257 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

BIN
.pio/build/f722/src/crsf.o Normal file

Binary file not shown.

BIN
.pio/build/f722/src/i2c1.o Normal file

Binary file not shown.

Binary file not shown.

BIN
.pio/build/f722/src/mag.o Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -166,6 +166,20 @@ services:
# ── Surround vision — 360° bird's-eye view + Nav2 camera obstacle layer ─────
saltybot-surround:
# ── rosbridge WebSocket server ────────────────────────────────────────────
# Serves ROS2 topics to the web dashboard (roslibjs) on ws://jetson:9090
#
# Topics exposed (whitelist in ros2_ws/src/saltybot_bringup/config/rosbridge_params.yaml):
# /map /scan /tf /tf_static /saltybot/imu /saltybot/balance_state
# /cmd_vel /person/* /camera/*/image_raw/compressed
#
# Also runs image_transport/republish nodes to compress 4× CSI camera streams.
# Raw 640×480 YUYV → JPEG-compressed sensor_msgs/CompressedImage.
#
# Install (already in image): apt install ros-humble-rosbridge-server
# ros-humble-image-transport-plugins
rosbridge:
image: saltybot/ros2-humble:jetson-orin
build:
context: .
@ -191,6 +205,29 @@ services:
start_cameras:=false
camera_height:=0.30
publish_rate:=5.0
container_name: saltybot-rosbridge
restart: unless-stopped
runtime: nvidia
network_mode: host # port 9090 is directly reachable on host
depends_on:
- saltybot-ros2 # needs /map, /tf published
- stm32-bridge # needs /saltybot/imu, /saltybot/balance_state
- csi-cameras # needs /camera/*/image_raw for compression
environment:
- ROS_DOMAIN_ID=42
- RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
volumes:
- ./ros2_ws/src:/ros2_ws/src:rw
- ./config:/config:ro
# Port 9090 is accessible on all host interfaces via network_mode: host.
# To restrict to a specific interface, set host: "192.168.x.x" in
# rosbridge_params.yaml instead of using Docker port mappings.
command: >
bash -c "
source /opt/ros/humble/setup.bash &&
source /ros2_ws/install/local_setup.bash 2>/dev/null || true &&
ros2 launch saltybot_bringup rosbridge.launch.py
"
# ── Nav2 autonomous navigation stack ────────────────────────────────────────

View File

@ -4,6 +4,60 @@ ROS2 Humble | ROS_DOMAIN_ID=42 | DDS: CycloneDDS
---
## rosbridge WebSocket (web dashboard)
The web dashboard connects to ROS2 topics via rosbridge WebSocket on **port 9090**.
```bash
# Start rosbridge (and compressed camera republishers):
ros2 launch saltybot_bringup rosbridge.launch.py
# Or via docker-compose:
docker compose up -d rosbridge
```
**Browser connection (roslibjs):**
```javascript
var ros = new ROSLIB.Ros({ url: 'ws://jetson.local:9090' });
ros.on('connection', () => console.log('connected'));
ros.on('error', (e) => console.error('error', e));
ros.on('close', () => console.log('disconnected'));
```
**Whitelisted topics** (configured in `config/rosbridge_params.yaml`):
| Topic | Type | Notes |
|-------|------|-------|
| `/map` | `nav_msgs/OccupancyGrid` | Use `throttle_rate: 5000` (0.2 Hz) |
| `/scan` | `sensor_msgs/LaserScan` | 5.5 Hz, 1440 pts |
| `/tf` | `tf2_msgs/TFMessage` | Use `throttle_rate: 100` (10 Hz) |
| `/tf_static` | `tf2_msgs/TFMessage` | One-shot on connect |
| `/saltybot/imu` | `sensor_msgs/Imu` | 50 Hz, pitch/roll/yaw |
| `/saltybot/balance_state` | `std_msgs/String` | JSON balance controller state |
| `/cmd_vel` | `geometry_msgs/Twist` | Subscribe to monitor Nav2 commands |
| `/person/target` | (custom) | Person tracking target |
| `/person/detections` | (custom) | Person detection results |
| `/camera/front/image_raw/compressed` | `sensor_msgs/CompressedImage` | JPEG 75%, 640×480 |
| `/camera/left/image_raw/compressed` | `sensor_msgs/CompressedImage` | JPEG 75%, 640×480 |
| `/camera/rear/image_raw/compressed` | `sensor_msgs/CompressedImage` | JPEG 75%, 640×480 |
| `/camera/right/image_raw/compressed` | `sensor_msgs/CompressedImage` | JPEG 75%, 640×480 |
**Bandwidth tips:**
- Raw 640×480 YUYV = ~900 KB/frame. rosbridge republishes JPEG-compressed ≈ 1525 KB/frame.
- Always use `throttle_rate` for cameras in roslibjs (e.g. `throttle_rate: 200` = max 5 fps).
- `/map` is large (~100 KB); use `throttle_rate: 5000` or subscribe only on demand.
**Verify connection:**
```bash
# Check WebSocket is listening
ss -tlnp | grep 9090
# List topics visible to rosbridge
ros2 topic list | grep -E 'map|scan|imu|camera|person|cmd_vel'
```
---
## Build & Run
```bash

View File

@ -0,0 +1,66 @@
# rosbridge_params.yaml — rosbridge_websocket server configuration
#
# WebSocket endpoint: ws://<jetson-ip>:9090
# Browser client lib: roslibjs (cdn.jsdelivr.net/npm/roslib)
#
# Topic whitelist limits bandwidth and prevents unintended exposure.
# Glob patterns follow Python fnmatch: * matches everything (incl. /).
#
# Bandwidth budget (worst case, all subscriptions active):
# /map ~100 KB/update × 0.2 Hz = ~20 KB/s
# /scan ~5 KB/scan × 5.5 Hz = ~28 KB/s
# /saltybot/imu ~0.2 KB/msg × 50 Hz = ~10 KB/s
# /tf ~1 KB/msg × 10 Hz = ~10 KB/s
# 4× cameras ~20 KB/frame × 5 Hz = ~400 KB/s (JPEG quality 75)
# ─────────────────────────────────────────────────────
# Total ~470 KB/s ≈ 3.8 Mbps
#
# Use throttle_rate in roslibjs subscribe() calls to cap per-topic rates:
# viewer.subscribe({ topic: '/map', throttle_rate: 5000 }) // 0.2 Hz
rosbridge_websocket:
ros__parameters:
# ── Network ──────────────────────────────────────────────────────────────
port: 9090
host: "0.0.0.0" # bind all interfaces (Jetson LAN + USB-C)
address: ""
# ── Authentication ────────────────────────────────────────────────────────
authenticate: false # no auth on local network; enable if exposed
# ── Message size ──────────────────────────────────────────────────────────
# OccupancyGrid /map can exceed 1 MB for large environments.
max_message_size: 10000000 # 10 MB
# ── Topic/service/param whitelists ────────────────────────────────────────
# JSON-encoded glob list. "*" in a glob matches any chars including "/".
# Only topics matching at least one pattern are accessible to clients.
topics_glob: >-
["/map",
"/person/target",
"/person/detections",
"/camera/*/image_raw/compressed",
"/scan",
"/cmd_vel",
"/saltybot/imu",
"/saltybot/balance_state",
"/tf",
"/tf_static"]
services_glob: "[]" # no service calls via WebSocket
params_glob: "[]" # no parameter access via WebSocket
# ── Connection management ─────────────────────────────────────────────────
# Time (s) before dropping a client that stops responding.
unregister_timeout: 10.0
# Fragment timeout: max seconds to wait for all fragments of a large message.
fragment_timeout: 600
# Delay between consecutive outgoing messages (ms). 0 = unlimited.
# Set > 0 (e.g. 10) if browser JS event loop is overwhelmed.
delay_between_messages: 0
# ── Logging ───────────────────────────────────────────────────────────────
# Set to true to log every publish/subscribe call (verbose, dev only).
bson_only_mode: false

View File

@ -0,0 +1,97 @@
"""
rosbridge.launch.py rosbridge WebSocket server + compressed image transport
Starts two things:
1. rosbridge_websocket WebSocket server on port 9090 (roslibjs-compatible)
2. image_transport/republish × 4 compresses each CSI camera's raw stream
/camera/<name>/image_raw /camera/<name>/image_raw/compressed
Prerequisites (already installed in saltybot/ros2-humble:jetson-orin):
apt install ros-humble-rosbridge-server
apt install ros-humble-image-transport-plugins # supplies compressed plugin
Browser connection:
var ros = new ROSLIB.Ros({ url: 'ws://<jetson-ip>:9090' });
Topic whitelist is configured in config/rosbridge_params.yaml.
Bandwidth tips:
- Subscribe to /camera/<name>/image_raw/compressed, not raw images.
- Use throttle_rate in ROSLIB.Topic to cap camera update rate:
new ROSLIB.Topic({ ... messageType: 'sensor_msgs/CompressedImage',
throttle_rate: 200 }) // max 5 fps per camera
- Subscribe to /map with throttle_rate: 5000 (0.2 Hz is enough for display).
Usage:
ros2 launch saltybot_bringup rosbridge.launch.py
Verify:
ros2 topic echo /rosbridge_websocket/status
# In browser console:
# var ros = new ROSLIB.Ros({ url: 'ws://jetson.local:9090' });
# ros.on('connection', () => console.log('connected'));
"""
import os
from launch import LaunchDescription
from launch_ros.actions import Node
from ament_index_python.packages import get_package_share_directory
# Camera names matching saltybot_cameras/launch/csi_cameras.launch.py
# Topics: /camera/<name>/image_raw (published by v4l2_camera_node)
_CAMERAS = ['front', 'left', 'rear', 'right']
# JPEG quality for compressed output (0100).
# 75 = good quality/size trade-off at 640×480: ~1525 KB/frame.
# Lower to 50 for tighter bandwidth budgets; raise to 90 for inspection use.
_JPEG_QUALITY = 75
def generate_launch_description():
pkg_share = get_package_share_directory('saltybot_bringup')
params_file = os.path.join(pkg_share, 'config', 'rosbridge_params.yaml')
# ── rosbridge WebSocket server ────────────────────────────────────────────
rosbridge = Node(
package='rosbridge_server',
executable='rosbridge_websocket',
name='rosbridge_websocket',
parameters=[params_file],
output='screen',
)
# ── Compressed image republishers ─────────────────────────────────────────
# image_transport/republish subscribes to raw sensor_msgs/Image and
# re-publishes as sensor_msgs/CompressedImage using the compressed plugin.
#
# Node arguments: ['raw', 'compressed']
# 'raw' — input transport type
# 'compressed' — output transport type
#
# Topic remappings:
# in → /camera/<name>/image_raw
# out/compressed → /camera/<name>/image_raw/compressed
#
# Parameter jpeg_quality controls JPEG encoder quality for the
# compressed publisher. The full parameter path in ROS2 is:
# /<node_name>/compressed/jpeg_quality
republishers = [
Node(
package='image_transport',
executable='republish',
name=f'compress_{name}',
arguments=['raw', 'compressed'],
remappings=[
('in', f'/camera/{name}/image_raw'),
('out/compressed', f'/camera/{name}/image_raw/compressed'),
],
parameters=[{
'compressed.jpeg_quality': _JPEG_QUALITY,
}],
output='screen',
)
for name in _CAMERAS
]
return LaunchDescription([rosbridge] + republishers)

View File

@ -15,6 +15,9 @@
<exec_depend>nav2_bringup</exec_depend>
<exec_depend>robot_state_publisher</exec_depend>
<exec_depend>tf2_ros</exec_depend>
<exec_depend>rosbridge_server</exec_depend>
<exec_depend>image_transport</exec_depend>
<exec_depend>image_transport_plugins</exec_depend>
<buildtool_depend>ament_python</buildtool_depend>