feat: WebSocket bridge for CAN monitor dashboard (Issue #697) #702
@ -49,6 +49,9 @@ rosbridge_websocket:
|
||||
"/cmd_vel",
|
||||
"/saltybot/imu",
|
||||
"/saltybot/balance_state",
|
||||
"/saltybot/barometer",
|
||||
"/vesc/left/state",
|
||||
"/vesc/right/state",
|
||||
"/tf",
|
||||
"/tf_static"]
|
||||
|
||||
|
||||
@ -0,0 +1,70 @@
|
||||
"""
|
||||
can_monitor.launch.py — Lightweight rosbridge server for CAN sensor dashboard (Issue #697)
|
||||
|
||||
Starts rosbridge_websocket on port 9090 with a whitelist limited to the five
|
||||
topics consumed by can_monitor_panel.html:
|
||||
|
||||
/saltybot/imu sensor_msgs/Imu — IMU attitude
|
||||
/saltybot/balance_state std_msgs/String (JSON) — balance PID state
|
||||
/saltybot/barometer std_msgs/String (JSON) — pressure / temp / altitude
|
||||
/vesc/left/state std_msgs/String (JSON) — left VESC telemetry
|
||||
/vesc/right/state std_msgs/String (JSON) — right VESC telemetry
|
||||
|
||||
Usage:
|
||||
ros2 launch saltybot_bringup can_monitor.launch.py
|
||||
|
||||
# Override port if needed:
|
||||
ros2 launch saltybot_bringup can_monitor.launch.py port:=9091
|
||||
|
||||
Verify:
|
||||
# From a browser on the same LAN:
|
||||
# var ros = new ROSLIB.Ros({ url: 'ws://<jetson-ip>:9090' });
|
||||
# ros.on('connection', () => console.log('connected'));
|
||||
"""
|
||||
|
||||
import os
|
||||
from launch import LaunchDescription
|
||||
from launch.actions import DeclareLaunchArgument
|
||||
from launch.substitutions import LaunchConfiguration
|
||||
from launch_ros.actions import Node
|
||||
|
||||
|
||||
# Topics exposed to the CAN monitor WebUI panel.
|
||||
_TOPICS = [
|
||||
'/saltybot/imu',
|
||||
'/saltybot/balance_state',
|
||||
'/saltybot/barometer',
|
||||
'/vesc/left/state',
|
||||
'/vesc/right/state',
|
||||
]
|
||||
|
||||
_TOPICS_GLOB = '[' + ', '.join(f'"{t}"' for t in _TOPICS) + ']'
|
||||
|
||||
|
||||
def generate_launch_description():
|
||||
port_arg = DeclareLaunchArgument(
|
||||
'port',
|
||||
default_value='9090',
|
||||
description='WebSocket port for rosbridge (default 9090)',
|
||||
)
|
||||
|
||||
rosbridge = Node(
|
||||
package='rosbridge_server',
|
||||
executable='rosbridge_websocket',
|
||||
name='rosbridge_websocket',
|
||||
parameters=[{
|
||||
'port': LaunchConfiguration('port'),
|
||||
'host': '0.0.0.0',
|
||||
'authenticate': False,
|
||||
'max_message_size': 1000000, # 1 MB — no large map payloads needed
|
||||
'topics_glob': _TOPICS_GLOB,
|
||||
'services_glob': '[]',
|
||||
'params_glob': '[]',
|
||||
'unregister_timeout': 10.0,
|
||||
'fragment_timeout': 600,
|
||||
'delay_between_messages': 0,
|
||||
}],
|
||||
output='screen',
|
||||
)
|
||||
|
||||
return LaunchDescription([port_arg, rosbridge])
|
||||
@ -293,11 +293,51 @@ function drawCompass(yaw) {
|
||||
|
||||
// ── ROS connection ────────────────────────────────────────────────────────────
|
||||
|
||||
function connect(url) {
|
||||
// ── Auto-reconnect state ──────────────────────────────────────────────────────
|
||||
|
||||
const RECONNECT_BASE_MS = 2000;
|
||||
const RECONNECT_MAX_MS = 30000;
|
||||
const RECONNECT_FACTOR = 1.5;
|
||||
|
||||
let reconnectDelay = RECONNECT_BASE_MS;
|
||||
let reconnectTimer = null;
|
||||
let reconnectTick = null;
|
||||
let reconnectTarget = null;
|
||||
|
||||
function cancelReconnect() {
|
||||
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
||||
if (reconnectTick) { clearInterval(reconnectTick); reconnectTick = null; }
|
||||
}
|
||||
|
||||
function scheduleReconnect(url) {
|
||||
cancelReconnect();
|
||||
let secs = Math.round(reconnectDelay / 1000);
|
||||
$('conn-label').textContent = `Retry in ${secs}s…`;
|
||||
|
||||
reconnectTick = setInterval(() => {
|
||||
secs = Math.max(0, secs - 1);
|
||||
if (secs > 0) $('conn-label').textContent = `Retry in ${secs}s…`;
|
||||
}, 1000);
|
||||
|
||||
reconnectTimer = setTimeout(() => {
|
||||
reconnectTimer = null;
|
||||
connect(url, true);
|
||||
}, reconnectDelay);
|
||||
|
||||
reconnectDelay = Math.min(reconnectDelay * RECONNECT_FACTOR, RECONNECT_MAX_MS);
|
||||
}
|
||||
|
||||
function connect(url, isAutoRetry) {
|
||||
cancelReconnect();
|
||||
reconnectTarget = url;
|
||||
if (!isAutoRetry) reconnectDelay = RECONNECT_BASE_MS;
|
||||
|
||||
if (ros) { try { ros.close(); } catch(_) {} }
|
||||
ros = new ROSLIB.Ros({ url });
|
||||
|
||||
ros.on('connection', () => {
|
||||
reconnectDelay = RECONNECT_BASE_MS;
|
||||
cancelReconnect();
|
||||
$('conn-dot').className = 'connected';
|
||||
$('conn-label').style.color = '#22c55e';
|
||||
$('conn-label').textContent = 'Connected';
|
||||
@ -315,7 +355,7 @@ function connect(url) {
|
||||
ros.on('close', () => {
|
||||
$('conn-dot').className = '';
|
||||
$('conn-label').style.color = '#6b7280';
|
||||
$('conn-label').textContent = 'Disconnected';
|
||||
scheduleReconnect(reconnectTarget || $('ws-input').value.trim());
|
||||
});
|
||||
}
|
||||
|
||||
@ -405,8 +445,8 @@ function setupTopics() {
|
||||
|
||||
// ── UI wiring ─────────────────────────────────────────────────────────────────
|
||||
|
||||
$('btn-connect').addEventListener('click', () => connect($('ws-input').value.trim()));
|
||||
$('ws-input').addEventListener('keydown', e => { if (e.key === 'Enter') connect($('ws-input').value.trim()); });
|
||||
$('btn-connect').addEventListener('click', () => connect($('ws-input').value.trim(), false));
|
||||
$('ws-input').addEventListener('keydown', e => { if (e.key === 'Enter') connect($('ws-input').value.trim(), false); });
|
||||
|
||||
// ── Auto-connect on load ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user