Files
Jungfraujoch/frontend/src/components/DetectorSettings.tsx
T
leonarski_fandClaude Opus 4.8 5d8e33d52c
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 11m26s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 12m45s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 14m0s
Build Packages / build:rpm (rocky8) (push) Successful in 14m3s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 14m16s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 14m46s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 15m46s
Build Packages / XDS test (durin plugin) (push) Successful in 8m3s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 9m51s
Build Packages / Generate python client (push) Successful in 22s
Build Packages / Create release (push) Skipped
Build Packages / build:rpm (rocky9) (push) Successful in 11m30s
Build Packages / Build documentation (push) Successful in 41s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 8m49s
Build Packages / XDS test (neggia plugin) (push) Successful in 7m53s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 9m54s
Build Packages / DIALS test (push) Successful in 11m46s
Build Packages / Unit tests (push) Successful in 59m17s
frontend: orange Upload button when panel is dirty
Match the unsaved-changes dot: the Upload button uses the warning color
while the panel has unedited changes, primary otherwise.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 22:14:10 +02:00

446 lines
20 KiB
TypeScript

import {ChangeEvent, memo, useEffect, useState} from 'react';
import {Checkbox, FormControlLabel, FormGroup, Grid, List, ListItem, Switch, Tooltip} from "@mui/material";
import Paper from "@mui/material/Paper";
import FormControl from "@mui/material/FormControl";
import InputLabel from "@mui/material/InputLabel";
import Select, {SelectChangeEvent} from "@mui/material/Select";
import MenuItem from "@mui/material/MenuItem";
import {detector_settings, detector_timing} from "../client";
import {putConfigDetectorMutation} from "../client/@tanstack/react-query.gen";
import NumberTextField from "./NumberTextField";
import _ from "lodash";
import ButtonWithSnackbar from "./ButtonWithSnackbar";
import Button from "@mui/material/Button";
import {PanelTitle, UploadSnackbarView} from "./SettingsPanel";
import {useUpload} from "./useUpload";
type MyProps = {
s?: detector_settings
}
function extractDepthForSelection(input: detector_settings) : string {
if (input.eiger_bit_depth === undefined)
return 'a';
else if (input.eiger_bit_depth === 16)
return '16';
else if (input.eiger_bit_depth === 32)
return '32';
else if (input.eiger_bit_depth === 8)
return '8';
else
return "";
}
function DetectorSettings({s: serverS}: MyProps) {
const [s, setS] = useState<detector_settings>({ frame_time_us: 1000 });
const [lastDownloadedS, setLastDownloadedS] = useState<detector_settings>({ frame_time_us: 1000 });
const [bitWidthSelection, setBitWidthSelection] = useState("a");
const [storageCellListValue, setStorageCellListValue] = useState("1");
const [timingModeListValue, setTimingModeListValue] = useState<detector_timing>(detector_timing.TRIGGER);
const [frameTimeErr, setFrameTimeErr] = useState(false);
const [countTimeErr, setCountTimeErr] = useState(false);
const [storageCellDelayErr, setStorageCellDelayErr] = useState(false);
const [detectorTriggerDelayErr, setDetectorTriggerDelayErr] = useState(false);
const [internalFrameGeneratorImagesErr, setInternalFrameGeneratorImagesErr] = useState(false);
const [pedestalG0FramesErr, setPedestalG0FramesErr] = useState(false);
const [pedestalG1FramesErr, setPedestalG1FramesErr] = useState(false);
const [pedestalG2FramesErr, setPedestalG2FramesErr] = useState(false);
const [pedestalMinImageCountErr, setPedestalMinImageCountErr] = useState(false);
const [eigerThresholdErr, setEigerThresholdErr] = useState(false);
const [eigerThresholdKeVOld, setEigerThresholdKeVOld] = useState(6.0);
const [downloadCounter, setDownloadCounter] = useState(0);
const { submit, pending, snackbar } = useUpload(putConfigDetectorMutation());
useEffect(() => {
if ((serverS !== undefined) && !_.isEqual(serverS, lastDownloadedS)) {
setDownloadCounter(c => c + 1);
setS(serverS);
setBitWidthSelection(extractDepthForSelection(serverS));
setLastDownloadedS(serverS);
setStorageCellListValue(String(serverS.jungfrau_storage_cell_count ?? "1"));
setTimingModeListValue(serverS.timing ?? detector_timing.TRIGGER);
setPedestalG0FramesErr(false);
setPedestalG1FramesErr(false);
setPedestalG2FramesErr(false);
setPedestalMinImageCountErr(false);
setStorageCellDelayErr(false);
setDetectorTriggerDelayErr(false);
setFrameTimeErr(false);
setCountTimeErr(false);
setInternalFrameGeneratorImagesErr(false);
setEigerThresholdErr(false);
setEigerThresholdKeVOld(6.0);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [serverS]);
const eigerThresholdToggle = (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.checked)
setS(prev => ({...prev, eiger_threshold_keV: eigerThresholdKeVOld}));
else
setS(prev => ({...prev, eiger_threshold_keV: undefined}));
};
const countTimeToggle = (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.checked)
setS(prev => ({...prev, count_time_us: prev.frame_time_us - 20}));
else
setS(prev => ({...prev, count_time_us: undefined}));
};
const internalFrameGeneratorToggle = (event: ChangeEvent<HTMLInputElement>) => {
setS(prev => ({...prev, internal_frame_generator: event.target.checked}));
};
const useGainHG0Toggle = (event: ChangeEvent<HTMLInputElement>) => {
setS(prev => ({...prev, jungfrau_use_gain_hg0: event.target.checked}));
};
const fixedGainG1Toggle = (event: ChangeEvent<HTMLInputElement>) => {
setS(prev => ({...prev, jungfrau_fixed_gain_g1: event.target.checked}));
};
const handleBitWidthChange = (event: SelectChangeEvent) => {
let val: detector_settings['eiger_bit_depth'] = undefined;
if (event.target.value === "a")
val = undefined;
else if (event.target.value === "8")
val = 8;
else if (event.target.value === "16")
val = 16;
else if (event.target.value === "32")
val = 32;
setBitWidthSelection(event.target.value);
setS(prev => ({...prev, eiger_bit_depth: val}));
};
const handleTimingChange = (event : SelectChangeEvent<detector_timing>) => {
let d : detector_timing = detector_timing.TRIGGER;
switch (event.target.value) {
case "trigger":
d = detector_timing.TRIGGER;
break;
case "auto":
d = detector_timing.AUTO;
break;
case "burst":
d = detector_timing.BURST;
break;
case "gated":
d = detector_timing.GATED;
break;
}
setTimingModeListValue(d);
setS(prev => ({...prev, timing: d}));
};
const handleChange = (event : SelectChangeEvent<String>) => {
setStorageCellListValue(String(event.target.value).toString());
setS(prev => ({...prev, jungfrau_storage_cell_count: Number(event.target.value)}));
};
const dirty = !_.isEqual(s, lastDownloadedS);
return <Paper style={{textAlign: 'center'}} sx={{ height: 1150, width: '100%' }}>
<Grid container>
<Grid item xs={1}/>
<Grid item xs={10}>
<br/><PanelTitle title="Detector settings" dirty={dirty}/>
<br/><br/><br/>
<NumberTextField start_val={s.frame_time_us}
label={"Internal frame time"}
units={"us"}
min={450}
fullWidth
counter={downloadCounter}
callback={(val: number, err: boolean) => {
setFrameTimeErr(err);
setS(prev => ({...prev, frame_time_us: val}));
}}/>
</Grid>
<Grid item xs={1}/>
<Grid item xs={2}/>
<Grid item xs={8}>
<List>
<ListItem>
<Switch onChange={countTimeToggle}
checked={s.count_time_us !== undefined}/>
<NumberTextField
start_val={s.count_time_us}
disabled={s.count_time_us === undefined}
label={"Count time"}
units={"us"}
min={0}
max={1980}
fullWidth
counter={downloadCounter}
callback={(val: number, err: boolean) => {
setCountTimeErr(err);
setS(prev => ({...prev, count_time_us: val}));
}}/>
</ListItem>
</List>
</Grid>
<Grid item xs={2}/>
<Grid item xs={1}/>
<Grid item xs={10}>
<NumberTextField start_val={s.detector_trigger_delay_ns}
label={"Detector delay"}
units={"ns"}
default={0}
min={0}
counter={downloadCounter}
callback={(val: number, err: boolean) => {
setDetectorTriggerDelayErr(err);
setS(prev => ({...prev, detector_trigger_delay_ns: val}));
}}
sx={{width: 120}}
/>&nbsp;&nbsp;
<FormControl>
<InputLabel id="timing-select-label">Timing mode</InputLabel>
<Select
sx={{width: 120}}
labelId="timing-select-label"
id="timing-select"
value={timingModeListValue}
label="Storage cell count"
onChange={handleTimingChange}
>
<MenuItem value={"trigger"}>Trigger</MenuItem>
<MenuItem value={"auto"}>Auto</MenuItem>
<MenuItem value={"burst"}>Burst (EIGER)</MenuItem>
<MenuItem value={"gated"}>Gated (EIGER)</MenuItem>
</Select>
</FormControl>
<br/><br/>
</Grid>
<Grid item xs={1}/>
<Grid item xs={1}/>
<Grid item xs={10}>
<b>Internal image generator (FPGA) </b><br/>
</Grid>
<Grid item xs={1}/>
<Grid item xs={3}/>
<Grid item xs={6}>
<br/>
<List>
<Tooltip title="Internal generator allows to test Jungfraujoch without having detector connected.">
<ListItem>
<Checkbox onChange={internalFrameGeneratorToggle}
checked={s.internal_frame_generator === true}/>
<FormControl fullWidth>
<NumberTextField start_val={s.internal_frame_generator_images}
label={"Images"}
min={1}
default={1}
max={128}
counter={downloadCounter}
callback={(val: number, err: boolean) => {
setInternalFrameGeneratorImagesErr(err);
setS(prev => ({...prev, internal_frame_generator_images: val}));
}}
disabled={!s.internal_frame_generator}
fullWidth/>
</FormControl>
</ListItem>
</Tooltip>
</List>
</Grid>
<Grid item xs={3}/>
<Grid item xs={1}/>
<Grid item xs={10}>
<b>JUNGFRAU settings </b><br/>
</Grid>
<Grid item xs={1}/>
<Grid item xs={3}/>
<Grid item xs={6}>
<FormGroup>
<FormControlLabel control={
<Checkbox onChange={useGainHG0Toggle} checked={s.jungfrau_use_gain_hg0 === true}/>
} label="HG0"/>
<FormControlLabel control={
<Checkbox onChange={fixedGainG1Toggle} checked={s.jungfrau_fixed_gain_g1 === true}/>
} label="Fixed G1"/>
</FormGroup><br/>
</Grid>
<Grid item xs={3}/>
<Grid item xs={1}/>
<Grid item xs={10}>
<b>JUNGFRAU storage cells </b><br/><br/>
</Grid>
<Grid item xs={1}/>
<Grid item xs={1}/>
<Grid item xs={10}>
<FormControl>
<InputLabel id="demo-simple-select-label">Count</InputLabel>
<Select
sx={{width: 120}}
labelId="demo-simple-select-label"
id="demo-simple-select"
value={storageCellListValue}
label="Storage cell count"
onChange={handleChange}
>
<MenuItem value={"1"}>1</MenuItem>
<MenuItem value={"2"}>2</MenuItem>
<MenuItem value={"4"}>4</MenuItem>
<MenuItem value={"8"}>8</MenuItem>
<MenuItem value={"15"}>15</MenuItem>
<MenuItem value={"16"}>16</MenuItem>
</Select>
</FormControl>&nbsp;&nbsp;
<NumberTextField start_val={s.jungfrau_storage_cell_delay_ns}
label={"Delay"}
units={"ns"}
min={2100}
default={2100}
counter={downloadCounter}
sx={{ width: "140px" }}
callback={(val: number, err: boolean) => {
setStorageCellDelayErr(err);
setS(prev => ({...prev, jungfrau_storage_cell_delay_ns: val}));
}}
disabled={storageCellListValue === "1"}
fullWidth/>
<br/><br/>
</Grid>
<Grid item xs={1}/>
<Grid item xs={1}/>
<Grid item xs={10}>
<b>JUNGFRAU Pedestal</b><br/><br/>
</Grid>
<Grid item xs={1}/>
<Grid item xs={1}/>
<Grid item xs={10}>
<NumberTextField start_val={s.jungfrau_pedestal_g0_frames}
label={"G0"}
min={0}
default={2000}
counter={downloadCounter}
callback={(val: number, err: boolean) => {
setPedestalG0FramesErr(err);
setS(prev => ({...prev, jungfrau_pedestal_g0_frames: val}));
}}
sx={{width: "80px"}}/>&nbsp;
<NumberTextField start_val={s.jungfrau_pedestal_g1_frames}
label={"G1"}
min={0}
default={300}
counter={downloadCounter}
callback={(val: number, err: boolean) => {
setPedestalG1FramesErr(err);
setS(prev => ({...prev, jungfrau_pedestal_g1_frames: val}));
}}
sx={{width: "80px"}}/>&nbsp;
<NumberTextField start_val={s.jungfrau_pedestal_g2_frames}
label={"G2"}
min={0}
default={300}
counter={downloadCounter}
callback={(val: number, err: boolean) => {
setPedestalG2FramesErr(err);
setS(prev => ({...prev, jungfrau_pedestal_g2_frames: val}));
}}
sx={{width: "80px"}}/>
&nbsp;<br/><br/>
</Grid>
<Grid item xs={1}/>
<Grid item xs={1}/>
<Grid item xs={10}>
<NumberTextField start_val={s.jungfrau_pedestal_min_image_count}
label={"Min. image count"}
min={0}
default={128}
counter={downloadCounter}
callback={(val: number, err: boolean) => {
setPedestalMinImageCountErr(err);
setS(prev => ({...prev, jungfrau_pedestal_min_image_count: val}));
}}
sx={{width: "120px"}}/><br/><br/>
</Grid>
<Grid item xs={1}/>
<Grid item xs={1}/>
<Grid item xs={10}>
<b>EIGER settings </b><br/><br/>
</Grid>
<Grid item xs={1}/>
<Grid item xs={1}/>
<Grid item xs={10}>
<ListItem>
<Checkbox onChange={eigerThresholdToggle}
checked={s.eiger_threshold_keV !== undefined}/>
<FormControl fullWidth>
<NumberTextField
default={eigerThresholdKeVOld}
start_val={s.eiger_threshold_keV}
label={"Energy threshold"}
min={1.0}
max={100.0}
units={"keV"}
float={true}
counter={downloadCounter}
callback={(val: number, err: boolean) => {
if (!err)
setEigerThresholdKeVOld(val);
setEigerThresholdErr(err);
setS(prev => ({...prev, eiger_threshold_keV: val}));
}}
disabled={s.eiger_threshold_keV === undefined}
/>
</FormControl>&nbsp;
<FormControl>
<InputLabel id="demo-simple-select-label">Read-out</InputLabel>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
label="Detector"
value={bitWidthSelection}
onChange={handleBitWidthChange}
sx={{minWidth:106}}
>
<MenuItem value={"a"}>Auto</MenuItem>
<MenuItem value={"8"}>8-bit</MenuItem>
<MenuItem value={"16"}>16-bit</MenuItem>
<MenuItem value={"32"}>32-bit</MenuItem>
</Select>
</FormControl><br/><br/>
</ListItem><br/>
</Grid>
<Grid item xs={1}/>
<Grid item xs={1}/>
<Grid item xs={10}>
<Button color={dirty ? "warning" : "primary"} variant="contained" disableElevation
onClick={() => submit(s)}
disabled={pending
|| countTimeErr
|| frameTimeErr
|| internalFrameGeneratorImagesErr
|| detectorTriggerDelayErr
|| storageCellDelayErr
|| pedestalG0FramesErr
|| pedestalG1FramesErr
|| pedestalG2FramesErr
|| pedestalMinImageCountErr}>Upload</Button>&nbsp;&nbsp;
<ButtonWithSnackbar
color={"primary"}
path={"/deactivate"}
text={"Deactivate"}
/>
<br/><br/>
</Grid>
<Grid item xs={1}/>
</Grid>
<UploadSnackbarView snackbar={snackbar}/>
</Paper>
}
export default memo(DetectorSettings);