diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f2e470b..5a5cbdc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,34 +2,50 @@ ## 0.3.0 - Merge three repos into one, remove Ultralytics as dependency - **DAQ/Server**: + - Added lifespan function for one-time initialization of core objects (devices, daq, config) on startup, reducing conflicting Redis and device connections. + - Big Aerotech upgrade: transitioned to Aerotech API with ported coordinate functions. + - Re-enabled and updated automation workflow 2.0 with new backend endpoints. + - Added `park_and_dry` functionality for repeated mount failures. - Workflow assertions to ensure scintillator is down when possible. - Improved aerotech movement and position calculations. - Implementation of camera auto-exposure and zoom-dependent settings. - Added SAS-TT school demo temporary raster function. - - Enhanced status polling with robust error handling for `Tell` and `Smargon` communication. - - `ml_box` updated with beamline-specific URLs and model class types. + - Enhanced status polling with robust error handling for `Tell`, `Smargon`, and `Aerotech` communication. + - `ml_box` updated with beamline-specific URLs, model class types, and new YOLO26 engine. - Added separate authentication pathway for staff-only beamline recovery operations. - Improved database message limits for omega (to 2 decimal places). - Added screenshot to database from GUI; face detection sequence and beamline recovery updates. + - Throttled `UserRightsException` logging to reduce log noise. +- **Baton & Sessions**: + - Major rework of the baton exchange procedure and session management. + - Added `NOT_BATON_HOLDER` error code and specialized handling for baton requests. + - Integrated baton status messages into the camera image display and status bar. + - Improved session state transitions with new `Pending` states and P-group selection logic. - **GUI**: + - Added `ui_state_manager` to handle `QSettings` for persistent UI state. - Major rework of `predict_subscriber` and prediction stream updates; corrected default ZMQ stream for ML box. - Added FPS counter and sharpness monitoring for sample camera. - Improved camera thread performance and authentication failure handling. - Added X10SA specific camera and prediction configurations. - New `display_status` for user-facing connection/error messages. - Integrated `dev_help` panel with logs and error code search. - - Added framework for building GUI tutorials. - - Added alert banner for critical disconnection issues (server/Tell). + - Added framework for building GUI tutorials with first draft of X10SA tutorial. + - Added alert banner for critical disconnection issues (server/Tell/Aerotech). - New screenshot button and controls in sample camera GUI. - Added SSE support for face detection panel updates during automation. - Integrated sample position resync in recovery panel to reduce Tell polling. -- **TELL**: + - Fixed matplotlib warnings in face detection panel and improved layout persistence. +- **TELL & Infrastructure**: + - Added mTLS certificate authentication for `aaredb`, `tellupdater`, and `spreadsheetupdater`. + - Improved SSE client resilience in `tellupdater` with retry mechanisms and enhanced error logging. - Major refactor: removed redundant code and added simulated client. - Converted wait functions to use inbuilt pshell functionality. - Added error handling and proxy support for connection failures. - Fixed event names for mounting, and added missing events for gripper detection and motion sync. - Refactored SSE connection handling for simplicity and robustness. - **Common & Devices**: + - Added BEC worker and Aerotech controller initialization in `devices.py`. + - Added magnet position sensor functions and centralized transmission settings. - Added centralized `error_codes` and refactored exception handling to use FastAPI handlers. - Expanded device support with new PVs (beamstop, scintillator, collimator, cryojet, etc.). - Cleaned up `mx_lib` wait functions and `my_motor` class; added `clean_filename` helper. diff --git a/src/aare/daq/daq.py b/src/aare/daq/daq.py index 39d375a9..b0d0d61b 100644 --- a/src/aare/daq/daq.py +++ b/src/aare/daq/daq.py @@ -812,6 +812,16 @@ class AareDAQ: raise Exception(f"Failed during raster: {e}") from e def measure_raster(self, r: RasterGridRequest, auto: bool) -> CompletedRasterGrid: + """ + Execute a raster scan. + + Args: + r: RasterGridRequest parameters for the scan. + auto: Boolean flag indicating if this is part of an automated sequence. + + Returns: + CompletedRasterGrid result. + """ self.__cfg.try_set_busy(timeout=ceil(360)) try: if auto: @@ -906,6 +916,15 @@ class AareDAQ: return CompletedRotationScan(request=copy.deepcopy(request), result=result) def measure_rotation(self, request: RotationScanRequest) -> CompletedRotationScan: + """ + Execute a rotation scan. + + Args: + request: RotationScanRequest parameters. + + Returns: + CompletedRotationScan result. + """ total_time = request.exp_time_s * request.steps logger.info(f"received rotation scan request: {request}, total time: {total_time}s, steps: {request.steps}") self.__cfg.try_set_busy(timeout=ceil(total_time + 360)) @@ -1117,6 +1136,16 @@ class AareDAQ: return SmargonCoordinate(sh_mm=coord), cls, classes def ml_bounding_box(self, sample_id: int | None = None, filename: str | None = None) -> RasterGridRequest | None: + """ + Request an ML-based bounding box for the sample. + + Args: + sample_id: Optional sample ID. + filename: Optional filename for saving/logging. + + Returns: + RasterGridRequest object representing the found bounding box, or None if failed. + """ try: self.__cfg.try_set_busy(timeout=360) r = self.__ml_bounding_box(sample_id, filename) @@ -1127,6 +1156,16 @@ class AareDAQ: raise def face_detection(self, steps: int = 14, step_size: int = 15) -> dict: + """ + Perform a face detection sequence by rotating the sample and using ML to find the flat face. + + Args: + steps: Number of rotation steps. + step_size: Size of each rotation step in degrees. + + Returns: + Dictionary containing face detection results, including found samples and fits. + """ self.__cfg.try_set_busy(timeout=360) try: logger.info("running face detection sequence") @@ -1391,6 +1430,19 @@ class AareDAQ: return False def auto_loop_center(self, sample_id: int | None = None) -> float: + """ + Automatically center the loop using ML-based detection. + This performs a multi-step sequence including rotation and centering. + + Args: + sample_id: Optional database ID of the sample. If None, uses current sample. + + Returns: + Time taken for the centering process (seconds) + + Raises: + LoopCenteringFailed: If the centering sequence fails + """ start = time.perf_counter() try: self.__cfg.try_set_busy(timeout=360) @@ -1430,6 +1482,13 @@ class AareDAQ: cv2.imwrite(f"{filename}.jpg", bgr_image) def save_screenshot_db(self, sample_id: int, filename: str): + """ + Capture a screenshot and upload it to the database for a specific sample. + + Args: + sample_id: Database ID of the sample. + filename: Name to give to the uploaded image. + """ #time.sleep(0.2) # Wait 200 ms to ensure camera image is stable bgr_image = cv2.cvtColor(self.camera_image, cv2.COLOR_RGB2BGR) self.__aare.upload_image(sample_id, filename, bgr_image) @@ -1549,6 +1608,16 @@ class AareDAQ: return default_params, "defaults" def measure(self, sample: SampleShortInfo) -> float: + """ + Main measurement sequence for a given sample. + Includes mounting, loop centering, face detection, auto-raster, and data collection. + + Args: + sample: SampleShortInfo object containing sample details + + Returns: + Total time taken for the measurement (seconds) + """ start = time.perf_counter() formatted_date = datetime.now().strftime('%Y%m%d') sample_prefix = "{}/{}/{:02d}/{}".format( diff --git a/src/aare/daq/server.py b/src/aare/daq/server.py index e824eb8b..0705b15e 100644 --- a/src/aare/daq/server.py +++ b/src/aare/daq/server.py @@ -127,6 +127,15 @@ register_exception_handlers(app) app.include_router(workflow_router) def _required_recovery_code() -> str: + """ + Get the required recovery confirmation code from environment variables. + + Returns: + The recovery confirmation code. + + Raises: + HTTPException: If AARE_RECOVERY_CODE is not configured. + """ code = os.getenv("AARE_RECOVERY_CODE", "").strip() if not code: logger.error("AARE_RECOVERY_CODE is not configured.") @@ -137,6 +146,15 @@ def _required_recovery_code() -> str: return code def _validate_recovery_code(confirmation_code: str) -> None: + """ + Validate the provided recovery confirmation code against the server configuration. + + Args: + confirmation_code: The code provided by the user. + + Raises: + HTTPException: If the code is invalid or not configured. + """ expected = _required_recovery_code() provided = str(confirmation_code or "").strip() if not hmac.compare_digest(provided, expected): @@ -147,12 +165,24 @@ def _validate_recovery_code(confirmation_code: str) -> None: ) def _sample_is_mounted() -> bool: + """ + Check if a sample is currently mounted on the beamline. + + Returns: + True if a sample is mounted, False otherwise. + """ try: return daq.sample is not None except Exception: return False def _push_face_detection_progress(payload: dict) -> None: + """ + Update the global face detection state with new progress information. + + Args: + payload: Dictionary containing face detection progress data. + """ global _face_detection_state try: next_seq = int(_face_detection_state.get("seq", 0)) + 1 @@ -165,6 +195,12 @@ def _push_face_detection_progress(payload: dict) -> None: async def face_detection_event_stream() -> AsyncGenerator[str, None]: + """ + Generator for Server-Sent Events (SSE) of face detection progress. + + Yields: + Formatted SSE data strings. + """ last_seq = -1 try: while True: @@ -179,6 +215,15 @@ async def face_detection_event_stream() -> AsyncGenerator[str, None]: @app.post("/token") async def login(form_data: OAuth2PasswordRequestForm = Depends()): + """ + Authenticate a user and return an access token. + + Args: + form_data: OAuth2 password request form containing username and password. + + Returns: + A dictionary containing the access token and token type. + """ data = await run_in_threadpool(auth.authenticate_user, cfg, form_data) return {"access_token": data, "token_type": "bearer"} @@ -192,6 +237,15 @@ async def meta_error_codes() -> dict[str, dict[str, str]]: @app.get("/status") async def status(token: str = Depends(oauth2_scheme)) -> DAQStatusModel: + """ + Get the current status of the DAQ system. + + Args: + token: OAuth2 access token. + + Returns: + DAQStatusModel containing the current status. + """ data = auth.parse_token(token) full = daq.status @@ -218,12 +272,31 @@ async def status(token: str = Depends(oauth2_scheme)) -> DAQStatusModel: async def sample_geometry( token: str = Depends(oauth2_scheme), ) -> SampleGeometryModel: + """ + Get the sample geometry information. + + Args: + token: OAuth2 access token. + + Returns: + SampleGeometryModel object. + """ auth.check_jwt_ro(cfg, auth.parse_token(token)) return daq.status.geom @app.put("/beamline/omega") async def omega(val: float, token: str = Depends(oauth2_scheme)): + """ + Set the omega angle of the goniometer. + + Args: + val: The target omega angle in degrees. + token: OAuth2 access token. + + Returns: + "OK" on success. + """ logger.debug(f"Setting omega to {val}") auth.check_jwt_rw(cfg, auth.parse_token(token)) daq.omega = val @@ -231,6 +304,16 @@ async def omega(val: float, token: str = Depends(oauth2_scheme)): @app.put("/beamline/omega_rel") async def omega(val: float, token: str = Depends(oauth2_scheme)): + """ + Move the omega angle relatively. + + Args: + val: The relative omega movement in degrees. + token: OAuth2 access token. + + Returns: + "OK" on success. + """ logger.debug(f"Moving omega by {val}") auth.check_jwt_rw(cfg, auth.parse_token(token)) daq.omega_rel(val) @@ -238,6 +321,16 @@ async def omega(val: float, token: str = Depends(oauth2_scheme)): @app.put("/beamline/front_light") async def front_light(val: float, token: str = Depends(oauth2_scheme)): + """ + Set the front light intensity. + + Args: + val: Light intensity value. + token: OAuth2 access token. + + Returns: + "OK" on success. + """ logger.debug(f"Setting light to {val}") auth.check_jwt_rw(cfg, auth.parse_token(token)) daq.front_light = val @@ -245,6 +338,16 @@ async def front_light(val: float, token: str = Depends(oauth2_scheme)): @app.put("/beamline/back_light") async def back_light(val: float, token: str = Depends(oauth2_scheme)): + """ + Set the back light intensity. + + Args: + val: Light intensity value. + token: OAuth2 access token. + + Returns: + "OK" on success. + """ logger.debug(f"Setting back light to {val}") auth.check_jwt_rw(cfg, auth.parse_token(token)) daq.back_light = val @@ -252,6 +355,16 @@ async def back_light(val: float, token: str = Depends(oauth2_scheme)): @app.put("/beamline/zoom") async def zoom(val: float, token: str = Depends(oauth2_scheme)): + """ + Set the camera zoom level. + + Args: + val: Zoom level. + token: OAuth2 access token. + + Returns: + "OK" on success. + """ logger.debug(f"Setting zoom to {val}") auth.check_jwt_rw(cfg, auth.parse_token(token)) daq.zoom = val @@ -260,6 +373,16 @@ async def zoom(val: float, token: str = Depends(oauth2_scheme)): @app.put("/beamline/smargon") async def smargon(val: SmargonCoordinate, token: str = Depends(oauth2_scheme)): + """ + Move the Smargon goniometer to specified coordinates. + + Args: + val: SmargonCoordinate object. + token: OAuth2 access token. + + Returns: + "OK" on success. + """ logger.debug(f"Setting smargon to {val}") auth.check_jwt_rw(cfg, auth.parse_token(token)) daq.smargon = val @@ -268,6 +391,16 @@ async def smargon(val: SmargonCoordinate, token: str = Depends(oauth2_scheme)): @app.post("/beamline/tweak_abr_meas_pos") async def tweak_abr_meas_pos(val: AerotechCoordinate, token: str = Depends(oauth2_scheme)): + """ + Tweak the Aerotech measurement position. Staff only. + + Args: + val: AerotechCoordinate object with tweaks. + token: OAuth2 access token. + + Returns: + "OK" on success. + """ auth.check_jwt_staff(cfg, auth.parse_token(token)) daq.tweak_abr_meas_pos(val) return "OK" @@ -275,6 +408,15 @@ async def tweak_abr_meas_pos(val: AerotechCoordinate, token: str = Depends(oauth @app.post("/beamline/save_abr_meas_pos") async def save_abr_meas_pos(token: str = Depends(oauth2_scheme)): + """ + Save the current Aerotech measurement position. Staff only. + + Args: + token: OAuth2 access token. + + Returns: + "OK" on success. + """ logger.debug(f"Save abr") auth.check_jwt_staff(cfg, auth.parse_token(token)) daq.save_abr_meas_pos() @@ -282,6 +424,16 @@ async def save_abr_meas_pos(token: str = Depends(oauth2_scheme)): @app.post("/beamline/anneal") async def anneal(time_s: float, token: str = Depends(oauth2_scheme)): + """ + Perform sample annealing for a specified duration. + + Args: + time_s: Annealing time in seconds. + token: OAuth2 access token. + + Returns: + "OK" on success. + """ logger.debug(f"Anneal {time_s}") auth.check_jwt_rw(cfg, auth.parse_token(token)) daq.anneal(time_s) @@ -289,6 +441,15 @@ async def anneal(time_s: float, token: str = Depends(oauth2_scheme)): @app.post("/beamline/goto_abr_meas_pos") async def goto_abr_meas_pos(token: str = Depends(oauth2_scheme)): + """ + Move the Aerotech to the saved measurement position. + + Args: + token: OAuth2 access token. + + Returns: + "OK" on success. + """ logger.debug(f"Go to ABR meas pos") auth.check_jwt_rw(cfg, auth.parse_token(token)) daq.goto_abr_meas_pos() @@ -296,6 +457,17 @@ async def goto_abr_meas_pos(token: str = Depends(oauth2_scheme)): @app.post("/beam_mark/add") async def mark_beam(x: float, y: float, token: str = Depends(oauth2_scheme)): + """ + Mark the beam position on the camera image. Staff only. + + Args: + x: X coordinate in pixels. + y: Y coordinate in pixels. + token: OAuth2 access token. + + Returns: + "OK" on success. + """ logger.debug(f"Marking beam to {x}, {y}") auth.check_jwt_staff(cfg, auth.parse_token(token)) daq.mark_beam(x, y) @@ -304,6 +476,15 @@ async def mark_beam(x: float, y: float, token: str = Depends(oauth2_scheme)): @app.post("/beam_mark/clear") async def clear_beam_mark(token: str = Depends(oauth2_scheme)): + """ + Clear the beam mark from the camera image. Staff only. + + Args: + token: OAuth2 access token. + + Returns: + "OK" on success. + """ logger.debug(f"Clear beam mark") auth.check_jwt_staff(cfg, auth.parse_token(token)) daq.clear_mark_beam() @@ -311,6 +492,17 @@ async def clear_beam_mark(token: str = Depends(oauth2_scheme)): @app.post("/beamline/beam_center") async def beam_center(x: float, y: float, token: str = Depends(oauth2_scheme)): + """ + Set the beam center position. Staff only. + + Args: + x: X coordinate in pixels. + y: Y coordinate in pixels. + token: OAuth2 access token. + + Returns: + "OK" on success. + """ logger.debug(f"Beam Center {x}, {y}") auth.check_jwt_staff(cfg, auth.parse_token(token)) daq.beam_center = (x, y) @@ -318,6 +510,17 @@ async def beam_center(x: float, y: float, token: str = Depends(oauth2_scheme)): @app.post("/beamline/beam_size_mm") async def beam_size_mm(x: float, y: float, token: str = Depends(oauth2_scheme)): + """ + Set the beam size in millimeters. Staff only. + + Args: + x: Beam width in mm. + y: Beam height in mm. + token: OAuth2 access token. + + Returns: + "OK" on success. + """ logger.debug(f"Beam Size {x}, {y}") auth.check_jwt_staff(cfg, auth.parse_token(token)) daq.beam_size_mm = Coordinate(x=x, y=y) @@ -325,6 +528,16 @@ async def beam_size_mm(x: float, y: float, token: str = Depends(oauth2_scheme)): @app.put("/beamline/samcam") async def samcam_settings(s: SampleCameraSettings, token: str = Depends(oauth2_scheme)): + """ + Update the sample camera settings (exposure, gain, etc.). + + Args: + s: SampleCameraSettings object. + token: OAuth2 access token. + + Returns: + "OK" on success. + """ logger.debug(f"SamCam settings: {s}") auth.check_jwt_rw(cfg, auth.parse_token(token)) daq.samcam_settings = s @@ -332,6 +545,16 @@ async def samcam_settings(s: SampleCameraSettings, token: str = Depends(oauth2_s @app.post("/samcam/autofocus") async def samcam_autofocus(s: AutofocusSettings, token: str = Depends(oauth2_scheme)): + """ + Trigger the sample camera autofocus procedure. + + Args: + s: AutofocusSettings object. + token: OAuth2 access token. + + Returns: + "OK" on success. + """ logger.debug(f"SamCam AutoFocus") auth.check_jwt_rw(cfg, auth.parse_token(token)) daq.auto_focus(s) @@ -339,6 +562,16 @@ async def samcam_autofocus(s: AutofocusSettings, token: str = Depends(oauth2_sch @app.post("/beamline/shutter") async def shutter(val: bool, token: str = Depends(oauth2_scheme)): + """ + Open or close the beamline shutter. + + Args: + val: True to open, False to close. + token: OAuth2 access token. + + Returns: + "OK" on success. + """ logger.debug(f"Shutter {val}") auth.check_jwt_rw(cfg, auth.parse_token(token)) daq.shutter = val @@ -346,6 +579,15 @@ async def shutter(val: bool, token: str = Depends(oauth2_scheme)): async def get_image(token: str = Depends(oauth2_scheme)): + """ + Get the current camera image as a JPEG stream. + + Args: + token: OAuth2 access token. + + Returns: + StreamingResponse containing the JPEG image. + """ auth.check_jwt_ro(cfg, auth.parse_token(token)) _, encoded_image = cv2.imencode( @@ -361,6 +603,15 @@ async def get_image(token: str = Depends(oauth2_scheme)): # TELL procedures @app.get("/sample/curr_sample") async def sample(token: str = Depends(oauth2_scheme)) -> SampleShortInfo: + """ + Get information about the currently mounted sample. + + Args: + token: OAuth2 access token. + + Returns: + SampleShortInfo object. + """ token_data = auth.parse_token(token) auth.check_jwt_ro(cfg, auth.parse_token(token)) s = daq.sample @@ -378,6 +629,15 @@ async def sample(token: str = Depends(oauth2_scheme)) -> SampleShortInfo: ) async def park_and_dry(token: str = Depends(oauth2_scheme)): + """ + Execute the 'park and dry' procedure for the sample changer (TELL). + + Args: + token: OAuth2 access token. + + Returns: + Dictionary with status and message. + """ token_data = auth.parse_token(token) auth.check_jwt_ro(cfg, auth.parse_token(token)) daq.park_and_dry() @@ -389,6 +649,17 @@ async def park_and_dry(token: str = Depends(oauth2_scheme)): @app.post("/sample/mount") async def mount(dbid: int, token: str = Depends(oauth2_scheme), reference: bool = False): + """ + Mount a sample from the spreadsheet onto the goniometer. + + Args: + dbid: Database ID of the sample to mount. + token: OAuth2 access token. + reference: Whether to look for the sample in the reference tools list. + + Returns: + Result of the mounting procedure. + """ token_data = auth.parse_token(token) logger.debug(f"Mount {dbid}") auth.check_jwt_rw(cfg, auth.parse_token(token)) @@ -415,6 +686,15 @@ async def mount(dbid: int, token: str = Depends(oauth2_scheme), reference: bool @app.post("/sample/unmount") async def unmount(token: str = Depends(oauth2_scheme)): + """ + Unmount the current sample from the goniometer. + + Args: + token: OAuth2 access token. + + Returns: + "OK" on success. + """ logger.debug(f"Unmount") auth.check_jwt_rw(cfg, auth.parse_token(token)) daq.sample = None @@ -423,6 +703,13 @@ async def unmount(token: str = Depends(oauth2_scheme)): @app.post("/sample/manual") async def manual(s: SampleShortInfo, token: str = Depends(oauth2_scheme)): + """ + Manually create or update a sample. + + Args: + s: SampleShortInfo object. + token: OAuth2 access token. + """ logger.debug(f"Manual Sample {s.sample_name}") auth.check_jwt_rw(cfg, auth.parse_token(token)) print(f"DB ID prior creating {s.db_id}") @@ -431,6 +718,15 @@ async def manual(s: SampleShortInfo, token: str = Depends(oauth2_scheme)): @app.post("/sample/resync") async def sample_resync(token: str = Depends(oauth2_scheme)) -> dict: + """ + Manually trigger a resynchronization of the sample information from the changer (TELL). + + Args: + token: OAuth2 access token. + + Returns: + Dictionary with status and message. + """ auth.check_jwt_rw(cfg, auth.parse_token(token)) daq.sync_current_sample_from_tell(force=True) logger.info("TELL sample cache resynced via API request.") @@ -441,15 +737,36 @@ async def sample_resync(token: str = Depends(oauth2_scheme)) -> dict: def get_spreadsheet(data: TokenData) -> SampleShortInfoList: + """ + Get the sample spreadsheet for the given user/pgroup. + + Args: + data: TokenData containing user information and pgroups. + + Returns: + SampleShortInfoList object. + """ if data.staff: return cfg.spreadsheet else: return cfg.spreadsheet_pgroup(data.pgroups) def get_reference_tools() -> SampleShortInfoList: + """ + Get the list of reference tools. + + Returns: + SampleShortInfoList object. + """ return cfg.reference_tools async def reference_tools_event_stream() -> AsyncGenerator[str, None]: + """ + Generator for SSE of reference tools updates. + + Yields: + JSON string of the reference tools list. + """ try: while True: yield get_reference_tools().model_dump_json() @@ -458,6 +775,15 @@ async def reference_tools_event_stream() -> AsyncGenerator[str, None]: return async def spreadsheet_event_stream(data: TokenData) -> AsyncGenerator[str, None]: + """ + Generator for SSE of sample spreadsheet updates. + + Args: + data: TokenData for filtering the spreadsheet. + + Yields: + JSON string of the user's sample spreadsheet. + """ try: while True: yield get_spreadsheet(data).model_dump_json() @@ -468,6 +794,15 @@ async def spreadsheet_event_stream(data: TokenData) -> AsyncGenerator[str, None] @app.get("/sse/spreadsheet") async def spreadsheet_sse(token: str = Depends(oauth2_scheme)): + """ + SSE endpoint for sample spreadsheet updates. + + Args: + token: OAuth2 access token. + + Returns: + StreamingResponse. + """ data = auth.parse_token(token) return StreamingResponse( spreadsheet_event_stream(data), @@ -482,6 +817,12 @@ async def spreadsheet_sse(token: str = Depends(oauth2_scheme)): @app.get("/sse/reference_tools") async def reference_tools_sse(): + """ + SSE endpoint for reference tools updates. + + Returns: + StreamingResponse. + """ return StreamingResponse( reference_tools_event_stream(), media_type="text/event-stream", @@ -495,10 +836,28 @@ async def reference_tools_sse(): @app.get("/sample/spreadsheet") async def spreadsheet(token: str = Depends(oauth2_scheme)) -> SampleShortInfoList: + """ + Get the current sample spreadsheet. + + Args: + token: OAuth2 access token. + + Returns: + SampleShortInfoList. + """ return get_spreadsheet(auth.parse_token(token)) @app.get("/sample/reference_tools") async def reference_tools(token: str = Depends(oauth2_scheme)) -> SampleShortInfoList: + """ + Get the reference tools list. + + Args: + token: OAuth2 access token. + + Returns: + SampleShortInfoList. + """ auth.check_jwt_ro(cfg, auth.parse_token(token)) return get_reference_tools() @@ -506,6 +865,15 @@ async def reference_tools(token: str = Depends(oauth2_scheme)) -> SampleShortInf # State transitions @app.post("/state/dewar_exchange") async def dewar_exchange(token: str = Depends(oauth2_scheme)): + """ + Transition beamline state to DewarTransfer. + + Args: + token: OAuth2 access token. + + Returns: + "OK" on success. + """ auth.check_jwt_rw(cfg, auth.parse_token(token)) daq.state = BeamlineStateEnum.DewarTransfer return "OK" @@ -513,6 +881,15 @@ async def dewar_exchange(token: str = Depends(oauth2_scheme)): @app.post("/state/sample_exchange") async def dewar_exchange(token: str = Depends(oauth2_scheme)): + """ + Transition beamline state to SampleExchange. + + Args: + token: OAuth2 access token. + + Returns: + "OK" on success. + """ auth.check_jwt_rw(cfg, auth.parse_token(token)) daq.state = BeamlineStateEnum.SampleExchange return "OK" @@ -520,16 +897,40 @@ async def dewar_exchange(token: str = Depends(oauth2_scheme)): @app.post("/state/sample_alignment") async def sample_alignment(token: str = Depends(oauth2_scheme)): + """ + Transition beamline state to SampleAlignment. + + Args: + token: OAuth2 access token. + + Returns: + "OK" on success. + """ auth.check_jwt_rw(cfg, auth.parse_token(token)) daq.state = BeamlineStateEnum.SampleAlignment @app.post("/state/beam_location") async def beam_location(token: str = Depends(oauth2_scheme)): + """ + Transition beamline state to BeamLocation. Staff only. + + Args: + token: OAuth2 access token. + """ auth.check_jwt_staff(cfg, auth.parse_token(token)) daq.state = BeamlineStateEnum.BeamLocation @app.post("/state/maintenance") async def maintenance(token: str = Depends(oauth2_scheme)) -> str: + """ + Transition beamline state to Maintenance. Staff only. + + Args: + token: OAuth2 access token. + + Returns: + "OK" on success. + """ auth.check_jwt_staff(cfg, auth.parse_token(token)) cfg.state = BeamlineStateEnum.Maintenance logger.warning("Beamline state set to Maintenance via protected endpoint.") @@ -537,6 +938,16 @@ async def maintenance(token: str = Depends(oauth2_scheme)) -> str: @app.post("/access/take_over_beamline") async def take_over_beamline(payload: RecoveryActionRequest, token: str = Depends(oauth2_scheme)) -> str: + """ + Take over the beamline session. Staff only. + + Args: + payload: RecoveryActionRequest with confirmation code. + token: OAuth2 access token. + + Returns: + "OK" on success. + """ data = auth.parse_token(token) auth.check_jwt_staff_only(data) _validate_recovery_code(payload.confirmation_code) @@ -549,6 +960,16 @@ async def take_over_beamline(payload: RecoveryActionRequest, token: str = Depend @app.post("/state/free_beamline") async def free_beamline(payload: RecoveryActionRequest, token: str = Depends(oauth2_scheme)) -> str: + """ + Clear the beamline busy flag. Staff only. + + Args: + payload: RecoveryActionRequest with confirmation code. + token: OAuth2 access token. + + Returns: + "OK" on success. + """ data = auth.parse_token(token) auth.check_jwt_staff_only(data) _validate_recovery_code(payload.confirmation_code) @@ -561,6 +982,16 @@ async def free_beamline(payload: RecoveryActionRequest, token: str = Depends(oau @app.post("/recovery/recover_beamline") async def recover_beamline(payload: RecoveryActionRequest, token: str = Depends(oauth2_scheme)) -> dict: + """ + Recover the beamline from an error state. Staff only. + + Args: + payload: RecoveryActionRequest with confirmation code. + token: OAuth2 access token. + + Returns: + Dictionary with status and previous state information. + """ data = auth.parse_token(token) auth.check_jwt_staff_only(data) _validate_recovery_code(payload.confirmation_code) @@ -592,6 +1023,16 @@ async def recover_beamline(payload: RecoveryActionRequest, token: str = Depends( } @app.post("/recovery/unmount_sample") async def recovery_unmount_sample(payload: RecoveryActionRequest, token: str = Depends(oauth2_scheme)) -> dict: + """ + Forcefully unmount a sample during recovery. Staff only. + + Args: + payload: RecoveryActionRequest with confirmation code. + token: OAuth2 access token. + + Returns: + Dictionary with status of the recovery unmount action. + """ data = auth.parse_token(token) auth.check_jwt_staff_only(data) _validate_recovery_code(payload.confirmation_code) @@ -641,17 +1082,48 @@ async def recovery_unmount_sample(payload: RecoveryActionRequest, token: str = D # Scans @app.post("/scan/raster") async def raster(val: RasterGridRequest, auto: bool = False, token: str = Depends(oauth2_scheme)) -> CompletedRasterGrid: + """ + Execute a raster scan. + + Args: + val: RasterGridRequest parameters. + auto: Whether to perform automated analysis after scan. + token: OAuth2 access token. + + Returns: + CompletedRasterGrid object. + """ auth.check_jwt_rw(cfg, auth.parse_token(token)) return daq.measure_raster(val, auto) @app.post("/scan/rotation") async def rotation(val: RotationScanRequest, token: str = Depends(oauth2_scheme)) -> CompletedRotationScan: + """ + Execute a rotation scan. + + Args: + val: RotationScanRequest parameters. + token: OAuth2 access token. + + Returns: + CompletedRotationScan object. + """ auth.check_jwt_rw(cfg, auth.parse_token(token)) return daq.measure_rotation(val) @app.post("/scan/auto") async def auto(s: SampleShortInfo, token: str = Depends(oauth2_scheme)): + """ + Execute a fully automated measurement sequence for a sample. + + Args: + s: SampleShortInfo object. + token: OAuth2 access token. + + Returns: + Formatted string of the total runtime. + """ auth.check_jwt_rw(cfg, auth.parse_token(token)) runtime = daq.measure(s) return f"{runtime:0.3f}" @@ -659,6 +1131,16 @@ async def auto(s: SampleShortInfo, token: str = Depends(oauth2_scheme)): @app.post("/scan/smart_params") async def set_smart_params(p: SimpleScanParameters, token: str = Depends(oauth2_scheme)) -> str: + """ + Set the 'smart' scan parameters. + + Args: + p: SimpleScanParameters object. + token: OAuth2 access token. + + Returns: + "OK" on success. + """ #token_data = auth.parse_token(token) #logger.debug(f"{token_data.session} Try to set smart params: {p}") auth.check_jwt_rw(cfg, auth.parse_token(token)) @@ -667,6 +1149,12 @@ async def set_smart_params(p: SimpleScanParameters, token: str = Depends(oauth2_ @app.post("/scan/cancel") async def cancel(token: str = Depends(oauth2_scheme)): + """ + Cancel the currently running scan or automation. + + Args: + token: OAuth2 access token. + """ logger.debug(f"Scan Cancel") auth.check_jwt_rw(cfg, auth.parse_token(token)) daq.cancel() @@ -674,6 +1162,15 @@ async def cancel(token: str = Depends(oauth2_scheme)): # ALC routines @app.post("/alc/center_loop") async def alc_center_loop(token: str = Depends(oauth2_scheme)) -> str: + """ + Trigger the automated loop centering procedure (ALC). + + Args: + token: OAuth2 access token. + + Returns: + "OK" on success. + """ logger.debug(f"ALC") auth.check_jwt_rw(cfg, auth.parse_token(token)) daq.auto_loop_center() @@ -682,11 +1179,31 @@ async def alc_center_loop(token: str = Depends(oauth2_scheme)) -> str: @app.post("/alc/ml_bounding_box") async def alc_ml_bounding_box(token: str = Depends(oauth2_scheme)) -> RasterGridRequest | None: + """ + Request an ML-based bounding box for the sample. + + Args: + token: OAuth2 access token. + + Returns: + RasterGridRequest if a box was found, else None. + """ auth.check_jwt_rw(cfg, auth.parse_token(token)) return daq.ml_bounding_box() @app.post("/face_detection/run") async def face_detection_run(steps: int, step_size: int, token: str = Depends(oauth2_scheme)) -> dict: + """ + Trigger the face detection sequence. + + Args: + steps: Number of rotation steps. + step_size: Degrees per step. + token: OAuth2 access token. + + Returns: + Dictionary containing face detection results. + """ logger.debug(f"Face detection run: {steps} steps, {step_size} step size") auth.check_jwt_rw(cfg, auth.parse_token(token)) _push_face_detection_progress({ @@ -701,6 +1218,15 @@ async def face_detection_run(steps: int, step_size: int, token: str = Depends(oa @app.get("/sse/face_detection") async def sse_face_detection(token: str = Depends(oauth2_scheme)): + """ + SSE endpoint for face detection progress updates. + + Args: + token: OAuth2 access token. + + Returns: + StreamingResponse. + """ auth.check_jwt_ro(cfg, auth.parse_token(token)) return StreamingResponse( face_detection_event_stream(), @@ -716,12 +1242,31 @@ async def sse_face_detection(token: str = Depends(oauth2_scheme)): # Access management @app.get("/access/pgroup") async def pgroup(token: str = Depends(oauth2_scheme)) -> str: + """ + Get the currently active pgroup. + + Args: + token: OAuth2 access token. + + Returns: + The pgroup string. + """ auth.check_jwt_ro(cfg, auth.parse_token(token)) return cfg.pgroup @app.put("/access/pgroup") async def set_pgroup(val: str, token: str = Depends(oauth2_scheme)) -> str: + """ + Set the active pgroup. + + Args: + val: The pgroup string to set. + token: OAuth2 access token. + + Returns: + "OK" on success. + """ data = auth.parse_token(token) holder = cfg.baton_holder @@ -737,18 +1282,46 @@ async def set_pgroup(val: str, token: str = Depends(oauth2_scheme)) -> str: @app.delete("/access/pgroup") async def del_pgroup(token: str = Depends(oauth2_scheme)) -> str: + """ + Clear the active pgroup. + + Args: + token: OAuth2 access token. + + Returns: + "OK" on success. + """ auth.check_jwt_ro(cfg, auth.parse_token(token)) cfg.pgroup = None return "OK" @app.put("/beamline/commissioning_mode") async def set_commissioning_mode(val: bool, token: str = Depends(oauth2_scheme)) -> str: + """ + Set the commissioning mode. Staff only. + + Args: + val: True to enable commissioning mode, False to disable. + token: OAuth2 access token. + + Returns: + "OK" on success. + """ auth.check_jwt_staff(cfg, auth.parse_token(token)) cfg.commissioning_mode = val return "OK" @app.post("/access/end_session") async def end_session(token: str = Depends(oauth2_scheme)) -> str: + """ + End the current session and release the baton. + + Args: + token: OAuth2 access token. + + Returns: + "OK" on success. + """ logger.debug(f"Try to end Session") # End active session will only delete session, if it is equal to token value # so no need to check R/W permissions @@ -759,6 +1332,15 @@ async def end_session(token: str = Depends(oauth2_scheme)) -> str: @app.post("/access/force_current_session") async def force_current_session(token: str = Depends(oauth2_scheme)) -> str: + """ + Forcefully set the current session as active. + + Args: + token: OAuth2 access token. + + Returns: + "OK" on success. + """ logger.debug(f"Try to grab session") data = auth.parse_token(token) # Counterintuitive, this operation requires only R/O permission @@ -771,7 +1353,15 @@ async def force_current_session(token: str = Depends(oauth2_scheme)) -> str: @app.get("/baton/status") async def baton_status(token: str = Depends(oauth2_scheme)) -> BatonStatus: - """Get the current baton status for the requesting user.""" + """ + Get the current baton status for the requesting user. + + Args: + token: OAuth2 access token. + + Returns: + BatonStatus object. + """ data = auth.parse_token(token) auth.resolve_baton_timeout_if_needed(cfg) return auth.get_baton_status(cfg, data) @@ -786,6 +1376,12 @@ async def baton_request(token: str = Depends(oauth2_scheme)) -> dict: - If staff requesting: granted immediately (or queued if busy) - If same level: creates pending request with timeout - Non-staff cannot request from staff + + Args: + token: OAuth2 access token. + + Returns: + Dictionary with request result. """ logger.debug(cfg.allow_non_staff_request_from_staff) data = auth.parse_token(token) @@ -798,6 +1394,13 @@ async def baton_respond(accept: bool, token: str = Depends(oauth2_scheme)) -> di - accept=true: transfers baton (or queues if busy) - accept=false: refuses the request + + Args: + accept: True to accept, False to refuse. + token: OAuth2 access token. + + Returns: + Dictionary with response result. """ data = auth.parse_token(token) return auth.respond_to_baton_request(cfg, data, accept) @@ -805,14 +1408,30 @@ async def baton_respond(accept: bool, token: str = Depends(oauth2_scheme)) -> di @app.post("/baton/release") async def baton_release(token: str = Depends(oauth2_scheme)) -> dict: - """Voluntarily release the baton, making the beamline vacant.""" + """ + Voluntarily release the baton, making the beamline vacant. + + Args: + token: OAuth2 access token. + + Returns: + Dictionary with release result. + """ data = auth.parse_token(token) return auth.release_baton(cfg, data) @app.post("/baton/cancel") async def baton_cancel(token: str = Depends(oauth2_scheme)) -> dict: - """Cancel your own pending baton request.""" + """ + Cancel your own pending baton request. + + Args: + token: OAuth2 access token. + + Returns: + Dictionary with cancellation result. + """ data = auth.parse_token(token) return auth.cancel_baton_request(cfg, data) @@ -822,6 +1441,12 @@ async def baton_check_timeout(token: str = Depends(oauth2_scheme)) -> dict: """ Check if a pending request has timed out and process it. Called by GUI to poll for timeout completion. + + Args: + token: OAuth2 access token. + + Returns: + Dictionary with timeout check result. """ data = auth.parse_token(token) @@ -854,13 +1479,31 @@ async def baton_check_timeout(token: str = Depends(oauth2_scheme)) -> dict: @app.put("/access/allow_non_staff_request_from_staff") async def set_allow_non_staff_request_from_staff(val: bool, token: str = Depends(oauth2_scheme)) -> str: + """ + Set whether non-staff users can request the baton from staff members. Staff only. + + Args: + val: True to allow, False to disallow. + token: OAuth2 access token. + + Returns: + "OK" on success. + """ data = auth.parse_token(token) auth.check_jwt_staff_only(data) cfg.allow_non_staff_request_from_staff = val return "OK" async def baton_status_event_stream(data: TokenData) -> AsyncGenerator[str, None]: - """SSE stream for baton status updates.""" + """ + SSE stream for baton status updates. + + Args: + data: TokenData for the requesting user. + + Yields: + Baton status as SSE data. + """ last_status = None try: while True: @@ -882,7 +1525,15 @@ async def baton_status_event_stream(data: TokenData) -> AsyncGenerator[str, None @app.get("/sse/baton") async def sse_baton(token: str = Depends(oauth2_scheme)): - """SSE endpoint for real-time baton status updates.""" + """ + SSE endpoint for real-time baton status updates. + + Args: + token: OAuth2 access token. + + Returns: + StreamingResponse. + """ data = auth.parse_token(token) return StreamingResponse( baton_status_event_stream(data), @@ -897,6 +1548,15 @@ async def sse_baton(token: str = Depends(oauth2_scheme)): @app.get("/beamline/settings") async def get_settings(token: str = Depends(oauth2_scheme)) -> BeamlineSettingsModel: + """ + Get the current beamline settings. Staff only. + + Args: + token: OAuth2 access token. + + Returns: + BeamlineSettingsModel. + """ logger.debug(f"Get settings") auth.check_jwt_staff(cfg, auth.parse_token(token)) return cfg.settings @@ -904,24 +1564,56 @@ async def get_settings(token: str = Depends(oauth2_scheme)) -> BeamlineSettingsM @app.put("/beamline/settings") async def put_settings(s: BeamlineSettingsModel, token: str = Depends(oauth2_scheme)): + """ + Update the beamline settings. Staff only. + + Args: + s: BeamlineSettingsModel object. + token: OAuth2 access token. + """ logger.debug(f"Putting settings: {s}") auth.check_jwt_staff(cfg, auth.parse_token(token)) cfg.settings = s @app.get("/beamline/cryo_settings") async def get_cryo_settings(token: str = Depends(oauth2_scheme)) -> CryojetSettingsModel: + """ + Get the current cryojet settings. Staff only. + + Args: + token: OAuth2 access token. + + Returns: + CryojetSettingsModel. + """ logger.debug(f"Get Cryo Settings") auth.check_jwt_staff(cfg, auth.parse_token(token)) return cfg.cryojet_settings @app.put("/beamline/cryo_settings") async def put_cryo_settings(s: CryojetSettingsModel, token: str = Depends(oauth2_scheme)): + """ + Update the cryojet settings. Staff only. + + Args: + s: CryojetSettingsModel object. + token: OAuth2 access token. + """ logger.debug(f"Put Cryo Settings: {s}") auth.check_jwt_staff(cfg, auth.parse_token(token)) cfg.cryojet_settings = s @app.put("/access/all_pgroups") async def get_all_pgroups(token: str = Depends(oauth2_scheme)): + """ + Get a list of all available pgroups. Staff only. + + Args: + token: OAuth2 access token. + + Returns: + List of pgroup strings. + """ logger.debug(f"Get all pgroups") auth.check_jwt_staff(cfg, auth.parse_token(token)) @@ -957,38 +1649,100 @@ async def get_all_pgroups(token: str = Depends(oauth2_scheme)): @app.post("/fluorimeter/spectrum") async def fluorimeter_spectrum(input: FluorescenceSpectrumParameterModel, token: str = Depends(oauth2_scheme)) -> FluorescenceSpectrumOutputModel: + """ + Request a fluorescence spectrum measurement. + + Args: + input: FluorescenceSpectrumParameterModel parameters. + token: OAuth2 access token. + + Returns: + FluorescenceSpectrumOutputModel. + """ auth.check_jwt_rw(cfg, auth.parse_token(token)) return daq.fluorimeter_take_spectrum(input) @app.post("/fluorimeter/start") async def fluorimeter_start(erase: bool = False, token: str = Depends(oauth2_scheme)) -> str: + """ + Start the fluorimeter measurement. + + Args: + erase: Whether to erase previous data. + token: OAuth2 access token. + + Returns: + "OK" on success. + """ auth.check_jwt_rw(cfg, auth.parse_token(token)) daq.fluorimeter_start(erase) return "OK" @app.post("/fluorimeter/stop") async def fluorimeter_stop(token: str = Depends(oauth2_scheme)) -> str: + """ + Stop the fluorimeter measurement. + + Args: + token: OAuth2 access token. + + Returns: + "OK" on success. + """ auth.check_jwt_rw(cfg, auth.parse_token(token)) daq.fluorimeter_stop() return "OK" @app.get("/fluorimeter/status") async def fluorimeter_status(token: str = Depends(oauth2_scheme)) -> int | None: + """ + Get the current fluorimeter status. + + Args: + token: OAuth2 access token. + + Returns: + Fluorimeter status code. + """ auth.check_jwt_ro(cfg, auth.parse_token(token)) return daq.fluorimeter_status() @app.get("/fluorimeter/data") async def fluorimeter_data(token: str = Depends(oauth2_scheme)) -> list[int] | None: + """ + Get the latest fluorimeter data. + + Args: + token: OAuth2 access token. + + Returns: + List of data points. + """ auth.check_jwt_ro(cfg, auth.parse_token(token)) return daq.fluorimeter_data() @app.get("/fluorimeter/background") async def fluorimeter_background(token: str = Depends(oauth2_scheme)) -> list[int] | None: + """ + Get the fluorimeter background data. + + Args: + token: OAuth2 access token. + + Returns: + List of background data points. + """ auth.check_jwt_ro(cfg, auth.parse_token(token)) return daq.fluorimeter_background() # Optional: SSE stream for live spectra while acquiring async def fluorimeter_stream() -> AsyncGenerator[str, None]: + """ + Generator for SSE of fluorimeter status and data. + + Yields: + JSON string of fluorimeter status/data as SSE. + """ logger.debug("start fluorimeter stream...") try: while True: @@ -1014,6 +1768,15 @@ async def fluorimeter_stream() -> AsyncGenerator[str, None]: @app.get("/sse/fluorimeter") async def sse_fluorimeter(token: str = Depends(oauth2_scheme)): + """ + SSE endpoint for fluorimeter updates. + + Args: + token: OAuth2 access token. + + Returns: + StreamingResponse. + """ auth.check_jwt_ro(cfg, auth.parse_token(token)) try: daq.fluorimeter_start(erase=False) @@ -1037,6 +1800,17 @@ async def send_screenshot_db( message: str | None = None, token: str = Depends(oauth2_scheme), ) -> str: + """ + Capture a screenshot and send it to the database. + + Args: + filename: Optional filename. + message: Optional message. + token: OAuth2 access token. + + Returns: + "OK" on success. + """ data = auth.parse_token(token) auth.check_jwt_rw(cfg, data) daq.send_screenshot_db(filename=filename, message=message) diff --git a/src/aare/gui/threads/daq_worker.py b/src/aare/gui/threads/daq_worker.py index f7d20730..4c17e614 100644 --- a/src/aare/gui/threads/daq_worker.py +++ b/src/aare/gui/threads/daq_worker.py @@ -68,6 +68,14 @@ class DAQWorker(QObject): baton_timeout_checked = Signal(dict) def __init__(self, base_url: str | None, token: str, parent=None): + """ + Initialize the DAQWorker. + + Args: + base_url: The base URL of the DAQ server. + token: The authentication token. + parent: The parent QObject. + """ super().__init__(parent) self._active_status_error_key = None self.__token = token @@ -230,6 +238,9 @@ class DAQWorker(QObject): @Slot() def regular_update(self): + """ + Periodically triggered slot to update spreadsheet and request status from server. + """ if self.__counter % SPREADHSEET_FREQUENCY == 0: self.load_spreadsheet() self.load_reference_tools() @@ -237,6 +248,9 @@ class DAQWorker(QObject): self.send_status_request() def send_status_request(self): + """ + Send an asynchronous HTTP GET request to the /status endpoint. + """ if self.__base_url is None: return @@ -252,6 +266,18 @@ class DAQWorker(QObject): @staticmethod def handle_response(reply: QNetworkReply): + """ + Common handler for QNetworkReply to read data and handle errors. + + Args: + reply: The QNetworkReply object. + + Returns: + The response data as a UTF-8 string. + + Raises: + RuntimeError: If the reply contains a network error. + """ if reply.error() == QNetworkReply.NetworkError.NoError: response_data = reply.readAll().data().decode("utf-8") reply.deleteLater() @@ -320,6 +346,12 @@ class DAQWorker(QObject): @Slot(QNetworkReply) def handle_status_response(self, reply: QNetworkReply): + """ + Handle the server's response to a status request. + + Args: + reply: The QNetworkReply object. + """ try: response_data = self.handle_response(reply) parsed_response = DAQStatusModel.model_validate_json(response_data) @@ -546,6 +578,13 @@ class DAQWorker(QObject): self.http_error.emit(str(e)) def generic_post(self, url: str, body: str = ""): + """ + Send a generic HTTP POST request to the server. + + Args: + url: The relative URL (endpoint). + body: The request body string. + """ if self.__base_url is None: logger.info(f"POST /{url}: {body}") return @@ -558,6 +597,13 @@ class DAQWorker(QObject): reply.finished.connect(lambda: self.handle_req_response(reply)) def generic_put(self, url: str, body: str = ""): + """ + Send a generic HTTP PUT request to the server. + + Args: + url: The relative URL (endpoint). + body: The request body string. + """ if self.__base_url is None: logger.info(f"PUT /{url}: {body}") return @@ -569,6 +615,12 @@ class DAQWorker(QObject): reply.finished.connect(lambda: self.handle_req_response(reply)) def generic_delete(self, url: str): + """ + Send a generic HTTP DELETE request to the server. + + Args: + url: The relative URL (endpoint). + """ if self.__base_url is None: logger.info(f"DELETE /{url}") return @@ -778,6 +830,12 @@ class DAQWorker(QObject): @Slot(RotationScanRequest) def standard_scan(self, r: RotationScanRequest): + """ + Trigger a standard rotation scan. + + Args: + r: The RotationScanRequest object. + """ self.run_number_incremented.emit() request = QNetworkRequest(QUrl(f"{self.__base_url}/scan/rotation")) @@ -826,6 +884,12 @@ class DAQWorker(QObject): @Slot(RasterGridRequest) def raster_scan(self, r: RasterGridRequest): + """ + Trigger a raster scan. + + Args: + r: The RasterGridRequest object. + """ self.run_number_incremented.emit() if self.__base_url is None: logger.info(f"POST /scan/raster: {r.model_dump_json()}") @@ -861,6 +925,12 @@ class DAQWorker(QObject): @Slot(RasterGridRequest) def raster_scan_auto(self, r: RasterGridRequest): + """ + Trigger an automated raster scan. + + Args: + r: The RasterGridRequest object. + """ self.run_number_incremented.emit() if self.__base_url is None: logger.info(f"POST /scan/raster: {r.model_dump_json()}") @@ -955,6 +1025,12 @@ class DAQWorker(QObject): @Slot(SampleShortInfo) def automated_scan(self, s: SampleShortInfo): + """ + Trigger a fully automated scan sequence for a sample. + + Args: + s: The SampleShortInfo object. + """ if self.__base_url is None: logger.info(f"POST /scan/auto: {s.model_dump_json()}") return @@ -1045,6 +1121,9 @@ class DAQWorker(QObject): @Slot() def ml_bounding_box(self): + """ + Request an ML-based bounding box for the current sample. + """ if self.__base_url is None: logger.info(f"POST /alc/ml_bounding_box") return @@ -1102,6 +1181,13 @@ class DAQWorker(QObject): @Slot() def face_detection(self, steps: int, step_size: int): + """ + Trigger the face detection procedure. + + Args: + steps: Number of rotation steps. + step_size: Degrees per step. + """ if self.__base_url is None: logger.info(f"POST /face_detection/run?steps={steps}&step_size={step_size}") return @@ -1355,7 +1441,9 @@ class DAQWorker(QObject): @Slot() def request_baton(self): - """Request the baton.""" + """ + Request the baton to gain write access to the beamline. + """ if self.__base_url is None: logger.info("POST /baton/request") return @@ -1388,7 +1476,12 @@ class DAQWorker(QObject): @Slot(bool) def respond_to_baton_request(self, accept: bool): - """Respond to an incoming baton request.""" + """ + Respond to an incoming baton request from another user. + + Args: + accept: True to grant the baton, False to refuse. + """ if self.__base_url is None: logger.info(f"POST /baton/respond?accept={accept}") return