+ {driveRosCommandPlaceholders.map((commandLabel) => (
+
+ ))}
+
+ {["Cylindrical Control", "Joint By Joint"].map((controlMode) => (
+
+ ))}
+
diff --git a/umdloop_gui_web/app/GUI functions/PageContent.js b/umdloop_gui_web/app/GUI functions/PageContent.js
index 5bf568c..ca0d164 100644
--- a/umdloop_gui_web/app/GUI functions/PageContent.js
+++ b/umdloop_gui_web/app/GUI functions/PageContent.js
@@ -5,6 +5,7 @@ import MapView from "./MapView";
import OperationsWall from "./OperationsWall";
import OperatorTab from "./OperatorTab";
import Navigation from "./Navigation";
+import ScienceMonitor from "./ScienceMonitor";
import TechnicianDashboard from "./TechnicianDashboard";
import SubsystemBar from "./SubsystemBar";
import { NAVIGATION_BUTTONS } from "./pageConstants";
@@ -32,6 +33,14 @@ export default function PageContent({
);
}
+ if (selectedMode === "Science") {
+ return (
+
diff --git a/umdloop_gui_web/app/GUI functions/ScienceMonitor.js b/umdloop_gui_web/app/GUI functions/ScienceMonitor.js
new file mode 100644
index 0000000..0c67399
--- /dev/null
+++ b/umdloop_gui_web/app/GUI functions/ScienceMonitor.js
@@ -0,0 +1,590 @@
+"use client";
+
+import React, { useEffect, useRef, useState } from "react";
+import RamanPlot from "../../spectrometer/RamanPlot";
+import CameraFeed from "./CameraFeed";
+import CameraManagerModal from "./CameraManagerModal";
+import { CAMERA_ROLES, SCIENCE_SUBSYSTEMS } from "./pageConstants";
+import SubsystemBar from "./SubsystemBar";
+
+const RAMAN_WS_URL = "ws://localhost:5001/ws/spectrum";
+const SCIENCE_CAMERA_ROLES = [CAMERA_ROLES.SCIENCE_1, CAMERA_ROLES.SCIENCE_2, CAMERA_ROLES.SCIENCE_3];
+
+export default function ScienceMonitor() {
+ const [selectedScienceTab, setSelectedScienceTab] = useState(SCIENCE_SUBSYSTEMS[0]);
+ const [fullscreenCam, setFullscreenCam] = useState(null);
+ const [lastPanoramaLabel, setLastPanoramaLabel] = useState("No panorama captured yet.");
+ const [panoramaShots, setPanoramaShots] = useState(0);
+ const [sciencePhotos, setSciencePhotos] = useState(0);
+ const [sciencePopup, setSciencePopup] = useState(null);
+ const [showCameraManager, setShowCameraManager] = useState(false);
+ const [stopwatchRunning, setStopwatchRunning] = useState(false);
+ const [stopwatchElapsedMs, setStopwatchElapsedMs] = useState(0);
+ const [cameraRotateDeg] = useState(0);
+ const stopwatchStartRef = useRef(null);
+ const isEquipmentSpecialistTab = selectedScienceTab.startsWith("Equipment Specialist");
+ const isScientist1Tab1 = selectedScienceTab === "Scientist 1 Tab 1";
+ const isScientist1Tab2 = selectedScienceTab === "Scientist 1 Tab 2";
+ const isScientist2Tab2 = selectedScienceTab === "Scientist 2 Tab 2";
+
+ const cameraBySlot = (slot) => {
+ if (slot >= 7 && slot <= 9) return SCIENCE_CAMERA_ROLES[slot - 7];
+ return SCIENCE_CAMERA_ROLES[slot % SCIENCE_CAMERA_ROLES.length];
+ };
+
+ useEffect(() => {
+ const handleKey = (e) => {
+ if (e.key === "Escape") {
+ setFullscreenCam(null);
+ setSciencePopup(null);
+ }
+ };
+
+ window.addEventListener("keydown", handleKey);
+ return () => {
+ window.removeEventListener("keydown", handleKey);
+ };
+ }, []);
+
+ 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 CameraImage = ({ cameraId, alt, style, ...imageProps }) => (
+
+ );
+
+ const CameraCard = ({ camera }) => (
+
setFullscreenCam(camera)}
+ style={{
+ background: "#2b2b2b",
+ borderRadius: "10px",
+ border: "1px solid #3d3d3d",
+ padding: "5px",
+ cursor: "pointer",
+ display: "flex",
+ flexDirection: "column",
+ minHeight: 0,
+ height: "100%",
+ }}
+ onMouseEnter={(e) => {
+ e.currentTarget.style.borderColor = "#c90202";
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.borderColor = "#3d3d3d";
+ }}
+ >
+
+ {camera.label} {camera.id ? `(${camera.id})` : "(No Cam)"}
+
+
+
+ );
+
+ 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);
+ };
+
+ const FullscreenOverlay = () =>
+ fullscreenCam && (
+
setFullscreenCam(null)}
+ style={{
+ position: "fixed",
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ background: "rgba(0, 0, 0, 0.95)",
+ display: "flex",
+ flexDirection: "column",
+ alignItems: "center",
+ justifyContent: "center",
+ zIndex: 1000,
+ padding: "20px",
+ }}
+ >
+
{fullscreenCam.label}
+
+
+ );
+
+ const SciencePopupOverlay = () => {
+ if (!sciencePopup) return null;
+
+ let title = "";
+ let body = null;
+
+ if (sciencePopup === "panorama") {
+ title = "Panorama Preview";
+ body = (
+
+
+
{lastPanoramaLabel}
+
Placeholder preview panel. Stitching/export will be wired later.
+
+ );
+ } else if (sciencePopup === "tasks") {
+ title = "Additional Science Tasks";
+ const taskItems = [
+ "Soil Core Collection",
+ "Rock Face Classification",
+ "Sample Bag Labeling",
+ "Drill Site Annotation",
+ "Spectrometer Calibration",
+ ];
+ body = (
+
+ {taskItems.map((task) => (
+
+ ))}
+
Task actions are UI placeholders for now.
+
+ );
+ } else if (sciencePopup === "soil") {
+ title = "Soil Moisture Analysis";
+ const points = [28, 34, 41, 39, 44, 48, 52];
+ body = (
+
+
Probe trend over last 7 reads
+
+ {points.map((p, i) => (
+
+ ))}
+
+
Graph is a placeholder UI panel.
+
+ );
+ } else if (sciencePopup === "spectral") {
+ title = "Raman Spectrum Analysis";
+ body = (
+
+
+ Live Raman spectrum from spectrometer backend on port 5001
+
+
+
+ Start with: python3 spectrometer/raman_backend.py
+
+
+ );
+ }
+
+ return (
+
setSciencePopup(null)}
+ style={{
+ position: "fixed",
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ background: "rgba(0, 0, 0, 0.75)",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ zIndex: 1100,
+ padding: "20px",
+ }}
+ >
+
e.stopPropagation()}
+ style={{ width: "min(760px, 96vw)", maxHeight: "85vh", overflowY: "auto", background: "#222", border: "1px solid #4a4a4a", borderRadius: "12px", padding: "14px" }}
+ >
+
+
{title}
+
+
+ {body}
+
+
+ );
+ };
+
+ const scienceCameras = [
+ { label: `${selectedScienceTab} Cam 1`, id: cameraBySlot(7) },
+ { label: `${selectedScienceTab} Cam 2`, id: cameraBySlot(8) },
+ { label: `${selectedScienceTab} Cam 3`, id: cameraBySlot(9) },
+ ];
+
+ const graphBar = (value, color) => (
+
+ );
+
+ const equipmentSpecialistCameras = [
+ { label: "Overhead of Scoops / Sampler", id: cameraBySlot(7) },
+ { label: "View of Scoops", id: cameraBySlot(8) },
+ { label: "View of Sampler", id: cameraBySlot(9) },
+ ];
+ const scientist1Cameras = [
+ { label: "Nightvision Camera", id: cameraBySlot(7) },
+ { label: "Rover Field View", id: cameraBySlot(9) },
+ ];
+ const isEquipmentSpecialistTab2 = selectedScienceTab === "Equipment Specialist Tab 2";
+ const visibleEquipmentSpecialistCameras = isEquipmentSpecialistTab2
+ ? [
+ equipmentSpecialistCameras[0],
+ { ...equipmentSpecialistCameras[2], label: "NightVision Camera" },
+ ]
+ : equipmentSpecialistCameras;
+ const visibleVerticalLayoutCameras = isScientist1Tab1 ? scientist1Cameras : visibleEquipmentSpecialistCameras;
+ const tab2NightVisionCamera = { label: "Nightvision Camera", id: cameraBySlot(7) };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {isEquipmentSpecialistTab || isScientist1Tab1 ? (
+
+
+
+ {selectedScienceTab}
+
+
+
+ Stopwatch
+
+
+ {formatStopwatch(stopwatchElapsedMs)}
+
+
+
+
+
+
+
+
+ {visibleVerticalLayoutCameras.map((camera) => (
+
+
+ Camera Feed
+
+
+ {camera.label}
+
+
setFullscreenCam(camera)}
+ style={{
+ width: "100%",
+ height: "100%",
+ objectFit: "cover",
+ borderRadius: "8px",
+ background: "black",
+ cursor: "pointer",
+ transform: `rotate(${cameraRotateDeg}deg)`,
+ transformOrigin: "center center",
+ }}
+ pausedStyle={{ fontSize: "12px" }}
+ />
+
+ ))}
+
+
+
+ ) : isScientist1Tab2 ? (
+
+
+
+ {selectedScienceTab}
+
+
+
+ Stratigraphic Profile Image
+
+
+ Profile placeholder
+
+
+
+
+ Spectro Data
+
+
+ Site 1
+
+
+
+
+
+
+
+ Spectro Data
+
+
+ Site 2
+
+
+
+
+
+
+
+ Camera Feed
+
+
+ Nightvision Camera
+
+
setFullscreenCam(tab2NightVisionCamera)}
+ style={{
+ width: "100%",
+ height: "100%",
+ objectFit: "cover",
+ borderRadius: "8px",
+ background: "black",
+ cursor: "pointer",
+ transform: `rotate(${cameraRotateDeg}deg)`,
+ transformOrigin: "center center",
+ }}
+ pausedStyle={{ fontSize: "12px" }}
+ />
+
+
+
+ ) : isScientist2Tab2 ? (
+
+
+
+ {selectedScienceTab}
+
+
+
+ Stopwatch
+
+
+ {formatStopwatch(stopwatchElapsedMs)}
+
+
+
+
+
+
+ {["Site 1", "Site 2"].map((siteLabel) => (
+
+
+ Fluoro Data
+
+
+ {siteLabel}
+
+
+ Fluorescence analysis placeholder
+
+
+ ))}
+
+
+ Camera Feed
+
+
+ Nightvision Camera inside science box
+
+
setFullscreenCam(tab2NightVisionCamera)}
+ style={{
+ width: "100%",
+ height: "100%",
+ objectFit: "cover",
+ borderRadius: "8px",
+ background: "black",
+ cursor: "pointer",
+ transform: `rotate(${cameraRotateDeg}deg)`,
+ transformOrigin: "center center",
+ }}
+ pausedStyle={{ fontSize: "12px" }}
+ />
+
+
+
+ ) : (
+
+
+
+
{selectedScienceTab} Imaging / Capture
+
+
+
+
+
+
+
+
+
+ Panoramas: {panoramaShots} | Photos: {sciencePhotos}
+
+
+ {lastPanoramaLabel}
+
+
+
+
{selectedScienceTab} Data Graphs
+
+
+
+
Thermal Delta
+ {graphBar(35, "#f97316")}
+
+
+
+
+
+ )}
+
+
+
+ {showCameraManager &&
setShowCameraManager(false)} />}
+
+ );
+}
diff --git a/umdloop_gui_web/app/GUI functions/SubsystemBar.js b/umdloop_gui_web/app/GUI functions/SubsystemBar.js
index d7b25be..a584cb7 100644
--- a/umdloop_gui_web/app/GUI functions/SubsystemBar.js
+++ b/umdloop_gui_web/app/GUI functions/SubsystemBar.js
@@ -2,18 +2,19 @@
import React, { useState } from "react";
-export default function SubsystemBar({ buttons, selected, setSelected }) {
+export default function SubsystemBar({ buttons, selected, setSelected, compact = false }) {
const [hoveredButtonId, setHoveredButtonId] = useState(null);
return (
setHoveredButtonId(null)}
onClick={() => setSelected(label)}
>
- {label}
+ {label}
);
})}
diff --git a/umdloop_gui_web/app/GUI functions/pageConstants.js b/umdloop_gui_web/app/GUI functions/pageConstants.js
index 0bcaeb8..bbeef9b 100644
--- a/umdloop_gui_web/app/GUI functions/pageConstants.js
+++ b/umdloop_gui_web/app/GUI functions/pageConstants.js
@@ -1,8 +1,18 @@
"use client";
-export const MODES = ["Operator", "Technician", "Drone", "Navigation", "Map"];
-export const MODE_ICONS = ["camera.png", "sensor.png", "camera.png", "navigation.png", "map.png"];
-export const SUBSYSTEMS = ["Drive", "Arm", "Science"];
+export const MODES = ["Operator", "Science", "Technician", "Drone", "Navigation", "Map"];
+export const MODE_ICONS = ["camera.png", "test-tube.png", "sensor.png", "camera.png", "navigation.png", "map.png"];
+export const NAV_MODES = MODES.filter((mode) => mode !== "Drone");
+export const NAV_MODE_ICONS = NAV_MODES.map((mode) => MODE_ICONS[MODES.indexOf(mode)]);
+export const SUBSYSTEMS = ["Drive (Default)", "Drive (Science)", "Arm"];
+export const SCIENCE_SUBSYSTEMS = [
+ "Scientist 1 Tab 1",
+ "Scientist 2 Tab 1",
+ "Scientist 1 Tab 2",
+ "Scientist 2 Tab 2",
+ "Equipment Specialist Tab 1",
+ "Equipment Specialist Tab 2",
+];
export const NAVIGATION_BUTTONS = ["Object Detection", "Control Panel", "Placeholder2"];
export const CAMERA_ROLES = {
@@ -10,6 +20,7 @@ export const CAMERA_ROLES = {
BACK: "back",
LEFT_SIDE: "left_side",
RIGHT_SIDE: "right_side",
+ RADIO_VIEW: "radio_view",
WHEEL_TL_A: "wheel_tl_a",
WHEEL_TL_B: "wheel_tl_b",
WHEEL_TR_A: "wheel_tr_a",
diff --git a/umdloop_gui_web/app/hooks/useWebRTCCameras.js b/umdloop_gui_web/app/hooks/useWebRTCCameras.js
index c8fb024..d872e27 100644
--- a/umdloop_gui_web/app/hooks/useWebRTCCameras.js
+++ b/umdloop_gui_web/app/hooks/useWebRTCCameras.js
@@ -3,10 +3,80 @@
import { useCallback, useEffect, useRef, useState } from "react";
const RECONNECT_DELAY_MS = 2000;
+const DEFAULT_CAMERAS = [
+ { id: "0", name: "TL Wheel A", role: "wheel_tl_a" },
+ { id: "1", name: "TL Wheel B", role: "wheel_tl_b" },
+ { id: "2", name: "TR Wheel A", role: "wheel_tr_a" },
+ { id: "3", name: "TR Wheel B", role: "wheel_tr_b" },
+ { id: "4", name: "BL Wheel A", role: "wheel_bl_a" },
+ { id: "5", name: "BL Wheel B", role: "wheel_bl_b" },
+ { id: "6", name: "BR Wheel A", role: "wheel_br_a" },
+ { id: "7", name: "BR Wheel B", role: "wheel_br_b" },
+ { id: "8", name: "Arm Base", role: "arm_base" },
+ { id: "9", name: "Arm Joint", role: "arm_joint" },
+ { id: "10", name: "Arm End Effector", role: "arm_ee" },
+ { id: "11", name: "Arm Gripper", role: "arm_gripper" },
+ { id: "12", name: "Science Cam 1 / Overhead Scoops / Nightvision", role: "science_1" },
+ { id: "13", name: "Science Cam 2 / View of Scoops", role: "science_2" },
+ { id: "14", name: "Science Cam 3 / View of Sampler / Rover Field View", role: "science_3" },
+ { id: "15", name: "Front Camera", role: "front" },
+].map((camera) => ({
+ ...camera,
+ device: "default camera slot",
+ enabled: false,
+ synthetic: true,
+}));
+
+function getCameraId(message) {
+ return message?.id ?? message?.cam_id ?? message?.cameraId ?? message?.camera_id ?? message?.camera;
+}
+
+function normalizeCamera(camera, index) {
+ const rawId = getCameraId(camera) ?? camera;
+ if (rawId == null) return null;
+
+ const id = String(rawId).trim();
+ if (!id) return null;
+
+ const defaults = DEFAULT_CAMERAS.find((defaultCamera) => defaultCamera.id === id);
+ const cameraConfig = typeof camera === "object" && camera != null ? camera : {};
+ const config = cameraConfig.config ?? {};
+
+ return {
+ ...defaults,
+ ...cameraConfig,
+ ...config,
+ id,
+ name: cameraConfig.name ?? config.name ?? defaults?.name ?? `Camera ${id}`,
+ role: cameraConfig.role ?? config.role ?? defaults?.role ?? null,
+ device: cameraConfig.device ?? cameraConfig.path ?? defaults?.device ?? `camera ${index + 1}`,
+ capabilities: cameraConfig.capabilities ?? defaults?.capabilities ?? [],
+ enabled: Boolean(cameraConfig.enabled ?? config.enabled ?? defaults?.enabled),
+ synthetic: Boolean(cameraConfig.synthetic ?? defaults?.synthetic),
+ };
+}
+
+function normalizeCameras(message) {
+ const source =
+ message?.cameras ??
+ message?.camera_ids ??
+ message?.cameraIds ??
+ message?.devices ??
+ message?.available_cameras ??
+ [];
+
+ if (!Array.isArray(source)) return DEFAULT_CAMERAS;
+
+ const normalized = source
+ .map((camera, index) => normalizeCamera(camera, index))
+ .filter(Boolean);
+
+ return normalized.length ? normalized : DEFAULT_CAMERAS;
+}
export default function useWebRTCCameras(url) {
const [connected, setConnected] = useState(false);
- const [cameras, setCameras] = useState([]);
+ const [cameras, setCameras] = useState(DEFAULT_CAMERAS);
const [streams, setStreams] = useState({});
const [stats, setStats] = useState({});
const [missions, setMissions] = useState([]);
@@ -31,7 +101,17 @@ export default function useWebRTCCameras(url) {
}
}, []);
- const handleOffer = useCallback(async ({ id, sdp }) => {
+ const updateCamera = useCallback((id, changes) => {
+ setCameras((prev) => prev.map((camera) => (
+ camera.id === String(id) ? { ...camera, ...changes } : camera
+ )));
+ }, []);
+
+ const handleOffer = useCallback(async (message) => {
+ const id = String(getCameraId(message) ?? "");
+ const sdp = typeof message?.sdp === "string" ? message.sdp : message?.description?.sdp;
+ if (!id || !sdp) return;
+
pcsRef.current.get(id)?.close();
const pc = new RTCPeerConnection({ iceServers: [] });
@@ -92,7 +172,9 @@ export default function useWebRTCCameras(url) {
switch (msg.type) {
case "state":
- setCameras(msg.cameras ?? []);
+ case "camera_state":
+ case "camera_list":
+ setCameras(normalizeCameras(msg));
break;
case "missions_state":
setMissions(msg.missions ?? []);
@@ -102,15 +184,22 @@ export default function useWebRTCCameras(url) {
await handleOffer(msg).catch((err) => console.error("WebRTC offer handling failed:", err));
break;
case "ice": {
- const pc = pcsRef.current.get(msg.id);
+ const pc = pcsRef.current.get(String(getCameraId(msg) ?? ""));
if (pc) {
- pc.addIceCandidate({ candidate: msg.candidate, sdpMLineIndex: msg.sdpMLineIndex }).catch(() => {});
+ pc.addIceCandidate({
+ candidate: msg.candidate,
+ sdpMLineIndex: msg.sdpMLineIndex ?? msg.mlineIndex ?? 0,
+ }).catch(() => {});
}
break;
}
- case "stats":
- setStats((prev) => ({ ...prev, [msg.camera_id]: { fps: msg.fps, bitrate: msg.bitrate } }));
+ case "stats": {
+ const id = String(getCameraId(msg) ?? "");
+ if (id) {
+ setStats((prev) => ({ ...prev, [id]: { fps: msg.fps, bitrate: msg.bitrate } }));
+ }
break;
+ }
}
};
}, [url, closeAllPeerConnections, handleOffer]);
@@ -126,11 +215,26 @@ export default function useWebRTCCameras(url) {
};
}, [connect, closeAllPeerConnections]);
- const enableCamera = useCallback((id) => send({ type: "enable", camera_id: id }), [send]);
- const disableCamera = useCallback((id) => send({ type: "disable", camera_id: id }), [send]);
- const renameCamera = useCallback((id, name) => send({ type: "rename", camera_id: id, name }), [send]);
- const setRole = useCallback((id, role) => send({ type: "set_config", camera_id: id, config: { role } }), [send]);
- const setConfig = useCallback((id, config) => send({ type: "set_config", camera_id: id, config }), [send]);
+ const enableCamera = useCallback((id) => {
+ updateCamera(id, { enabled: true });
+ send({ type: "enable", camera_id: id });
+ }, [send, updateCamera]);
+ const disableCamera = useCallback((id) => {
+ updateCamera(id, { enabled: false });
+ send({ type: "disable", camera_id: id });
+ }, [send, updateCamera]);
+ const renameCamera = useCallback((id, name) => {
+ updateCamera(id, { name });
+ send({ type: "rename", camera_id: id, name });
+ }, [send, updateCamera]);
+ const setRole = useCallback((id, role) => {
+ updateCamera(id, { role });
+ send({ type: "set_config", camera_id: id, config: { role } });
+ }, [send, updateCamera]);
+ const setConfig = useCallback((id, config) => {
+ updateCamera(id, config);
+ send({ type: "set_config", camera_id: id, config });
+ }, [send, updateCamera]);
const saveMission = useCallback((name, id) => send({ type: "save_mission", name, ...(id != null ? { id } : {}) }), [send]);
const loadMission = useCallback((id) => send({ type: "load_mission", id }), [send]);
const deleteMission = useCallback((id) => send({ type: "delete_mission", id }), [send]);
diff --git a/umdloop_gui_web/app/page.js b/umdloop_gui_web/app/page.js
index 8c45d31..9f8fffa 100644
--- a/umdloop_gui_web/app/page.js
+++ b/umdloop_gui_web/app/page.js
@@ -15,6 +15,7 @@ export default function LoopGui() {
const [selectedNavItem, setSelectedNavItem] = useState(NAVIGATION_BUTTONS[0]);
const showSubsystemBar =
+ selectedMode !== "Science" &&
selectedMode !== "Navigation" &&
selectedMode !== "Drone" &&
selectedMode !== "Technician" &&
diff --git a/umdloop_gui_web/public/test-tube.png b/umdloop_gui_web/public/test-tube.png
new file mode 100644
index 0000000..39ba04a
Binary files /dev/null and b/umdloop_gui_web/public/test-tube.png differ
diff --git a/umdloop_gui_web/spectrometer/RamanPlot.jsx b/umdloop_gui_web/spectrometer/RamanPlot.jsx
index 3ea3b9d..3a9b650 100644
--- a/umdloop_gui_web/spectrometer/RamanPlot.jsx
+++ b/umdloop_gui_web/spectrometer/RamanPlot.jsx
@@ -53,6 +53,7 @@ const RamanPlot = ({
wsUrl = "ws://localhost:5000/ws/spectrum",
width = 900,
height = 400,
+ fillContainer = false,
testMode = false,
}) => {
const canvasRef = useRef(null);
@@ -186,7 +187,14 @@ const RamanPlot = ({
}, [wsUrl, testMode, updatePlotData]);
return (
-