From 7f2a5e06842e2c9299cf32d5abdd42649b6265bc Mon Sep 17 00:00:00 2001 From: Ojas P Date: Sun, 3 May 2026 15:23:51 -0400 Subject: [PATCH 1/5] implemented all of science GUI stylistically --- README.md | 56 ++ .../app/GUI functions/ArmOperatorTab.js | 66 ++ .../app/GUI functions/DriveOperatorTab.js | 432 +++++++++++ .../app/GUI functions/Navigation.js | 73 ++ .../app/GUI functions/NavigationBar.js | 18 +- .../app/GUI functions/OperationsWall.js | 95 ++- .../app/GUI functions/OperatorTab.js | 703 ++++++++++-------- .../app/GUI functions/PageContent.js | 9 + .../app/GUI functions/ScienceMonitor.js | 700 +++++++++++++++++ .../app/GUI functions/SubsystemBar.js | 11 +- .../app/GUI functions/WebRtcCameraVideo.js | 62 ++ .../app/GUI functions/pageConstants.js | 16 +- .../app/GUI functions/useCameraStreams.js | 351 +++++++++ umdloop_gui_web/app/config.js | 25 + umdloop_gui_web/app/page.js | 1 + umdloop_gui_web/public/test-tube.png | Bin 0 -> 8231 bytes umdloop_gui_web/spectrometer/RamanPlot.jsx | 12 +- 17 files changed, 2281 insertions(+), 349 deletions(-) create mode 100644 umdloop_gui_web/app/GUI functions/ArmOperatorTab.js create mode 100644 umdloop_gui_web/app/GUI functions/DriveOperatorTab.js create mode 100644 umdloop_gui_web/app/GUI functions/ScienceMonitor.js create mode 100644 umdloop_gui_web/app/GUI functions/WebRtcCameraVideo.js create mode 100644 umdloop_gui_web/app/GUI functions/useCameraStreams.js create mode 100644 umdloop_gui_web/public/test-tube.png diff --git a/README.md b/README.md index 39c30a2..5ca0cfe 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,62 @@ npm install npm run dev ``` +TEST THE GUI FROM ATHENA CODE: +```bash +#In Terminal 1 Start RosBridge +cd ~/Downloads/athena-code-main +source /opt/ros/humble/setup.bash +source install/setup.bash +ros2 launch rosbridge_server rosbridge_websocket_launch.xml + +#In Terminal 2 Start Athena Code +cd ~/Downloads/athena-code-main +source /opt/ros/humble/setup.bash +source install/setup.bash +ros2 launch simulation bringup.launch.py publish_ground_truth_tf:=true rviz:=true + +#In terminal 3 Start Nav Code +cd ~/Downloads/athena-code-main +source /opt/ros/humble/setup.bash +source install/setup.bash +ros2 launch athena_planner navigation.launch.py sim:=true + +#In Terminal 4 Test Rover moving in a Cirlce +cd ~/Downloads/athena-code-main +source /opt/ros/humble/setup.bash +source install/setup.bash +ros2 topic pub -r 10 /rear_ackermann_controller/reference geometry_msgs/msg/TwistStamped "{twist: {linear: {x: 0.25}, angular: {z: 0.35}}}" + +``` + +## Required ROS Topics For GUI + +The GUI does not need the full ROS graph. These are the minimal topics currently required by the web app based on what it actually displays. + +### Minimal required set +- `/gps/fix` +- `/localization/odom` +- `/imu/filtered` +- `/joint_states` +- `/heading` +- `/diagnostics` + +### What each topic is used for +- `/gps/fix`: rover marker in the map view +- `/localization/odom`: rover speed in the Technician tab +- `/imu/filtered`: tilt and stability display in the Technician tab +- `/joint_states`: wheel velocity, steering orientation, and mobility diagnostics in the Technician tab +- `/heading`: heading readout in the Technician tab +- `/diagnostics`: system health summary and diagnostics detail cards in the Technician tab + +### Optional topics +These are useful only if the GUI grows to display them explicitly. +- `/odom/ground_truth`: simulation/debug comparison +- `/dynamic_joint_states`: richer controller telemetry +- `/aruco_pose`: ArUco tracking status +- `/zed/zed_node/left/image_rect_color`: ROS image-driven camera widgets +- `/plan`: live path visualization outside of the current navigation flow + Run the web server: ```bash diff --git a/umdloop_gui_web/app/GUI functions/ArmOperatorTab.js b/umdloop_gui_web/app/GUI functions/ArmOperatorTab.js new file mode 100644 index 0000000..1e7c936 --- /dev/null +++ b/umdloop_gui_web/app/GUI functions/ArmOperatorTab.js @@ -0,0 +1,66 @@ +import React from "react"; + +export default function ArmOperatorTab({ + armClampDistance, + cameraBySlot, + CameraCard, + emergencyStop, + FullscreenOverlay, + setArmClampDistance, + setEmergencyStop, +}) { + const armCameras = [ + { label: "Base Arm", id: cameraBySlot(4) }, + { label: "Joint", id: cameraBySlot(5) }, + { label: "End Effector", id: cameraBySlot(6) }, + { label: "Gripper", id: cameraBySlot(7) }, + ]; + + return ( +
+
+
+
Arm Safety
+
Emergency Stop: {emergencyStop ? "ON" : "OFF"}
+ +
+
+
+
Clamp Distance to Fully Close
+ setArmClampDistance(Number(e.target.value))} style={{ width: "100%" }} /> +
{armClampDistance}%
+
+
+ {["Cylindrical Control", "Joint By Joint"].map((controlMode) => ( + + ))} +
+
+
+
+ {armCameras.map((cam) => )} +
+ +
+ ); +} diff --git a/umdloop_gui_web/app/GUI functions/DriveOperatorTab.js b/umdloop_gui_web/app/GUI functions/DriveOperatorTab.js new file mode 100644 index 0000000..432d1af --- /dev/null +++ b/umdloop_gui_web/app/GUI functions/DriveOperatorTab.js @@ -0,0 +1,432 @@ +import React, { useEffect, useRef, useState } from "react"; + +export default function DriveOperatorTab({ + activeDriveCameraIds, + cameraButtonStyle, + cameraControlStatus, + cameraDebug, + cameraLastMessage, + cameraRotateDeg, + cameraSignalUrl, + cameraSocketDetail, + cameraSocketStatus, + cameraStats, + CameraCard, + driveCameraBySlot, + DRIVE_CAMERA_TILE_COUNT, + emergencyStop, + fps, + FullscreenOverlay, + grayscale, + isRestartingFeeds, + MAX_ACTIVE_DRIVE_CAMERAS, + restartAllFeeds, + selectedSubsystem, + setActiveDriveCameraIds, + setAllCameraFramerate, + setCameraControlStatus, + setCameraRotateDeg, + setEmergencyStop, + setGstreamerGrayscale, + streamPlaying, + summarizeCameraLoadState, + toggleDriveCamera, + toggleStreamPlaying, +}) { + const driveCameras = Array.from({ length: DRIVE_CAMERA_TILE_COUNT }, (_, index) => ({ + label: `Drive Cam ${index + 1}`, + id: driveCameraBySlot(index), + })); + const driveCameraIds = driveCameras.map((camera) => camera.id); + const isDriveCameraActive = (cameraId) => activeDriveCameraIds.includes(cameraId); + const topRowCameras = driveCameras.slice(0, 2); + const bottomLeftCameras = driveCameras.slice(2, 5); + const bottomRightCameras = driveCameras.slice(5, 8); + const isDriveScienceTab = selectedSubsystem === "Drive (Science)"; + const [stopwatchRunning, setStopwatchRunning] = useState(false); + const [stopwatchElapsedMs, setStopwatchElapsedMs] = useState(0); + const [showCameraDebugDetails, setShowCameraDebugDetails] = useState(false); + const [locationReached, setLocationReached] = useState(false); + const stopwatchStartRef = useRef(null); + const driveRosCommandPlaceholders = ["ROS2 Command 1", "ROS2 Command 2", "ROS2 Command 3", "ROS2 Command 4"]; + const confettiPieces = Array.from({ length: 42 }, (_, index) => ({ + id: index, + color: ["#ff4d4d", "#ffd166", "#06d6a0", "#4cc9f0", "#f72585", "#ffffff"][index % 6], + delay: `${(index % 7) * 0.08}s`, + duration: `${1.55 + (index % 5) * 0.16}s`, + rotation: `${(index * 37) % 360}deg`, + burstX: `${((index % 10) - 4.5) * 10}px`, + burstY: `${-185 - (index % 7) * 24}px`, + fallX: `${((index % 12) - 5.5) * 24}px`, + fallY: `${300 + (index % 8) * 36}px`, + })); + + useEffect(() => { + if (!locationReached) return undefined; + + const onKeyDown = (event) => { + if (event.key === "Escape") { + setLocationReached(false); + } + }; + + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }, [locationReached]); + + useEffect(() => { + if (!stopwatchRunning) return undefined; + + const intervalId = window.setInterval(() => { + const startedAt = stopwatchStartRef.current ?? Date.now(); + setStopwatchElapsedMs(Date.now() - startedAt); + }, 100); + + return () => window.clearInterval(intervalId); + }, [stopwatchRunning]); + + const formatStopwatch = (elapsedMs) => { + const totalTenths = Math.floor(elapsedMs / 100); + const minutes = String(Math.floor(totalTenths / 600)).padStart(2, "0"); + const seconds = String(Math.floor((totalTenths % 600) / 10)).padStart(2, "0"); + const tenths = totalTenths % 10; + return `${minutes}:${seconds}.${tenths}`; + }; + + const startStopwatch = () => { + stopwatchStartRef.current = Date.now() - stopwatchElapsedMs; + setStopwatchRunning(true); + }; + + const pauseStopwatch = () => { + const startedAt = stopwatchStartRef.current ?? Date.now(); + setStopwatchElapsedMs(Date.now() - startedAt); + setStopwatchRunning(false); + }; + + const resetStopwatch = () => { + stopwatchStartRef.current = Date.now(); + setStopwatchElapsedMs(0); + setStopwatchRunning(false); + }; + + if (isDriveScienceTab) { + const scienceDrivePanels = [ + { title: "Wide-Angle Panorama Image", tone: "#2d4f62" }, + { title: "Stratigraphic Profile Image", tone: "#6a5234" }, + { title: "Close-Up High Res. Image", tone: "#5c3f2d" }, + { title: "GNSS Coords. / Elevation", tone: "#2d3b4f" }, + ]; + + return ( +
+
+
+ Rover Operator (Driver) +
+
+
+ Stopwatch +
+
+ {formatStopwatch(stopwatchElapsedMs)} +
+
+ + +
+
+ {scienceDrivePanels.map((panel) => ( +
+
+ {panel.title} +
+
+ {panel.title === "GNSS Coords. / Elevation" ? "GNSS Coords. / Elevation" : panel.title} +
+
+ ))} +
+
+ ); + } + + return ( +
+
+
+
Control State + Safety
+ +
+ {driveRosCommandPlaceholders.map((commandLabel) => ( + + ))} +
+
+ +
+
Vision + Stream Control
+
+ FPS + setAllCameraFramerate(Number(e.target.value))} + style={{ width: "100%" }} + /> + {fps} +
+
+ + + + +
+
Target framerate: {fps} FPS
+
+
+ {cameraControlStatus || `Signal: ${cameraSocketStatus}`} +
+
setShowCameraDebugDetails(true)} + onMouseLeave={() => setShowCameraDebugDetails(false)} + style={{ + position: "relative", + flex: "0 0 auto", + borderRadius: "9999px", + border: "1px solid #555", + background: "#303030", + color: "#d8d8d8", + fontSize: "10px", + fontWeight: 900, + padding: "4px 8px", + cursor: "default", + }} + > + Signal Details + {showCameraDebugDetails ? ( +
+
Camera signaling: {cameraSignalUrl}
+
Drive cameras: {driveCameraIds.filter(Boolean).join(", ") || "none detected"}
+
Camera load: {summarizeCameraLoadState(activeDriveCameraIds)} | active {activeDriveCameraIds.length}/{MAX_ACTIVE_DRIVE_CAMERAS}
+
Signal: {cameraSocketStatus} | last {cameraLastMessage || "none"} | stats keys {Object.keys(cameraStats || {}).length}
+
{cameraSocketDetail}
+
Sent: {cameraDebug?.lastSentRaw || "none"} | recv: {cameraDebug?.lastReceivedRaw || "none"}
+
+ ) : null} +
+
+
+
+ +
+ Rotate: + + + + +
+ +
+
+ {topRowCameras.map((camera, index) => ( + toggleDriveCamera(camera.id)} + /> + ))} +
+
+ {[bottomLeftCameras, bottomRightCameras].map((cameraGroup, groupIndex) => ( +
+ {cameraGroup.map((camera, index) => ( + toggleDriveCamera(camera.id)} + /> + ))} +
+ ))} +
+
+ + {locationReached ? ( +
setLocationReached(false)} + style={{ + position: "fixed", + inset: 0, + zIndex: 1000, + background: "rgba(0,0,0,0.86)", + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "24px", + overflow: "hidden", + }} + > + + {confettiPieces.map((piece) => ( +
+ ))} +
event.stopPropagation()} + style={{ + width: "min(900px, 92vw)", + minHeight: "min(360px, 70vh)", + borderRadius: "14px", + border: "3px solid #2f7d3a", + background: "#102215", + boxShadow: "0 18px 60px rgba(0,0,0,0.55)", + color: "white", + display: "flex", + alignItems: "center", + justifyContent: "center", + textAlign: "center", + fontSize: "clamp(44px, 8vw, 96px)", + fontWeight: 1000, + letterSpacing: "0.04em", + }} + > + LOCATION REACHED +
+
+ ) : null} +
+ ); +} diff --git a/umdloop_gui_web/app/GUI functions/Navigation.js b/umdloop_gui_web/app/GUI functions/Navigation.js index 7aac68e..6783a3f 100644 --- a/umdloop_gui_web/app/GUI functions/Navigation.js +++ b/umdloop_gui_web/app/GUI functions/Navigation.js @@ -10,6 +10,8 @@ export default function Navigation({ selectedNavItem }) { const [longitude, setLongitude] = useState(""); const [navMode, setNavMode] = useState("GNSS"); const [pathPlanStatus, setPathPlanStatus] = useState(""); + const [rosCommand, setRosCommand] = useState(""); + const [rosCommandStatus, setRosCommandStatus] = useState(""); const fetchStatus = async () => { try { @@ -77,6 +79,17 @@ export default function Navigation({ selectedNavItem }) { } }; + const onRosCommandSubmit = () => { + const command = rosCommand.trim(); + + if (!command) { + setRosCommandStatus("Enter a ROS2 command first"); + return; + } + + setRosCommandStatus(`Ready to send: ${command}`); + }; + useEffect(() => { if (selectedNavItem !== "Object Detection") return undefined; @@ -221,6 +234,66 @@ export default function Navigation({ selectedNavItem }) {
)} + + {selectedNavItem === "Placeholder2" && ( +
+
+

ROS2 Command

+