feat: WebSocket bridge for CAN monitor dashboard (Issue #697) #702
@ -49,6 +49,9 @@ rosbridge_websocket:
|
|||||||
"/cmd_vel",
|
"/cmd_vel",
|
||||||
"/saltybot/imu",
|
"/saltybot/imu",
|
||||||
"/saltybot/balance_state",
|
"/saltybot/balance_state",
|
||||||
|
"/saltybot/barometer",
|
||||||
|
"/vesc/left/state",
|
||||||
|
"/vesc/right/state",
|
||||||
"/tf",
|
"/tf",
|
||||||
"/tf_static"]
|
"/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 ────────────────────────────────────────────────────────────
|
// ── 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(_) {} }
|
if (ros) { try { ros.close(); } catch(_) {} }
|
||||||
ros = new ROSLIB.Ros({ url });
|
ros = new ROSLIB.Ros({ url });
|
||||||
|
|
||||||
ros.on('connection', () => {
|
ros.on('connection', () => {
|
||||||
|
reconnectDelay = RECONNECT_BASE_MS;
|
||||||
|
cancelReconnect();
|
||||||
$('conn-dot').className = 'connected';
|
$('conn-dot').className = 'connected';
|
||||||
$('conn-label').style.color = '#22c55e';
|
$('conn-label').style.color = '#22c55e';
|
||||||
$('conn-label').textContent = 'Connected';
|
$('conn-label').textContent = 'Connected';
|
||||||
@ -315,7 +355,7 @@ function connect(url) {
|
|||||||
ros.on('close', () => {
|
ros.on('close', () => {
|
||||||
$('conn-dot').className = '';
|
$('conn-dot').className = '';
|
||||||
$('conn-label').style.color = '#6b7280';
|
$('conn-label').style.color = '#6b7280';
|
||||||
$('conn-label').textContent = 'Disconnected';
|
scheduleReconnect(reconnectTarget || $('ws-input').value.trim());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -405,8 +445,8 @@ function setupTopics() {
|
|||||||
|
|
||||||
// ── UI wiring ─────────────────────────────────────────────────────────────────
|
// ── UI wiring ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
$('btn-connect').addEventListener('click', () => 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()); });
|
$('ws-input').addEventListener('keydown', e => { if (e.key === 'Enter') connect($('ws-input').value.trim(), false); });
|
||||||
|
|
||||||
// ── Auto-connect on load ──────────────────────────────────────────────────────
|
// ── Auto-connect on load ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user