From de6ec00aa27b5522423e68af29697ddf1dbd91c6 Mon Sep 17 00:00:00 2001 From: brockgilman Date: Sun, 28 Sep 2025 19:42:45 -0400 Subject: [PATCH 1/6] Add SimulationControl component, good for testing current and future metrics --- package.json | 2 +- src/components/SimulationControl.css | 247 ++++++++++++++++++++++++ src/components/SimulationControl.tsx | 279 +++++++++++++++++++++++++++ src/components/preflight.css | 102 ++++++---- src/components/preflight.tsx | 44 ++++- 5 files changed, 623 insertions(+), 51 deletions(-) create mode 100644 src/components/SimulationControl.css create mode 100644 src/components/SimulationControl.tsx diff --git a/package.json b/package.json index 6ef2212..d560b4d 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "@types/react-dom": "^19.1.9", "react": "^19.1.1", "react-dom": "^19.1.1", - "react-scripts": "5.0.1", + "react-scripts": "^5.0.1", "roslib": "^1.3.0", "typescript": "^4.9.5", "web-vitals": "^2.1.4" diff --git a/src/components/SimulationControl.css b/src/components/SimulationControl.css new file mode 100644 index 0000000..dfd317b --- /dev/null +++ b/src/components/SimulationControl.css @@ -0,0 +1,247 @@ +/* Simulation Control - layout, header, collapsed content, and status borders */ +.simulation-wrapper { + width: 100%; + display: flex; + justify-content: flex-start; + margin-bottom: 16px; + transition: margin-bottom 0.3s ease; +} + +.simulation-wrapper:not(.expanded) { + width: 100%; + height: fit-content; + margin-bottom: 2px; +} + +.simulation-control { + /* removed debug border */ + border-radius: 8px; + margin-bottom: 0; + background-color: #ffffff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + overflow: hidden; + transition: all 0.3s ease; + width: 100%; + min-height: fit-content; + border: 2px solid transparent; +} + +/* Status-based borders */ +.simulation-control.stopped { + border-color: #ef4444; /* red */ +} + +@keyframes borderBlinkGreen { + 0%, 100% { border-color: #10b981; box-shadow: 0 0 0 0 rgba(16,185,129,0.5); } + 50% { border-color: #34d399; box-shadow: 0 0 0 4px rgba(16,185,129,0.15); } +} + +.simulation-control.running { + border-color: #10b981; /* green */ + animation: borderBlinkGreen 1.2s ease-in-out infinite; +} + +.simulation-control:not(.expanded) { + margin-bottom: 0; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + width: 100%; + min-width: 260px; + height: fit-content; + min-height: auto; +} + +.simulation-header { + cursor: pointer; + background-color: #f8fafc; + padding: 0; + transition: background-color 0.2s ease, border-radius 0.3s ease; + user-select: none; +} + +.simulation-header:hover { + background-color: #f1f5f9; +} + +.simulation-control:not(.expanded) .simulation-header { + border-radius: 7px; +} + +.header-content { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + transition: padding 0.3s ease; + white-space: nowrap; +} + +.header-content h4 { + margin: 0; + font-size: 0.95rem; + font-weight: 600; + color: #334155; + flex-shrink: 0; + line-height: 1.2; +} + +.header-status { + display: flex; + align-items: center; + gap: 12px; + flex-shrink: 0; +} + +.status-badge { + padding: 3px 8px; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.025em; + transition: all 0.3s ease; +} + +.status-badge.running { + background-color: #dcfce7; + color: #166534; +} + +.status-badge.stopped { + background-color: #fef2f2; + color: #991b1b; +} + +.expand-chevron { + font-size: 0.8rem; + color: #64748b; + transition: transform 0.3s ease; + transform: rotate(-90deg); +} + +.expand-chevron.expanded { + transform: rotate(0deg); +} + +.simulation-content { + overflow: hidden; + transition: max-height 0.3s ease-out, padding 0.3s ease-out, opacity 0.2s ease-out; + max-height: 0; + padding: 0 16px; + opacity: 0; +} + +.simulation-content.expanded { + max-height: 200px; + padding: 16px; + border-top: 1px solid #e2e8f0; + opacity: 1; +} + +.simulation-content.collapsed { + max-height: 0 !important; + padding: 0 16px !important; + margin: 0 !important; + border-top: 0 !important; +} + +.simulation-grid { + display: flex; + flex-direction: column; + gap: 12px; +} + +.control-group { + display: flex; + flex-direction: column; + gap: 6px; +} + +.control-group label { + font-size: 0.875rem; + font-weight: 500; + color: #374151; +} + +.scenario-select { + padding: 8px 12px; + border: 1px solid #d1d5db; + border-radius: 6px; + background-color: #ffffff; + color: #374151; + font-size: 0.875rem; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.scenario-select:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.scenario-select:disabled { + background-color: #f3f4f6; + color: #9ca3af; + cursor: not-allowed; +} + +.sim-toggle-btn { + padding: 10px 16px; + border: none; + border-radius: 6px; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + align-self: flex-start; +} + +.sim-toggle-btn.start { + background-color: #10b981; + color: white; +} + +.sim-toggle-btn.start:hover { + background-color: #059669; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(16, 185, 129, 0.3); +} + +.sim-toggle-btn.stop { + background-color: #ef4444; + color: white; +} + +.sim-toggle-btn.stop:hover { + background-color: #dc2626; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(239, 68, 68, 0.3); +} + +.simulation-info { + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid #f1f5f9; +} + +.simulation-info span { + color: #64748b; + font-size: 0.8rem; + font-style: italic; +} + +@media (min-width: 640px) { + .simulation-grid { + flex-direction: row; + align-items: end; + gap: 16px; + } + + .control-group { + flex: 1; + } + + .sim-toggle-btn { + align-self: end; + white-space: nowrap; + } +} diff --git a/src/components/SimulationControl.tsx b/src/components/SimulationControl.tsx new file mode 100644 index 0000000..8858d3e --- /dev/null +++ b/src/components/SimulationControl.tsx @@ -0,0 +1,279 @@ +/** + * SimulationControl + * - Collapsible panel to publish fake sensor data for IMU/Depth/DVL at 10Hz + * - Provides scenarios (idle, dive, surface, forward, circle, wobble) + * - Status-aware header and ROS bridge topic publishers + */ +import React, { useState, useRef, useEffect } from 'react'; +import { useRos } from './RosContext'; +import * as ROSLIB from 'roslib'; +import './SimulationControl.css'; + +interface SimulationControlProps { + connected: boolean; +} + +const SimulationControl: React.FC = ({ connected }) => { + // Simulation state + const [isSimulating, setIsSimulating] = useState(false); + const [simulationScenario, setSimulationScenario] = useState('idle'); + const [isExpanded, setIsExpanded] = useState(false); + const simulationRef = useRef(null); + + const { ros } = useRos(); + + // Publishers for fake data + const imuPublisher = useRef(null); + const depthPublisher = useRef(null); + const dvlPublisher = useRef(null); + + // Simulation state variables + const simStateRef = useRef({ + time: 0, + depth: -2.0, // Starting depth (negative is underwater) + orientation: { roll: 0, pitch: 0, yaw: 0 }, + velocity: { x: 0, y: 0, z: 0 }, + position: { x: 0, y: 0, z: -2.0 } + }); + + // Initialize publishers when connected + useEffect(() => { + if (connected && ros) { + imuPublisher.current = new ROSLIB.Topic({ + ros: ros, + name: '/imu/data', + messageType: 'sensor_msgs/Imu' + }); + + depthPublisher.current = new ROSLIB.Topic({ + ros: ros, + name: '/depth/pose', + messageType: 'geometry_msgs/PoseWithCovarianceStamped' + }); + + dvlPublisher.current = new ROSLIB.Topic({ + ros: ros, + name: '/dvl/odom', + messageType: 'nav_msgs/Odometry' + }); + } + }, [connected, ros]); + + // Helper function to convert euler angles to quaternion + const eulerToQuaternion = (roll: number, pitch: number, yaw: number) => { + const cy = Math.cos(yaw * 0.5); + const sy = Math.sin(yaw * 0.5); + const cp = Math.cos(pitch * 0.5); + const sp = Math.sin(pitch * 0.5); + const cr = Math.cos(roll * 0.5); + const sr = Math.sin(roll * 0.5); + + return { + w: cr * cp * cy + sr * sp * sy, + x: sr * cp * cy - cr * sp * sy, + y: cr * sp * cy + sr * cp * sy, + z: cr * cp * sy - sr * sp * cy + }; + }; + + // Simulation scenarios + const runSimulationStep = () => { + const state = simStateRef.current; + state.time += 0.1; // 100ms timestep + + // Apply scenario-specific motions + switch (simulationScenario) { + case 'dive': + state.velocity.z = -0.5; // Diving down + state.position.z += state.velocity.z * 0.1; + state.depth = -state.position.z; + break; + + case 'surface': + state.velocity.z = 0.3; // Rising up + state.position.z += state.velocity.z * 0.1; + state.depth = -state.position.z; + if (state.depth < 0) state.depth = 0; // Can't go above surface + break; + + case 'forward': + state.velocity.x = 1.0; // Moving forward + state.position.x += state.velocity.x * 0.1; + break; + + case 'circle': + const radius = 5.0; + const angularVel = 0.1; + state.position.x = radius * Math.cos(state.time * angularVel); + state.position.y = radius * Math.sin(state.time * angularVel); + state.velocity.x = -radius * angularVel * Math.sin(state.time * angularVel); + state.velocity.y = radius * angularVel * Math.cos(state.time * angularVel); + state.orientation.yaw = state.time * angularVel; + break; + + case 'wobble': + // Simulate rough waters or instability + state.orientation.roll = 0.1 * Math.sin(state.time * 2); + state.orientation.pitch = 0.05 * Math.cos(state.time * 3); + break; + + default: // idle + state.velocity.x = 0; + state.velocity.y = 0; + state.velocity.z = 0; + } + + // Add some realistic noise + const noise = () => (Math.random() - 0.5) * 0.01; + + // Publish IMU data + if (imuPublisher.current) { + const quat = eulerToQuaternion( + state.orientation.roll + noise(), + state.orientation.pitch + noise(), + state.orientation.yaw + noise() + ); + + const imuMsg = new ROSLIB.Message({ + header: { + stamp: { sec: Math.floor(Date.now() / 1000), nanosec: 0 }, + frame_id: 'imu_link' + }, + orientation: quat, + linear_acceleration: { + x: state.velocity.x * 0.1 + noise(), + y: state.velocity.y * 0.1 + noise(), + z: 9.81 + state.velocity.z * 0.1 + noise() + }, + angular_velocity: { + x: (state.orientation.roll - (simStateRef.current.orientation.roll || 0)) / 0.1 + noise(), + y: (state.orientation.pitch - (simStateRef.current.orientation.pitch || 0)) / 0.1 + noise(), + z: (state.orientation.yaw - (simStateRef.current.orientation.yaw || 0)) / 0.1 + noise() + } + }); + imuPublisher.current.publish(imuMsg); + } + + // Publish depth data + if (depthPublisher.current) { + const depthMsg = new ROSLIB.Message({ + header: { + stamp: { sec: Math.floor(Date.now() / 1000), nanosec: 0 }, + frame_id: 'depth_link' + }, + pose: { + pose: { + position: { + x: state.position.x, + y: state.position.y, + z: state.depth + noise() + }, + orientation: { x: 0, y: 0, z: 0, w: 1 } + } + } + }); + depthPublisher.current.publish(depthMsg); + } + + // Publish DVL data + if (dvlPublisher.current) { + const dvlMsg = new ROSLIB.Message({ + header: { + stamp: { sec: Math.floor(Date.now() / 1000), nanosec: 0 }, + frame_id: 'dvl_link' + }, + twist: { + twist: { + linear: { + x: state.velocity.x + noise(), + y: state.velocity.y + noise(), + z: state.velocity.z + noise() + }, + angular: { x: 0, y: 0, z: 0 } + } + } + }); + dvlPublisher.current.publish(dvlMsg); + } + }; + + // Start/stop simulation + const toggleSimulation = () => { + if (isSimulating) { + if (simulationRef.current) { + clearInterval(simulationRef.current); + simulationRef.current = null; + } + setIsSimulating(false); + } else { + simulationRef.current = setInterval(runSimulationStep, 100); // 10Hz + setIsSimulating(true); + } + }; + + // Cleanup on unmount + useEffect(() => { + return () => { + if (simulationRef.current) { + clearInterval(simulationRef.current); + } + }; + }, []); + + if (!connected) return null; + + return ( +
+
+
setIsExpanded(!isExpanded)}> +
+

Sensor Data Simulation

+
+ + {isSimulating ? 'Running' : 'Stopped'} + + + ▼ + +
+
+
+ +
+
+
+ + +
+ + +
+ +
+ Publishing to /imu/data, /depth/pose, /dvl/odom at 10Hz +
+
+
+
+ ); +}; + +export default SimulationControl; diff --git a/src/components/preflight.css b/src/components/preflight.css index 75ea440..69a26ac 100644 --- a/src/components/preflight.css +++ b/src/components/preflight.css @@ -11,23 +11,61 @@ max-width: 1200px; margin-left: auto; margin-right: auto; + gap: 0; } -/* Top row containing bad and good cards */ -.preflight-card::before { - content: ""; - display: flex; - flex: 1; +/* ROS Connection Status Styling */ +.ros-connection-status { + padding: 12px 16px; + border-radius: 8px; + margin-bottom: 16px; + text-align: center; + transition: all 0.3s ease; +} + +.ros-connection-status.connected { + background-color: #e8f5e8; + border: 2px solid #4caf50; + color: #2e7d32; +} + +.ros-connection-status.disconnected { + background-color: #ffebee; + border: 2px solid #f44336; + color: #c62828; +} + +.ros-connection-status h3 { + margin: 0 0 8px 0; + font-size: 1.2em; +} + +.ros-connection-status p { + margin: 0; + font-size: 0.9em; +} + +/* Simulation container */ +.simulation-container { margin-bottom: 16px; + transition: margin-bottom 0.3s ease; + min-height: fit-content; +} + +/* Compact spacing when simulation panel is collapsed */ +.simulation-container:has(.simulation-control:not(.expanded)) { + margin-bottom: 2px; } -/* Bad and good card container (flex row) */ -.preflight-card > div:not(.preflight-logs-card) { + + +/* Topic status row (bad/good columns) */ +.preflight-top-row { display: flex; flex-direction: row; gap: 16px; margin-bottom: 16px; - height: 200px; /* Fixed height for top cards */ + align-items: stretch; } /* Bad card - for error messages */ @@ -67,7 +105,7 @@ overflow-y: auto; } -/* Style for good card items - UPDATED to prevent resizing */ +/* Style for good card items (prevent layout shift) */ .good-card-li { color: #2e7d32; font-weight: bold; @@ -77,7 +115,7 @@ border-radius: 4px; list-style-type: none; - /* NEW: Fixed width properties to prevent layout shifts */ + /* Fixed width properties to prevent layout shifts */ width: calc(100% - 24px); box-sizing: border-box; display: flex; @@ -85,7 +123,7 @@ font-family: 'Courier New', monospace; } -/* NEW: Create a container for the changing Hz value */ +/* Reserved area for the Hz value */ .good-card-li::after { content: ""; display: inline-block; @@ -93,7 +131,7 @@ text-align: right; } -/* Logs card - UPDATED: increased height */ +/* Logs card */ .preflight-logs-card { display: flex; flex-direction: row; @@ -101,22 +139,22 @@ border-radius: 8px; padding: 12px; background-color: #f3e5f5; - height: 300px; /* CHANGED: Increased from 200px to 300px */ - overflow-y: hidden; /* CHANGED: from auto to hidden */ + height: 300px; + overflow-y: hidden; } -/* NEW: Completely redesigned IMU log section */ +/* IMU log section */ .imu-log { flex: 1; padding: 8px; border-right: 1px dashed #b39ddb; display: grid; - grid-template-columns: 1fr 1fr; /* Create 2 columns */ + grid-template-columns: 1fr 1fr; grid-column-gap: 10px; align-content: start; } -/* NEW: Add header to IMU section */ +/* IMU header */ .imu-log::before { content: "IMU Data"; grid-column: 1 / -1; @@ -126,7 +164,7 @@ border-bottom: 1px solid #b39ddb; } -/* UPDATED: Log lists within IMU section */ +/* IMU list container */ .imu-log .log-list { list-style-type: none; padding: 0; @@ -134,7 +172,7 @@ display: contents; /* Make list part of grid */ } -/* UPDATED: Log items within IMU section */ +/* IMU list items */ .imu-log .log-list-item { font-family: monospace; padding: 4px 0; @@ -145,7 +183,7 @@ font-size: 0.9em; } -/* NEW: Visual grouping for IMU data types */ +/* Visual grouping for IMU data types */ .imu-log .log-list-item:nth-child(-n+4) { background-color: rgba(179, 157, 219, 0.1); /* Quaternion background */ } @@ -158,7 +196,7 @@ background-color: rgba(100, 181, 246, 0.1); /* Angular twist background */ } -/* Updated Depth log section to match IMU and DVL styles */ +/* Depth log section */ .depth-log { flex: 1; padding: 8px; @@ -166,8 +204,6 @@ display: flex; flex-direction: column; } - -/* Add header to Depth section similar to others */ .depth-log::before { content: "Depth Sensor"; font-weight: bold; @@ -176,36 +212,28 @@ border-bottom: 1px solid #b39ddb; } -/* Style the depth value container */ -.depth-log { +/* Depth value presentation (container, label, and units) */ +.depth-log::before { display: flex; flex-direction: column; } - -/* Create a styled container for the depth value */ .depth-log::after { content: attr(data-unit); font-size: 0.9em; color: #7e57c2; margin-top: 4px; } - -/* Style for the actual depth value */ .depth-log { position: relative; display: flex; flex-direction: column; } - -/* Create proper labeling for depth value */ .depth-log::after { content: "joe handsome"; font-size: 0.8em; color: #7e57c2; margin-top: 4px; } - -/* Make depth value stand out */ .depth-log > div { background-color: rgba(179, 157, 219, 0.2); border-radius: 8px; @@ -218,8 +246,6 @@ font-family: 'Courier New', monospace; position: relative; } - -/* Add depth label */ .depth-log > div::before { content: "Z Position:"; position: absolute; @@ -229,13 +255,9 @@ font-weight: normal; color: #5e35b1; } - -/* Add decimal precision formatting for depth */ .depth-log > div { position: relative; } - -/* Format the depth value to show proper decimals */ .depth-log > div::after { content: " m"; font-size: 20px; @@ -276,7 +298,7 @@ align-self: center; margin: auto; } -/* DVL log section - CSS only solution */ +/* DVL log section */ .dvl-log { flex: 1; padding: 8px; diff --git a/src/components/preflight.tsx b/src/components/preflight.tsx index 93f395d..5133ff3 100644 --- a/src/components/preflight.tsx +++ b/src/components/preflight.tsx @@ -1,9 +1,11 @@ -import React, { useEffect, useRef } from 'react' +import React from 'react' import './preflight.css' import { useTopic } from '../hooks/useTopic'; +import { useRos } from './RosContext'; import { PoseWithCovarianceStamped } from '../ros_msg_types/geometry_msgs' import { Odometry } from '../ros_msg_types/nav_msgs'; import { Imu } from '../ros_msg_types/sensor_msgs'; +import SimulationControl from './SimulationControl'; /* * @@ -21,6 +23,7 @@ function create_dead_topic_list_item(topic_hz: number, msg: string) { return (
  • {msg}
  • ); } +/** Helper: render a good item if the topic is publishing (hz > 0). */ function create_alive_topic_list_item(topic_hz: number, msg: string) { const topic_is_dead = topic_hz === 0 if (topic_is_dead) { return } @@ -29,6 +32,9 @@ function create_alive_topic_list_item(topic_hz: number, msg: string) { } function Preflight() { + // Get ROS connection status + const { connected } = useRos(); + // TODO add camera topics and display what the cameras see (either before or after AI) const [imu_msg, imu_hz, _imu_topic] = useTopic('/imu/data', 'sensor_msgs/Imu'); const [depth_msg, depth_hz, _depth_topic] = useTopic('/depth/pose', 'geometry_msgs/PoseWithCovarianceStamped'); @@ -37,19 +43,37 @@ function Preflight() { return ( <>
    -
    - {create_dead_topic_list_item(imu_hz, "imu lost!")} - {create_dead_topic_list_item(dvl_hz, "dvl lost!")} - {create_dead_topic_list_item(depth_hz, "depth lost!")} + {/* ROS connection status */} +
    +

    ROS Bridge Status: {connected ? '🟢 Connected' : '🔴 Disconnected'}

    + {connected ? ( +

    ✅ Successfully connected to ROS bridge server (ws://localhost:9090)

    + ) : ( +

    ❌ Unable to connect to ROS bridge server. Make sure it's running.

    + )}
    -
    - {create_alive_topic_list_item(imu_hz, `Imu Hz: ${imu_hz}`)} - {create_alive_topic_list_item(dvl_hz, `dvl Hz: ${dvl_hz}`)} - {create_alive_topic_list_item(depth_hz, `Depth Hz: ${depth_hz}`)} + {/* Simulation Control */} +
    +
    -
    +
    +
    + {!connected &&
  • ROS Bridge disconnected!
  • } + {create_dead_topic_list_item(imu_hz, "imu lost!")} + {create_dead_topic_list_item(dvl_hz, "dvl lost!")} + {create_dead_topic_list_item(depth_hz, "depth lost!")} +
    + +
    + {create_alive_topic_list_item(imu_hz, `Imu Hz: ${imu_hz}`)} + {create_alive_topic_list_item(dvl_hz, `dvl Hz: ${dvl_hz}`)} + {create_alive_topic_list_item(depth_hz, `Depth Hz: ${depth_hz}`)} +
    +
    + +
    • {`quat x: ${imu_msg?.orientation.x}`}
    • From b416ff9674d484f8b3bed1944a160a3d41b26d36 Mon Sep 17 00:00:00 2001 From: brockgilman Date: Sat, 4 Oct 2025 11:55:47 -0400 Subject: [PATCH 2/6] tweaked border color and other visual issues --- src/components/SimulationControl.css | 10 ++-------- src/components/SimulationControl.tsx | 23 +++++++++++++++++++++-- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/components/SimulationControl.css b/src/components/SimulationControl.css index dfd317b..e2a8c4c 100644 --- a/src/components/SimulationControl.css +++ b/src/components/SimulationControl.css @@ -28,17 +28,11 @@ /* Status-based borders */ .simulation-control.stopped { - border-color: #ef4444; /* red */ -} - -@keyframes borderBlinkGreen { - 0%, 100% { border-color: #10b981; box-shadow: 0 0 0 0 rgba(16,185,129,0.5); } - 50% { border-color: #34d399; box-shadow: 0 0 0 4px rgba(16,185,129,0.15); } + border-color: #e53935; /* match bad card red */ } .simulation-control.running { - border-color: #10b981; /* green */ - animation: borderBlinkGreen 1.2s ease-in-out infinite; + border-color: #4caf50; /* match success green */ } .simulation-control:not(.expanded) { diff --git a/src/components/SimulationControl.tsx b/src/components/SimulationControl.tsx index 8858d3e..a08b5a6 100644 --- a/src/components/SimulationControl.tsx +++ b/src/components/SimulationControl.tsx @@ -16,8 +16,18 @@ interface SimulationControlProps { const SimulationControl: React.FC = ({ connected }) => { // Simulation state const [isSimulating, setIsSimulating] = useState(false); - const [simulationScenario, setSimulationScenario] = useState('idle'); - const [isExpanded, setIsExpanded] = useState(false); + const [simulationScenario, setSimulationScenario] = useState(() => { + try { + const v = localStorage.getItem('sim.scenario'); + return v ?? 'idle'; + } catch { return 'idle'; } + }); + const [isExpanded, setIsExpanded] = useState(() => { + try { + const v = localStorage.getItem('sim.expanded'); + return v ? v === '1' : false; + } catch { return false; } + }); const simulationRef = useRef(null); const { ros } = useRos(); @@ -211,6 +221,15 @@ const SimulationControl: React.FC = ({ connected }) => { } }; + // Persist scenario and expanded state + useEffect(() => { + try { localStorage.setItem('sim.scenario', simulationScenario); } catch {} + }, [simulationScenario]); + + useEffect(() => { + try { localStorage.setItem('sim.expanded', isExpanded ? '1' : '0'); } catch {} + }, [isExpanded]); + // Cleanup on unmount useEffect(() => { return () => { From 257622c2e97715f5fdfa8550ab357255ef47cc55 Mon Sep 17 00:00:00 2001 From: brockgilman Date: Sun, 12 Oct 2025 13:48:17 -0400 Subject: [PATCH 3/6] this week, i fixed the ROS bridge server IP to be dynamic, on local host right now so will still be localhost9090, unless the default needs to be changed. also tackled the sensor hz to the respected values. will tackle looking into bag data in the next few days --- src/components/RosContext.tsx | 38 ++++++++++- src/components/SimulationControl.tsx | 99 +++++++++++++++++++++------- 2 files changed, 112 insertions(+), 25 deletions(-) diff --git a/src/components/RosContext.tsx b/src/components/RosContext.tsx index 323902f..7b44cb0 100644 --- a/src/components/RosContext.tsx +++ b/src/components/RosContext.tsx @@ -37,8 +37,44 @@ export function RosProvider({ children }: RosProviderProps) { }); + /** + * resolve the ROS bridge websocket URL, made it ***Dynamic*** :) + * priority (highest to lowest): + * - URL query params: ?ros_host=HOST&ros_port=PORT or ?ros_ws=ws://host:port + * - localStorage: ros.host, ros.port, ros.ws + * - ENV (build-time): REACT_APP_ROSBRIDGE_HOST, REACT_APP_ROSBRIDGE_PORT + * - if page host is not localhost/127.0.0.1, use window.location.hostname + * - fallback default: localhost:9090 (works with WSL2) + */ + function getRosBridgeUrl(): string { + try { + const params = new URLSearchParams(window.location.search); + const qpWs = params.get('ros_ws'); + if (qpWs) return qpWs; + + const lsWs = localStorage.getItem('ros.ws'); + if (lsWs) return lsWs; + + const qpHost = params.get('ros_host') ?? localStorage.getItem('ros.host') ?? process.env.REACT_APP_ROSBRIDGE_HOST ?? ''; + const qpPort = params.get('ros_port') ?? localStorage.getItem('ros.port') ?? process.env.REACT_APP_ROSBRIDGE_PORT ?? ''; + + const pageHost = window.location.hostname; + // default to localhost (works with WSL2), or use page hostname if deployed remotely + const defaultHost = (pageHost && pageHost !== 'localhost' && pageHost !== '127.0.0.1') ? pageHost : 'localhost'; + const host = qpHost || defaultHost; + const port = qpPort || '9090'; + const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'; + return `${protocol}://${host}:${port}`; + } catch { + // safe fallback + return 'ws://localhost:9090'; + } + } + function connect_to_ros() { - rosRef.current.connect('ws://localhost:9090'); + const url = getRosBridgeUrl(); + console.log(`[ROS] connecting to ${url}`); + rosRef.current.connect(url); } // on start up diff --git a/src/components/SimulationControl.tsx b/src/components/SimulationControl.tsx index a08b5a6..af423b9 100644 --- a/src/components/SimulationControl.tsx +++ b/src/components/SimulationControl.tsx @@ -1,6 +1,7 @@ /** * SimulationControl - * - Collapsible panel to publish fake sensor data for IMU/Depth/DVL at 10Hz + * - Collapsible panel to publish fake sensor data + * - IMU: ~20Hz, DVL: ~10Hz, Depth: ~16Hz (realistic rates) * - Provides scenarios (idle, dive, surface, forward, circle, wobble) * - Status-aware header and ROS bridge topic publishers */ @@ -28,7 +29,11 @@ const SimulationControl: React.FC = ({ connected }) => { return v ? v === '1' : false; } catch { return false; } }); - const simulationRef = useRef(null); + // Use drift-compensated timer for IMU to hit ~20Hz reliably in browsers + const imuTimerRef = useRef(null); + const dvlTimerRef = useRef(null); + const depthTimerRef = useRef(null); + const stateTimerRef = useRef(null); const { ros } = useRos(); @@ -86,29 +91,29 @@ const SimulationControl: React.FC = ({ connected }) => { }; }; - // Simulation scenarios - const runSimulationStep = () => { + // Simulation scenarios - update state based on scenario + const updateSimulationState = (dt: number) => { const state = simStateRef.current; - state.time += 0.1; // 100ms timestep + state.time += dt; // Apply scenario-specific motions switch (simulationScenario) { case 'dive': state.velocity.z = -0.5; // Diving down - state.position.z += state.velocity.z * 0.1; + state.position.z += state.velocity.z * dt; state.depth = -state.position.z; break; case 'surface': state.velocity.z = 0.3; // Rising up - state.position.z += state.velocity.z * 0.1; + state.position.z += state.velocity.z * dt; state.depth = -state.position.z; if (state.depth < 0) state.depth = 0; // Can't go above surface break; case 'forward': state.velocity.x = 1.0; // Moving forward - state.position.x += state.velocity.x * 0.1; + state.position.x += state.velocity.x * dt; break; case 'circle': @@ -132,11 +137,13 @@ const SimulationControl: React.FC = ({ connected }) => { state.velocity.y = 0; state.velocity.z = 0; } - - // Add some realistic noise + }; + + // Publish IMU data (~20Hz) + const publishIMU = () => { + const state = simStateRef.current; const noise = () => (Math.random() - 0.5) * 0.01; - // Publish IMU data if (imuPublisher.current) { const quat = eulerToQuaternion( state.orientation.roll + noise(), @@ -156,15 +163,20 @@ const SimulationControl: React.FC = ({ connected }) => { z: 9.81 + state.velocity.z * 0.1 + noise() }, angular_velocity: { - x: (state.orientation.roll - (simStateRef.current.orientation.roll || 0)) / 0.1 + noise(), - y: (state.orientation.pitch - (simStateRef.current.orientation.pitch || 0)) / 0.1 + noise(), - z: (state.orientation.yaw - (simStateRef.current.orientation.yaw || 0)) / 0.1 + noise() + x: (state.orientation.roll - (simStateRef.current.orientation.roll || 0)) / 0.05 + noise(), + y: (state.orientation.pitch - (simStateRef.current.orientation.pitch || 0)) / 0.05 + noise(), + z: (state.orientation.yaw - (simStateRef.current.orientation.yaw || 0)) / 0.05 + noise() } }); imuPublisher.current.publish(imuMsg); } + }; + + // Publish Depth data (~16Hz) + const publishDepth = () => { + const state = simStateRef.current; + const noise = () => (Math.random() - 0.5) * 0.01; - // Publish depth data if (depthPublisher.current) { const depthMsg = new ROSLIB.Message({ header: { @@ -184,8 +196,13 @@ const SimulationControl: React.FC = ({ connected }) => { }); depthPublisher.current.publish(depthMsg); } + }; + + // Publish DVL data (~10Hz) + const publishDVL = () => { + const state = simStateRef.current; + const noise = () => (Math.random() - 0.5) * 0.01; - // Publish DVL data if (dvlPublisher.current) { const dvlMsg = new ROSLIB.Message({ header: { @@ -210,13 +227,46 @@ const SimulationControl: React.FC = ({ connected }) => { // Start/stop simulation const toggleSimulation = () => { if (isSimulating) { - if (simulationRef.current) { - clearInterval(simulationRef.current); - simulationRef.current = null; + // Stop all sensor publishers and state updater + if (stateTimerRef.current) { + clearInterval(stateTimerRef.current); + stateTimerRef.current = null; + } + if (imuTimerRef.current !== null) { + window.clearTimeout(imuTimerRef.current); + imuTimerRef.current = null; + } + if (dvlTimerRef.current) { + clearInterval(dvlTimerRef.current); + dvlTimerRef.current = null; + } + if (depthTimerRef.current) { + clearInterval(depthTimerRef.current); + depthTimerRef.current = null; } setIsSimulating(false); } else { - simulationRef.current = setInterval(runSimulationStep, 100); // 10Hz + // Start state updater at high frequency + stateTimerRef.current = setInterval(() => updateSimulationState(0.02), 20); // 50Hz state updates + + // Start IMU with drift-compensated timer (~20Hz) + const imuPeriod = 50; // ms + const startIMULoop = () => { + let next = performance.now() + imuPeriod; + const tick = () => { + publishIMU(); + const now = performance.now(); + // schedule next run compensating for drift + next += imuPeriod; + const delay = Math.max(0, next - now); + imuTimerRef.current = window.setTimeout(tick, delay) as unknown as number; + }; + imuTimerRef.current = window.setTimeout(tick, imuPeriod) as unknown as number; + }; + startIMULoop(); + // Start remaining publishers at their respective rates + dvlTimerRef.current = setInterval(publishDVL, 100); // ~10Hz + depthTimerRef.current = setInterval(publishDepth, 62.5); // ~16Hz setIsSimulating(true); } }; @@ -233,9 +283,10 @@ const SimulationControl: React.FC = ({ connected }) => { // Cleanup on unmount useEffect(() => { return () => { - if (simulationRef.current) { - clearInterval(simulationRef.current); - } + if (stateTimerRef.current) clearInterval(stateTimerRef.current); + if (imuTimerRef.current !== null) window.clearTimeout(imuTimerRef.current); + if (dvlTimerRef.current) clearInterval(dvlTimerRef.current); + if (depthTimerRef.current) clearInterval(depthTimerRef.current); }; }, []); @@ -287,7 +338,7 @@ const SimulationControl: React.FC = ({ connected }) => {
    - Publishing to /imu/data, /depth/pose, /dvl/odom at 10Hz + Publishing to /imu/data (~20Hz), /dvl/odom (~10Hz), /depth/pose (~16Hz)
    From c4d1f65b96c2d570dbb2ade8783fd2cbf11f1fb5 Mon Sep 17 00:00:00 2001 From: brockgilman Date: Sun, 19 Oct 2025 12:06:38 -0400 Subject: [PATCH 4/6] implemented ros2 bag function, need yaml data and more research, will look into more this week --- src/components/SimulationControl.tsx | 138 +++++++++++++++++++++++++-- src/hooks/useTopic.ts | 10 +- 2 files changed, 135 insertions(+), 13 deletions(-) diff --git a/src/components/SimulationControl.tsx b/src/components/SimulationControl.tsx index af423b9..9d95b24 100644 --- a/src/components/SimulationControl.tsx +++ b/src/components/SimulationControl.tsx @@ -7,6 +7,7 @@ */ import React, { useState, useRef, useEffect } from 'react'; import { useRos } from './RosContext'; +import { useTopic } from '../hooks/useTopic'; import * as ROSLIB from 'roslib'; import './SimulationControl.css'; @@ -36,6 +37,17 @@ const SimulationControl: React.FC = ({ connected }) => { const stateTimerRef = useRef(null); const { ros } = useRos(); + // Data source selection + const [dataSource, setDataSource] = useState<'synthetic' | 'bag'>(() => { + try { return (localStorage.getItem('sim.source') as any) || 'synthetic'; } catch { return 'synthetic'; } + }); + // Subscribe to topics for seeding + const [imuMsg] = useTopic('/imu/data', 'sensor_msgs/Imu'); + const [depthMsg] = useTopic('/depth/pose', 'geometry_msgs/PoseWithCovarianceStamped'); + const [dvlMsg] = useTopic('/dvl/odom', 'nav_msgs/Odometry'); + // Seeding feedback state + const [seededAt, setSeededAt] = useState(null); + const [seedSummary, setSeedSummary] = useState(''); // Publishers for fake data const imuPublisher = useRef(null); @@ -53,7 +65,7 @@ const SimulationControl: React.FC = ({ connected }) => { // Initialize publishers when connected useEffect(() => { - if (connected && ros) { + if (connected && ros && dataSource === 'synthetic') { imuPublisher.current = new ROSLIB.Topic({ ros: ros, name: '/imu/data', @@ -72,7 +84,12 @@ const SimulationControl: React.FC = ({ connected }) => { messageType: 'nav_msgs/Odometry' }); } - }, [connected, ros]); + if (connected && ros && dataSource === 'bag') { + if (imuPublisher.current) { imuPublisher.current.unadvertise(); imuPublisher.current = null; } + if (depthPublisher.current) { depthPublisher.current.unadvertise(); depthPublisher.current = null; } + if (dvlPublisher.current) { dvlPublisher.current.unadvertise(); dvlPublisher.current = null; } + } + }, [connected, ros, dataSource]); // Helper function to convert euler angles to quaternion const eulerToQuaternion = (roll: number, pitch: number, yaw: number) => { @@ -91,6 +108,62 @@ const SimulationControl: React.FC = ({ connected }) => { }; }; + // Quaternion -> Euler (for seeding from IMU) + const quaternionToEuler = (x: number, y: number, z: number, w: number) => { + const sinr_cosp = 2 * (w * x + y * z); + const cosr_cosp = 1 - 2 * (x * x + y * y); + const roll = Math.atan2(sinr_cosp, cosr_cosp); + + const sinp = 2 * (w * y - z * x); + const pitch = Math.abs(sinp) >= 1 ? Math.sign(sinp) * Math.PI / 2 : Math.asin(sinp); + + const siny_cosp = 2 * (w * z + x * y); + const cosy_cosp = 1 - 2 * (y * y + z * z); + const yaw = Math.atan2(siny_cosp, cosy_cosp); + return { roll, pitch, yaw }; + }; + + // Seed initial sim state from latest ROS topics + const seedFromRosTopics = () => { + const s = simStateRef.current; + let seededParts: string[] = []; + + const q = (imuMsg as any)?.orientation; + if (q && typeof q.x === 'number' && typeof q.y === 'number' && typeof q.z === 'number' && typeof q.w === 'number') { + const e = quaternionToEuler(q.x, q.y, q.z, q.w); + s.orientation = { roll: e.roll, pitch: e.pitch, yaw: e.yaw }; + seededParts.push('orientation from /imu/data'); + } + + const p = (depthMsg as any)?.pose?.pose?.position; + if (p && typeof p.z === 'number') { + s.position.z = p.z; + s.depth = -p.z; // assume z-up + seededParts.push('depth from /depth/pose'); + } + + const v = (dvlMsg as any)?.twist?.twist?.linear; + if (v && (typeof v.x === 'number' || typeof v.y === 'number' || typeof v.z === 'number')) { + s.velocity = { x: v.x ?? 0, y: v.y ?? 0, z: v.z ?? 0 }; + seededParts.push('velocity from /dvl/odom'); + } + + if (seededParts.length === 0) { + setSeedSummary('No recent messages available to seed. Make sure rosbag2 is playing and topics are active.'); + setSeededAt(Date.now()); + return; + } + + setSeedSummary(`Seeded ${seededParts.join(', ')}`); + setSeededAt(Date.now()); + // Force a tiny state change to ensure any UI bound to sim state can reflect updates + // (simStateRef updates alone do not trigger a re-render) + // We reuse seedSummary/seededAt states above for this purpose. + // Optional: console for debugging + // eslint-disable-next-line no-console + console.log('[Simulation] Seeded from ROS topics:', { orientation: q, positionZ: p?.z, velocity: v }); + }; + // Simulation scenarios - update state based on scenario const updateSimulationState = (dt: number) => { const state = simStateRef.current; @@ -226,6 +299,7 @@ const SimulationControl: React.FC = ({ connected }) => { // Start/stop simulation const toggleSimulation = () => { + if (dataSource === 'bag') return; // don't publish in bag mode if (isSimulating) { // Stop all sensor publishers and state updater if (stateTimerRef.current) { @@ -280,6 +354,18 @@ const SimulationControl: React.FC = ({ connected }) => { try { localStorage.setItem('sim.expanded', isExpanded ? '1' : '0'); } catch {} }, [isExpanded]); + // Persist data source and stop timers when switching to bag + useEffect(() => { + try { localStorage.setItem('sim.source', dataSource); } catch {} + if (dataSource === 'bag' && isSimulating) { + if (stateTimerRef.current) { clearInterval(stateTimerRef.current); stateTimerRef.current = null; } + if (imuTimerRef.current !== null) { window.clearTimeout(imuTimerRef.current); imuTimerRef.current = null; } + if (dvlTimerRef.current) { clearInterval(dvlTimerRef.current); dvlTimerRef.current = null; } + if (depthTimerRef.current) { clearInterval(depthTimerRef.current); depthTimerRef.current = null; } + setIsSimulating(false); + } + }, [dataSource]); + // Cleanup on unmount useEffect(() => { return () => { @@ -311,13 +397,25 @@ const SimulationControl: React.FC = ({ connected }) => {
    +
    + + +
    - + {dataSource === 'synthetic' ? ( + + ) : ( + + )}
    - Publishing to /imu/data (~20Hz), /dvl/odom (~10Hz), /depth/pose (~16Hz) + {dataSource === 'synthetic' ? ( + Publishing to /imu/data (~20Hz), /dvl/odom (~10Hz), /depth/pose (~16Hz) + ) : ( + Listening to /imu/data, /dvl/odom, /depth/pose (run: ros2 bag play ...) + )}
    + {seededAt && ( +
    + {seedSummary} — {new Date(seededAt).toLocaleTimeString()} +
    + )}
    diff --git a/src/hooks/useTopic.ts b/src/hooks/useTopic.ts index 62368b4..6f58711 100644 --- a/src/hooks/useTopic.ts +++ b/src/hooks/useTopic.ts @@ -30,10 +30,14 @@ export function useTopic(topicName: string, messageT intervalRef.current = setInterval(() => { - const topic_is_dead = timesRef.current.length === 0 || Date.now() - timesRef.current[timesRef.current.length - 1] > TOPIC_TIMEOUT_SECONDS * 1000 + const topic_is_dead = + timesRef.current.length === 0 || + Date.now() - timesRef.current[timesRef.current.length - 1] > TOPIC_TIMEOUT_SECONDS * 1000; if (topic_is_dead) { - timesRef.current = [] - setHz(0) + timesRef.current = []; + setHz(0); + // Clear last message so dependent UI knows the topic is inactive + setMessage(null as any); } }, TOPIC_TIMEOUT_SECONDS * 1000); From a1c924b97feb53cb8ab12a7ff3066f73732e377e Mon Sep 17 00:00:00 2001 From: brockgilman Date: Sun, 26 Oct 2025 12:16:55 -0400 Subject: [PATCH 5/6] added play and record bag scripts to run easily for testing --- package-lock.json | 297 +++++++++++++++++++++--------------------- package.json | 5 +- scripts/README.md | 106 +++++++++++++++ scripts/play-bag.sh | 36 +++++ scripts/record-bag.sh | 68 ++++++++++ 5 files changed, 363 insertions(+), 149 deletions(-) create mode 100644 scripts/README.md create mode 100644 scripts/play-bag.sh create mode 100644 scripts/record-bag.sh diff --git a/package-lock.json b/package-lock.json index 34c4380..fe0f097 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@types/react-dom": "^19.1.9", "react": "^19.1.1", "react-dom": "^19.1.1", - "react-scripts": "5.0.1", + "react-scripts": "^5.0.1", "roslib": "^1.3.0", "typescript": "^4.9.5", "web-vitals": "^2.1.4" @@ -45,19 +45,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -73,30 +60,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", - "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.3", - "@babel/parser": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.3", - "@babel/types": "^7.28.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -121,9 +108,9 @@ } }, "node_modules/@babel/eslint-parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.0.tgz", - "integrity": "sha512-N4ntErOlKvcbTt01rr5wj3y55xnIdx1ymrfIr8C2WnM1Y9glFgWaGDEULJIazOX3XM9NRzhfJ6zZnQ1sBNWU+w==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.4.tgz", + "integrity": "sha512-Aa+yDiH87980jR6zvRfFuCR1+dLb00vBydhTL+zI992Rz/wQhSvuxjmOOuJOgO3XmakO6RykRGD2S1mq1AtgHA==", "license": "MIT", "dependencies": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", @@ -443,25 +430,25 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", - "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2" + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", - "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.2" + "@babel/types": "^7.28.4" }, "bin": { "parser": "bin/babel-parser.js" @@ -1012,9 +999,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz", - "integrity": "sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.4.tgz", + "integrity": "sha512-1yxmvN0MJHOhPVmAsmoW5liWwoILobu/d/ShymZmj867bAdxGbehIrew1DuLpw2Ukv+qDSSPQdYW1dLNE7t11A==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1059,9 +1046,9 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.3.tgz", - "integrity": "sha512-DoEWC5SuxuARF2KdKmGUq3ghfPMO6ZzR12Dnp5gubwbeWJo4dbNWXJPVlwvh4Zlq6Z7YVvL8VFxeSOJgjsx4Sg==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", + "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -1069,7 +1056,7 @@ "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/traverse": "^7.28.4" }, "engines": { "node": ">=6.9.0" @@ -1455,16 +1442,16 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz", - "integrity": "sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", + "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.0" + "@babel/traverse": "^7.28.4" }, "engines": { "node": ">=6.9.0" @@ -1664,9 +1651,9 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.3.tgz", - "integrity": "sha512-K3/M/a4+ESb5LEldjQb+XSrpY0nF+ZBFlTCbSnKaYAMfD8v33O6PMs4uYnOk19HlcsI8WMu3McdFPTiQHF/1/A==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", + "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -2066,17 +2053,17 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", - "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.3", + "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2", + "@babel/types": "^7.28.4", "debug": "^4.3.1" }, "engines": { @@ -2084,9 +2071,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -2389,9 +2376,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -2518,9 +2505,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", - "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", "engines": { "node": ">=12" @@ -2530,9 +2517,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", "engines": { "node": ">=12" @@ -2559,9 +2546,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -2919,6 +2906,16 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -2945,9 +2942,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -3879,9 +3876,9 @@ } }, "node_modules/@types/semver": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", - "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", "license": "MIT" }, "node_modules/@types/send": { @@ -5240,6 +5237,15 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.8.tgz", + "integrity": "sha512-be0PUaPsQX/gPWWgFsdD+GFzaoig5PXaUC1xLkQiYdDnANU8sMnHoQd8JhbJQuvTWrWLyeFN9Imb5Qtfvr4RrQ==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -5385,9 +5391,9 @@ "license": "BSD-2-Clause" }, "node_modules/browserslist": { - "version": "4.25.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz", - "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==", + "version": "4.26.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", + "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", "funding": [ { "type": "opencollective", @@ -5404,9 +5410,10 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001737", - "electron-to-chromium": "^1.5.211", - "node-releases": "^2.0.19", + "baseline-browser-mapping": "^2.8.3", + "caniuse-lite": "^1.0.30001741", + "electron-to-chromium": "^1.5.218", + "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { @@ -5552,9 +5559,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001737", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz", - "integrity": "sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==", + "version": "1.0.30001745", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz", + "integrity": "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==", "funding": [ { "type": "opencollective", @@ -6536,9 +6543,9 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -6940,9 +6947,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.211", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.211.tgz", - "integrity": "sha512-IGBvimJkotaLzFnwIVgW9/UD/AOJ2tByUmeOrtqBfACSbAw5b1G0XpvdaieKyc7ULmbwXVx+4e4Be8pOPBrYkw==", + "version": "1.5.225", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.225.tgz", + "integrity": "sha512-Oiv6+nGcMg0xuSUeYumk+eE0pDk0PuQ6Gnz16pErrPtlitaFZxq95hUtvGRg90kcJ/AdsM+AW5VkVUl3fGk+SQ==", "license": "ISC" }, "node_modules/emittery": { @@ -7080,9 +7087,9 @@ } }, "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -10933,9 +10940,9 @@ } }, "node_modules/jest-watch-typeahead/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -10948,9 +10955,9 @@ } }, "node_modules/jest-watch-typeahead/node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", - "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", "engines": { "node": ">=12" @@ -11748,9 +11755,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.21", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", + "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", "license": "MIT" }, "node_modules/normalize-path": { @@ -11808,9 +11815,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.21", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz", - "integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==", + "version": "2.2.22", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz", + "integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==", "license": "MIT" }, "node_modules/object-assign": { @@ -12824,9 +12831,19 @@ } }, "node_modules/postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { "camelcase-css": "^2.0.1" @@ -12834,10 +12851,6 @@ "engines": { "node": "^12 || ^14 || >= 16" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, "peerDependencies": { "postcss": "^8.4.21" } @@ -14262,9 +14275,9 @@ "license": "MIT" }, "node_modules/regenerate-unicode-properties": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", - "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", "license": "MIT", "dependencies": { "regenerate": "^1.4.2" @@ -14306,17 +14319,17 @@ } }, "node_modules/regexpu-core": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", - "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", "license": "MIT", "dependencies": { "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.0", + "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", - "regjsparser": "^0.12.0", + "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" + "unicode-match-property-value-ecmascript": "^2.2.1" }, "engines": { "node": ">=4" @@ -14329,29 +14342,17 @@ "license": "MIT" }, "node_modules/regjsparser": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", - "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", "license": "BSD-2-Clause", "dependencies": { - "jsesc": "~3.0.2" + "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", @@ -16310,13 +16311,13 @@ } }, "node_modules/terser": { - "version": "5.43.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", - "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", + "version": "5.44.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", + "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.14.0", + "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -16744,18 +16745,18 @@ } }, "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", - "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", "license": "MIT", "engines": { "node": ">=4" diff --git a/package.json b/package.json index d560b4d..6f43056 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,10 @@ "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", - "eject": "react-scripts eject" + "eject": "react-scripts eject", + "bag:record": "wsl bash -c './scripts/record-bag.sh \"$@\"' --", + "bag:play": "wsl bash -c './scripts/play-bag.sh \"$@\"' --", + "bag:list": "wsl bash -c 'ls -lh bags/ 2>/dev/null || echo \"No bags found\"'" }, "eslintConfig": { "extends": [ diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..6a0cd42 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,106 @@ +# ROS Bag Helper Scripts + +Quick commands to record and play back ROS2 bags for testing the simulation GUI. + +## Quick Start + +### 1. Record a test bag from the GUI + +```powershell +# In PowerShell (Windows) +npm run bag:record +``` + +This will: +- Prompt you to start the GUI simulation first +- Record `/imu/data`, `/dvl/odom`, and `/depth/pose` for 15 seconds +- Save to `bags/pool_test_01/` + +**Custom bag name and duration:** +```powershell +npm run bag:record -- my_test_bag 30 +``` + +### 2. Play back a bag + +```powershell +# In PowerShell (Windows) +npm run bag:play +``` + +This will: +- Play `bags/pool_test_01/` in loop mode at normal speed +- Press Ctrl+C to stop + +**Custom bag name and playback rate:** +```powershell +npm run bag:play -- my_test_bag 2.0 +``` + +### 3. List available bags + +```powershell +npm run bag:list +``` + +## Manual Usage (from WSL) + +If you prefer to run the scripts directly in WSL: + +```bash +# Record +./scripts/record-bag.sh [bag_name] [duration_seconds] + +# Play +./scripts/play-bag.sh [bag_name] [rate] + +# Examples +./scripts/record-bag.sh pool_test_02 20 +./scripts/play-bag.sh pool_test_02 1.5 +``` + +## Workflow for Testing Bag Mode + +1. **Record a bag:** + ```powershell + npm start # Start GUI + # Set Data Source: Synthetic Simulation, click Start + npm run bag:record # In another terminal + ``` + +2. **Play it back:** + ```powershell + npm run bag:play # Plays in loop + # In GUI: switch to "Bag Playback (listen only)" + # Click "Seed from ROS topics" + ``` + +3. **Verify seeding works:** + - Topics should show Hz values (not "lost!") + - Seed button should be enabled + - After clicking "Seed from ROS topics", you should see: + - Green status line: "Seeded orientation from /imu/data, depth from /depth/pose, velocity from /dvl/odom — HH:MM:SS" + +## Troubleshooting + +**"Bag not found" error:** +- Check `npm run bag:list` to see available bags +- Make sure you recorded a bag first with `npm run bag:record` + +**"No topics" when recording:** +- Ensure rosbridge is running in WSL: `ros2 launch rosbridge_server rosbridge_websocket_launch.xml` +- Start the GUI and begin synthetic simulation +- Verify topics with: `wsl ros2 topic list` + +**Bag playback but GUI shows "lost!" banners:** +- Verify topics are publishing: `wsl ros2 topic hz /imu/data` +- Check rosbridge is connected (green banner in GUI) +- Ensure bag contains the right topics: `wsl ros2 bag info bags/pool_test_01` + +## Notes + +- All scripts run inside WSL2 (where ROS2 is installed) +- The npm commands are wrappers that invoke WSL automatically +- Bags are stored in the `bags/` directory at the repo root +- Recording uses a 15-second default duration (configurable) +- Playback runs in loop mode by default diff --git a/scripts/play-bag.sh b/scripts/play-bag.sh new file mode 100644 index 0000000..442de02 --- /dev/null +++ b/scripts/play-bag.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# Helper script to play back a recorded bag +# Usage: ./scripts/play-bag.sh [bag_name] [rate] + +BAG_NAME="${1:-pool_test_01}" +RATE="${2:-1.0}" +BAG_DIR="$(cd "$(dirname "$0")/.." && pwd)/bags/${BAG_NAME}" + +echo "=========================================" +echo "Playing ROS2 bag" +echo "Bag: ${BAG_NAME}" +echo "Path: ${BAG_DIR}" +echo "Rate: ${RATE}x" +echo "=========================================" +echo "" + +# Check if bag exists +if [ ! -f "${BAG_DIR}/metadata.yaml" ]; then + echo "ERROR: Bag not found at ${BAG_DIR}" + echo "" + echo "Available bags:" + ls -1 "$(dirname "$0")/../bags" 2>/dev/null || echo " (none)" + echo "" + echo "To record a new bag:" + echo " npm run bag:record -- ${BAG_NAME}" + exit 1 +fi + +# Show bag info +ros2 bag info "$BAG_DIR" +echo "" +echo "Playing in loop mode. Press Ctrl+C to stop." +echo "" + +# Play bag in loop mode +ros2 bag play "$BAG_DIR" -l -r "$RATE" diff --git a/scripts/record-bag.sh b/scripts/record-bag.sh new file mode 100644 index 0000000..e9bdc60 --- /dev/null +++ b/scripts/record-bag.sh @@ -0,0 +1,68 @@ +#!/bin/bash +# Helper script to record a test bag from the GUI's synthetic simulation +# Usage: ./scripts/record-bag.sh [bag_name] [duration_seconds] + +BAG_NAME="${1:-pool_test_01}" +DURATION="${2:-15}" +BAG_DIR="$(cd "$(dirname "$0")/.." && pwd)/bags/${BAG_NAME}" + +echo "=========================================" +echo "Recording ROS2 bag for ${DURATION} seconds" +echo "Bag: ${BAG_NAME}" +echo "Path: ${BAG_DIR}" +echo "=========================================" +echo "" +echo "Topics to record:" +echo " - /imu/data (sensor_msgs/Imu)" +echo " - /dvl/odom (nav_msgs/Odometry)" +echo " - /depth/pose (geometry_msgs/PoseWithCovarianceStamped)" +echo "" +echo "Before recording:" +echo " 1. Start the GUI (npm start)" +echo " 2. Set Data Source: Synthetic Simulation" +echo " 3. Click 'Start Simulation'" +echo " 4. Wait a few seconds for topics to stabilize" +echo "" +read -p "Press Enter when simulation is running, or Ctrl+C to cancel..." + +# Remove existing bag if present +if [ -d "$BAG_DIR" ]; then + echo "Removing existing bag at $BAG_DIR" + rm -rf "$BAG_DIR" +fi + +echo "" +echo "Recording for ${DURATION} seconds..." +echo "Press Ctrl+C to stop early." +echo "" + +# Record with timeout +timeout ${DURATION}s ros2 bag record \ + /imu/data \ + /dvl/odom \ + /depth/pose \ + -o "$BAG_DIR" \ + || true + +echo "" +echo "=========================================" +echo "Recording complete!" +echo "=========================================" +echo "" + +# Show bag info +if [ -f "${BAG_DIR}/metadata.yaml" ]; then + ros2 bag info "$BAG_DIR" + echo "" + echo "To play this bag:" + echo " npm run bag:play -- ${BAG_NAME}" + echo "" + echo "Or manually:" + echo " ros2 bag play ${BAG_DIR} -l" +else + echo "ERROR: No metadata.yaml found. Recording may have failed." + echo "Make sure:" + echo " - rosbridge is running (ros2 launch rosbridge_server rosbridge_websocket_launch.xml)" + echo " - GUI simulation is publishing topics" + echo " - You can see topics with: ros2 topic list" +fi From d74602c407a45676db7f53d1cf70ec073f440755 Mon Sep 17 00:00:00 2001 From: brockgilman Date: Sun, 2 Nov 2025 15:43:21 -0500 Subject: [PATCH 6/6] fixed some bugs when running the scripts, bags can now record and playback --- package.json | 6 +++--- scripts/README.md | 10 ++++++---- scripts/play-bag.sh | 3 +++ scripts/record-bag.sh | 32 +++++++++++++++++++++++++++----- 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 6f43056..8898b5b 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,9 @@ "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", - "bag:record": "wsl bash -c './scripts/record-bag.sh \"$@\"' --", - "bag:play": "wsl bash -c './scripts/play-bag.sh \"$@\"' --", - "bag:list": "wsl bash -c 'ls -lh bags/ 2>/dev/null || echo \"No bags found\"'" + "bag:record": "wsl bash ./scripts/record-bag.sh", + "bag:play": "wsl bash ./scripts/play-bag.sh", + "bag:list": "wsl bash -c \"mkdir -p bags && ls -lh bags/\"" }, "eslintConfig": { "extends": [ diff --git a/scripts/README.md b/scripts/README.md index 6a0cd42..331d1d3 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -7,7 +7,7 @@ Quick commands to record and play back ROS2 bags for testing the simulation GUI. ### 1. Record a test bag from the GUI ```powershell -# In PowerShell (Windows) +# In PowerShell (Windows) - default 15 seconds, saves to bags/pool_test_01 npm run bag:record ``` @@ -18,13 +18,14 @@ This will: **Custom bag name and duration:** ```powershell -npm run bag:record -- my_test_bag 30 +# Record to a custom bag name for 30 seconds +wsl bash ./scripts/record-bag.sh my_test_bag 30 ``` ### 2. Play back a bag ```powershell -# In PowerShell (Windows) +# In PowerShell (Windows) - plays bags/pool_test_01 in loop npm run bag:play ``` @@ -34,7 +35,8 @@ This will: **Custom bag name and playback rate:** ```powershell -npm run bag:play -- my_test_bag 2.0 +# Play custom bag at 2x speed +wsl bash ./scripts/play-bag.sh my_test_bag 2.0 ``` ### 3. List available bags diff --git a/scripts/play-bag.sh b/scripts/play-bag.sh index 442de02..749a823 100644 --- a/scripts/play-bag.sh +++ b/scripts/play-bag.sh @@ -2,6 +2,9 @@ # Helper script to play back a recorded bag # Usage: ./scripts/play-bag.sh [bag_name] [rate] +# Source ROS2 setup +source /opt/ros/humble/setup.bash + BAG_NAME="${1:-pool_test_01}" RATE="${2:-1.0}" BAG_DIR="$(cd "$(dirname "$0")/.." && pwd)/bags/${BAG_NAME}" diff --git a/scripts/record-bag.sh b/scripts/record-bag.sh index e9bdc60..7ea3301 100644 --- a/scripts/record-bag.sh +++ b/scripts/record-bag.sh @@ -2,6 +2,9 @@ # Helper script to record a test bag from the GUI's synthetic simulation # Usage: ./scripts/record-bag.sh [bag_name] [duration_seconds] +# Source ROS2 setup +source /opt/ros/humble/setup.bash + BAG_NAME="${1:-pool_test_01}" DURATION="${2:-15}" BAG_DIR="$(cd "$(dirname "$0")/.." && pwd)/bags/${BAG_NAME}" @@ -33,16 +36,35 @@ fi echo "" echo "Recording for ${DURATION} seconds..." -echo "Press Ctrl+C to stop early." +echo "Press Ctrl+C to stop recording." echo "" -# Record with timeout -timeout ${DURATION}s ros2 bag record \ +# Start recording in background and capture its PID +ros2 bag record \ /imu/data \ /dvl/odom \ /depth/pose \ - -o "$BAG_DIR" \ - || true + -o "$BAG_DIR" & + +RECORD_PID=$! + +# Function to kill the recording process +cleanup() { + echo "" + echo "Stopping recording..." + kill $RECORD_PID 2>/dev/null + wait $RECORD_PID 2>/dev/null + echo "Recording stopped." +} + +# Set up trap to catch Ctrl+C +trap cleanup SIGINT SIGTERM + +# Wait for the specified duration +sleep ${DURATION} + +# Stop recording +cleanup echo "" echo "========================================="