From 1ec4d3fc5860d0af2a106d19452537be8d671452 Mon Sep 17 00:00:00 2001 From: sl-webui Date: Fri, 20 Mar 2026 16:23:27 -0400 Subject: [PATCH] feat: WebSocket bridge for CAN monitor dashboard (Issue #697) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rosbridge config: - rosbridge_params.yaml: add /saltybot/barometer, /vesc/left/state, /vesc/right/state to topics_glob whitelist (were missing, blocked the CAN monitor panel from receiving data) - can_monitor.launch.py: new lightweight launch — rosbridge only, whitelist scoped to the 5 CAN monitor topics, port overridable via launch arg (ros2 launch saltybot_bringup can_monitor.launch.py port:=9091) can_monitor_panel.js auto-reconnect: - Exponential backoff: 2s → 3s → 4.5s → ... → 30s cap (×1.5 factor) - Countdown displayed in conn-label ("Retry in Xs…") during wait - Backoff resets to 2s on successful connection - Manual CONNECT / Enter resets backoff and cancels pending timer Co-Authored-By: Claude Sonnet 4.6 --- .../config/rosbridge_params.yaml | 3 + .../launch/can_monitor.launch.py | 70 +++++++++++++++++++ ui/can_monitor_panel.js | 48 +++++++++++-- 3 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 jetson/ros2_ws/src/saltybot_bringup/launch/can_monitor.launch.py diff --git a/jetson/ros2_ws/src/saltybot_bringup/config/rosbridge_params.yaml b/jetson/ros2_ws/src/saltybot_bringup/config/rosbridge_params.yaml index 40ae7a3..efb2a53 100644 --- a/jetson/ros2_ws/src/saltybot_bringup/config/rosbridge_params.yaml +++ b/jetson/ros2_ws/src/saltybot_bringup/config/rosbridge_params.yaml @@ -49,6 +49,9 @@ rosbridge_websocket: "/cmd_vel", "/saltybot/imu", "/saltybot/balance_state", + "/saltybot/barometer", + "/vesc/left/state", + "/vesc/right/state", "/tf", "/tf_static"] diff --git a/jetson/ros2_ws/src/saltybot_bringup/launch/can_monitor.launch.py b/jetson/ros2_ws/src/saltybot_bringup/launch/can_monitor.launch.py new file mode 100644 index 0000000..fd81dad --- /dev/null +++ b/jetson/ros2_ws/src/saltybot_bringup/launch/can_monitor.launch.py @@ -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://: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]) diff --git a/ui/can_monitor_panel.js b/ui/can_monitor_panel.js index 54b2f30..51db963 100644 --- a/ui/can_monitor_panel.js +++ b/ui/can_monitor_panel.js @@ -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 ────────────────────────────────────────────────────── -- 2.47.2