Skip to content

Commit ba33cf2

Browse files
Fix Firmware Flashing for merged binaries via USB (#246)
This pull request introduces significant enhancements to the autofocus functionality in the frontend, adding support for a new hill-climbing autofocus method, expanding live focus monitoring capabilities, and improving the user interface for configuring autofocus parameters. It also includes small updates to documentation and backend API utilities. **Autofocus Method Enhancements:** * Added support for hill-climbing autofocus, including UI for selecting the method, configuring its parameters (initial step, min step, reduction factor, max iterations), and backend API integration (`frontend/src/components/AutofocusController.js`, `frontend/src/axon/experiment-designer/ZFocusDimension.js`, `frontend/src/backendapi/apiAutofocusControllerHillClimbing.js`). [[1]](diffhunk://#diff-c3de947737de070529a7c4962892c79fe159cfc6329c99af1f87600bf0398199L163-R188) [[2]](diffhunk://#diff-c3de947737de070529a7c4962892c79fe159cfc6329c99af1f87600bf0398199R55-R59) [[3]](diffhunk://#diff-45e9e9a07fe68dc37fa41fbbc4fae253b9599cd23bdee02c65292d4d81cfe513L358-R388) [[4]](diffhunk://#diff-45e9e9a07fe68dc37fa41fbbc4fae253b9599cd23bdee02c65292d4d81cfe513R532-R552) [[5]](diffhunk://#diff-45e9e9a07fe68dc37fa41fbbc4fae253b9599cd23bdee02c65292d4d81cfe513R565-R607) [[6]](diffhunk://#diff-5fe7141615a2256c23b61476b37cbc704ba4ede629a4fabdf4b0a4bd80aa8a2dR1-R43) * The autofocus parameter UI now conditionally displays controls for either Z-sweep or hill-climbing, and provides tooltips and explanations for each method (`frontend/src/axon/experiment-designer/ZFocusDimension.js`). [[1]](diffhunk://#diff-45e9e9a07fe68dc37fa41fbbc4fae253b9599cd23bdee02c65292d4d81cfe513L358-R388) [[2]](diffhunk://#diff-45e9e9a07fe68dc37fa41fbbc4fae253b9599cd23bdee02c65292d4d81cfe513R491) [[3]](diffhunk://#diff-45e9e9a07fe68dc37fa41fbbc4fae253b9599cd23bdee02c65292d4d81cfe513L475-L492) [[4]](diffhunk://#diff-45e9e9a07fe68dc37fa41fbbc4fae253b9599cd23bdee02c65292d4d81cfe513R532-R552) [[5]](diffhunk://#diff-45e9e9a07fe68dc37fa41fbbc4fae253b9599cd23bdee02c65292d4d81cfe513R565-R607) **Live Focus Monitoring Improvements:** * Introduced a live focus monitoring section with configurable update period, focus method, and crop size, as well as a rolling plot of the last 20 focus values for real-time feedback (`frontend/src/components/AutofocusController.js`). [[1]](diffhunk://#diff-c3de947737de070529a7c4962892c79fe159cfc6329c99af1f87600bf0398199R38-R39) [[2]](diffhunk://#diff-c3de947737de070529a7c4962892c79fe159cfc6329c99af1f87600bf0398199R157-R166) [[3]](diffhunk://#diff-c3de947737de070529a7c4962892c79fe159cfc6329c99af1f87600bf0398199L263-R419) **Point List Editor Usability:** * Added a "Set Z" button in the Point List Editor to quickly set a point's Z coordinate to the current stage position, improving workflow efficiency (`frontend/src/axon/PointListEditorComponent.js`). [[1]](diffhunk://#diff-ee91e602319fa9e01c59ef542d6ebeea361bc859b06e8c47ec7a6440e5068c14R10) [[2]](diffhunk://#diff-ee91e602319fa9e01c59ef542d6ebeea361bc859b06e8c47ec7a6440e5068c14R44) [[3]](diffhunk://#diff-ee91e602319fa9e01c59ef542d6ebeea361bc859b06e8c47ec7a6440e5068c14R85-R90) [[4]](diffhunk://#diff-ee91e602319fa9e01c59ef542d6ebeea361bc859b06e8c47ec7a6440e5068c14L447-R465) **API Utilities:** * Added a new backend API utility for probing device state, useful for verifying firmware after flashing (`frontend/src/backendapi/apiUC2ConfigControllerProbeDeviceState.js`). **Documentation:** * Updated installation instructions in `README.md` to clarify the use of `uv sync` and provide a recommended workflow. --------- Co-authored-by: Florian Paproth <florian@paproth.biz>
1 parent 76deb20 commit ba33cf2

26 files changed

+1945
-476
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ cd ImSwitch
2626

2727
# Create a virtual environment and install ImSwitch with UV
2828
uv venv
29+
uv sync
30+
31+
# then start it in headless mode with the API server:
32+
uv run python main.py --headless --http-port 8001
33+
```
34+
35+
# Alternative installation with uv pip (not recommended, may cause dependency issues):
36+
37+
```bash
2938
source .venv/bin/activate
3039
# Yes, we need to start using `uv sync` instead for reproducible installs with our lockfile...but we're still ignoring the lockfile right now:
3140
uv pip install -e .[dev]
@@ -130,12 +139,15 @@ uv venv --python /usr/local/bin/python3.11 .venv_x86_311
130139

131140
# Verify architecture
132141
arch -x86_64 .venv_x86_311/bin/python3.11 -c "import platform; print(platform.machine())"
142+
uv run main.py --venv .venv_x86_311 --headless --http-port 8001
143+
```
133144
# → x86_64
134145

135146
source .venv_x86_311/bin/activate
136147
uv pip install -e .
137148

138149
# Always launch under x86_64 so Rosetta uses the x86_64 dylibs
150+
source .venv_x86_311/bin/activate
139151
arch -x86_64 python main.py --headless --http-port 8001
140152
```
141153

frontend/src/axon/PointListEditorComponent.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import GenericTabBar from "./GenericTabBar";
77
//import { FixedSizeList as List } from "react-window";
88

99
import * as experimentSlice from "../state/slices/ExperimentSlice";
10+
import * as positionSlice from "../state/slices/PositionSlice.js";
1011

1112
import apiPositionerControllerMovePositioner from "../backendapi/apiPositionerControllerMovePositioner.js";
1213

@@ -40,6 +41,7 @@ const PointListEditorComponent = () => {
4041

4142
// Access global Redux state
4243
const experimentState = useSelector(experimentSlice.getExperimentState);
44+
const positionState = useSelector(positionSlice.getPositionState);
4345
//console.log("PointListEditorComponent", experimentState);
4446
//console.log("PointListEditorComponent", experimentState.pointList);
4547

@@ -80,6 +82,12 @@ const PointListEditorComponent = () => {
8082
dispatch(experimentSlice.createPoint(newPoint));
8183
};
8284

85+
//##################################################################################
86+
const handleSetCurrentZ = (index) => {
87+
// Override the point's Z with the current stage Z position from Redux
88+
handlePointChanged(index, "z", positionState.z);
89+
};
90+
8391
//##################################################################################
8492
const handleDeleteAll = () => {
8593
// Update Redux state
@@ -444,7 +452,17 @@ const PointListEditorComponent = () => {
444452
</>
445453
)}
446454

447-
{/* dummy button*/}
455+
{/* Set current Z position */}
456+
{viewMode == ViewMode.POSITION && (
457+
<Button
458+
sx={{ padding: "0px" }}
459+
onClick={() => handleSetCurrentZ(index)}
460+
title="Set Z to current stage position"
461+
>
462+
Set Z
463+
</Button>
464+
)}
465+
{/* Goto button */}
448466
{viewMode != ViewMode.READONLY && (
449467
<Button
450468
sx={{ padding: "0px" }}

frontend/src/axon/experiment-designer/ZFocusDimension.js

Lines changed: 99 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -355,12 +355,37 @@ const ZFocusDimension = () => {
355355
value={parameterValue.autoFocusMode || "software"}
356356
onChange={(e) => dispatch(experimentSlice.setAutoFocusMode(e.target.value))}
357357
>
358-
<MenuItem value="software">Software (Z-Sweep)</MenuItem>
358+
<MenuItem value="software">Software</MenuItem>
359359
<MenuItem value="hardware">Hardware (FocusLock One-Shot)</MenuItem>
360360
</Select>
361361
</FormControl>
362362
</Box>
363363

364+
{/* Software autofocus method selector */}
365+
{parameterValue.autoFocusMode !== "hardware" && (
366+
<Box sx={{ mb: 2 }}>
367+
<Box sx={{ display: "flex", alignItems: "center", gap: 1, mb: 1 }}>
368+
<Typography variant="caption" sx={{ fontWeight: 500 }}>
369+
Software Method
370+
</Typography>
371+
<Tooltip title="Z-Sweep: scans through a range of Z positions and fits a Gaussian. Hill Climbing: iteratively searches for peak contrast by gradient ascent (faster, fewer images).">
372+
<IconButton size="small">
373+
<InfoIcon fontSize="small" />
374+
</IconButton>
375+
</Tooltip>
376+
</Box>
377+
<FormControl size="small" fullWidth>
378+
<Select
379+
value={parameterValue.autoFocusSoftwareMethod || "scan"}
380+
onChange={(e) => dispatch(experimentSlice.setAutoFocusSoftwareMethod(e.target.value))}
381+
>
382+
<MenuItem value="scan">Z-Sweep (Scan)</MenuItem>
383+
<MenuItem value="hillClimbing">Hill Climbing</MenuItem>
384+
</Select>
385+
</FormControl>
386+
</Box>
387+
)}
388+
364389
{/* Hardware autofocus specific parameters */}
365390
{parameterValue.autoFocusMode === "hardware" && (
366391
<>
@@ -463,6 +488,7 @@ const ZFocusDimension = () => {
463488
{/* Software autofocus parameters */}
464489
{parameterValue.autoFocusMode !== "hardware" && (
465490
<>
491+
{/* Common parameters (shared by scan and hill climbing) */}
466492
<TextField
467493
label="Settle Time (s)"
468494
type="number"
@@ -472,24 +498,6 @@ const ZFocusDimension = () => {
472498
inputProps={{ step: 0.01, min: 0, max: 10 }}
473499
/>
474500

475-
<TextField
476-
label="Range (±μm)"
477-
type="number"
478-
size="small"
479-
value={parameterValue.autoFocusRange || 100}
480-
onChange={(e) => dispatch(experimentSlice.setAutoFocusRange(Number(e.target.value)))}
481-
inputProps={{ step: 1, min: 1 }}
482-
/>
483-
484-
<TextField
485-
label="Resolution (μm)"
486-
type="number"
487-
size="small"
488-
value={parameterValue.autoFocusResolution || 10}
489-
onChange={(e) => dispatch(experimentSlice.setAutoFocusResolution(Number(e.target.value)))}
490-
inputProps={{ step: 0.1, min: 0.1 }}
491-
/>
492-
493501
<TextField
494502
label="Crop Size (px)"
495503
type="number"
@@ -521,16 +529,80 @@ const ZFocusDimension = () => {
521529
inputProps={{ step: 0.1, min: -100, max: 100 }}
522530
/>
523531

524-
<FormControlLabel
525-
control={
526-
<Switch
532+
{/* Scan-specific parameters */}
533+
{(parameterValue.autoFocusSoftwareMethod || "scan") === "scan" && (
534+
<>
535+
<TextField
536+
label="Range (±μm)"
537+
type="number"
527538
size="small"
528-
checked={parameterValue.autoFocusTwoStage || false}
529-
onChange={(e) => dispatch(experimentSlice.setAutoFocusTwoStage(e.target.checked))}
539+
value={parameterValue.autoFocusRange || 100}
540+
onChange={(e) => dispatch(experimentSlice.setAutoFocusRange(Number(e.target.value)))}
541+
inputProps={{ step: 1, min: 1 }}
530542
/>
531-
}
532-
label={<Typography variant="caption">Two-Stage Focus</Typography>}
533-
/>
543+
544+
<TextField
545+
label="Resolution (μm)"
546+
type="number"
547+
size="small"
548+
value={parameterValue.autoFocusResolution || 10}
549+
onChange={(e) => dispatch(experimentSlice.setAutoFocusResolution(Number(e.target.value)))}
550+
inputProps={{ step: 0.1, min: 0.1 }}
551+
/>
552+
553+
<FormControlLabel
554+
control={
555+
<Switch
556+
size="small"
557+
checked={parameterValue.autoFocusTwoStage || false}
558+
onChange={(e) => dispatch(experimentSlice.setAutoFocusTwoStage(e.target.checked))}
559+
/>
560+
}
561+
label={<Typography variant="caption">Two-Stage Focus</Typography>}
562+
/>
563+
</>
564+
)}
565+
566+
{/* Hill Climbing-specific parameters */}
567+
{(parameterValue.autoFocusSoftwareMethod) === "hillClimbing" && (
568+
<>
569+
<TextField
570+
label="Initial Step (μm)"
571+
type="number"
572+
size="small"
573+
value={parameterValue.autoFocusHillClimbingInitialStep ?? 20}
574+
onChange={(e) => dispatch(experimentSlice.setAutoFocusHillClimbingInitialStep(Number(e.target.value)))}
575+
inputProps={{ step: 1, min: 1, max: 200 }}
576+
/>
577+
578+
<TextField
579+
label="Min Step (μm)"
580+
type="number"
581+
size="small"
582+
value={parameterValue.autoFocusHillClimbingMinStep ?? 1}
583+
onChange={(e) => dispatch(experimentSlice.setAutoFocusHillClimbingMinStep(Number(e.target.value)))}
584+
inputProps={{ step: 0.1, min: 0.1, max: 50 }}
585+
/>
586+
587+
<TextField
588+
label="Step Reduction Factor"
589+
type="number"
590+
size="small"
591+
value={parameterValue.autoFocusHillClimbingStepReduction ?? 0.5}
592+
onChange={(e) => dispatch(experimentSlice.setAutoFocusHillClimbingStepReduction(Number(e.target.value)))}
593+
inputProps={{ step: 0.05, min: 0.1, max: 0.9 }}
594+
/>
595+
596+
<TextField
597+
label="Max Iterations"
598+
type="number"
599+
size="small"
600+
value={parameterValue.autoFocusHillClimbingMaxIterations ?? 50}
601+
onChange={(e) => dispatch(experimentSlice.setAutoFocusHillClimbingMaxIterations(Number(e.target.value)))}
602+
inputProps={{ step: 1, min: 5, max: 200 }}
603+
/>
604+
</>
605+
)}
534606
</>
535607
)}
536608
</Box>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// src/backendapi/apiAutofocusControllerHillClimbing.js
2+
import createAxiosInstance from "./createAxiosInstance";
3+
4+
/**
5+
* Run hill-climbing autofocus via AutofocusController.autoFocusHillClimbing.
6+
* Iteratively searches for peak contrast by moving in the gradient direction,
7+
* reversing and halving the step on decline, until convergence.
8+
*
9+
* @param {Object} params - Hill-climbing autofocus parameters
10+
* @param {number} [params.initial_step=20] - Starting step size (µm)
11+
* @param {number} [params.min_step=1] - Min step size / convergence criterion (µm)
12+
* @param {number} [params.step_reduction=0.5] - Factor to reduce step on reversal
13+
* @param {number} [params.max_iterations=50] - Max iterations safety limit
14+
* @param {number} [params.tSettle=0.1] - Settle time after each Z step (s)
15+
* @param {number} [params.nCropsize=2048] - Crop size for focus algorithm
16+
* @param {string} [params.focusAlgorithm="LAPE"] - Focus quality algorithm
17+
* @param {number} [params.nGauss=0] - Gaussian blur sigma
18+
* @param {number} [params.static_offset=0] - Static Z offset after focusing
19+
* @returns {Promise<Object>} { status: "started", centerZ, method } or error
20+
*/
21+
const apiAutofocusControllerHillClimbing = async (params = {}) => {
22+
const axiosInstance = createAxiosInstance();
23+
24+
const response = await axiosInstance.get(
25+
"/AutofocusController/autoFocusHillClimbing",
26+
{
27+
params: {
28+
initial_step: params.initial_step ?? 20,
29+
min_step: params.min_step ?? 1,
30+
step_reduction: params.step_reduction ?? 0.5,
31+
max_iterations: params.max_iterations ?? 50,
32+
tSettle: params.tSettle ?? 0.1,
33+
nCropsize: params.nCropsize ?? 2048,
34+
focusAlgorithm: params.focusAlgorithm ?? "LAPE",
35+
nGauss: params.nGauss ?? 0,
36+
static_offset: params.static_offset ?? 0,
37+
},
38+
}
39+
);
40+
return response.data;
41+
};
42+
43+
export default apiAutofocusControllerHillClimbing;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import createAxiosInstance from "./createAxiosInstance";
2+
3+
/**
4+
* Send {"task":"/state_get"} to a device and return the raw response.
5+
* Use this to verify that firmware is running correctly after flashing.
6+
* @param {string} port - Serial port device path (e.g. "/dev/ttyACM0")
7+
* @param {number} baud - Serial baudrate (115200 or 921600)
8+
* @param {number} timeout - Serial read timeout in seconds (default 2.0)
9+
* @returns {Promise<Object>} Result with status, state_response, and firmware_ok
10+
*/
11+
const apiUC2ConfigControllerProbeDeviceState = async (
12+
port,
13+
baud = 115200,
14+
timeout = 2.0
15+
) => {
16+
const axiosInstance = createAxiosInstance();
17+
const response = await axiosInstance.get(
18+
"/UC2ConfigController/probeDeviceState",
19+
{
20+
params: {
21+
port: port,
22+
baud: baud,
23+
timeout: timeout,
24+
},
25+
timeout: 15000, // 15 seconds
26+
}
27+
);
28+
return response.data;
29+
};
30+
31+
export default apiUC2ConfigControllerProbeDeviceState;

0 commit comments

Comments
 (0)