diff --git a/src/backendapi/apiStageCenterCalibrationGetStatus.js b/src/backendapi/apiStageCenterCalibrationGetStatus.js new file mode 100644 index 0000000..f095bb0 --- /dev/null +++ b/src/backendapi/apiStageCenterCalibrationGetStatus.js @@ -0,0 +1,29 @@ +// src/backendapi/apiStageCenterCalibrationGetStatus.js +import createAxiosInstance from "./createAxiosInstance"; + +const apiStageCenterCalibrationGetStatus = async () => { + try { + const axiosInstance = createAxiosInstance(); + + // Send GET request to check calibration status + const response = await axiosInstance.get("/StageCenterCalibrationController/getIsCalibrationRunning"); + + return response.data; // Return the data from the response + } catch (error) { + console.error("Error getting stage center calibration status:", error); + throw error; // Throw error to be handled by the caller + } +}; + +export default apiStageCenterCalibrationGetStatus; + +/* +Example: +apiStageCenterCalibrationGetStatus() + .then(isRunning => { + console.log("Calibration running:", isRunning); + }) + .catch(error => { + console.error("Error:", error); + }); +*/ \ No newline at end of file diff --git a/src/backendapi/apiStageCenterCalibrationPerformCalibration.js b/src/backendapi/apiStageCenterCalibrationPerformCalibration.js new file mode 100644 index 0000000..6c4905d --- /dev/null +++ b/src/backendapi/apiStageCenterCalibrationPerformCalibration.js @@ -0,0 +1,60 @@ +// src/backendapi/apiStageCenterCalibrationPerformCalibration.js +import createAxiosInstance from "./createAxiosInstance"; + +const apiStageCenterCalibrationPerformCalibration = async ({ + start_x = 0, + start_y = 0, + exposure_time_us = 3000, + speed = 5000, + step_um = 50.0, + max_radius_um = 2000.0, + brightness_factor = 1.4, +}) => { + try { + const axiosInstance = createAxiosInstance(); + + // Build the query string dynamically + let url = "/StageCenterCalibrationController/performCalibration?"; + const queryParams = []; + + queryParams.push(`start_x=${encodeURIComponent(start_x)}`); + queryParams.push(`start_y=${encodeURIComponent(start_y)}`); + queryParams.push(`exposure_time_us=${encodeURIComponent(exposure_time_us)}`); + queryParams.push(`speed=${encodeURIComponent(speed)}`); + queryParams.push(`step_um=${encodeURIComponent(step_um)}`); + queryParams.push(`max_radius_um=${encodeURIComponent(max_radius_um)}`); + queryParams.push(`brightness_factor=${encodeURIComponent(brightness_factor)}`); + + // Join all query parameters with '&' + url += queryParams.join("&"); + + // Send GET request with the constructed URL + const response = await axiosInstance.get(url); + + return response.data; // Return the data from the response + } catch (error) { + console.error("Error performing stage center calibration:", error); + throw error; // Throw error to be handled by the caller + } +}; + +export default apiStageCenterCalibrationPerformCalibration; + +/* +Example: +apiStageCenterCalibrationPerformCalibration({ + start_x: 0, + start_y: 0, + exposure_time_us: 3000, + speed: 5000, + step_um: 50.0, + max_radius_um: 2000.0, + brightness_factor: 1.4 +}) + .then(positions => { + console.log("Calibration positions:", positions); + }) + .catch(error => { + console.error("Error:", error); + }); +*/ \ No newline at end of file diff --git a/src/backendapi/apiStageCenterCalibrationStopCalibration.js b/src/backendapi/apiStageCenterCalibrationStopCalibration.js new file mode 100644 index 0000000..ec3edd1 --- /dev/null +++ b/src/backendapi/apiStageCenterCalibrationStopCalibration.js @@ -0,0 +1,29 @@ +// src/backendapi/apiStageCenterCalibrationStopCalibration.js +import createAxiosInstance from "./createAxiosInstance"; + +const apiStageCenterCalibrationStopCalibration = async () => { + try { + const axiosInstance = createAxiosInstance(); + + // Send GET request to stop calibration + const response = await axiosInstance.get("/StageCenterCalibrationController/stopCalibration"); + + return response.data; // Return the data from the response + } catch (error) { + console.error("Error stopping stage center calibration:", error); + throw error; // Throw error to be handled by the caller + } +}; + +export default apiStageCenterCalibrationStopCalibration; + +/* +Example: +apiStageCenterCalibrationStopCalibration() + .then(result => { + console.log("Calibration stopped:", result); + }) + .catch(error => { + console.error("Error:", error); + }); +*/ \ No newline at end of file diff --git a/src/components/JoystickController.js b/src/components/JoystickController.js index 0e4b3f8..11b3923 100644 --- a/src/components/JoystickController.js +++ b/src/components/JoystickController.js @@ -14,20 +14,23 @@ import { } from "@mui/material"; import { useWebSocket } from "../context/WebSocketContext"; import * as stageOffsetCalibrationSlice from "../state/slices/StageOffsetCalibrationSlice.js"; +import LiveViewControlWrapper from "../axon/LiveViewControlWrapper"; const JoystickController = ({ hostIP, hostPort }) => { const socket = useWebSocket(); const dispatch = useDispatch(); - + // Access Redux state for image display - const stageOffsetState = useSelector(stageOffsetCalibrationSlice.getStageOffsetCalibrationState); + const stageOffsetState = useSelector( + stageOffsetCalibrationSlice.getStageOffsetCalibrationState + ); const imageUrls = stageOffsetState.imageUrls; const detectors = stageOffsetState.detectors; - + // Step size states const [stepSizeXY, setStepSizeXY] = useState(100); const [stepSizeZ, setStepSizeZ] = useState(10); - + // Fetch the list of detectors from the server useEffect(() => { const fetchDetectorNames = async () => { @@ -54,10 +57,12 @@ const JoystickController = ({ hostIP, hostPort }) => { if (jdata.name === "sigUpdateImage") { const detectorName = jdata.detectorname; const imgSrc = `data:image/jpeg;base64,${jdata.image}`; - dispatch(stageOffsetCalibrationSlice.updateImageUrl({ - detector: detectorName, - url: imgSrc - })); + dispatch( + stageOffsetCalibrationSlice.updateImageUrl({ + detector: detectorName, + url: imgSrc, + }) + ); } } catch (error) { console.error("Error parsing signal data:", error); @@ -120,21 +125,27 @@ const JoystickController = ({ hostIP, hostPort }) => { }; return ( - - {/* Live Stream */} + - - - Live Stream - {imageUrls[detectors[0]] && ( - Live Stream - )} - - + + Live Stream + + + + {/* Joystick Controls */} @@ -144,7 +155,7 @@ const JoystickController = ({ hostIP, hostPort }) => { Advanced Joystick Control - + {/* Step Size Controls */} @@ -181,7 +192,13 @@ const JoystickController = ({ hostIP, hostPort }) => { {/* SVG Joystick */} - + - - - + + + - - - + + + {/* Home All */} homeAll()}> - - + + {/* Home X */} homeAxis("X")}> - - - X + + + + {" "} + X + {/* Home Y */} homeAxis("Y")}> - - - Y + + + + {" "} + Y + {/* Home Z */} homeAxis("Z")}> - - - Z + + + + {" "} + Z + {/* XY Movement Rings - Outermost */} - handleJoystickClick("Y+", stepSizeXY)}> - + handleJoystickClick("Y+", stepSizeXY)} + > + - handleJoystickClick("X+", stepSizeXY)}> - + handleJoystickClick("X+", stepSizeXY)} + > + - handleJoystickClick("Y-", stepSizeXY)}> - + handleJoystickClick("Y-", stepSizeXY)} + > + - handleJoystickClick("X-", stepSizeXY)}> - + handleJoystickClick("X-", stepSizeXY)} + > + {/* XY Movement Rings - Middle */} - handleJoystickClick("Y+", stepSizeXY / 10)}> - + handleJoystickClick("Y+", stepSizeXY / 10)} + > + - handleJoystickClick("X+", stepSizeXY / 10)}> - + handleJoystickClick("X+", stepSizeXY / 10)} + > + - handleJoystickClick("Y-", stepSizeXY / 10)}> - + handleJoystickClick("Y-", stepSizeXY / 10)} + > + - handleJoystickClick("X-", stepSizeXY / 10)}> - + handleJoystickClick("X-", stepSizeXY / 10)} + > + {/* XY Movement Rings - Inner */} - handleJoystickClick("Y+", stepSizeXY / 100)}> - + handleJoystickClick("Y+", stepSizeXY / 100)} + > + - handleJoystickClick("X+", stepSizeXY / 100)}> - + handleJoystickClick("X+", stepSizeXY / 100)} + > + - handleJoystickClick("Y-", stepSizeXY / 100)}> - + handleJoystickClick("Y-", stepSizeXY / 100)} + > + - handleJoystickClick("X-", stepSizeXY / 100)}> - + handleJoystickClick("X-", stepSizeXY / 100)} + > + {/* Z Movement Buttons - Multiple Step Sizes */} {/* Z+ Full Step */} - handleJoystickClick("Z+", stepSizeZ)}> - - - +{stepSizeZ} + handleJoystickClick("Z+", stepSizeZ)} + > + + + + {" "} + +{stepSizeZ} + - + {/* Z+ 1/10 Step */} - handleJoystickClick("Z+", stepSizeZ / 10)}> - - - +{stepSizeZ/10} + handleJoystickClick("Z+", stepSizeZ / 10)} + > + + + + {" "} + +{stepSizeZ / 10} + - + {/* Z+ 1/100 Step */} - handleJoystickClick("Z+", stepSizeZ / 100)}> - - - +{stepSizeZ/100} + handleJoystickClick("Z+", stepSizeZ / 100)} + > + + + + {" "} + +{stepSizeZ / 100} + - + {/* Z- 1/100 Step */} - handleJoystickClick("Z-", stepSizeZ / 100)}> - - - -{stepSizeZ/100} + handleJoystickClick("Z-", stepSizeZ / 100)} + > + + + + {" "} + -{stepSizeZ / 100} + - + {/* Z- 1/10 Step */} - handleJoystickClick("Z-", stepSizeZ / 10)}> - - - -{stepSizeZ/10} + handleJoystickClick("Z-", stepSizeZ / 10)} + > + + + + {" "} + -{stepSizeZ / 10} + - + {/* Z- Full Step */} - handleJoystickClick("Z-", stepSizeZ)}> - - - -{stepSizeZ} + handleJoystickClick("Z-", stepSizeZ)} + > + + + + {" "} + -{stepSizeZ} + {/* Direction indicators */} - - - - - - +Y - -Y - -X - +X + + + + + + + {" "} + +Y + + + {" "} + -Y + + + {" "} + -X + + + {" "} + +X + @@ -338,17 +630,29 @@ const JoystickController = ({ hostIP, hostPort }) => { {/* Manual Home Buttons */} - - - @@ -365,4 +669,4 @@ const JoystickController = ({ hostIP, hostPort }) => { ); }; -export default JoystickController; \ No newline at end of file +export default JoystickController; diff --git a/src/components/LiveStreamTile.js b/src/components/LiveStreamTile.js new file mode 100644 index 0000000..753123c --- /dev/null +++ b/src/components/LiveStreamTile.js @@ -0,0 +1,117 @@ +import React, { useEffect } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { Box, Card, CardContent, Typography } from "@mui/material"; +import { Videocam as VideocamIcon } from "@mui/icons-material"; +import { useWebSocket } from "../context/WebSocketContext"; +import * as stageOffsetCalibrationSlice from "../state/slices/StageOffsetCalibrationSlice.js"; + +const LiveStreamTile = ({ hostIP, hostPort, width = 200, height = 150 }) => { + const socket = useWebSocket(); + const dispatch = useDispatch(); + + // Access Redux state for image display + const stageOffsetState = useSelector(stageOffsetCalibrationSlice.getStageOffsetCalibrationState); + const imageUrls = stageOffsetState.imageUrls; + const detectors = stageOffsetState.detectors; + + // Fetch the list of detectors from the server + useEffect(() => { + const fetchDetectorNames = async () => { + try { + const response = await fetch( + `${hostIP}:${hostPort}/SettingsController/getDetectorNames` + ); + const data = await response.json(); + dispatch(stageOffsetCalibrationSlice.setDetectors(data || [])); + } catch (error) { + console.error("Error fetching detector names:", error); + } + }; + + fetchDetectorNames(); + }, [hostIP, hostPort, dispatch]); + + // Handle socket signals for live stream + useEffect(() => { + if (!socket) return; + const handleSignal = (rawData) => { + try { + const jdata = JSON.parse(rawData); + if (jdata.name === "sigUpdateImage") { + const detectorName = jdata.detectorname; + const imgSrc = `data:image/jpeg;base64,${jdata.image}`; + dispatch(stageOffsetCalibrationSlice.updateImageUrl({ + detector: detectorName, + url: imgSrc + })); + } + } catch (error) { + console.error("Error parsing signal data:", error); + } + }; + socket.on("signal", handleSignal); + return () => { + socket.off("signal", handleSignal); + }; + }, [socket, dispatch]); + + const hasImage = detectors.length > 0 && imageUrls[detectors[0]]; + + return ( + + + {/* Header */} + + + + Live Stream + + + + {/* Image or placeholder */} + + {hasImage ? ( + Live Stream + ) : ( + + + + No stream + + + )} + + + + ); +}; + +export default LiveStreamTile; \ No newline at end of file diff --git a/src/components/StageCenterCalibrationWizard.js b/src/components/StageCenterCalibrationWizard.js new file mode 100644 index 0000000..0502c09 --- /dev/null +++ b/src/components/StageCenterCalibrationWizard.js @@ -0,0 +1,174 @@ +import React from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { + Dialog, + DialogTitle, + DialogContent, + Stepper, + Step, + StepLabel, + Box, + useTheme, + useMediaQuery, + IconButton, +} from "@mui/material"; +import { Close } from "@mui/icons-material"; +import * as stageCenterCalibrationSlice from "../state/slices/StageCenterCalibrationSlice"; + +// Import wizard steps +import StageCenterStep1 from "./wizard-steps/StageCenterStep1"; +import StageCenterStep2 from "./wizard-steps/StageCenterStep2"; +import StageCenterStep3 from "./wizard-steps/StageCenterStep3"; +import StageCenterStep4 from "./wizard-steps/StageCenterStep4"; +import StageCenterStep5 from "./wizard-steps/StageCenterStep5"; +import StageCenterStep6 from "./wizard-steps/StageCenterStep6"; + +const steps = [ + "Setup & Overview", + "Manual Position Entry", + "Stage Map Visualization", + "Automatic Detection", + "Review Results", + "Complete" +]; + +const StageCenterCalibrationWizard = ({ hostIP, hostPort }) => { + const dispatch = useDispatch(); + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down('md')); + + const stageCenterState = useSelector(stageCenterCalibrationSlice.getStageCenterCalibrationState); + const { isWizardOpen, activeStep } = stageCenterState; + + const handleNext = () => { + dispatch(stageCenterCalibrationSlice.nextStep()); + }; + + const handleBack = () => { + dispatch(stageCenterCalibrationSlice.previousStep()); + }; + + const handleStepClick = (step) => { + dispatch(stageCenterCalibrationSlice.setActiveStep(step)); + }; + + const handleClose = () => { + dispatch(stageCenterCalibrationSlice.setWizardOpen(false)); + }; + + const getStepContent = (step) => { + const commonProps = { + hostIP, + hostPort, + onNext: handleNext, + onBack: handleBack, + activeStep, + totalSteps: steps.length, + }; + + switch (step) { + case 0: + return ; + case 1: + return ; + case 2: + return ; + case 3: + return ; + case 4: + return ; + case 5: + return ; + default: + return ; + } + }; + + return ( + + + Stage Center Calibration Wizard + + + + + + + + + {steps.map((label, index) => { + const stepProps = {}; + const labelProps = {}; + + return ( + handleStepClick(index)} + > + + {label} + + + ); + })} + + + + + {getStepContent(activeStep)} + + + + ); +}; + +export default StageCenterCalibrationWizard; \ No newline at end of file diff --git a/src/components/StageMapCanvas.js b/src/components/StageMapCanvas.js new file mode 100644 index 0000000..5058866 --- /dev/null +++ b/src/components/StageMapCanvas.js @@ -0,0 +1,43 @@ +import React, { useEffect } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { Box, Typography, Paper } from "@mui/material"; +import WellSelectorCanvas from "../axon/WellSelectorCanvas"; +import { Mode } from "../axon/WellSelectorCanvas"; +import * as wellSelectorSlice from "../state/slices/WellSelectorSlice"; +import * as experimentSlice from "../state/slices/ExperimentSlice"; + +const StageMapCanvas = ({ hostIP, hostPort, width = 500, height = 400 }) => { + const dispatch = useDispatch(); + + // Initialize the well selector state for camera movement mode + useEffect(() => { + // Set the mode to MOVE_CAMERA so clicking moves the stage + dispatch(wellSelectorSlice.setMode(Mode.MOVE_CAMERA)); + }, [dispatch]); + + return ( + + + Interactive Stage Map + + + +
+ +
+
+ + + Click anywhere on the map to move the stage to that position +
+ Use the mouse wheel to zoom in/out and drag to pan the view +
+
+ ); +}; + +export default StageMapCanvas; \ No newline at end of file diff --git a/src/components/StageMapVisualization.js b/src/components/StageMapVisualization.js new file mode 100644 index 0000000..1bc3b59 --- /dev/null +++ b/src/components/StageMapVisualization.js @@ -0,0 +1,207 @@ +import React from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { Box, Paper, Typography } from "@mui/material"; +import { MyLocation as LocationIcon } from "@mui/icons-material"; +import * as stageCenterCalibrationSlice from "../state/slices/StageCenterCalibrationSlice"; +import apiPositionerControllerMovePositioner from "../backendapi/apiPositionerControllerMovePositioner"; + +const StageMapVisualization = ({ hostIP, hostPort, width = 400, height = 400 }) => { + const dispatch = useDispatch(); + const stageCenterState = useSelector(stageCenterCalibrationSlice.getStageCenterCalibrationState); + + const { + currentX, + currentY, + stageMapWidth, + stageMapHeight, + stageMapCenterX, + stageMapCenterY, + foundCenterX, + foundCenterY, + manualCenterX, + manualCenterY, + } = stageCenterState; + + // Convert stage coordinates to SVG coordinates + const stageToSvg = (stageX, stageY) => { + const svgX = ((stageX - stageMapCenterX + stageMapWidth / 2) / stageMapWidth) * width; + const svgY = height - ((stageY - stageMapCenterY + stageMapHeight / 2) / stageMapHeight) * height; + return { x: svgX, y: svgY }; + }; + + // Convert SVG coordinates to stage coordinates + const svgToStage = (svgX, svgY) => { + const stageX = ((svgX / width) * stageMapWidth) - (stageMapWidth / 2) + stageMapCenterX; + const stageY = (((height - svgY) / height) * stageMapHeight) - (stageMapHeight / 2) + stageMapCenterY; + return { x: Math.round(stageX), y: Math.round(stageY) }; + }; + + const handleMapClick = async (event) => { + const rect = event.currentTarget.getBoundingClientRect(); + const svgX = event.clientX - rect.left; + const svgY = event.clientY - rect.top; + + const { x: stageX, y: stageY } = svgToStage(svgX, svgY); + + dispatch(stageCenterCalibrationSlice.setIsLoading(true)); + try { + // Move to clicked position + await apiPositionerControllerMovePositioner({ + positionerName: "ESP32Stage", + axis: "X", + dist: stageX, + isAbsolute: true, + isBlocking: false, + speed: 1000, + }); + + await apiPositionerControllerMovePositioner({ + positionerName: "ESP32Stage", + axis: "Y", + dist: stageY, + isAbsolute: true, + isBlocking: false, + speed: 1000, + }); + + dispatch(stageCenterCalibrationSlice.setSuccessMessage(`Moving to (${stageX}, ${stageY})`)); + } catch (error) { + console.error("Error moving to clicked position:", error); + dispatch(stageCenterCalibrationSlice.setError("Failed to move to clicked position")); + } finally { + dispatch(stageCenterCalibrationSlice.setIsLoading(false)); + } + }; + + // Calculate positions for display + const currentPos = stageToSvg(currentX, currentY); + const manualCenterPos = manualCenterX && manualCenterY ? stageToSvg(parseFloat(manualCenterX), parseFloat(manualCenterY)) : null; + const foundCenterPos = foundCenterX !== null && foundCenterY !== null ? stageToSvg(foundCenterX, foundCenterY) : null; + + return ( + + + Interactive Stage Map + + + + + {/* Grid lines */} + + + + + + + + {/* Center crosshair */} + + + + {/* Current position */} + + + Current ({currentX.toFixed(0)}, {currentY.toFixed(0)}) + + + {/* Manual center position */} + {manualCenterPos && ( + <> + + + Manual Center + + + )} + + {/* Found center position */} + {foundCenterPos && ( + <> + + + Found Center + + + )} + + + + {/* Legend */} + + +
+ Current Position + + {manualCenterPos && ( + +
+ Manual Center + + )} + {foundCenterPos && ( + +
+ Found Center + + )} + + + + Click anywhere on the map to move the stage to that position +
+ Map range: ±{(stageMapWidth/2).toFixed(0)}μm (X), ±{(stageMapHeight/2).toFixed(0)}μm (Y) +
+ + ); +}; + +export default StageMapVisualization; \ No newline at end of file diff --git a/src/components/StageOffsetCalibrationController.js b/src/components/StageOffsetCalibrationController.js index 6323139..237b6c2 100644 --- a/src/components/StageOffsetCalibrationController.js +++ b/src/components/StageOffsetCalibrationController.js @@ -8,9 +8,14 @@ import { Typography, Card, CardContent, + Box, } from "@mui/material"; import { useWebSocket } from "../context/WebSocketContext"; import * as stageOffsetCalibrationSlice from "../state/slices/StageOffsetCalibrationSlice.js"; +import * as stageCenterCalibrationSlice from "../state/slices/StageCenterCalibrationSlice.js"; +import StageCenterCalibrationWizard from "./StageCenterCalibrationWizard"; +import JoystickController from "./JoystickController"; + const StageOffsetCalibration = ({ hostIP, hostPort }) => { const socket = useWebSocket(); @@ -207,113 +212,67 @@ const StageOffsetCalibration = ({ hostIP, hostPort }) => { return ( - {/* Live Image */} + {/* Title and Prominent Wizard Button */} - - - Live Stream - {/* If there's only one detector feed, you can do: */} - {imageUrls[detectors[0]] && ( - Live Stream - )} - {/* Or map over multiple detectors if needed */} - - + + + Stage Offset Calibration + + + + Use the guided wizard to find your stage center with automatic detection + + - {/* Joystick Controls (cross layout) */} - - - - - Joystick Controls - - - - - - - - - - - - - - - - - Fine Moves - - - - - - - - - - - - - - - - - + {/* Joystick Controller - Full Width */} + + {/* Stage Offset Calibration */} - + - Stage Offset Calibration + Manual Stage Offset Calibration @@ -490,6 +449,12 @@ const StageOffsetCalibration = ({ hostIP, hostPort }) => { + + {/* Stage Center Calibration Wizard */} + ); }; diff --git a/src/components/wizard-steps/StageCenterStep1.js b/src/components/wizard-steps/StageCenterStep1.js new file mode 100644 index 0000000..0d0981b --- /dev/null +++ b/src/components/wizard-steps/StageCenterStep1.js @@ -0,0 +1,240 @@ +import React from "react"; +import { + Box, + Button, + Typography, + Paper, + Alert, + List, + ListItem, + ListItemIcon, + ListItemText, + Grid, + Card, + CardContent, +} from "@mui/material"; +import { + Info as InfoIcon, + Warning as WarningIcon, + CheckCircle as CheckIcon, + CenterFocusStrong as CenterIcon, + Search as SearchIcon, + TouchApp as TouchIcon, + Settings as SettingsIcon, +} from "@mui/icons-material"; +import { useTheme } from '@mui/material/styles'; +import LiveStreamTile from "../LiveStreamTile"; + +const StageCenterStep1 = ({ hostIP, hostPort, onNext, activeStep, totalSteps }) => { + const theme = useTheme(); + const placeholderImageStyle = { + width: "100%", + height: "200px", + backgroundColor: theme.palette.background.paper, + border: "2px dashed #ccc", + borderRadius: "8px", + display: "flex", + alignItems: "center", + justifyContent: "center", + marginBottom: "16px", + }; + + return ( + + {/* Live Stream Tile - positioned in top right */} + + + + + + + Welcome to the Stage Center Calibration Wizard + + This advanced wizard will help you find and calibrate the center position of your microscope stage + using both manual methods and automatic bright spot detection. Monitor the live stream during calibration. + + + + + + What This Wizard Will Do + + + + + + + + + Manual Calibration + + + • Enter known center positions manually
+ • Use current position as reference
+ • Apply offset corrections +
+
+
+
+ + + + + + + Automatic Detection + + + • Spiral scan to find bright spots
+ • Configurable search parameters
+ • Real-time position tracking +
+
+
+
+ + + + + + + Interactive Stage Map + + + • Visual stage representation
+ • Click-to-move functionality
+ • Real-time position display +
+
+
+
+ + + + + + + Precise Results + + + • Accurate center calculation
+ • Multiple validation methods
+ • Easy result application +
+
+
+
+
+
+ + + + + Wizard Steps Overview + + + + + + = 0 ? "success" : "disabled"} /> + + + + + + + = 1 ? "success" : "disabled"} /> + + + + + + + = 2 ? "success" : "disabled"} /> + + + + + + + = 3 ? "success" : "disabled"} /> + + + + + + + = 4 ? "success" : "disabled"} /> + + + + + + + = 5 ? "success" : "disabled"} /> + + + + + + + + + Important Notes: + + + • Ensure your stage is properly connected and responsive
+ • For automatic detection, ensure there are bright spots or samples on the stage
+ • The wizard will guide you through each step with clear instructions
+ • You can navigate between steps using the stepper above +
+
+ + + + 🔬 Stage Center Calibration Overview +
+ (Reference diagram showing stage coordinate system and center calibration concept) +
+
+ + + {/* Placeholder for back button */} + + + + ); +}; + +export default StageCenterStep1; \ No newline at end of file diff --git a/src/components/wizard-steps/StageCenterStep2.js b/src/components/wizard-steps/StageCenterStep2.js new file mode 100644 index 0000000..2e468ed --- /dev/null +++ b/src/components/wizard-steps/StageCenterStep2.js @@ -0,0 +1,318 @@ +import React, { useEffect } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { + Box, + Button, + Typography, + Paper, + Alert, + Grid, + TextField, + Card, + CardContent, + Divider, +} from "@mui/material"; +import { + Refresh as RefreshIcon, + MyLocation as LocationIcon, + Save as SaveIcon, + Input as InputIcon, +} from "@mui/icons-material"; +import * as stageCenterCalibrationSlice from "../../state/slices/StageCenterCalibrationSlice"; +import apiPositionerControllerMovePositioner from "../../backendapi/apiPositionerControllerMovePositioner"; +import LiveStreamTile from "../LiveStreamTile"; +import { useTheme } from '@mui/material/styles'; + +const StageCenterStep2 = ({ hostIP, hostPort, onNext, onBack, activeStep, totalSteps }) => { + const theme = useTheme(); + const dispatch = useDispatch(); + const stageCenterState = useSelector(stageCenterCalibrationSlice.getStageCenterCalibrationState); + + const { + currentX, + currentY, + manualCenterX, + manualCenterY, + isLoading, + error, + successMessage + } = stageCenterState; + + // Fetch current position on component mount + useEffect(() => { + fetchCurrentPosition(); + }, []); + + const fetchCurrentPosition = async () => { + dispatch(stageCenterCalibrationSlice.setIsLoading(true)); + try { + const response = await fetch(`${hostIP}:${hostPort}/PositionerController/getPositionerPositions`); + const data = await response.json(); + + if (data.ESP32Stage) { + dispatch(stageCenterCalibrationSlice.setCurrentPosition({ + x: data.ESP32Stage.X, + y: data.ESP32Stage.Y + })); + } else if (data.VirtualStage) { + dispatch(stageCenterCalibrationSlice.setCurrentPosition({ + x: data.VirtualStage.X, + y: data.VirtualStage.Y + })); + } + dispatch(stageCenterCalibrationSlice.setSuccessMessage("Position updated successfully")); + } catch (error) { + console.error("Error fetching position:", error); + dispatch(stageCenterCalibrationSlice.setError("Failed to fetch current position")); + } finally { + dispatch(stageCenterCalibrationSlice.setIsLoading(false)); + } + }; + + const moveToPosition = async (x, y) => { + if (!x || !y) { + dispatch(stageCenterCalibrationSlice.setError("Please enter valid X and Y coordinates")); + return; + } + + dispatch(stageCenterCalibrationSlice.setIsLoading(true)); + try { + // Move to X position + await apiPositionerControllerMovePositioner({ + positionerName: "ESP32Stage", + axis: "X", + dist: parseFloat(x), + isAbsolute: true, + isBlocking: false, + speed: 1000, + }); + + // Move to Y position + await apiPositionerControllerMovePositioner({ + positionerName: "ESP32Stage", + axis: "Y", + dist: parseFloat(y), + isAbsolute: true, + isBlocking: false, + speed: 1000, + }); + + dispatch(stageCenterCalibrationSlice.setSuccessMessage(`Moving to position (${x}, ${y})`)); + + // Update current position after a short delay + setTimeout(() => { + fetchCurrentPosition(); + }, 2000); + } catch (error) { + console.error("Error moving to position:", error); + dispatch(stageCenterCalibrationSlice.setError("Failed to move to position")); + } finally { + dispatch(stageCenterCalibrationSlice.setIsLoading(false)); + } + }; + + const useCurrentAsCenter = () => { + dispatch(stageCenterCalibrationSlice.setManualCenter({ + x: currentX.toString(), + y: currentY.toString() + })); + dispatch(stageCenterCalibrationSlice.setSuccessMessage("Current position set as center")); + }; + + const moveToManualCenter = () => { + moveToPosition(manualCenterX, manualCenterY); + }; + + const clearMessages = () => { + dispatch(stageCenterCalibrationSlice.clearMessages()); + }; + + return ( + + {/* Live Stream Tile - positioned in top right */} + + + + + + + Step 2: Manual Position Entry + + Enter known center coordinates or use the current stage position as your reference point. + Watch the live stream to verify your position. + + + {error && ( + + {error} + + )} + + {successMessage && ( + + {successMessage} + + )} + + + {/* Current Position Card */} + + + + + + Current Stage Position + + + + + + + + + + + + + + + + + + + + {/* Manual Entry Card */} + + + + + + Manual Center Entry + + + + + dispatch(stageCenterCalibrationSlice.setManualCenterX(e.target.value))} + fullWidth + variant="outlined" + type="number" + placeholder="0" + /> + + + dispatch(stageCenterCalibrationSlice.setManualCenterY(e.target.value))} + fullWidth + variant="outlined" + type="number" + placeholder="0" + /> + + + + + + + + + + + + + + + + Manual Calibration Instructions + + + Method 1: If you know the exact center coordinates of your stage, enter them + in the "Manual Center Entry" fields above and click "Move to Position" to test. + + + Method 2: Manually move your stage to what you believe is the center position + using the joystick controls or other methods, then click "Use as Center" to save the current position. + + + Tip: You can combine both methods - move roughly to the center area, then fine-tune + with manual coordinate entry. + + + + + + + + + ); +}; + +export default StageCenterStep2; \ No newline at end of file diff --git a/src/components/wizard-steps/StageCenterStep3.js b/src/components/wizard-steps/StageCenterStep3.js new file mode 100644 index 0000000..26aecd2 --- /dev/null +++ b/src/components/wizard-steps/StageCenterStep3.js @@ -0,0 +1,216 @@ +import React, { useEffect } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { + Box, + Button, + Typography, + Paper, + Alert, + Grid, + TextField, + Card, + CardContent, +} from "@mui/material"; +import { + Map as MapIcon, + Refresh as RefreshIcon, +} from "@mui/icons-material"; +import * as stageCenterCalibrationSlice from "../../state/slices/StageCenterCalibrationSlice"; +import StageMapCanvas from "../StageMapCanvas"; +import LiveStreamTile from "../LiveStreamTile"; +import { useTheme } from '@mui/material/styles'; + +const StageCenterStep3 = ({ hostIP, hostPort, onNext, onBack, activeStep, totalSteps }) => { + const dispatch = useDispatch(); + const theme = useTheme(); + const stageCenterState = useSelector(stageCenterCalibrationSlice.getStageCenterCalibrationState); + + const { + currentX, + currentY, + isLoading, + error, + successMessage + } = stageCenterState; + + // Fetch current position on component mount + useEffect(() => { + fetchCurrentPosition(); + const interval = setInterval(fetchCurrentPosition, 2000); // Update every 2 seconds + return () => clearInterval(interval); + }, []); + + const fetchCurrentPosition = async () => { + try { + const response = await fetch(`${hostIP}:${hostPort}/PositionerController/getPositionerPositions`); + const data = await response.json(); + + if (data.ESP32Stage) { + dispatch(stageCenterCalibrationSlice.setCurrentPosition({ + x: data.ESP32Stage.X, + y: data.ESP32Stage.Y + })); + } else if (data.VirtualStage) { + dispatch(stageCenterCalibrationSlice.setCurrentPosition({ + x: data.VirtualStage.X, + y: data.VirtualStage.Y + })); + } + } catch (error) { + console.error("Error fetching position:", error); + } + }; + + const handleMapWidthChange = (event, newValue) => { + // Map dimension changes are handled by the WellSelectorCanvas internally + }; + + const handleMapHeightChange = (event, newValue) => { + // Map dimension changes are handled by the WellSelectorCanvas internally + }; + + const clearMessages = () => { + dispatch(stageCenterCalibrationSlice.clearMessages()); + }; + + return ( + + {/* Live Stream Tile - positioned in top right */} + + + + + + + Step 3: Stage Map Visualization + + Use the interactive stage map to visualize your current position and navigate to different areas. + Click anywhere on the map to move the stage to that location. The live stream shows your current view. + + + {error && ( + + {error} + + )} + + {successMessage && ( + + {successMessage} + + )} + + + {/* Stage Map - taking full width */} + + + + + + Interactive Stage Map + + + + + + + + {/* Current Position and Controls */} + + + + + Current Position + + + + + + + + + + + + + + + + + + + + How to Use the Stage Map + + + Navigation: Click anywhere on the grid to move the stage to that position. + The current position is shown in real-time and updates automatically. + + + Map Controls: Use mouse wheel to zoom in/out and drag to pan the view. + The map shows the coordinate system and allows precise positioning. + + + Live Stream: The camera feed on the top right shows what you're currently viewing. + This helps you navigate to find bright spots or specific features. + + + + + + + + + ); +}; + +export default StageCenterStep3; \ No newline at end of file diff --git a/src/components/wizard-steps/StageCenterStep4.js b/src/components/wizard-steps/StageCenterStep4.js new file mode 100644 index 0000000..b5451e7 --- /dev/null +++ b/src/components/wizard-steps/StageCenterStep4.js @@ -0,0 +1,423 @@ +import React, { useState, useEffect } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { + Box, + Button, + Typography, + Paper, + Alert, + Grid, + TextField, + Card, + CardContent, + LinearProgress, + Chip, + Accordion, + AccordionSummary, + AccordionDetails, +} from "@mui/material"; +import { + Search as SearchIcon, + PlayArrow as PlayIcon, + Stop as StopIcon, + Settings as SettingsIcon, + ExpandMore as ExpandMoreIcon, + Brightness7 as BrightnessIcon, + Speed as SpeedIcon, +} from "@mui/icons-material"; +import * as stageCenterCalibrationSlice from "../../state/slices/StageCenterCalibrationSlice"; +import apiStageCenterCalibrationPerformCalibration from "../../backendapi/apiStageCenterCalibrationPerformCalibration"; +import apiStageCenterCalibrationGetStatus from "../../backendapi/apiStageCenterCalibrationGetStatus"; +import apiStageCenterCalibrationStopCalibration from "../../backendapi/apiStageCenterCalibrationStopCalibration"; +import LiveStreamTile from "../LiveStreamTile"; +import { useTheme } from '@mui/material/styles'; + +const StageCenterStep4 = ({ hostIP, hostPort, onNext, onBack, activeStep, totalSteps }) => { + const dispatch = useDispatch(); + const theme = useTheme(); + const stageCenterState = useSelector(stageCenterCalibrationSlice.getStageCenterCalibrationState); + + const { + startX, + startY, + exposureTimeUs, + speed, + stepUm, + maxRadiusUm, + brightnessFactor, + isCalibrationRunning, + calibrationResults, + currentX, + currentY, + isLoading, + error, + successMessage + } = stageCenterState; + + const [statusCheckInterval, setStatusCheckInterval] = useState(null); + + // Check calibration status periodically when running + useEffect(() => { + if (isCalibrationRunning) { + const interval = setInterval(checkCalibrationStatus, 1000); + setStatusCheckInterval(interval); + return () => clearInterval(interval); + } else if (statusCheckInterval) { + clearInterval(statusCheckInterval); + setStatusCheckInterval(null); + } + }, [isCalibrationRunning]); + + const checkCalibrationStatus = async () => { + try { + const isRunning = await apiStageCenterCalibrationGetStatus(); + dispatch(stageCenterCalibrationSlice.setIsCalibrationRunning(isRunning)); + + if (!isRunning && calibrationResults.length === 0) { + // Calibration finished, might need to fetch results + dispatch(stageCenterCalibrationSlice.setSuccessMessage("Calibration completed")); + } + } catch (error) { + console.error("Error checking calibration status:", error); + } + }; + + const startCalibration = async () => { + dispatch(stageCenterCalibrationSlice.setIsLoading(true)); + dispatch(stageCenterCalibrationSlice.clearMessages()); + + try { + const results = await apiStageCenterCalibrationPerformCalibration({ + start_x: startX, + start_y: startY, + exposure_time_us: exposureTimeUs, + speed: speed, + step_um: stepUm, + max_radius_um: maxRadiusUm, + brightness_factor: brightnessFactor, + }); + + dispatch(stageCenterCalibrationSlice.setIsCalibrationRunning(true)); + dispatch(stageCenterCalibrationSlice.setCalibrationResults(results)); + + if (results && results.length > 0) { + // Use the last position as the found center (this is where the bright spot was found) + const lastPos = results[results.length - 1]; + dispatch(stageCenterCalibrationSlice.setFoundCenter({ + x: lastPos[0], + y: lastPos[1] + })); + dispatch(stageCenterCalibrationSlice.setSuccessMessage( + `Bright spot found at (${lastPos[0].toFixed(1)}, ${lastPos[1].toFixed(1)})` + )); + } + } catch (error) { + console.error("Error starting calibration:", error); + dispatch(stageCenterCalibrationSlice.setError("Failed to start automatic calibration")); + } finally { + dispatch(stageCenterCalibrationSlice.setIsLoading(false)); + } + }; + + const stopCalibration = async () => { + try { + await apiStageCenterCalibrationStopCalibration(); + dispatch(stageCenterCalibrationSlice.setIsCalibrationRunning(false)); + dispatch(stageCenterCalibrationSlice.setSuccessMessage("Calibration stopped")); + } catch (error) { + console.error("Error stopping calibration:", error); + dispatch(stageCenterCalibrationSlice.setError("Failed to stop calibration")); + } + }; + + const useCurrentAsStart = () => { + dispatch(stageCenterCalibrationSlice.setStartX(currentX)); + dispatch(stageCenterCalibrationSlice.setStartY(currentY)); + dispatch(stageCenterCalibrationSlice.setSuccessMessage("Current position set as start point")); + }; + + const clearMessages = () => { + dispatch(stageCenterCalibrationSlice.clearMessages()); + }; + + return ( + + {/* Live Stream Tile - positioned in top right */} + + + + + + + Step 4: Automatic Bright Spot Detection + + Configure and run an automated spiral scan to find bright spots on your stage. The system will + scan in an expanding spiral pattern until it finds an area with increased brightness. Monitor the live stream. + + + {error && ( + + {error} + + )} + + {successMessage && ( + + {successMessage} + + )} + + {isCalibrationRunning && ( + + + + Calibration in Progress + + Scanning stage for bright spots... This may take several minutes. + + + + + + + )} + + + {/* Control Panel */} + + + + + + Calibration Control + + + + + dispatch(stageCenterCalibrationSlice.setStartX(parseFloat(e.target.value) || 0))} + fullWidth + type="number" + size="small" + /> + + + dispatch(stageCenterCalibrationSlice.setStartY(parseFloat(e.target.value) || 0))} + fullWidth + type="number" + size="small" + /> + + + + + + + + + + + + {/* Results */} + {calibrationResults.length > 0 && ( + + + + Detection Results + + + Positions scanned: {calibrationResults.length} + + {calibrationResults.length > 0 && ( + + + + )} + + + )} + + + {/* Advanced Settings */} + + + + + + Current Position + + + + + + + + + + + + + }> + Advanced Parameters + + + + + dispatch(stageCenterCalibrationSlice.setExposureTimeUs(parseInt(e.target.value) || 3000))} + fullWidth + type="number" + size="small" + InputProps={{ + startAdornment: , + }} + /> + + + dispatch(stageCenterCalibrationSlice.setSpeed(parseInt(e.target.value) || 5000))} + fullWidth + type="number" + size="small" + InputProps={{ + startAdornment: , + }} + /> + + + dispatch(stageCenterCalibrationSlice.setStepUm(parseFloat(e.target.value) || 50))} + fullWidth + type="number" + size="small" + /> + + + dispatch(stageCenterCalibrationSlice.setMaxRadiusUm(parseFloat(e.target.value) || 2000))} + fullWidth + type="number" + size="small" + /> + + + dispatch(stageCenterCalibrationSlice.setBrightnessFactor(parseFloat(e.target.value) || 1.4))} + fullWidth + type="number" + size="small" + helperText="Detection threshold (e.g., 1.4 = 40% brighter than baseline)" + /> + + + + + + + + + + + + How Automatic Detection Works + + + Spiral Scan: The system moves the stage in an expanding spiral pattern, + starting from your specified start position. + + + Brightness Detection: At each position, it captures an image and measures + the mean brightness. When the brightness increases by the specified factor, it stops. + + + Parameters: Adjust the step size for scan resolution, max radius for scan area, + and brightness factor for detection sensitivity. + + + + + + + + + ); +}; + +export default StageCenterStep4; \ No newline at end of file diff --git a/src/components/wizard-steps/StageCenterStep5.js b/src/components/wizard-steps/StageCenterStep5.js new file mode 100644 index 0000000..dc0bdf6 --- /dev/null +++ b/src/components/wizard-steps/StageCenterStep5.js @@ -0,0 +1,372 @@ +import React from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { + Box, + Button, + Typography, + Paper, + Alert, + Grid, + Card, + CardContent, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Chip, + Divider, +} from "@mui/material"; +import { + CheckCircle as CheckIcon, + Assessment as AssessmentIcon, + CenterFocusStrong as CenterIcon, + Save as SaveIcon, +} from "@mui/icons-material"; +import { useTheme } from '@mui/material/styles'; +import * as stageCenterCalibrationSlice from "../../state/slices/StageCenterCalibrationSlice"; +import StageMapCanvas from "../StageMapCanvas"; +import LiveStreamTile from "../LiveStreamTile"; + +const StageCenterStep5 = ({ hostIP, hostPort, onNext, onBack, activeStep, totalSteps }) => { + const dispatch = useDispatch(); + const theme = useTheme(); + const stageCenterState = useSelector(stageCenterCalibrationSlice.getStageCenterCalibrationState); + + const { + currentX, + currentY, + manualCenterX, + manualCenterY, + foundCenterX, + foundCenterY, + calibrationResults, + startX, + startY, + stepUm, + maxRadiusUm, + brightnessFactor, + error, + successMessage + } = stageCenterState; + + const hasManualCenter = manualCenterX && manualCenterY; + const hasFoundCenter = foundCenterX !== null && foundCenterY !== null; + const hasCalibrationData = calibrationResults.length > 0; + + const calculateDistance = (x1, y1, x2, y2) => { + return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); + }; + + const applyManualCenter = async () => { + if (!hasManualCenter) return; + + try { + const response = await fetch( + `${hostIP}:${hostPort}/PositionerController/setStageOffsetAxis?knownPosition=${manualCenterX}¤tPosition=0&axis=X` + ); + await response.json(); + + const response2 = await fetch( + `${hostIP}:${hostPort}/PositionerController/setStageOffsetAxis?knownPosition=${manualCenterY}¤tPosition=0&axis=Y` + ); + await response2.json(); + + dispatch(stageCenterCalibrationSlice.setSuccessMessage("Manual center position applied successfully")); + } catch (error) { + console.error("Error applying manual center:", error); + dispatch(stageCenterCalibrationSlice.setError("Failed to apply manual center position")); + } + }; + + const applyFoundCenter = async () => { + if (!hasFoundCenter) return; + + try { + const response = await fetch( + `${hostIP}:${hostPort}/PositionerController/setStageOffsetAxis?knownPosition=${foundCenterX}¤tPosition=0&axis=X` + ); + await response.json(); + + const response2 = await fetch( + `${hostIP}:${hostPort}/PositionerController/setStageOffsetAxis?knownPosition=${foundCenterY}¤tPosition=0&axis=Y` + ); + await response2.json(); + + dispatch(stageCenterCalibrationSlice.setSuccessMessage("Found center position applied successfully")); + } catch (error) { + console.error("Error applying found center:", error); + dispatch(stageCenterCalibrationSlice.setError("Failed to apply found center position")); + } + }; + + const clearMessages = () => { + dispatch(stageCenterCalibrationSlice.clearMessages()); + }; + + return ( + + {/* Live Stream Tile - positioned in top right */} + + + + + + + Step 5: Review Calibration Results + + Review the calibration results from manual entry and automatic detection. Choose which + center position to apply or proceed to the final step. Check the live stream for verification. + + + {error && ( + + {error} + + )} + + {successMessage && ( + + {successMessage} + + )} + + + {/* Results Summary */} + + + + + + Calibration Summary + + + + {/* Current Position */} + + + + Current Stage Position + + + X: {currentX.toFixed(2)} μm, Y: {currentY.toFixed(2)} μm + + + + + {/* Manual Center */} + {hasManualCenter && ( + + + + + Manual Center Position + + + + + X: {parseFloat(manualCenterX).toFixed(2)} μm, Y: {parseFloat(manualCenterY).toFixed(2)} μm + + + Distance from current: {calculateDistance( + currentX, currentY, + parseFloat(manualCenterX), parseFloat(manualCenterY) + ).toFixed(1)} μm + + + + + + + )} + + {/* Found Center */} + {hasFoundCenter && ( + + + + + Auto-Detected Center + + + + + X: {foundCenterX.toFixed(2)} μm, Y: {foundCenterY.toFixed(2)} μm + + + Distance from current: {calculateDistance( + currentX, currentY, foundCenterX, foundCenterY + ).toFixed(1)} μm + + + + + + + )} + + {/* Comparison */} + {hasManualCenter && hasFoundCenter && ( + + + + Center Comparison + + + Distance between centers: {calculateDistance( + parseFloat(manualCenterX), parseFloat(manualCenterY), + foundCenterX, foundCenterY + ).toFixed(1)} μm + + + + )} + + + + + {/* Calibration Parameters */} + {hasCalibrationData && ( + + + + Detection Parameters + + + + + + Start Position + ({startX.toFixed(1)}, {startY.toFixed(1)}) μm + + + Step Size + {stepUm} μm + + + Max Radius + {maxRadiusUm} μm + + + Brightness Factor + {brightnessFactor}x + + + Positions Scanned + {calibrationResults.length} + + +
+
+
+
+ )} +
+ + {/* Visual Results */} + + + + + + Visual Results + + + + + + +
+ + {/* Status Messages */} + {!hasManualCenter && !hasFoundCenter && ( + + No calibration results available. Please go back and perform manual entry or automatic detection. + + )} + + {(hasManualCenter || hasFoundCenter) && ( + + + + Calibration Results Available + + + You can apply one of the found center positions or proceed to the final step. + {hasManualCenter && hasFoundCenter && + " Compare the manual and automatic results to choose the most accurate one." + } + + + )} + + + + + + Recommendation + + + If both methods found centers: Compare the positions and consider the + accuracy of your manual positioning versus the reliability of the automatic detection. + + + For automatic results: The system found the brightest spot within the + search area. This is typically a reliable indicator of a sample or reference point. + + + For manual results: Use this if you have precise knowledge of where + the center should be or if automatic detection failed to find the correct spot. + + + + + + + +
+ ); +}; + +export default StageCenterStep5; \ No newline at end of file diff --git a/src/components/wizard-steps/StageCenterStep6.js b/src/components/wizard-steps/StageCenterStep6.js new file mode 100644 index 0000000..250bfab --- /dev/null +++ b/src/components/wizard-steps/StageCenterStep6.js @@ -0,0 +1,270 @@ +import React from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { + Box, + Button, + Typography, + Paper, + Alert, + Grid, + Card, + CardContent, + List, + ListItem, + ListItemIcon, + ListItemText, + Divider, +} from "@mui/material"; +import { + CheckCircle as CheckIcon, + Celebration as CelebrationIcon, + Refresh as RefreshIcon, + Home as HomeIcon, + Save as SaveIcon, +} from "@mui/icons-material"; +import * as stageCenterCalibrationSlice from "../../state/slices/StageCenterCalibrationSlice"; +import LiveStreamTile from "../LiveStreamTile"; +import { useTheme } from '@mui/material/styles'; + +const StageCenterStep6 = ({ hostIP, hostPort, onComplete, activeStep, totalSteps }) => { + const dispatch = useDispatch(); + const theme = useTheme(); + const stageCenterState = useSelector(stageCenterCalibrationSlice.getStageCenterCalibrationState); + + const { + currentX, + currentY, + manualCenterX, + manualCenterY, + foundCenterX, + foundCenterY, + calibrationResults, + } = stageCenterState; + + const hasManualCenter = manualCenterX && manualCenterY; + const hasFoundCenter = foundCenterX !== null && foundCenterY !== null; + const hasCalibrationData = calibrationResults.length > 0; + + const resetCalibration = () => { + dispatch(stageCenterCalibrationSlice.resetCalibrationResults()); + dispatch(stageCenterCalibrationSlice.setActiveStep(0)); + }; + + const completeAndClose = () => { + dispatch(stageCenterCalibrationSlice.resetWizard()); + onComplete(); + }; + + const saveCalibrationSummary = () => { + const summary = { + timestamp: new Date().toISOString(), + currentPosition: { x: currentX, y: currentY }, + manualCenter: hasManualCenter ? { x: parseFloat(manualCenterX), y: parseFloat(manualCenterY) } : null, + foundCenter: hasFoundCenter ? { x: foundCenterX, y: foundCenterY } : null, + scanResults: calibrationResults, + scanCount: calibrationResults.length, + }; + + const dataStr = JSON.stringify(summary, null, 2); + const dataBlob = new Blob([dataStr], { type: "application/json" }); + const url = URL.createObjectURL(dataBlob); + const link = document.createElement("a"); + link.href = url; + link.download = `stage_center_calibration_${new Date().toISOString().split('T')[0]}.json`; + link.click(); + URL.revokeObjectURL(url); + }; + + return ( + + {/* Live Stream Tile - positioned in top right */} + + + + + + + + Stage Center Calibration Complete! + + Your stage center calibration has been completed successfully. Review the summary below + and choose your next steps. The live stream shows your final position. + + + + {/* Calibration Summary */} + + + + + Calibration Summary + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Next Steps */} + + + + + What's Next? + + + + Your calibration data has been collected and is available for use. You can: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Action Buttons */} + + + + + + + + + + + + + + + + + Tips for Future Use + + + Accuracy: The automatic detection works best when there are clear, bright + features on your stage. For subtle samples, manual positioning might be more accurate. + + + Repeatability: Save your calibration parameters for future use. The same + scan settings can be applied to similar samples. + + + Validation: Always verify that the found center position makes sense for + your specific setup and sample type before applying it to important experiments. + + + + + + Thank you for using the Stage Center Calibration Wizard! +
+ Your feedback helps us improve the calibration process. +
+
+
+ ); +}; + +export default StageCenterStep6; \ No newline at end of file diff --git a/src/state/slices/StageCenterCalibrationSlice.js b/src/state/slices/StageCenterCalibrationSlice.js new file mode 100644 index 0000000..2011fdf --- /dev/null +++ b/src/state/slices/StageCenterCalibrationSlice.js @@ -0,0 +1,213 @@ +// src/state/slices/StageCenterCalibrationSlice.js +import { createSlice } from "@reduxjs/toolkit"; + +const initialState = { + // Wizard state + isWizardOpen: false, + activeStep: 0, + + // Calibration parameters + startX: 0, + startY: 0, + exposureTimeUs: 3000, + speed: 5000, + stepUm: 50.0, + maxRadiusUm: 2000.0, + brightnessFactor: 1.4, + + // Current position + currentX: 0, + currentY: 0, + + // Calibration state + isCalibrationRunning: false, + calibrationResults: [], + foundCenterX: null, + foundCenterY: null, + + // Stage map state + stageMapWidth: 5000, // in micrometers + stageMapHeight: 5000, + stageMapCenterX: 0, + stageMapCenterY: 0, + + // Manual entry state + manualCenterX: "", + manualCenterY: "", + + // UI state + isLoading: false, + error: null, + successMessage: null, +}; + +const stageCenterCalibrationSlice = createSlice({ + name: "stageCenterCalibration", + initialState, + reducers: { + // Wizard control + setWizardOpen: (state, action) => { + state.isWizardOpen = action.payload; + if (!action.payload) { + state.activeStep = 0; // Reset to first step when closing + } + }, + setActiveStep: (state, action) => { + state.activeStep = action.payload; + }, + nextStep: (state) => { + state.activeStep = Math.min(state.activeStep + 1, 5); // 6 steps total (0-5) + }, + previousStep: (state) => { + state.activeStep = Math.max(state.activeStep - 1, 0); + }, + + // Calibration parameters + setStartX: (state, action) => { + state.startX = action.payload; + }, + setStartY: (state, action) => { + state.startY = action.payload; + }, + setExposureTimeUs: (state, action) => { + state.exposureTimeUs = action.payload; + }, + setSpeed: (state, action) => { + state.speed = action.payload; + }, + setStepUm: (state, action) => { + state.stepUm = action.payload; + }, + setMaxRadiusUm: (state, action) => { + state.maxRadiusUm = action.payload; + }, + setBrightnessFactor: (state, action) => { + state.brightnessFactor = action.payload; + }, + setCalibrationParameters: (state, action) => { + const { startX, startY, exposureTimeUs, speed, stepUm, maxRadiusUm, brightnessFactor } = action.payload; + if (startX !== undefined) state.startX = startX; + if (startY !== undefined) state.startY = startY; + if (exposureTimeUs !== undefined) state.exposureTimeUs = exposureTimeUs; + if (speed !== undefined) state.speed = speed; + if (stepUm !== undefined) state.stepUm = stepUm; + if (maxRadiusUm !== undefined) state.maxRadiusUm = maxRadiusUm; + if (brightnessFactor !== undefined) state.brightnessFactor = brightnessFactor; + }, + + // Current position + setCurrentX: (state, action) => { + state.currentX = action.payload; + }, + setCurrentY: (state, action) => { + state.currentY = action.payload; + }, + setCurrentPosition: (state, action) => { + const { x, y } = action.payload; + if (x !== undefined) state.currentX = x; + if (y !== undefined) state.currentY = y; + }, + + // Calibration state + setIsCalibrationRunning: (state, action) => { + state.isCalibrationRunning = action.payload; + }, + setCalibrationResults: (state, action) => { + state.calibrationResults = action.payload; + }, + setFoundCenter: (state, action) => { + const { x, y } = action.payload; + state.foundCenterX = x; + state.foundCenterY = y; + }, + + // Stage map state + setStageMapDimensions: (state, action) => { + const { width, height } = action.payload; + if (width !== undefined) state.stageMapWidth = width; + if (height !== undefined) state.stageMapHeight = height; + }, + setStageMapCenter: (state, action) => { + const { x, y } = action.payload; + if (x !== undefined) state.stageMapCenterX = x; + if (y !== undefined) state.stageMapCenterY = y; + }, + + // Manual entry state + setManualCenterX: (state, action) => { + state.manualCenterX = action.payload; + }, + setManualCenterY: (state, action) => { + state.manualCenterY = action.payload; + }, + setManualCenter: (state, action) => { + const { x, y } = action.payload; + if (x !== undefined) state.manualCenterX = x; + if (y !== undefined) state.manualCenterY = y; + }, + + // UI state + setIsLoading: (state, action) => { + state.isLoading = action.payload; + }, + setError: (state, action) => { + state.error = action.payload; + }, + setSuccessMessage: (state, action) => { + state.successMessage = action.payload; + }, + clearMessages: (state) => { + state.error = null; + state.successMessage = null; + }, + + // Reset actions + resetCalibrationResults: (state) => { + state.calibrationResults = []; + state.foundCenterX = null; + state.foundCenterY = null; + }, + resetWizard: (state) => { + return initialState; + }, + }, +}); + +// Export actions +export const { + setWizardOpen, + setActiveStep, + nextStep, + previousStep, + setStartX, + setStartY, + setExposureTimeUs, + setSpeed, + setStepUm, + setMaxRadiusUm, + setBrightnessFactor, + setCalibrationParameters, + setCurrentX, + setCurrentY, + setCurrentPosition, + setIsCalibrationRunning, + setCalibrationResults, + setFoundCenter, + setStageMapDimensions, + setStageMapCenter, + setManualCenterX, + setManualCenterY, + setManualCenter, + setIsLoading, + setError, + setSuccessMessage, + clearMessages, + resetCalibrationResults, + resetWizard, +} = stageCenterCalibrationSlice.actions; + +// Export selector +export const getStageCenterCalibrationState = (state) => state.stageCenterCalibration; + +// Export reducer +export default stageCenterCalibrationSlice.reducer; \ No newline at end of file diff --git a/src/state/store.js b/src/state/store.js index dc5c0ba..40a434f 100644 --- a/src/state/store.js +++ b/src/state/store.js @@ -24,11 +24,12 @@ import histoScanReducer from "./slices/HistoScanSlice"; import widgetReducer from "./slices/WidgetSlice"; import lepmonReducer from "./slices/LepmonSlice"; import uc2Reducer from "./slices/UC2Slice"; -import stageOffsetCalibrationReducer from "./slices/StageOffsetCalibrationSlice"; -import flowStopReducer from "./slices/FlowStopSlice"; -import lightsheetReducer from "./slices/LightsheetSlice"; -import zarrinitialZarrReducer from "./slices/OmeZarrTileStreamSlice"; -import stresstestReducer from "./slices/StresstestSlice"; +import stageOffsetCalibrationReducer from "./slices/StageOffsetCalibrationSlice"; +import stageCenterCalibrationReducer from "./slices/StageCenterCalibrationSlice"; +import flowStopReducer from "./slices/FlowStopSlice"; +import lightsheetReducer from "./slices/LightsheetSlice"; +import zarrinitialZarrReducer from "./slices/OmeZarrTileStreamSlice"; +import stresstestReducer from "./slices/StresstestSlice"; import workflowReducer from "./slices/WorkflowSlice"; import stormReducer from "./slices/STORMSlice"; import focusLockReducer from "./slices/FocusLockSlice"; @@ -56,8 +57,9 @@ const rootReducer = combineReducers({ histoScanState: histoScanReducer, widgetState: widgetReducer, lepmon: lepmonReducer, - uc2State: uc2Reducer, - stageOffsetCalibration: stageOffsetCalibrationReducer, + uc2State: uc2Reducer, + stageOffsetCalibration: stageOffsetCalibrationReducer, + stageCenterCalibration: stageCenterCalibrationReducer, flowStop: flowStopReducer, lightsheet: lightsheetReducer, omeZarrState: zarrinitialZarrReducer,