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:
parent
3d2d317431
commit
6420e07487
BIN
.pio/build/f722/src/crsf.o
Normal file
BIN
.pio/build/f722/src/crsf.o
Normal file
Binary file not shown.
BIN
.pio/build/f722/src/i2c1.o
Normal file
BIN
.pio/build/f722/src/i2c1.o
Normal file
Binary file not shown.
BIN
.pio/build/f722/src/jetson_cmd.o
Normal file
BIN
.pio/build/f722/src/jetson_cmd.o
Normal file
Binary file not shown.
BIN
.pio/build/f722/src/mag.o
Normal file
BIN
.pio/build/f722/src/mag.o
Normal file
Binary file not shown.
BIN
.pio/build/f722/src/mode_manager.o
Normal file
BIN
.pio/build/f722/src/mode_manager.o
Normal file
Binary file not shown.
BIN
.pio/build/f722/src/motor_driver.o
Normal file
BIN
.pio/build/f722/src/motor_driver.o
Normal file
Binary file not shown.
BIN
.pio/build/f722/src/mpu6000.o
Normal file
BIN
.pio/build/f722/src/mpu6000.o
Normal file
Binary file not shown.
BIN
.pio/build/f722/src/safety.o
Normal file
BIN
.pio/build/f722/src/safety.o
Normal file
Binary file not shown.
@ -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 ────────────────────────────────────────
|
||||
|
||||
@ -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 ≈ 15–25 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
|
||||
|
||||
@ -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
|
||||
@ -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 (0–100).
|
||||
# 75 = good quality/size trade-off at 640×480: ~15–25 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)
|
||||
@ -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>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user