Beamline dispatch feature and tidy-up #120

Merged
perl_d merged 11 commits from feat/beamline_dispatch into main 2026-07-31 14:48:28 +02:00
79 changed files with 2449 additions and 2243 deletions
+63
View File
@@ -1,9 +1,15 @@
name: CI
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-latest
@@ -42,6 +48,63 @@ jobs:
index_username: ${{ secrets.GITEA_USER }}
index_password: ${{ secrets.GITEA_TOKEN }}
- name: Run Pytest
env:
QT_QPA_PLATFORM: offscreen
BEAMLINE: SIMULATED
run: |
source .venv/bin/activate
pytest ./tests/unit
test-with-beamline-plugins:
runs-on: ubuntu-latest
needs: ["lint", "test"]
strategy:
matrix:
plugin_repo: ["pxi_bec", "pxii_bec", "pxiii_bec"]
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Setup
uses: ./.gitea/actions/install
with:
python_version: "3.12"
index_username: ${{ secrets.GITEA_USER }}
index_password: ${{ secrets.GITEA_TOKEN }}
- name: Install beamline plugin
run: |
source .venv/bin/activate
git clone https://gitea.psi.ch/bec/mx_bec.git
git clone https://gitea.psi.ch/bec/${{ matrix.plugin_repo }}.git
uv pip install -e ./mx_bec
uv pip install -e ./${{ matrix.plugin_repo }}
- name: Run Pytest
env:
QT_QPA_PLATFORM: offscreen
BEAMLINE: SIMULATED
run: |
source .venv/bin/activate
pytest ./tests/unit
test-with-coverage:
runs-on: ubuntu-latest
needs: ["lint", "test", "test-with-beamline-plugins"]
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Setup
uses: ./.gitea/actions/install
with:
python_version: "3.12"
index_username: ${{ secrets.GITEA_USER }}
index_password: ${{ secrets.GITEA_TOKEN }}
- name: Run Pytest with Coverage
env:
QT_QPA_PLATFORM: offscreen
+12 -49
View File
@@ -1,23 +1,24 @@
import json
import threading
import time
from typing import Any, Dict
import cv2
import numpy as np
import zmq
import time
import threading
from typing import Dict, Any
class ImageStatsReceiver:
def __init__(self, zmq_url: str):
"""Initialize ZeroMQ receiver for image statistics calculation"""
context = zmq.Context()
self.__socket = context.socket(zmq.SUB)
self.__socket.setsockopt(zmq.SUBSCRIBE, b"")
self.__socket.setsockopt(zmq.RCVTIMEO, 1000) # 1 second timeout
self.__socket.setsockopt(zmq.LINGER, 0) # Don't linger on close
self._socket = context.socket(zmq.SUB)
self._socket.setsockopt(zmq.SUBSCRIBE, b"")
self._socket.setsockopt(zmq.RCVTIMEO, 1000) # 1 second timeout
self._socket.setsockopt(zmq.LINGER, 0) # Don't linger on close
try:
self.__socket.connect(zmq_url)
self._socket.connect(zmq_url)
print(f"Connected to ZeroMQ socket: {zmq_url}")
except Exception as e:
print(f"Failed to connect to {zmq_url}: {e}")
@@ -187,7 +188,7 @@ class ImageStatsReceiver:
def process_message(self):
"""Process a single ZeroMQ message"""
try:
r = self.__socket.recv_multipart(zmq.NOBLOCK)
r = self._socket.recv_multipart(zmq.NOBLOCK)
if len(r) != 2:
return None
@@ -373,8 +374,8 @@ class ImageStatsReceiver:
def stop(self):
"""Stop the receiver and all threads"""
self.running = False
if self.__socket:
self.__socket.close()
if self._socket:
self._socket.close()
print("Image stats receiver stopped")
@@ -415,16 +416,6 @@ def get_latest_image_stats():
return None
def get_latest_image_stats():
"""Get the latest image statistics including radial profile"""
global stats_receiver
if stats_receiver and stats_receiver.latest_stats:
with stats_receiver.stats_lock:
return stats_receiver.latest_stats.copy()
return None
def get_radial_profile():
"""Get just the radial profile data"""
global stats_receiver
@@ -441,31 +432,3 @@ def save_current_radial_profile(filename: str = None):
if stats_receiver:
return stats_receiver.save_radial_profile_to_file(filename)
return None
# # Auto-start when daq.py is imported/run
# if __name__ == "__main__":
# # If running daq.py directly
# start_image_stats_receiver()
#
# try:
# # Your existing daq.py code continues here
# print("DAQ system running with image statistics monitoring...")
#
# # Keep the program running
# while True:
# time.sleep(1)
#
# # Optional: Access latest stats in your main code
# latest = get_latest_image_stats()
# if latest and latest['message_count'] % 10 == 0: # Every 10th message
# print(f"Main thread sees: Mean={latest['mean']:.1f}")
#
# except KeyboardInterrupt:
# print("\nShutting down...")
# finally:
# stop_image_stats_receiver()
#
# else:
# # If daq.py is imported as a module, auto-start the receiver
# start_image_stats_receiver()
+1
View File
@@ -0,0 +1 @@
"""A single API to dispatch functions based on the current beamline"""
@@ -0,0 +1,24 @@
from aarecommon.config.beamline import MXBeamline, mx_beamline
from aare.beamline_dispatch.protocols import BeamlineDispatch
def get_beamline_dispatch() -> BeamlineDispatch:
"""Fetch the dispatch object for the beamline detected from the BEAMLINE environment variable."""
match mx_beamline():
case MXBeamline.SIMULATED:
from .simulated import SimulatedDispatch
return SimulatedDispatch()
case MXBeamline.X06DA:
from .x06da import X06daDispatch
return X06daDispatch()
case MXBeamline.X06SA:
from .x06sa import X06saDispatch
return X06saDispatch()
case MXBeamline.X10SA:
from .x10sa import X10saDispatch
return X10saDispatch()
@@ -0,0 +1,3 @@
from .beamline_dispatch import DefaultDispatch
__all__ = ["DefaultDispatch"]
@@ -0,0 +1,24 @@
import os
from aare.beamline_dispatch.protocols import AuthDispatch, BeamlineDispatch
class DefaultAuthDispatch(AuthDispatch):
def get_jwt_key(self) -> str:
if (key := os.environ.get("JWT_AAREDAQ_KEY")) is None:
raise Exception(
"JWT_AAREDAQ_KEY environment variable not set, cannot guarantee safe authentication."
)
return key
class DefaultDispatch(BeamlineDispatch):
"""Default implementation for anything which can vary between beamlines and/or simulation.
Should be safe and fail rather than assuming anything."""
def __init__(self) -> None:
self._auth = DefaultAuthDispatch()
@property
def auth(self):
return self._auth
+37
View File
@@ -0,0 +1,37 @@
from abc import ABC, abstractmethod
from typing import Any
class AuthDispatch(ABC):
@abstractmethod
def get_jwt_key(self) -> str: ...
class BecMacros(ABC):
@staticmethod
@abstractmethod
def save_and_reload() -> tuple[Any, Any]: ...
@staticmethod
@abstractmethod
def save_current_position(device, position, axis=None, force=False, max_delta=0.5) -> None: ...
@staticmethod
@abstractmethod
def init_beamline_environment() -> tuple[Any, Any]: ...
@staticmethod
@abstractmethod
def bl_energy(energy_ev, move_gap=True, mono_scan=True, plot=True): ...
@staticmethod
@abstractmethod
def get_current_energy(): ...
@staticmethod
@abstractmethod
def mono_pitch_scan(plot=True): ...
class BeamlineDispatch(ABC):
@property
@abstractmethod
def auth(self) -> AuthDispatch: ...
@property
@abstractmethod
def bec_macros(self) -> BecMacros: ...
@@ -0,0 +1,3 @@
from .beamline_dispatch import SimulatedDispatch
__all__ = ["SimulatedDispatch"]
@@ -0,0 +1,38 @@
from typing import Any
from aare.beamline_dispatch.default.beamline_dispatch import DefaultDispatch
from aare.beamline_dispatch.protocols import AuthDispatch, BecMacros
class SimulatedAuthDispatch(AuthDispatch):
def get_jwt_key(self) -> str:
return "Ns93ijN8VHv4ybvXaGNEDKUb3Sif4m4MYpfcEBIcs1h"
class SimulatedBecMacros(BecMacros):
@staticmethod
def save_and_reload() -> tuple[Any, Any]:
return (None, None)
@staticmethod
def save_current_position(device, position, axis=None, force=False, max_delta=0.5) -> None: ...
@staticmethod
def init_beamline_environment() -> tuple[Any, Any]:
return (None, None)
@staticmethod
def bl_energy(energy_ev, move_gap=True, mono_scan=True, plot=True): ...
@staticmethod
def get_current_energy(): ...
@staticmethod
def mono_pitch_scan(plot=True): ...
class SimulatedDispatch(DefaultDispatch):
def __init__(self) -> None:
self._auth = SimulatedAuthDispatch()
self._bec_macros = SimulatedBecMacros()
@property
def bec_macros(self):
return self._bec_macros
@@ -0,0 +1,3 @@
from .beamline_dispatch import X06daDispatch
__all__ = ["X06daDispatch"]
@@ -0,0 +1,4 @@
from aare.beamline_dispatch.default.beamline_dispatch import DefaultDispatch
class X06daDispatch(DefaultDispatch): ...
@@ -0,0 +1,3 @@
from .beamline_dispatch import X06saDispatch
__all__ = ["X06saDispatch"]
@@ -0,0 +1,4 @@
from aare.beamline_dispatch.default.beamline_dispatch import DefaultDispatch
class X06saDispatch(DefaultDispatch): ...
@@ -0,0 +1,3 @@
from .beamline_dispatch import X10saDispatch
__all__ = ["X10saDispatch"]
@@ -0,0 +1,48 @@
from typing import Any
from aare.beamline_dispatch.default.beamline_dispatch import DefaultDispatch
from aare.beamline_dispatch.protocols import BecMacros
class X10SaBecMacros(BecMacros):
def __init__(self) -> None:
from pxii_bec.macros.build_devices import ( # type: ignore
save_and_reload,
save_current_position,
)
from pxii_bec.macros.init_beamline import init_beamline_environment # type: ignore
from pxii_bec.macros.pxii_energy import ( # type: ignore
bl_energy,
get_current_energy,
mono_pitch_scan,
)
self.save_and_reload = save_and_reload
self.save_current_position = save_current_position
self.init_beamline_environment = init_beamline_environment
self.bl_energy = bl_energy
self.get_current_energy = get_current_energy
self.mono_pitch_scan = mono_pitch_scan
@staticmethod
def save_and_reload() -> tuple[Any, Any]: ...
@staticmethod
def save_current_position(device, position, axis=None, force=False, max_delta=0.5) -> None: ...
@staticmethod
def init_beamline_environment() -> tuple[Any, Any]: ...
@staticmethod
def bl_energy(energy_ev, move_gap=True, mono_scan=True, plot=True): ...
@staticmethod
def get_current_energy(): ...
@staticmethod
def mono_pitch_scan(plot=True): ...
class X10saDispatch(DefaultDispatch):
def __init__(self) -> None:
super().__init__()
self._bec_macros = X10SaBecMacros()
@property
def bec_macros(self) -> BecMacros:
return self._bec_macros
+27 -27
View File
@@ -64,16 +64,16 @@ class AareWrapper:
# Identity Forwarding (Optional now that mTLS is active, but safe to keep)
self.client.default_headers["X-Shared-Password"] = os.getenv("AAREDB_SHARED_PASSWORD")
self.__host = host
self.__tell_api = aareDB.TellsRunnerApi(self.client)
self.__sample_api = aareDB.SamplesRunnerApi(self.client)
self.__proc_api = aareDB.ProcessingsRunnerApi(self.client)
self.__raster_api = aareDB.GridscanRunnerApi(self.client)
self.__bl = bl
self._host = host
self._tell_api = aareDB.TellsRunnerApi(self.client)
self._sample_api = aareDB.SamplesRunnerApi(self.client)
self._proc_api = aareDB.ProcessingsRunnerApi(self.client)
self._raster_api = aareDB.GridscanRunnerApi(self.client)
self._bl = bl
self.__ssl_ca_cert = configuration.ssl_ca_cert
self.__cert_file = configuration.cert_file
self.__key_file = configuration.key_file
self._ssl_ca_cert = configuration.ssl_ca_cert
self._cert_file = configuration.cert_file
self._key_file = configuration.key_file
@log_timing(logger, "AareDB call")
def set_pucks_beamline(self, input_list: List[PuckLoadedInfo]):
@@ -84,8 +84,8 @@ class AareWrapper:
puck_name=i.puck_name, segment=i.location.segment, puck_in_segment=i.location.pos
)
o.append(t)
payload = SetTellPositionRequest(pucks=o, tell=self.__bl.value.upper())
ret = self.__tell_api.set_tell_positions(set_tell_position_request=payload)
payload = SetTellPositionRequest(pucks=o, tell=self._bl.value.upper())
ret = self._tell_api.set_tell_positions(set_tell_position_request=payload)
logger.debug(ret)
@log_timing(logger, "AareDB call")
@@ -97,7 +97,7 @@ class AareWrapper:
)
try:
s.db_id = self.__sample_api.insert_sample(manual_sample).id
s.db_id = self._sample_api.insert_sample(manual_sample).id
except Exception as e:
logger.error(f"Error inserting sample: {e}")
@@ -114,7 +114,7 @@ class AareWrapper:
)
return
try:
self.__sample_api.create_sample_event(
self._sample_api.create_sample_event(
sample_id=sample_id,
sample_event_create=SampleEventCreate(event_type=event_type, comment=comment),
)
@@ -127,7 +127,7 @@ class AareWrapper:
):
_, buffer = cv2.imencode(".jpg", bgr_image)
jpeg_bytes = io.BytesIO(buffer)
url = f"{self.__host}/protected_router/sample_runner/{sample_id}/upload-images"
url = f"{self._host}/protected_router/sample_runner/{sample_id}/upload-images"
headers = {
"accept": "application/json",
"X-Shared-Password": os.getenv("AAREDB_SHARED_PASSWORD"),
@@ -135,8 +135,8 @@ class AareWrapper:
request_kwargs = {
"files": {"uploaded_file": (filename + ".jpg", jpeg_bytes, "image/jpeg")},
"verify": self.__ssl_ca_cert,
"cert": (self.__cert_file, self.__key_file),
"verify": self._ssl_ca_cert,
"cert": (self._cert_file, self._key_file),
"headers": headers,
}
if message is not None:
@@ -147,15 +147,15 @@ class AareWrapper:
@log_timing(logger, "AareDB call")
def upload_jpg(self, sample_id: int, filename: str, jpg_image, message: Optional[str] = None):
logger.debug(f"jppg_image of type: {type(jpg_image)}")
url = f"{self.__host}/protected_router/sample_runner/{sample_id}/upload-images"
url = f"{self._host}/protected_router/sample_runner/{sample_id}/upload-images"
headers = {
"accept": "application/json",
"X-Shared-Password": os.getenv("AAREDB_SHARED_PASSWORD"),
}
request_kwargs = {
"files": {"uploaded_file": (filename + ".jpg", jpg_image, "image/jpeg")},
"verify": self.__ssl_ca_cert,
"cert": (self.__cert_file, self.__key_file),
"verify": self._ssl_ca_cert,
"cert": (self._cert_file, self._key_file),
"headers": headers,
}
if message is not None:
@@ -229,7 +229,7 @@ class AareWrapper:
experiment_params_payload = ExperimentParametersCreate(
type="standard", beamline_parameters=beamline_params, sample_id=s.db_id
)
self.__sample_api.create_experiment_parameters_for_sample(
self._sample_api.create_experiment_parameters_for_sample(
sample_id=s.db_id, experiment_parameters_create=experiment_params_payload
)
# logger.debug("Experiment parameters created:", response)
@@ -291,7 +291,7 @@ class AareWrapper:
experiment_params_payload = ExperimentParametersCreate(
type="standard", beamline_parameters=beamline_params, sample_id=s.db_id
)
self.__sample_api.create_experiment_parameters_for_sample(
self._sample_api.create_experiment_parameters_for_sample(
sample_id=s.db_id, experiment_parameters_create=experiment_params_payload
)
# logger.info("Experiment parameters created:", response)
@@ -319,7 +319,7 @@ class AareWrapper:
return
payload = payload_model.model_dump()
url = f"{self.__host}/protected_router/gridscan_runner/ingest"
url = f"{self._host}/protected_router/gridscan_runner/ingest"
headers = {
"accept": "application/json",
"X-Shared-Password": os.getenv("AAREDB_SHARED_PASSWORD"),
@@ -330,8 +330,8 @@ class AareWrapper:
headers=headers,
data=json.dumps(payload),
timeout=30,
verify=self.__ssl_ca_cert,
cert=(self.__cert_file, self.__key_file),
verify=self._ssl_ca_cert,
cert=(self._cert_file, self._key_file),
)
response.raise_for_status()
@@ -417,7 +417,7 @@ class AareWrapper:
return
payload = payload_model.model_dump()
url = f"{self.__host}/protected_router/scan_runner/ingest"
url = f"{self._host}/protected_router/scan_runner/ingest"
headers = {
"accept": "application/json",
"X-Shared-Password": os.getenv("AAREDB_SHARED_PASSWORD"),
@@ -428,8 +428,8 @@ class AareWrapper:
headers=headers,
data=json.dumps(payload),
timeout=30,
verify=self.__ssl_ca_cert,
cert=(self.__cert_file, self.__key_file),
verify=self._ssl_ca_cert,
cert=(self._cert_file, self._key_file),
)
response.raise_for_status()
+17 -22
View File
@@ -1,5 +1,4 @@
import grp
import ipaddress
import logging
import os
import pwd
@@ -20,16 +19,12 @@ from fastapi import Depends, Request
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel
from aare.beamline_dispatch.protocols import AuthDispatch
from aare.daq.config import BeamlineConfig
logger = logging.getLogger("aareDAQ")
if os.environ.get("JWT_AAREDAQ_KEY") is None:
raise Exception(
"JWT_AAREDAQ_KEY environment variable not set, cannot guarantee safe authentication."
)
SECRET_KEY = os.environ.get("JWT_AAREDAQ_KEY")
_SECRET_KEY: str | None = None
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 24 * 60 * 7 # 1 week
SESSION_EXPIRE_SECONDS = 60 * 10
@@ -48,26 +43,26 @@ class TokenData(BaseModel):
staff: bool = False
def init_jwt_key(dispatch: AuthDispatch):
global _SECRET_KEY
_SECRET_KEY = dispatch.get_jwt_key()
def jwt_key() -> str:
if _SECRET_KEY is None:
raise RuntimeError(
"Run auth.init_jwt_key() to initialize it from the environment before trying to use the key."
)
return _SECRET_KEY
def create_access_token(token: TokenData):
to_encode = token.model_dump()
to_encode.update({"exp": datetime.now(UTC) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
encoded_jwt = jwt.encode(to_encode, jwt_key(), algorithm=ALGORITHM)
return encoded_jwt
def _is_loopback(host: str | None) -> bool:
if host is None:
return False
try:
addr = ipaddress.ip_address(host)
# Unwrap IPv4-mapped IPv6 (e.g. ::ffff:127.0.0.1) before checking
if isinstance(addr, ipaddress.IPv6Address) and addr.ipv4_mapped is not None:
return addr.ipv4_mapped.is_loopback
return addr.is_loopback
except ValueError:
return False
def authenticate_from_proxy_header(request: Request) -> str:
"""
Extract the pre-authenticated username from the trusted Apache proxy header.
@@ -104,7 +99,7 @@ def authenticate_user(cfg: BeamlineConfig, username: str) -> str:
def parse_token(token: str = Depends(oauth2_scheme)) -> TokenData:
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
payload = jwt.decode(token, jwt_key(), algorithms=[ALGORITHM])
token = TokenData(**payload)
return token
except jwt.PyJWTError as e:
+169 -169
View File
@@ -40,6 +40,7 @@ from aarecommon.models.models import (
ZoomModel,
zoom_manager,
)
from aare.daq.config_model import LocalContactConfigModel
# TODO WHAT SHOULD THIS BE? This should be in the YAMl file it is beamline specific
@@ -94,13 +95,13 @@ class BeamlineConfig:
GUI_SESSION_EXPIRE_SECONDS = 60 * 10
def __init__(self, bl: MXBeamline):
self.__mxb = bl
self.__bl = bl.value.lower()
self._mxb = bl
self._bl = bl.value.lower()
if bl is MXBeamline.SIMULATED:
host = "localhost"
else:
host = cfg_get("daq.hardware.redis_url", f"{self.__bl}-redis.psi.ch")
self.__client = redis.Redis(host=host, port=6379, db=0, decode_responses=True)
host = cfg_get("daq.hardware.redis_url", f"{self._bl}-redis.psi.ch")
self._client = redis.Redis(host=host, port=6379, db=0, decode_responses=True)
self.simulated_detector = bl is MXBeamline.SIMULATED
self._initialize_optional_yaml_defaults()
@@ -127,17 +128,17 @@ class BeamlineConfig:
# GUI session management
def _gui_sessions_index_key(self) -> str:
return f"{self.__bl}:gui_sessions"
return f"{self._bl}:gui_sessions"
def _gui_session_key(self, session: int) -> str:
return f"{self.__bl}:gui_session:{session}"
return f"{self._bl}:gui_session:{session}"
def _write_gui_session(
self, payload: OpenGuiSessionInfo, expiry_sec: int | None = None
) -> None:
expiry = int(expiry_sec or self.GUI_SESSION_EXPIRE_SECONDS)
pipe = self.__client.pipeline()
pipe = self._client.pipeline()
pipe.set(self._gui_session_key(payload.session), payload.model_dump_json())
pipe.expire(self._gui_session_key(payload.session), expiry)
pipe.sadd(self._gui_sessions_index_key(), payload.session)
@@ -145,7 +146,7 @@ class BeamlineConfig:
def _current_gui_session_ttl(self, session: int) -> int | None:
try:
ttl = int(self.__client.ttl(self._gui_session_key(session)))
ttl = int(self._client.ttl(self._gui_session_key(session)))
except Exception:
return None
@@ -154,7 +155,7 @@ class BeamlineConfig:
return None
def _read_gui_session(self, session: int) -> OpenGuiSessionInfo | None:
raw = self.__client.get(self._gui_session_key(session))
raw = self._client.get(self._gui_session_key(session))
if raw is None:
return None
try:
@@ -192,7 +193,7 @@ class BeamlineConfig:
) -> OpenGuiSessionInfo | None:
payload = self._read_gui_session(session)
if payload is None:
self.__client.srem(self._gui_sessions_index_key(), session)
self._client.srem(self._gui_sessions_index_key(), session)
return None
payload.last_interaction_ts = last_interaction_ts
@@ -205,7 +206,7 @@ class BeamlineConfig:
) -> OpenGuiSessionInfo | None:
payload = self._read_gui_session(session)
if payload is None:
self.__client.srem(self._gui_sessions_index_key(), session)
self._client.srem(self._gui_sessions_index_key(), session)
return None
payload.close_requested = True
@@ -220,7 +221,7 @@ class BeamlineConfig:
def clear_gui_close_request(self, session: int) -> None:
payload = self._read_gui_session(session)
if payload is None:
self.__client.srem(self._gui_sessions_index_key(), session)
self._client.srem(self._gui_sessions_index_key(), session)
return
payload.close_requested = False
@@ -232,29 +233,29 @@ class BeamlineConfig:
self._write_gui_session(payload, expiry_sec=ttl)
def remove_gui_session(self, session: int) -> None:
pipe = self.__client.pipeline()
pipe = self._client.pipeline()
pipe.delete(self._gui_session_key(session))
pipe.srem(self._gui_sessions_index_key(), session)
pipe.execute()
self.purge_expired_gui_sessions()
def purge_expired_gui_sessions(self) -> None:
session_ids = self.__client.smembers(self._gui_sessions_index_key())
session_ids = self._client.smembers(self._gui_sessions_index_key())
if not session_ids:
return
expired_ids: list[str] = []
for session_id in session_ids:
if not self.__client.exists(self._gui_session_key(int(session_id))):
if not self._client.exists(self._gui_session_key(int(session_id))):
expired_ids.append(session_id)
if expired_ids:
self.__client.srem(self._gui_sessions_index_key(), *expired_ids)
self._client.srem(self._gui_sessions_index_key(), *expired_ids)
def get_open_gui_sessions(self) -> list[OpenGuiSessionInfo]:
self.purge_expired_gui_sessions()
session_ids = self.__client.smembers(self._gui_sessions_index_key())
session_ids = self._client.smembers(self._gui_sessions_index_key())
if not session_ids:
return []
@@ -273,7 +274,7 @@ class BeamlineConfig:
def get_gui_session(self, session: int) -> OpenGuiSessionInfo | None:
payload = self._read_gui_session(session)
if payload is None:
self.__client.srem(self._gui_sessions_index_key(), session)
self._client.srem(self._gui_sessions_index_key(), session)
return None
holder = self.baton_holder
@@ -284,7 +285,7 @@ class BeamlineConfig:
@property
def allow_non_staff_request_from_staff(self) -> bool:
raw = self.__client.get(f"{self.__bl}:allow_non_staff_request_from_staff")
raw = self._client.get(f"{self._bl}:allow_non_staff_request_from_staff")
if raw is None:
return False
return str(raw).strip().lower() in {"1", "true", "yes", "on"}
@@ -292,12 +293,12 @@ class BeamlineConfig:
@allow_non_staff_request_from_staff.setter
def allow_non_staff_request_from_staff(self, enabled: bool) -> None:
if enabled:
self.__client.set(f"{self.__bl}:allow_non_staff_request_from_staff", "1")
self._client.set(f"{self._bl}:allow_non_staff_request_from_staff", "1")
else:
self.__client.delete(f"{self.__bl}:allow_non_staff_request_from_staff")
self._client.delete(f"{self._bl}:allow_non_staff_request_from_staff")
def generate_session(self) -> int:
return int(self.__client.incr(f"{self.__bl}:session"))
return int(self._client.incr(f"{self._bl}:session"))
@property
def active_session(self) -> int | None:
@@ -342,60 +343,60 @@ class BeamlineConfig:
return SessionsStateEnum.OwnedByElse
def try_set_active_session(self, session: int, expiry_sec: int) -> None:
with redis_lock.Lock(self.__client, f"{self.__bl}:active_session_lock", expire=10):
with redis_lock.Lock(self._client, f"{self._bl}:active_session_lock", expire=10):
active = self.active_session
if active is None:
self.__client.set(f"{self.__bl}:active_session", session)
self._client.set(f"{self._bl}:active_session", session)
elif active != session:
raise Exception(
"There is already active session with different id. Try again later."
)
self.__client.expire(f"{self.__bl}:active_session", expiry_sec)
self._client.expire(f"{self._bl}:active_session", expiry_sec)
# TODO finish setting this up!
def try_extend_active_session(self, session: int, expiry_sec: int) -> None:
with redis_lock.Lock(self.__client, f"{self.__bl}:active_session_lock", expire=10):
with redis_lock.Lock(self._client, f"{self._bl}:active_session_lock", expire=10):
active = self.active_session
if active is None:
raise Exception("There is no active session with given id. Try again later.")
# if active == session:
# self.__client.expire(f"{self.__bl}:active_session", expiry_sec, gt=True)
# self._client.expire(f"{self._bl}:active_session", expiry_sec, gt=True)
if active == session:
key = f"{self.__bl}:active_session"
current_ttl = self.__client.ttl(key)
key = f"{self._bl}:active_session"
current_ttl = self._client.ttl(key)
# Redis compatibility:
# some Redis servers do not support EXPIRE with GT option.
# Emulate "extend only if greater" manually while holding the lock.
if current_ttl is None or current_ttl < 0 or current_ttl < expiry_sec:
self.__client.expire(key, expiry_sec)
self._client.expire(key, expiry_sec)
else:
raise Exception(
"There is already active session with different id. Try again later."
)
def end_active_session(self, session: int) -> None:
with redis_lock.Lock(self.__client, f"{self.__bl}:active_session_lock", expire=10):
with redis_lock.Lock(self._client, f"{self._bl}:active_session_lock", expire=10):
active = self.active_session
if active is None:
return
if active == session:
self.__client.delete(f"{self.__bl}:active_session")
self.__client.delete(f"{self.__bl}:baton_holder")
self._client.delete(f"{self._bl}:active_session")
self._client.delete(f"{self._bl}:baton_holder")
def force_set_active_session(self, session: int, expiry_sec: int) -> None:
# Ensure that there is no active try-set for active session
with redis_lock.Lock(self.__client, f"{self.__bl}:active_session_lock", expire=10):
self.__client.set(f"{self.__bl}:active_session", session)
self.__client.expire(f"{self.__bl}:active_session", expiry_sec)
with redis_lock.Lock(self._client, f"{self._bl}:active_session_lock", expire=10):
self._client.set(f"{self._bl}:active_session", session)
self._client.expire(f"{self._bl}:active_session", expiry_sec)
# ========== BATON SYSTEM ==========
@property
def baton_holder(self) -> BatonHolderInfo | None:
"""Get information about the current baton holder."""
tmp = self.__client.get(f"{self.__bl}:baton_holder")
tmp = self._client.get(f"{self._bl}:baton_holder")
if tmp is None:
return None
try:
@@ -406,14 +407,14 @@ class BeamlineConfig:
@baton_holder.setter
def baton_holder(self, info: BatonHolderInfo | None) -> None:
if info is None:
self.__client.delete(f"{self.__bl}:baton_holder")
self._client.delete(f"{self._bl}:baton_holder")
else:
self.__client.set(f"{self.__bl}:baton_holder", info.model_dump_json())
self._client.set(f"{self._bl}:baton_holder", info.model_dump_json())
@property
def pending_baton_request(self) -> BatonRequest | None:
"""Get the current pending baton request, if any."""
tmp = self.__client.get(f"{self.__bl}:baton_request")
tmp = self._client.get(f"{self._bl}:baton_request")
if tmp is None:
return None
try:
@@ -426,19 +427,19 @@ class BeamlineConfig:
) -> None:
"""Set a pending baton request with auto-expiry for timeout."""
if request is None:
self.__client.delete(f"{self.__bl}:baton_request")
self._client.delete(f"{self._bl}:baton_request")
else:
self.__client.set(f"{self.__bl}:baton_request", request.model_dump_json())
self._client.set(f"{self._bl}:baton_request", request.model_dump_json())
# Add a few seconds buffer so we can detect timeout vs expiry
self.__client.expire(f"{self.__bl}:baton_request", timeout_sec + 5)
self._client.expire(f"{self._bl}:baton_request", timeout_sec + 5)
def clear_pending_baton_request(self) -> None:
self.__client.delete(f"{self.__bl}:baton_request")
self._client.delete(f"{self._bl}:baton_request")
@property
def queued_baton_transfer(self) -> BatonTransferQueue | None:
"""Get queued transfer waiting for beamline to be available."""
tmp = self.__client.get(f"{self.__bl}:baton_transfer_queue")
tmp = self._client.get(f"{self._bl}:baton_transfer_queue")
if tmp is None:
return None
try:
@@ -449,9 +450,9 @@ class BeamlineConfig:
@queued_baton_transfer.setter
def queued_baton_transfer(self, transfer: BatonTransferQueue | None) -> None:
if transfer is None:
self.__client.delete(f"{self.__bl}:baton_transfer_queue")
self._client.delete(f"{self._bl}:baton_transfer_queue")
else:
self.__client.set(f"{self.__bl}:baton_transfer_queue", transfer.model_dump_json())
self._client.set(f"{self._bl}:baton_transfer_queue", transfer.model_dump_json())
def can_transfer_baton_now(self) -> bool:
"""Check if baton can be transferred (beamline not mid-operation)."""
@@ -475,9 +476,9 @@ class BeamlineConfig:
Atomically transfer the baton to a new holder.
Use existing active_session_lock for consistency.
"""
with redis_lock.Lock(self.__client, f"{self.__bl}:active_session_lock", expire=10):
self.__client.set(f"{self.__bl}:active_session", to_session)
self.__client.expire(f"{self.__bl}:active_session", expiry_sec)
with redis_lock.Lock(self._client, f"{self._bl}:active_session_lock", expire=10):
self._client.set(f"{self._bl}:active_session", to_session)
self._client.expire(f"{self._bl}:active_session", expiry_sec)
self.baton_holder = BatonHolderInfo(
username=to_username, session=to_session, is_staff=to_is_staff, pgroup=to_pgroup
)
@@ -508,19 +509,19 @@ class BeamlineConfig:
@property
def pgroup(self) -> str | None:
tmp = self.__client.get(f"{self.__bl}:pgroup")
tmp = self._client.get(f"{self._bl}:pgroup")
return tmp
@pgroup.setter
def pgroup(self, pgroup: str | None) -> None:
if pgroup is None:
self.__client.delete(f"{self.__bl}:pgroup")
self._client.delete(f"{self._bl}:pgroup")
else:
self.__client.set(f"{self.__bl}:pgroup", pgroup)
self._client.set(f"{self._bl}:pgroup", pgroup)
@property
def commissioning_mode(self) -> bool:
tmp = self.__client.get(f"{self.__bl}:commissioning_mode")
tmp = self._client.get(f"{self._bl}:commissioning_mode")
if tmp is None:
return False
return True
@@ -528,19 +529,19 @@ class BeamlineConfig:
@commissioning_mode.setter
def commissioning_mode(self, commisioning_mode: bool) -> None:
if commisioning_mode:
self.__client.set(f"{self.__bl}:commissioning_mode", "1")
self._client.set(f"{self._bl}:commissioning_mode", "1")
else:
self.__client.delete(f"{self.__bl}:commissioning_mode")
self._client.delete(f"{self._bl}:commissioning_mode")
# Beamline state management
# Atomic check if beamline is busy and if not set state to busy
def try_set_busy(self, timeout: int | None = None):
with redis_lock.Lock(self.__client, f"{self.__bl}:move_state_lock", expire=10):
with redis_lock.Lock(self._client, f"{self._bl}:move_state_lock", expire=10):
if self.state_busy:
raise BeamlineBusyException("Beamline is busy")
self.state_busy = True
if timeout is not None:
self.__client.expire(f"{self.__bl}:busy", timeout)
self._client.expire(f"{self._bl}:busy", timeout)
def set_busy(self, target: BeamlineStateEnum, timeout: int | None = None):
self.try_set_busy(timeout=timeout)
@@ -562,7 +563,7 @@ class BeamlineConfig:
@property
def state(self) -> BeamlineStateEnum:
raw_value = self.__client.get(f"{self.__bl}:state")
raw_value = self._client.get(f"{self._bl}:state")
if raw_value is None:
return BeamlineStateEnum.Maintenance
@@ -576,24 +577,24 @@ class BeamlineConfig:
@state.setter
def state(self, state: BeamlineStateEnum):
self.__client.set(f"{self.__bl}:state", state.value)
self._client.set(f"{self._bl}:state", state.value)
@property
def state_busy(self) -> bool:
return self.__client.get(f"{self.__bl}:busy") is not None
return self._client.get(f"{self._bl}:busy") is not None
@state_busy.setter
def state_busy(self, i: bool):
if i:
self.__client.set(f"{self.__bl}:busy", "1")
self._client.set(f"{self._bl}:busy", "1")
else:
self.__client.delete(f"{self.__bl}:busy")
self._client.delete(f"{self._bl}:busy")
# Other beamline settings
@property
def tell_mount_count(self) -> int:
return int(self.__client.incr(f"{self.__bl}:tell_mount_count"))
return int(self._client.incr(f"{self._bl}:tell_mount_count"))
def pixel_to_mm(self, zoom: float) -> float:
cfg = self.settings
@@ -635,8 +636,8 @@ class BeamlineConfig:
@property
def beam_center(self) -> Tuple[float, float]:
tmp_x = self.__client.get(f"{self.__bl}:beam_center_x")
tmp_y = self.__client.get(f"{self.__bl}:beam_center_y")
tmp_x = self._client.get(f"{self._bl}:beam_center_x")
tmp_y = self._client.get(f"{self._bl}:beam_center_y")
if tmp_x:
val_x = float(tmp_x)
else:
@@ -649,13 +650,13 @@ class BeamlineConfig:
@beam_center.setter
def beam_center(self, data: Tuple[float, float]):
self.__client.set(f"{self.__bl}:beam_center_x", data[0])
self.__client.set(f"{self.__bl}:beam_center_y", data[1])
self._client.set(f"{self._bl}:beam_center_x", data[0])
self._client.set(f"{self._bl}:beam_center_y", data[1])
@property
def beam_size_mm(self) -> Coordinate:
tmp_x = self.__client.get(f"{self.__bl}:beam_size_x")
tmp_y = self.__client.get(f"{self.__bl}:beam_size_y")
tmp_x = self._client.get(f"{self._bl}:beam_size_x")
tmp_y = self._client.get(f"{self._bl}:beam_size_y")
if tmp_x:
val_x = float(tmp_x)
else:
@@ -668,11 +669,11 @@ class BeamlineConfig:
@beam_size_mm.setter
def beam_size_mm(self, data: Coordinate):
self.__client.set(f"{self.__bl}:beam_size_x", data.x)
self.__client.set(f"{self.__bl}:beam_size_y", data.y)
self._client.set(f"{self._bl}:beam_size_x", data.x)
self._client.set(f"{self._bl}:beam_size_y", data.y)
def __get_settings(self) -> BeamlineSettingsModel:
tmp = self.__client.get(f"{self.__bl}:settings")
def _get_settings(self) -> BeamlineSettingsModel:
tmp = self._client.get(f"{self._bl}:settings")
if tmp is None:
return BeamlineSettingsModel()
@@ -682,19 +683,19 @@ class BeamlineConfig:
@property
def settings(self) -> BeamlineSettingsModel:
with redis_lock.Lock(self.__client, f"{self.__bl}:settings_lock", expire=10):
return self.__get_settings()
with redis_lock.Lock(self._client, f"{self._bl}:settings_lock", expire=10):
return self._get_settings()
@settings.setter
def settings(self, data: BeamlineSettingsModel):
with redis_lock.Lock(self.__client, f"{self.__bl}:settings_lock", expire=10):
current = self.__get_settings()
with redis_lock.Lock(self._client, f"{self._bl}:settings_lock", expire=10):
current = self._get_settings()
updated_data = current.model_copy(update=data.model_dump(exclude_unset=True))
self.__client.set(f"{self.__bl}:settings", updated_data.model_dump_json())
self._client.set(f"{self._bl}:settings", updated_data.model_dump_json())
@property
def cryojet_settings(self) -> CryojetSettingsModel:
tmp = self.__client.get(f"{self.__bl}:cryojet_settings")
tmp = self._client.get(f"{self._bl}:cryojet_settings")
if tmp is None:
return CryojetSettingsModel()
@@ -703,17 +704,17 @@ class BeamlineConfig:
@cryojet_settings.setter
def cryojet_settings(self, data: CryojetSettingsModel):
self.__client.set(f"{self.__bl}:cryojet_settings", data.model_dump_json())
self._client.set(f"{self._bl}:cryojet_settings", data.model_dump_json())
def get_alc_bkg(self, zoom: float, exp: float, gain: float) -> np.ndarray | None:
return base64_to_numpy(self.__client.get(f"{self.__bl}:bkg{zoom:.1f}_{exp:.2f}_{gain:.1f}"))
return base64_to_numpy(self._client.get(f"{self._bl}:bkg{zoom:.1f}_{exp:.2f}_{gain:.1f}"))
def put_alc_bkg(self, zoom: float, exp: float, gain: float, data: np.ndarray):
self.__client.set(f"{self.__bl}:bkg{zoom:.1f}_{exp:.2f}_{gain:.1f}", numpy_to_base64(data))
self._client.set(f"{self._bl}:bkg{zoom:.1f}_{exp:.2f}_{gain:.1f}", numpy_to_base64(data))
@property
def spreadsheet(self) -> SampleShortInfoList:
tmp = self.__client.get(f"{self.__bl}:sample_spreadsheet")
tmp = self._client.get(f"{self._bl}:sample_spreadsheet")
if tmp is None:
return SampleShortInfoList(s=[])
@@ -727,17 +728,17 @@ class BeamlineConfig:
@spreadsheet.setter
def spreadsheet(self, data: SampleShortInfoList):
self.__client.set(f"{self.__bl}:sample_spreadsheet", data.model_dump_json())
self._client.set(f"{self._bl}:sample_spreadsheet", data.model_dump_json())
def listen_changes_spreadsheet(self) -> redis.client.PubSub:
self.__client.config_set("notify-keyspace-events", "KEA")
pubsub = self.__client.pubsub()
pubsub.psubscribe(f"__keyspace@0__:{self.__bl}:sample_spreadsheet")
self._client.config_set("notify-keyspace-events", "KEA")
pubsub = self._client.pubsub()
pubsub.psubscribe(f"__keyspace@0__:{self._bl}:sample_spreadsheet")
return pubsub
@property
def reference_tools(self) -> SampleShortInfoList:
tmp = self.__client.get(f"{self.__bl}:reference-tools")
tmp = self._client.get(f"{self._bl}:reference-tools")
if tmp is None:
return SampleShortInfoList(s=[])
@@ -746,17 +747,17 @@ class BeamlineConfig:
@reference_tools.setter
def reference_tools(self, data: SampleShortInfoList):
self.__client.set(f"{self.__bl}:reference-tools", data.model_dump_json())
self._client.set(f"{self._bl}:reference-tools", data.model_dump_json())
def listen_changes_reference_tools(self) -> redis.client.PubSub:
self.__client.config_set("notify-keyspace-events", "KEA")
pubsub = self.__client.pubsub()
pubsub.psubscribe(f"__keyspace@0__:{self.__bl}:reference-tools")
self._client.config_set("notify-keyspace-events", "KEA")
pubsub = self._client.pubsub()
pubsub.psubscribe(f"__keyspace@0__:{self._bl}:reference-tools")
return pubsub
@property
def current_sample(self) -> SampleShortInfo | None:
tmp = self.__client.get(f"{self.__bl}:current_sample")
tmp = self._client.get(f"{self._bl}:current_sample")
if tmp is None:
return None
@@ -766,13 +767,13 @@ class BeamlineConfig:
@current_sample.setter
def current_sample(self, sample: SampleShortInfo | None):
if sample is None:
self.__client.delete(f"{self.__bl}:current_sample")
self._client.delete(f"{self._bl}:current_sample")
else:
self.__client.set(f"{self.__bl}:current_sample", sample.model_dump_json())
self._client.set(f"{self._bl}:current_sample", sample.model_dump_json())
@property
def beam_mark_coeff(self) -> BeamMarkCoeffModel:
tmp = self.__client.get(f"{self.__bl}:beam_center_camera")
tmp = self._client.get(f"{self._bl}:beam_center_camera")
if tmp is None:
return BeamMarkCoeffModel()
@@ -781,12 +782,12 @@ class BeamlineConfig:
@beam_mark_coeff.setter
def beam_mark_coeff(self, data: BeamMarkCoeffModel):
self.__client.set(f"{self.__bl}:beam_center_camera", data.model_dump_json())
self._client.set(f"{self._bl}:beam_center_camera", data.model_dump_json())
# TODO tidy up zoom functions
@property
def zoom_mode(self) -> ZoomModeEnum:
raw_value = self.__client.get(f"{self.__bl}:zoom_mode")
raw_value = self._client.get(f"{self._bl}:zoom_mode")
if raw_value is None:
print("no zoom mode given, defaulting to user mode")
return ZoomModeEnum.User
@@ -800,7 +801,7 @@ class BeamlineConfig:
@zoom_mode.setter
def zoom_mode(self, mode: ZoomModeEnum):
self.__client.set(f"{self.__bl}:zoom_mode", mode.value)
self._client.set(f"{self._bl}:zoom_mode", mode.value)
@staticmethod
def zoom_setting_string(mode: ZoomModeEnum = ZoomModeEnum.User) -> str:
@@ -820,9 +821,9 @@ class BeamlineConfig:
mode = self.zoom_mode
if not mode or not isinstance(mode, ZoomModeEnum):
raise Exception("incorrect zoom settings mode used")
tmp = self.__client.get(f"{self.__bl}:{self.zoom_setting_string(mode)}")
tmp = self._client.get(f"{self._bl}:{self.zoom_setting_string(mode)}")
if tmp is None:
return zoom_manager(mode, self.__mxb)
return zoom_manager(mode, self._mxb)
data_dict = json.loads(tmp)
return ZoomModel(**data_dict)
@@ -831,7 +832,7 @@ class BeamlineConfig:
mode = self.zoom_mode
if not mode or not isinstance(mode, ZoomModeEnum):
raise Exception("incorrect zoom settings mode used")
self.__client.set(f"{self.__bl}:{self.zoom_setting_string(mode)}", data.model_dump_json())
self._client.set(f"{self._bl}:{self.zoom_setting_string(mode)}", data.model_dump_json())
def save_zoom_camera_setting(
self, zoom_value: float, settings: SampleCameraSettings, mode: ZoomModeEnum | None = None
@@ -840,15 +841,15 @@ class BeamlineConfig:
mode's table (persisted to Redis under that mode's key). Defaults to the
active zoom mode; pass ``mode`` to target a specific one explicitly."""
mode = mode or self.zoom_mode
key = f"{self.__bl}:{self.zoom_setting_string(mode)}"
tmp = self.__client.get(key)
model = ZoomModel(**json.loads(tmp)) if tmp is not None else zoom_manager(mode, self.__mxb)
key = f"{self._bl}:{self.zoom_setting_string(mode)}"
tmp = self._client.get(key)
model = ZoomModel(**json.loads(tmp)) if tmp is not None else zoom_manager(mode, self._mxb)
model.z[zoom_value] = settings
self.__client.set(key, model.model_dump_json())
self._client.set(key, model.model_dump_json())
@property
def abr_meas_pos(self) -> AerotechCoordinate:
tmp = self.__client.get(f"{self.__bl}:abr_meas_pos")
tmp = self._client.get(f"{self._bl}:abr_meas_pos")
if tmp is None:
return ABR_POS_MOUNT
@@ -857,33 +858,33 @@ class BeamlineConfig:
@abr_meas_pos.setter
def abr_meas_pos(self, data: AerotechCoordinate):
self.__client.set(f"{self.__bl}:abr_meas_pos", data.model_dump_json())
self._client.set(f"{self._bl}:abr_meas_pos", data.model_dump_json())
@property
def dtz(self) -> float | None:
tmp = self.__client.get(f"{self.__bl}:dtz")
tmp = self._client.get(f"{self._bl}:dtz")
if tmp is None:
return None
return float(tmp)
@dtz.setter
def dtz(self, dtz: float):
self.__client.set(f"{self.__bl}:dtz", dtz)
self._client.set(f"{self._bl}:dtz", dtz)
@property
def dtz_safe_position(self) -> float | None:
tmp = self.__client.get(f"{self.__bl}:dtz_safe_position")
tmp = self._client.get(f"{self._bl}:dtz_safe_position")
if tmp is None:
return None
return float(tmp)
@dtz_safe_position.setter
def dtz_safe_position(self, dtz: float):
self.__client.set(f"{self.__bl}:dtz_safe_position", dtz)
self._client.set(f"{self._bl}:dtz_safe_position", dtz)
@property
def xrf(self) -> FluorescenceSpectrumOutputModel | None:
tmp = self.__client.get(f"{self.__bl}:xrf")
tmp = self._client.get(f"{self._bl}:xrf")
if tmp is None:
return None
data_dict = json.loads(tmp)
@@ -892,18 +893,18 @@ class BeamlineConfig:
@xrf.setter
def xrf(self, data: FluorescenceSpectrumOutputModel | None):
if data is None:
self.__client.delete(f"{self.__bl}:xrf")
self._client.delete(f"{self._bl}:xrf")
else:
self.__client.set(f"{self.__bl}:xrf", data.model_dump_json())
self._client.set(f"{self._bl}:xrf", data.model_dump_json())
def clear_mark_beam(self):
self.__client.delete(f"{self.__bl}:beam_mark")
self._client.delete(f"{self._bl}:beam_mark")
def mark_beam(self, x_pxl: float, y_pxl: float, zoom: float):
self.__client.hset(
f"{self.__bl}:beam_mark", mapping={f"{zoom}": json.dumps({"x": x_pxl, "y": y_pxl})}
self._client.hset(
f"{self._bl}:beam_mark", mapping={f"{zoom}": json.dumps({"x": x_pxl, "y": y_pxl})}
)
vals = self.__client.hgetall(f"{self.__bl}:beam_mark")
vals = self._client.hgetall(f"{self._bl}:beam_mark")
if len(vals) >= 3:
zooms = []
@@ -928,7 +929,7 @@ class BeamlineConfig:
@property
def crystal_size(self) -> CrystalSize:
tmp = self.__client.get(f"{self.__bl}:crystal_size")
tmp = self._client.get(f"{self._bl}:crystal_size")
if tmp is None:
return CrystalSize(x=0, y=0, z=0)
data_dict = json.loads(tmp)
@@ -936,11 +937,11 @@ class BeamlineConfig:
@crystal_size.setter
def crystal_size(self, xtal_size: CrystalSize):
self.__client.set(f"{self.__bl}:crystal_size", xtal_size.model_dump_json())
self._client.set(f"{self._bl}:crystal_size", xtal_size.model_dump_json())
@property
def last_best_res(self) -> float | None:
tmp = self.__client.get(f"{self.__bl}:last_best_res")
tmp = self._client.get(f"{self._bl}:last_best_res")
if tmp is None:
return None
return float(tmp)
@@ -948,13 +949,13 @@ class BeamlineConfig:
@last_best_res.setter
def last_best_res(self, best_res: float | None):
if best_res is None:
self.__client.delete(f"{self.__bl}:last_best_res")
self._client.delete(f"{self._bl}:last_best_res")
else:
self.__client.set(f"{self.__bl}:last_best_res", best_res)
self._client.set(f"{self._bl}:last_best_res", best_res)
@property
def last_best_b_factor(self) -> float | None:
tmp = self.__client.get(f"{self.__bl}:last_best_b_factor")
tmp = self._client.get(f"{self._bl}:last_best_b_factor")
if tmp is None:
return None
return float(tmp)
@@ -962,22 +963,22 @@ class BeamlineConfig:
@last_best_b_factor.setter
def last_best_b_factor(self, last_best_b_factor: float | None):
if last_best_b_factor is None:
self.__client.delete(f"{self.__bl}:last_best_b_factor")
self._client.delete(f"{self._bl}:last_best_b_factor")
else:
self.__client.set(f"{self.__bl}:last_best_b_factor", last_best_b_factor)
self._client.set(f"{self._bl}:last_best_b_factor", last_best_b_factor)
def _mount_failure_streak_key(self) -> str:
return f"{self.__bl}:mount_fail_count"
return f"{self._bl}:mount_fail_count"
def get_mount_failure_streak(self) -> int:
value = self.__client.get(self._mount_failure_streak_key())
value = self._client.get(self._mount_failure_streak_key())
return int(value) if value else 0
def increment_mount_failure_streak(self) -> int:
return int(self.__client.incr(self._mount_failure_streak_key()))
return int(self._client.incr(self._mount_failure_streak_key()))
def reset_mount_failure_streak(self) -> None:
self.__client.delete(self._mount_failure_streak_key())
self._client.delete(self._mount_failure_streak_key())
def get_mount_fail_count(self) -> int:
return self.get_mount_failure_streak()
@@ -990,7 +991,7 @@ class BeamlineConfig:
@property
def simple_input_parameters(self) -> SimpleStrategyInputModel | None:
tmp = self.__client.get(f"{self.__bl}:simple_input_params")
tmp = self._client.get(f"{self._bl}:simple_input_params")
if tmp is None:
return None
data_dict = json.loads(tmp)
@@ -999,15 +1000,15 @@ class BeamlineConfig:
@simple_input_parameters.setter
def simple_input_parameters(self, input_params: SimpleStrategyInputModel | None):
if input_params is None:
self.__client.delete(f"{self.__bl}:simple_input_params")
self._client.delete(f"{self._bl}:simple_input_params")
else:
self.__client.set(f"{self.__bl}:simple_input_params", input_params.model_dump_json())
self._client.set(f"{self._bl}:simple_input_params", input_params.model_dump_json())
@property
def auto_params(self) -> SimpleScanParameters | None:
tmp = self.__client.get(f"{self.__bl}:auto_params")
tmp = self._client.get(f"{self._bl}:auto_params")
if tmp is None:
logger.debug(f"auto_params missing in redis key {self.__bl}:auto_params")
logger.debug(f"auto_params missing in redis key {self._bl}:auto_params")
return None
try:
data_dict = json.loads(tmp)
@@ -1019,23 +1020,23 @@ class BeamlineConfig:
@auto_params.setter
def auto_params(self, params: SimpleScanParameters | None):
if params is None:
self.__client.delete(f"{self.__bl}:auto_params")
self._client.delete(f"{self._bl}:auto_params")
else:
self.__client.set(f"{self.__bl}:auto_params", params.model_dump_json())
self._client.set(f"{self._bl}:auto_params", params.model_dump_json())
def _automation_progress_key(self) -> str:
return f"{self.__bl}:automation_progress"
return f"{self._bl}:automation_progress"
def _automation_progress_seq_key(self) -> str:
return f"{self.__bl}:automation_progress_seq"
return f"{self._bl}:automation_progress_seq"
def reset_automation_progress(self) -> None:
self.__client.set(self._automation_progress_seq_key(), 0)
self.__client.delete(self._automation_progress_key())
self._client.set(self._automation_progress_seq_key(), 0)
self._client.delete(self._automation_progress_key())
def get_automation_progress_state(self) -> dict:
seq_raw = self.__client.get(self._automation_progress_seq_key())
payload_raw = self.__client.get(self._automation_progress_key())
seq_raw = self._client.get(self._automation_progress_seq_key())
payload_raw = self._client.get(self._automation_progress_key())
seq = int(seq_raw) if seq_raw is not None else 0
progress = json.loads(payload_raw) if payload_raw else None
@@ -1057,8 +1058,8 @@ class BeamlineConfig:
else:
raise TypeError(f"Unsupported automation progress type: {type(progress).__name__}")
next_seq = int(self.__client.incr(self._automation_progress_seq_key()))
self.__client.set(
next_seq = int(self._client.incr(self._automation_progress_seq_key()))
self._client.set(
self._automation_progress_key(),
json.dumps(payload, separators=(",", ":"), default=_json_default),
)
@@ -1067,7 +1068,7 @@ class BeamlineConfig:
@property
def failed_mount_count(self) -> int:
tmp = self.__client.get(f"{self.__bl}:failed_mount_count")
tmp = self._client.get(f"{self._bl}:failed_mount_count")
if tmp is None:
return 0
try:
@@ -1075,25 +1076,25 @@ class BeamlineConfig:
except (TypeError, ValueError):
logger.warning(
"Failed Mount Count is not an integer, resetting to 0.",
extra={"beamline": self.__bl, "tmp": tmp},
extra={"beamline": self._bl, "tmp": tmp},
)
return 0
@failed_mount_count.setter
def failed_mount_count(self, count: int):
if count == 0:
self.__client.delete(f"{self.__bl}:failed_mount_count")
self._client.delete(f"{self._bl}:failed_mount_count")
else:
self.__client.set(f"{self.__bl}:failed_mount_count", count)
self._client.set(f"{self._bl}:failed_mount_count", count)
def increment_failed_mount_count(self) -> int:
return int(self.__client.incr(f"{self.__bl}:failed_mount_count"))
return int(self._client.incr(f"{self._bl}:failed_mount_count"))
def _runtime_sim_key(self, name: str) -> str:
return f"{self.__bl}:runtime:simulate:{name}"
return f"{self._bl}:runtime:simulate:{name}"
def get_runtime_simulated(self, name: str, default: bool = False) -> bool:
raw = self.__client.get(self._runtime_sim_key(name))
raw = self._client.get(self._runtime_sim_key(name))
if raw is None:
return default
return str(raw).strip().lower() in {"1", "true", "yes", "on"}
@@ -1101,9 +1102,9 @@ class BeamlineConfig:
def set_runtime_simulated(self, name: str, enabled: bool) -> None:
key = self._runtime_sim_key(name)
if enabled:
self.__client.set(key, "1")
self._client.set(key, "1")
else:
self.__client.delete(key)
self._client.delete(key)
@property
def simulate_bec(self) -> bool:
@@ -1169,11 +1170,11 @@ class BeamlineConfig:
return None
def _detector_metadata_key(self) -> str:
return f"{self.__bl}:detector_metadata"
return f"{self._bl}:detector_metadata"
def get_detector_metadata(self) -> dict:
try:
raw = self.__client.get(self._detector_metadata_key())
raw = self._client.get(self._detector_metadata_key())
if raw in (None, "", b""):
return {}
@@ -1194,7 +1195,7 @@ class BeamlineConfig:
safe_payload.get("pixel_size_mm")
)
safe_payload["updated_at"] = datetime.now().isoformat(timespec="seconds")
self.__client.set(self._detector_metadata_key(), json.dumps(safe_payload))
self._client.set(self._detector_metadata_key(), json.dumps(safe_payload))
return safe_payload
@property
@@ -1216,19 +1217,18 @@ class BeamlineConfig:
detector_frontend = None
smargon_frontend = cfg_get(
"daq.hardware.smargon_frontend_url",
f"http://{self.__mxb.name.lower()}-smargopolo.psi.ch:8080/",
f"http://{self._mxb.name.lower()}-smargopolo.psi.ch:8080/",
)
aerotech_frontend = cfg_get(
"daq.hardware.aerotech_url",
f"http://mx-{self.__mxb.name.lower()}-queue-01.psi.ch:5234/",
"daq.hardware.aerotech_url", f"http://mx-{self._mxb.name.lower()}-queue-01.psi.ch:5234/"
)
tell_hint = "Please check TELL status via Remmina / VNC."
if self.__mxb is MXBeamline.X06DA:
if self._mxb is MXBeamline.X06DA:
detector_frontend = "http://sls-gpu-001:8080/frontend"
elif self.__mxb is MXBeamline.X10SA:
elif self._mxb is MXBeamline.X10SA:
detector_frontend = "http://sls-gpu-002:8080/frontend"
elif self.__mxb is MXBeamline.SIMULATED:
elif self._mxb is MXBeamline.SIMULATED:
detector_frontend = None
smargon_frontend = None
aerotech_frontend = None
@@ -1244,8 +1244,8 @@ class BeamlineConfig:
default = LocalContactConfigModel()
try:
redis_key = f"{self.__bl}:local_contact_config"
raw_value = self.__client.get(redis_key)
redis_key = f"{self._bl}:local_contact_config"
raw_value = self._client.get(redis_key)
if raw_value in (None, "", b""):
return default
@@ -1263,8 +1263,8 @@ class BeamlineConfig:
) -> LocalContactConfigModel:
validated = LocalContactConfigModel.model_validate(config)
try:
redis_key = f"{self.__bl}:local_contact_config"
self.__client.set(redis_key, validated.model_dump_json())
redis_key = f"{self._bl}:local_contact_config"
self._client.set(redis_key, validated.model_dump_json())
logger.info(f"Saved Local Contact config to Redis: {redis_key}")
except Exception as e:
logger.error(f"Failed to write Local Contact config to Redis: {e}")
+431 -437
View File
File diff suppressed because it is too large Load Diff
+49 -49
View File
@@ -35,25 +35,25 @@ class BeamlineDevices:
logger.debug("initialising BEC worker")
self.bec_worker = BECClientWorker(beamline)
logger.debug("initialising BEC worker done")
self.__smargon = smargon.Smargon(beamline)
self._smargon = smargon.Smargon(beamline)
self.exp_shutter = ExperimentalHutchShutter(beamline=self._beamline)
# Personnel Safety System: gates whether the robot is allowed to move.
self.pss = PssState(beamline=self._beamline)
# faster to define the dtz object here than in functions and then use
self.__dtz = self.bec_worker.dev.det_z
self._dtz = self.bec_worker.dev.det_z
self.dtz_mod = cfg_get("daq.detector_distance_limit_modifier", 1.0)
# TODO convert epics pvs to BEC
self.__sample_cam = epicsAD(f"{BEAMLINE}-ES-MS:")
self._sample_cam = epicsAD(f"{BEAMLINE}-ES-MS:")
self.__front_light = PredefinedPV(
self._front_light = PredefinedPV(
name="front_light",
setpv=f"{BEAMLINE}-ES-FL:SET",
getpv=f"{BEAMLINE}-ES-FL:SET",
predefs={"off": 1.49, "half": 2.0, "max": 3.0},
timeout=10.0,
)
self.__back_light = PredefinedPV(
self._back_light = PredefinedPV(
name="back_light",
setpv=f"{BEAMLINE}-ES-BL:SET",
getpv=f"{BEAMLINE}-ES-BL:SET",
@@ -61,33 +61,33 @@ class BeamlineDevices:
timeout=10.0,
)
# self.__front_light = self.bec_worker.dev.fl_bright # need wrapper on bec_worker layer
# self.__back_light = self.bec_worker.dev.bl_bright #need wrapper on bec_worker layer
# self._front_light = self.bec_worker.dev.fl_bright # need wrapper on bec_worker layer
# self._back_light = self.bec_worker.dev.bl_bright #need wrapper on bec_worker layer
self.__back_light_pos = EnumPV(
self._back_light_pos = EnumPV(
name="back_light_pos",
setpv=f"{BEAMLINE}-ES-BL:POS-SET",
getpv=f"{BEAMLINE}-ES-BL:POS-GET",
timeout=10.0,
)
self.__ringcurrent = self.bec_worker.ring_current
self._ringcurrent = self.bec_worker.ring_current
self.__zoom = SetGetPV(
self._zoom = SetGetPV(
name="zoom", setpv=f"{BEAMLINE}-ES-MS:ZOOM.VAL", getpv=f"{BEAMLINE}-ES-MS:ZOOM.RBV"
)
self.__cryojet_pos = EnumPV(
self._cryojet_pos = EnumPV(
name="cryojet_pos",
setpv=f"{BEAMLINE}-ES-CS:POS-SET",
getpv=f"{BEAMLINE}-ES-CS:POS-GET",
timeout=10.0,
)
self.__cryojet_x = MyMotor(f"{BEAMLINE}-ES-CS:TRX") # currently in is 5 out is 15?
self._cryojet_x = MyMotor(f"{BEAMLINE}-ES-CS:TRX") # currently in is 5 out is 15?
self.__cryojet_temperature_get = PV(f"{BEAMLINE}-ES-CS:TEMP_RBV")
self.__cryojet_temperature_set = PV(f"{BEAMLINE}-ES-CS:TEMP.VAL")
self._cryojet_temperature_get = PV(f"{BEAMLINE}-ES-CS:TEMP_RBV")
self._cryojet_temperature_set = PV(f"{BEAMLINE}-ES-CS:TEMP.VAL")
if self._beamline == MXBeamline.X06DA:
self._transmission = SetGetPV(
@@ -96,13 +96,13 @@ class BeamlineDevices:
getpv=f"{BEAMLINE}-ES-BCFI:TRANSM-GET",
)
else:
self.__transmission = SetGetPV(
self._transmission = SetGetPV(
name="transmission",
setpv=f"{BEAMLINE}-ES-SSFI:TRANSM-SET",
getpv=f"{BEAMLINE}-ES-SSFI:TRANSM-GET",
)
self.__fast_shutter = PV(f"{BEAMLINE}-ES-SHUTTER:SET")
self._fast_shutter = PV(f"{BEAMLINE}-ES-SHUTTER:SET")
self.magnet_position_sensor = PV(f"{BEAMLINE}-ES-DFS:CBOX-CMP1")
self.magnet_position_sensor_readout = PV(f"{BEAMLINE}-ES-DFS:CBOX-USER1")
@@ -134,7 +134,7 @@ class BeamlineDevices:
def restart_smargon(self, simulated: bool = False) -> None:
beamline = MXBeamline.SIMULATED if simulated else self._beamline
logger.info(f"Restarting Smargon controller with simulated={simulated}")
self.__smargon = smargon.Smargon(beamline)
self._smargon = smargon.Smargon(beamline)
# Transmission
@property
@@ -152,38 +152,38 @@ class BeamlineDevices:
# Lamp light
@property
def lamp_light(self) -> float:
return self.__front_light.value
return self._front_light.value
@lamp_light.setter
def lamp_light(self, v: float):
self.set_front_light(v, wait=False)
def set_front_light(self, v: float, /, wait: bool = True):
self.__front_light.move(v, wait=wait)
self._front_light.move(v, wait=wait)
# Back light
@property
def back_light(self) -> float:
return self.__back_light.value
return self._back_light.value
@back_light.setter
def back_light(self, v: float):
self.set_back_light(v, wait=False)
def set_back_light(self, v: float, /, wait: bool = True):
self.__back_light.move(v, wait=wait)
self._back_light.move(v, wait=wait)
# Zoom
@property
def zoom(self) -> float:
return self.__zoom.value
return self._zoom.value
@zoom.setter
def zoom(self, value: float):
self.set_zoom(value, wait=True)
def set_zoom(self, value: float, /, wait: bool = True):
self.__zoom.move(value, wait=wait)
self._zoom.move(value, wait=wait)
# Optics
@property
@@ -212,7 +212,7 @@ class BeamlineDevices:
# Cryojet
@property
def cryojet_temp(self) -> float:
return self.__cryojet_temperature_get.get()
return self._cryojet_temperature_get.get()
def anneal(self, wait_time: float):
logger.warning("Untested annealing procedure, use with caution")
@@ -223,66 +223,66 @@ class BeamlineDevices:
@property
def cryojet_pos(self) -> StagePositionEnum:
return StagePositionEnum(self.__cryojet_pos.position.upper())
return StagePositionEnum(self._cryojet_pos.position.upper())
@cryojet_pos.setter
def cryojet_pos(self, value: StagePositionEnum):
self.cryojet_pos_setter(value, wait=True)
def cryojet_pos_setter(self, value: StagePositionEnum, wait: bool = False):
self.__cryojet_pos.move(value, wait=wait)
self._cryojet_pos.move(value, wait=wait)
# Shutter
@property
def shutter(self) -> bool:
return self.__fast_shutter.get()
return self._fast_shutter.get()
@shutter.setter
def shutter(self, opened: bool):
self.__fast_shutter.put(opened)
self._fast_shutter.put(opened)
# Sample camera
@property
def samcam_settings(self) -> SampleCameraSettings:
return SampleCameraSettings(
gain=self.__sample_cam.gain_rbv.value, exposure=self.__sample_cam.expo_rbv.value
gain=self._sample_cam.gain_rbv.value, exposure=self._sample_cam.expo_rbv.value
)
@samcam_settings.setter
def samcam_settings(self, settings: SampleCameraSettings):
self.__sample_cam.setup(settings.gain, settings.exposure)
self._sample_cam.setup(settings.gain, settings.exposure)
@log_timing(logger, "auto exposure of camera")
def samcam_auto(self, state: AutoEnum):
self.__sample_cam.set_auto(state)
self._sample_cam.set_auto(state)
def samcam_frame_id(self) -> int:
"""
Camera UniqueId for the last produced frame (monotonic counter from AreaDetector).
"""
return int(self.__sample_cam.uid.get())
return int(self._sample_cam.uid.get())
# Detector Z
@property
def dtz(self) -> float:
return self.__dtz.read()["det_z"]["value"]
return self._dtz.read()["det_z"]["value"]
@dtz.setter
def dtz(self, value: float):
self.set_dtz(value, wait=True)
def set_dtz(self, value: float, /, wait: bool = True):
if value < self.__dtz.low_limit:
if value < self._dtz.low_limit:
# raise ValueError(f"Detector distance cannot be less than {self.detector_distance_minimum} mm")
logger.warning(
f"Requested detector distance {value} is less than minimum: {self.__dtz.low_limit}, setting to minimum"
f"Requested detector distance {value} is less than minimum: {self._dtz.low_limit}, setting to minimum"
)
value = self.__dtz.low_limit + self.dtz_mod
if value > self.__dtz.high_limit:
value = self._dtz.low_limit + self.dtz_mod
if value > self._dtz.high_limit:
logger.warning(
f"Requested detector distance {value} is greater than maximum: {self.__dtz.high_limit}, setting to maximum"
f"Requested detector distance {value} is greater than maximum: {self._dtz.high_limit}, setting to maximum"
)
value = self.__dtz.high_limit - self.dtz_mod
value = self._dtz.high_limit - self.dtz_mod
if wait:
status = self.bec_worker.det_z(value, timeout=60)
return status
@@ -292,13 +292,13 @@ class BeamlineDevices:
@property
def dtz_low(self) -> float:
logger.info(f"dtz_low: {self.__dtz.low_limit}")
return self.__dtz.low_limit
logger.info(f"dtz_low: {self._dtz.low_limit}")
return self._dtz.low_limit
@property
def dtz_high(self) -> float:
logger.info(f"dtz_high: {self.__dtz.high_limit}")
return self.__dtz.high_limit
logger.info(f"dtz_high: {self._dtz.high_limit}")
return self._dtz.high_limit
@property
def aerotech_pos(self) -> AerotechCoordinate:
@@ -330,26 +330,26 @@ class BeamlineDevices:
# Smargon goniometer
@property
def smargon_pos(self) -> SmargonCoordinate:
return self.__smargon.readback
return self._smargon.readback
def set_smargon_pos(self, pos: SmargonCoordinate, /, wait: bool = True):
self.__smargon.target = pos
self._smargon.target = pos
@smargon_pos.setter
def smargon_pos(self, pos: SmargonCoordinate):
self.set_smargon_pos(pos, wait=True)
def smargon_wait(self, timeout: float = 10.0):
self.__smargon.wait(timeout=timeout)
self._smargon.wait(timeout=timeout)
def smargon_move_home(self):
self.__smargon.move_home(wait=True)
self._smargon.move_home(wait=True)
def smargon_aerotech_wait(self):
self.__smargon.wait_aerotech(timeout=10.0)
self._smargon.wait_aerotech(timeout=10.0)
def smargon_initialize(self):
self.__smargon.initialize()
self._smargon.initialize()
if __name__ == "__main__":
+5 -5
View File
@@ -52,10 +52,10 @@ class MlBox:
Optional injected inference wrapper for tests.
If None, the production wrapper is created lazily.
"""
self.__beamline = bl
self._beamline = bl
if wrapper is not None:
self.__wrapper = wrapper
self._wrapper = wrapper
return
if bl == MXBeamline.SIMULATED:
@@ -64,13 +64,13 @@ class MlBox:
elif bl == MXBeamline.X06DA:
from aarecommon.lc_infer_wrapper import AareLCInferWrapper
self.__wrapper = AareLCInferWrapper(bl)
self._wrapper = AareLCInferWrapper(bl)
elif bl == MXBeamline.X10SA:
# Lazy import keeps module importable without external client installed
from aarecommon.lc_infer_wrapper import AareLCInferWrapper
self.__wrapper = AareLCInferWrapper(bl)
self._wrapper = AareLCInferWrapper(bl)
elif bl == MXBeamline.X06SA:
raise NotImplementedError(f"MLBox bundle mode not implemented for {bl}")
@@ -120,7 +120,7 @@ class MlBox:
last_error = None
for attempt in range(1, self.RETRY_COUNT + 1):
try:
bundle = self.__wrapper.get_latest_prediction_bundle()
bundle = self._wrapper.get_latest_prediction_bundle()
logger.debug(f"Fetched prediction bundle on attempt {attempt}/{self.RETRY_COUNT}")
return bundle
except Exception as e:
+26 -68
View File
@@ -3,7 +3,6 @@ import hmac
import importlib
import json
import os
import random
import time
from contextlib import asynccontextmanager
from typing import AsyncGenerator, Optional
@@ -48,6 +47,8 @@ from fastapi.security import OAuth2PasswordBearer
from starlette.responses import StreamingResponse
from uvicorn.workers import UvicornWorker # deprecated shim, present in pinned 0.34.2
from aare.beamline_dispatch.beamline_dispatch import get_beamline_dispatch
from aare.beamline_dispatch.protocols import BeamlineDispatch
from aare.daq import auth
from aare.daq.config import BeamlineConfig
from aare.daq.config_model import LocalContactConfigModel
@@ -60,9 +61,9 @@ logger = setup_logger("aareDAQ")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
# ── Per-worker state: populated inside the lifespan, after fork ──
bl = None
cfg = None
daq = None
cfg: BeamlineConfig
daq: AareDAQ
bl_dispatch: BeamlineDispatch
_all_pgroups_cache: dict[str, tuple[list[str], float]] = {}
_ALL_PGROUPS_TTL_S = 60.0 # adjust TTL as needed
@@ -93,9 +94,11 @@ async def lifespan(application: FastAPI):
All stateful / connection-opening initialisation belongs here so that
each worker gets its own fresh Redis, BEC, EPICS, and TELL connections.
"""
global bl, cfg, daq
await asyncio.sleep(random.uniform(0.5, 3.0))
global cfg, daq, bl_dispatch
logger.info(f"Worker {os.getpid()} setting up JWT authentication...")
bl_dispatch = get_beamline_dispatch()
auth.init_jwt_key(bl_dispatch.auth)
logger.info(f"Worker {os.getpid()} starting initialisation...")
# ── Core objects (Redis, EPICS PVs, BEC, TELL, JFJoch, etc.) ──
@@ -203,9 +206,6 @@ def _get_automation_progress_state() -> dict:
"""
Read automation progress state from shared config/Redis storage.
"""
if cfg is None:
return {"seq": 0, "progress": None}
try:
return cfg.get_automation_progress_state()
except Exception as e:
@@ -220,9 +220,6 @@ def _push_automation_progress(progress: AutomationProgress) -> None:
Args:
progress: Current automation progress model.
"""
if cfg is None:
return
try:
state = cfg.set_automation_progress_state(progress)
logger.info(
@@ -371,7 +368,7 @@ async def sample_geometry(token: str = Depends(oauth2_scheme)) -> SampleGeometry
@app.put("/beamline/omega")
async def omega(val: float, token: str = Depends(oauth2_scheme)):
async def set_omega_abs(val: float, token: str = Depends(oauth2_scheme)):
"""
Set the omega angle of the goniometer.
@@ -389,7 +386,7 @@ 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)):
async def set_omega_rel(val: float, token: str = Depends(oauth2_scheme)):
"""
Move the omega angle relatively.
@@ -689,34 +686,34 @@ async def bec_save_current_aerotech_position(token: str = Depends(oauth2_scheme)
def initialise_aerotech(self):
self.__cfg.try_set_busy(timeout=360)
self._cfg.try_set_busy(timeout=360)
try:
self.__devs.aerotech.home_aerotech()
self.__cfg.state_busy = False
self._devs.aerotech.home_aerotech()
self._cfg.state_busy = False
except Exception as e:
self.__cfg.state_busy = False
self._cfg.state_busy = False
logger.error(f"Failed to initialise Aerotech: {e}")
raise
def detector_take_pedestal(self):
self.__cfg.try_set_busy(timeout=360)
self._cfg.try_set_busy(timeout=360)
try:
self.__jfjoch.take_pedestal()
self.__cfg.state_busy = False
self._jfjoch.take_pedestal()
self._cfg.state_busy = False
except Exception as e:
self.__cfg.state_busy = False
self._cfg.state_busy = False
logger.error(f"Failed to take detector pedestal: {e}")
raise
def initialise_detector(self):
self.__cfg.try_set_busy(timeout=360)
self._cfg.try_set_busy(timeout=360)
try:
self.__jfjoch.initialize()
self.__cfg.state_busy = False
self._jfjoch.initialize()
self._cfg.state_busy = False
except Exception as e:
self.__cfg.state_busy = False
self._cfg.state_busy = False
logger.error(f"Failed to initialise detector: {e}")
raise
@@ -838,28 +835,6 @@ async def local_contact_set_config(
return daq.set_local_contact_config(payload)
@app.get("/local_contact/config")
async def local_contact_config(token: str = Depends(oauth2_scheme)) -> LocalContactConfigModel:
"""
Return Local Contact config values. Staff only.
"""
data = auth.parse_token(token)
auth.check_jwt_staff_only(data)
return daq.get_local_contact_config()
@app.put("/local_contact/config")
async def local_contact_set_config(
payload: LocalContactConfigModel, token: str = Depends(oauth2_scheme)
) -> LocalContactConfigModel:
"""
Update Local Contact config values. Staff only.
"""
data = auth.parse_token(token)
auth.check_jwt_staff_only(data)
return daq.set_local_contact_config(payload)
@app.post("/beamline/goto_abr_meas_pos")
async def goto_abr_meas_pos(token: str = Depends(oauth2_scheme)):
"""
@@ -1344,7 +1319,7 @@ async def dewar_exchange(token: str = Depends(oauth2_scheme)):
@app.post("/state/sample_exchange")
async def dewar_exchange(token: str = Depends(oauth2_scheme)):
async def sample_exchange(token: str = Depends(oauth2_scheme)):
"""
Transition beamline state to SampleExchange.
@@ -1435,7 +1410,7 @@ async def data_collection(token: str = Depends(oauth2_scheme)):
@app.post("/state/robot_sample_exchange")
async def dewar_exchange(token: str = Depends(oauth2_scheme)):
async def robot_sample_exchange(token: str = Depends(oauth2_scheme)):
"""
Transition beamline state to SampleExchange.
@@ -1466,7 +1441,7 @@ async def xray_fluorescence(token: str = Depends(oauth2_scheme)):
@app.post("/state/xtal_snapshot")
async def beam_location(token: str = Depends(oauth2_scheme)):
async def xtal_snapshot(token: str = Depends(oauth2_scheme)):
"""
Transition beamline state to BeamLocation. Staff only.
@@ -1477,23 +1452,6 @@ async def beam_location(token: str = Depends(oauth2_scheme)):
daq.state = BeamlineStateEnum.XtalSnapshot
@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.")
return "OK"
@app.post("/access/take_over_beamline")
async def take_over_beamline(
payload: RecoveryActionRequest, token: str = Depends(oauth2_scheme)
+6 -6
View File
@@ -27,7 +27,7 @@ def set_spreadsheet_in_redis(spreadsheet):
"[REDIS][DEBUG] Data to write:", json.dumps(spreadsheet, indent=4)
) # Pretty-print the data
print("[REDIS][INFO] Writing spreadsheet to Redis...")
config.__client.set(f"{config._BeamlineConfig__bl}:spreadsheet", json.dumps(spreadsheet))
config.__client.set(f"{config._bl}:spreadsheet", json.dumps(spreadsheet))
def on_message(ws, message):
@@ -88,21 +88,21 @@ def on_message(ws, message):
target_list.append(info)
# Write normal pucks to sample_spreadsheet
normal_key = f"{config._BeamlineConfig__bl}:sample_spreadsheet"
normal_key = f"{config._bl}:sample_spreadsheet"
normal_list = SampleShortInfoList(s=normal_short_infos)
config._BeamlineConfig__client.set(normal_key, normal_list.model_dump_json())
config._client.set(normal_key, normal_list.model_dump_json())
print("[REDIS][INFO] Written normal spreadsheet to:", normal_key)
# Write reference tools to reference-tools
ref_key = f"{config._BeamlineConfig__bl}:reference-tools"
ref_key = f"{config._bl}:reference-tools"
if reference_short_infos:
ref_list = SampleShortInfoList(s=reference_short_infos)
config._BeamlineConfig__client.set(ref_key, ref_list.model_dump_json())
config._client.set(ref_key, ref_list.model_dump_json())
print("[REDIS][INFO] Written reference tools to:", ref_key)
else:
# Clear key if empty
try:
config._BeamlineConfig__client.delete(ref_key)
config._client.delete(ref_key)
print("[REDIS][INFO] Cleared reference tools key:", ref_key)
except Exception:
pass
+2 -2
View File
@@ -98,8 +98,8 @@ def _get_redis_context() -> tuple[Any | None, str | None]:
logger.debug("[REDIS] BeamlineConfig unavailable; skipping TELL redis write")
return None, None
redis_client = getattr(config, "_BeamlineConfig__client", None)
beamline_key = getattr(config, "_BeamlineConfig__bl", None)
redis_client = getattr(config, "_client", None)
beamline_key = getattr(config, "_bl", None)
if redis_client is None or beamline_key is None:
logger.error("[REDIS] BeamlineConfig internals unavailable; skipping TELL redis write")
return None, None
+49 -49
View File
@@ -1,6 +1,7 @@
from typing import Optional, Union
from aarecommon.config.beamline import cfg_get, mx_beamline
from aarecommon.config.logger import setup_logger
from aarecommon.errors.exception_handler import AerotechCommunicationError
from aarecommon.math.coordinate import AerotechCoordinate, Coordinate
from aarecommon.models.beamline import MXBeamline
@@ -18,7 +19,6 @@ from aarescan_client.models.screen_request import ScreenRequest
AEROTECH_HOME = AerotechCoordinate(at_mm=Coordinate(x=0, y=0, z=0), omega_deg=0)
from aarecommon.config.logger import setup_logger
logger = setup_logger("aareDAQ")
@@ -26,29 +26,29 @@ logger = setup_logger("aareDAQ")
class AerotechController(object):
def __init__(self, bl: MXBeamline):
if bl == MXBeamline.X06DA:
self.__simulated = False
self.__base = cfg_get(
self._simulated = False
self._base = cfg_get(
"daq.hardware.aerotech_url", "http://mx-x06da-queue-01.psi.ch:5234"
)
elif bl == MXBeamline.X10SA:
self.__simulated = False
self.__base = cfg_get(
self._simulated = False
self._base = cfg_get(
"daq.hardware.aerotech_url", "http://mx-x10sa-queue-01.psi.ch:5234"
)
elif bl == MXBeamline.X06SA:
raise NotImplementedError("Not implemented aerotech url for X06SA")
elif bl == MXBeamline.SIMULATED:
self.__simulated = True
self.__pos = AEROTECH_HOME
self.__vel = 0
self._simulated = True
self._pos = AEROTECH_HOME
self._vel = 0
else:
raise Exception("unknown beamline")
if not self.__simulated:
self.__client = ApiClient(Configuration(host=self.__base))
self.__api = DefaultApi(self.__client)
if not self._simulated:
self._client = ApiClient(Configuration(host=self._base))
self._api = DefaultApi(self._client)
def __make_aerotech_target(
def _make_aerotech_target(
self, coord: AerotechCoordinate, wait: bool = False, incremental: bool = False
) -> Target:
at_mm = coord.at_mm
@@ -62,31 +62,31 @@ class AerotechController(object):
incremental=incremental,
)
def __make_aerotech_coordinate(self, target: Target) -> AerotechCoordinate:
def _make_aerotech_coordinate(self, target: Target) -> AerotechCoordinate:
return AerotechCoordinate(
at_mm=Coordinate(x=target.x, y=target.y, z=target.z), omega_deg=target.u
)
def cancel(self):
try:
self.__api.cancel_post()
self._api.cancel_post()
except Exception as e:
raise AerotechCommunicationError(
"Aerotech cancel failed",
endpoint="cancel_post",
base_url=self.__base,
base_url=self._base,
operation="POST",
) from e
def is_idle(self) -> bool:
try:
status = self.__api.status_get()
status = self._api.status_get()
return status.state == "Idle"
except Exception as e:
raise AerotechCommunicationError(
"Aerotech status check failed",
endpoint="status_get",
base_url=self.__base,
base_url=self._base,
operation="GET",
) from e
@@ -97,36 +97,36 @@ class AerotechController(object):
)
def status(self) -> Status:
if self.__simulated:
if self._simulated:
return Status(
state=Status.State.IDLE,
x=AxisStatus(
pos=self.__pos.x,
vel=self.__vel,
pos=self._pos.x,
vel=self._vel,
enabled=False,
homed=False,
moving=False,
fault=False,
),
y=AxisStatus(
pos=self.__pos.y,
vel=self.__vel,
pos=self._pos.y,
vel=self._vel,
enabled=False,
homed=False,
moving=False,
fault=False,
),
z=AxisStatus(
pos=self.__pos.z,
vel=self.__vel,
pos=self._pos.z,
vel=self._vel,
enabled=False,
homed=False,
moving=False,
fault=False,
),
u=AxisStatus(
pos=self.__pos.u,
vel=self.__vel,
pos=self._pos.u,
vel=self._vel,
enabled=False,
homed=False,
moving=False,
@@ -134,52 +134,52 @@ class AerotechController(object):
),
)
try:
return self.__api.status_get()
return self._api.status_get()
except Exception as e:
raise AerotechCommunicationError(
"Aerotech status request failed",
endpoint="status_get",
base_url=self.__base,
base_url=self._base,
operation="GET",
) from e
def move_home(self, wait: bool = True, incremental: bool = False):
if self.__simulated:
self.__pos = AEROTECH_HOME
return self.__pos
if self._simulated:
self._pos = AEROTECH_HOME
return self._pos
return self.position(AEROTECH_HOME, wait=wait, incremental=incremental)
def home_aerotech(self):
try:
return self.__api.home_post()
return self._api.home_post()
except Exception as e:
raise AerotechCommunicationError(
"Aerotech home failed", endpoint="home_post", base_url=self.__base, operation="POST"
"Aerotech home failed", endpoint="home_post", base_url=self._base, operation="POST"
) from e
def wait_till_done(self, timeout=60):
try:
return self.__api.wait_till_done_post(timeout=timeout)
return self._api.wait_till_done_post(timeout=timeout)
except Exception as e:
raise AerotechCommunicationError(
"Aerotech wait_till_done failed",
endpoint="wait_till_done_post",
base_url=self.__base,
base_url=self._base,
operation="POST",
) from e
def position(self, target: AerotechCoordinate, /, wait: bool = True, incremental: bool = False):
if self.__simulated:
return self.__pos
if self._simulated:
return self._pos
payload = self.__make_aerotech_target(target, wait=wait, incremental=incremental)
payload = self._make_aerotech_target(target, wait=wait, incremental=incremental)
try:
return self.__api.position_post(payload)
return self._api.position_post(payload)
except Exception as e:
raise AerotechCommunicationError(
"Aerotech position move failed",
endpoint="position_post",
base_url=self.__base,
base_url=self._base,
operation="POST",
) from e
@@ -196,16 +196,16 @@ class AerotechController(object):
start_pos_deg=start_pos_deg,
run_async=run_async,
)
if self.__simulated:
if self._simulated:
return payload
try:
return self.__api.rotation_scan_post(payload)
return self._api.rotation_scan_post(payload)
except Exception as e:
raise AerotechCommunicationError(
"Aerotech rotation scan failed",
endpoint="rotation_scan_post",
base_url=self.__base,
base_url=self._base,
operation="POST",
) from e
@@ -226,15 +226,15 @@ class AerotechController(object):
time_sec=time_sec,
run_async=run_async,
)
if self.__simulated:
if self._simulated:
return payload
try:
return self.__api.grid_scan_post(payload)
return self._api.grid_scan_post(payload)
except Exception as e:
raise AerotechCommunicationError(
"Aerotech grid scan failed",
endpoint="grid_scan_post",
base_url=self.__base,
base_url=self._base,
operation="POST",
) from e
@@ -253,16 +253,16 @@ class AerotechController(object):
steps=steps,
run_async=run_async,
)
if self.__simulated:
if self._simulated:
return payload
try:
logger.info("sending screening scan request to aerotech")
return self.__api.screening_post(payload)
return self._api.screening_post(payload)
except Exception as e:
raise AerotechCommunicationError(
"Aerotech screening scan failed",
endpoint="screening_post",
base_url=self.__base,
base_url=self._base,
operation="POST",
) from e
+4 -3
View File
@@ -57,14 +57,15 @@ class epicsAD(object):
self.gain_max = epics.PV(prefix + cam + "GC_AutGainUpperLimit")
self.exp_max = epics.PV(prefix + cam + "GC_AutExpTimUppLimit")
self.auto_white_balance = epics.PV(prefix + cam + "GC_BalanceWhiteAuto")
self.__init_auto_exp()
self._init_auto_exp()
try:
epics.ca.pend_io()
except:
except Exception as e:
print(f"EPICS error: {e}")
pass
def __init_auto_exp(self, settings: AutoExposureSettings = AutoExposureSettings()):
def _init_auto_exp(self, settings: AutoExposureSettings = AutoExposureSettings()):
self.acquire.put(0)
self.aoi_start_x.put(settings.aoi_offset_x)
self.aoi_start_y.put(settings.aoi_offset_y)
+39 -46
View File
@@ -13,19 +13,10 @@ from bec_lib.device import RPCError, ScanRequestError
from bec_lib.procedures.helper import FrontendProcedureHelper
from bec_lib.service_config import ServiceConfig
from aare.beamline_dispatch.beamline_dispatch import get_beamline_dispatch
logger = setup_logger("aareDAQ")
# specify up to 10 queue to runs in parallel, request more if needed!
# st = client.proc.request_new("sleep", ((), {"time_s":5}), queue="test")
# to see all deevices
# devs.show_all
# helper fucntions
# helper.get.active_and_pending_queue_names()
# helper.get.running_procedures()
# helper.request.abort_queue()
class DetectorCoverEnum(str, Enum):
"""Enum for the detector cover position
@@ -73,6 +64,8 @@ class BECClientWorker:
else:
raise ValueError(f"Unknown beamline: {beamline}")
self.dispatch = get_beamline_dispatch()
if self.beamline is MXBeamline.SIMULATED:
self.simulated = True
@@ -90,37 +83,37 @@ class BECClientWorker:
self.dev = self.client.device_manager.devices
print(self.dev.keys())
self.scans = self.client.scans
self.macros = self.client.macros
self.__load_user_macros()
print(self.__list_all_macros())
self.macros = self.dispatch.bec_macros
self.client.macros._load_user_macros()
print(self._list_all_macros())
self.helper = FrontendProcedureHelper(self.client.connector)
self.__set_scilog_tags()
self._set_scilog_tags()
try:
self.__init_beamline_environment()
self._init_beamline_environment()
except Exception as e:
logger.error(f"Error initialising BEC devices: {e}")
exit(1)
logger.debug(f"simulated is {self.simulated}")
def __init_beamline_environment(self):
def _init_beamline_environment(self):
try:
self.position_devices, self.planner = init_beamline_environment()
self.__backlight_brightness = self.position_devices["bl_bright"]
self.__frontlight_brightness = self.position_devices["fl_bright"]
self.__zoom = self.dev.scam_zoom
self.position_devices, self.planner = self.macros.init_beamline_environment()
self._backlight_brightness = self.position_devices["bl_bright"]
self._frontlight_brightness = self.position_devices["fl_bright"]
self._zoom = self.dev.scam_zoom
self._ring_current = self.dev.sls_current
except Exception as e:
logger.error(f"Error initialising planar and position devices: {e}")
self.position_devices = None
self.planner = None
self.__backlight_brightness = None
self.__frontlight_brightness = None
self._backlight_brightness = None
self._frontlight_brightness = None
try:
self.__zoom = self.dev.scam_zoom
self._zoom = self.dev.scam_zoom
self._ring_current = self.dev.sls_current
except Exception as e:
logger.error(f"Error initialising zoom and ring_current: {e}")
self.__zoom = None
self._zoom = None
self.ring_current = None
raise Exception(f"Error initialising BEC devices: {e}")
@@ -163,7 +156,7 @@ class BECClientWorker:
raise BECCommunicationError(message, operation=operation, exception=exc) from exc
def __set_scilog_tags(self, tags: Optional[List[str]] = None):
def _set_scilog_tags(self, tags: Optional[List[str]] = None):
try:
if tags:
self.client.messaging.scilog.set_default_tags(tags)
@@ -298,7 +291,7 @@ class BECClientWorker:
logger.debug("Simulating list_all_user_macros")
return []
try:
self.__list_all_macros()
self._list_all_macros()
raw_macros = [name for name, _ in self.client.macros._update_handler.macros.items()]
if raw_macros is None:
logger.warning("BEC returned no user macros; treating as empty list")
@@ -307,8 +300,8 @@ class BECClientWorker:
except Exception as e:
self._raise_bec_error(e, operation="list_all_user_macros")
def __list_all_macros(self):
result = self.macros.list_user_macros()
def _list_all_macros(self):
result = self.client.macros.list_user_macros()
if result is None:
return []
return result
@@ -318,11 +311,11 @@ class BECClientWorker:
logger.debug("Simulating load_user_macros")
return None
try:
return self.__load_user_macros()
return self._load_user_macros()
except Exception as e:
self._raise_bec_error(e, operation="load_user_macros")
def __load_user_macros(self):
def _load_user_macros(self):
result = self.macros.load_all_user_macros()
if result is None:
logger.warning("BEC load_all_user_macros returned None")
@@ -348,7 +341,7 @@ class BECClientWorker:
self.client.config.update_session_with_file(
f"/sls/{self.beamline}/config/bec/production/{self._beamline_name}_bec/{self._beamline_name}_bec/device_configs/{self._beamline_name}-devices.yaml"
)
self.__init_beamline_environment()
self._init_beamline_environment()
logger.info(f"Reinitialised BEC planner and position devices using method={method}")
return self.list_position_devices()
except Exception as e:
@@ -361,7 +354,7 @@ class BECClientWorker:
def mono_pitch_scan_runner(self, plot: bool = False):
try:
mono_pitch_scan(plot)
self.macros.mono_pitch_scan(plot)
except Exception as e:
self._raise_bec_error(e, operation="mono_pitch_scan", tags=["mono_pitch_scan"])
if self.beamline is MXBeamline.X06DA:
@@ -378,7 +371,7 @@ class BECClientWorker:
def check_current_energy(self):
"""Get the current energy from the BEC in eV"""
energy_ev = get_current_energy()
energy_ev = self.macros.get_current_energy()
energy_kev = energy_ev / 1000
return energy_kev
@@ -387,7 +380,7 @@ class BECClientWorker:
logger.info(f"Current energy: {current_energy:.1f} eV")
logger.info(f"Change energy requested: from {current_energy:.1f} to {value:.1f} eV")
try:
bl_energy(value, move_gap=False, mono_scan=True, plot=plot)
self.macros.bl_energy(value, move_gap=False, mono_scan=True, plot=plot)
except Exception as e:
self._raise_bec_error(
e,
@@ -460,7 +453,7 @@ class BECClientWorker:
"""returns backlight brightness as an Enum: 'off' or 'on', can also be a value...
How to handle"""
try:
return BrightnessEnum(self.__backlight_brightness.actual)
return BrightnessEnum(self._backlight_brightness.actual)
except Exception as e:
self._raise_bec_error(
e,
@@ -475,14 +468,14 @@ class BECClientWorker:
if self.simulated:
return
try:
self.__backlight_brightness.move(value)
self._backlight_brightness.move(value)
except Exception as e:
self._raise_bec_error(e, operation=f"backlight_brightness:{value}", tags=["backlight"])
raise
def get_backlight_pos(self) -> BrightnessEnum:
"""Returns the current backlight brightness position"""
return BrightnessEnum(self.__backlight_brightness.pos)
return BrightnessEnum(self._backlight_brightness.pos)
def backlight_toggle(self):
"""Turn the backlight on or off"""
@@ -503,28 +496,28 @@ class BECClientWorker:
)
def save_current_bs_pos(self):
save_current_position(self.dev.bs_z, "safe")
self.macros.save_current_position(self.dev.bs_z, "safe")
def save_current_collimator_pos(self):
save_current_position(self.dev.coll_y, "work")
self.macros.save_current_position(self.dev.coll_y, "work")
def save_current_aerotech_position(self):
save_current_position(self.dev.aerotech, "work", axis="x")
save_current_position(self.dev.aerotech, "work", axis="y")
save_current_position(self.dev.aerotech, "work", axis="z")
self.macros.save_current_position(self.dev.aerotech, "work", axis="x")
self.macros.save_current_position(self.dev.aerotech, "work", axis="y")
self.macros.save_current_position(self.dev.aerotech, "work", axis="z")
self.save_config_and_reload_devices()
def save_config_and_reload_devices(self):
self.position_devices, self.planner = save_and_reload()
self.position_devices, self.planner = self.macros.save_and_reload()
@property
def zoom(self):
return self.__zoom.position
return self._zoom.position
@zoom.setter
def zoom(self, value: float):
self.scans.umv(self.__zoom, value, relative=False)
self.scans.umv(self._zoom, value, relative=False)
@property
def ring_current(self) -> float:
+10 -10
View File
@@ -5,12 +5,12 @@ from epics import PV
class ExperimentalHutchShutter:
def __init__(self, beamline: MXBeamline):
BEAMLINE = beamline.value.upper()
self.__close = PV(f"{BEAMLINE}-EH1-PSYS:SH-A-CLOSE-SET") # 1, 0
self.__open = PV(f"{BEAMLINE}-EH1-PSYS:SH-A-OPEN-SET")
self.__state = PV(f"{BEAMLINE}-OP-PSH1-EMLS-0010:OPEN")
self._close = PV(f"{BEAMLINE}-EH1-PSYS:SH-A-CLOSE-SET") # 1, 0
self._open = PV(f"{BEAMLINE}-EH1-PSYS:SH-A-OPEN-SET")
self._state = PV(f"{BEAMLINE}-OP-PSH1-EMLS-0010:OPEN")
def state(self):
state = self.__state.get()
state = self._state.get()
if state == "Open" or state == 1:
return True
elif state == "Not Open" or state == 0:
@@ -20,11 +20,11 @@ class ExperimentalHutchShutter:
return False
def open(self):
self.__open.put(0)
self.__open.put(1)
# print(self.__open.get())
self.__open.put(0)
self._open.put(0)
self._open.put(1)
# print(self._open.get())
self._open.put(0)
def close(self):
self.__close.put(1)
self.__close.put(0)
self._close.put(1)
self._close.put(0)
+8 -8
View File
@@ -11,18 +11,18 @@ class FilterTransmission:
self._fail_on_timeout = fail_on_timeout
self._timeout = timeout
self.__set = PV(f"{BEAMLINE}-ES-FI:TRANSM-SET")
self.__get = PV(f"{BEAMLINE}-ES-FI:TRANSM-GET")
self.__done = PV(f"{BEAMLINE}-ES-FI:DONE")
self._set = PV(f"{BEAMLINE}-ES-FI:TRANSM-SET")
self._get = PV(f"{BEAMLINE}-ES-FI:TRANSM-GET")
self._done = PV(f"{BEAMLINE}-ES-FI:DONE")
def __repr__(self):
return f"<FilterTransmission: {self.__set.get():.4f}>"
return f"<FilterTransmission: {self._set.get():.4f}>"
def __str__(self):
return f"{self.__get.get():.4f}"
return f"{self._get.get():.4f}"
def is_done(self):
return 1 == int(self.__done.value)
return 1 == int(self._done.value)
def is_busy(self):
return not self.is_done()
@@ -33,7 +33,7 @@ class FilterTransmission:
raise RuntimeError(
f"requested transmission ({val:.4f}) is out of bounds --- valid range : 0 <= trans <= 1.0"
)
self.__set.put(val)
self._set.put(val)
poll(0.2)
if wait:
self.wait()
@@ -41,7 +41,7 @@ class FilterTransmission:
def get(self) -> float | None:
if self.is_busy():
return None
return round(self.__get.value, 5)
return round(self._get.value, 5)
def wait(self):
timeisup = False
+50 -50
View File
@@ -11,38 +11,38 @@ class Fluorimeter(object):
def __init__(self, beamline: MXBeamline, **kwargs):
BEAMLINE = beamline.value.upper()
self.__start = PV(f"{BEAMLINE}-ES-SiD:mca1Start") # 0 done ,1 start
self.__stop = PV(f"{BEAMLINE}-ES-SiD:mca1Stop") # 0 done ,1 stop
self.__erase_and_start = PV(f"{BEAMLINE}-ES-SiD:mca1EraseStart") # 0 done,1 start
self.__erase = PV(f"{BEAMLINE}-ES-SiD:mca1Erase") # 0 done,1 erase
self._start = PV(f"{BEAMLINE}-ES-SiD:mca1Start") # 0 done ,1 start
self._stop = PV(f"{BEAMLINE}-ES-SiD:mca1Stop") # 0 done ,1 stop
self._erase_and_start = PV(f"{BEAMLINE}-ES-SiD:mca1EraseStart") # 0 done,1 start
self._erase = PV(f"{BEAMLINE}-ES-SiD:mca1Erase") # 0 done,1 erase
self.__preset_mode = PV(f"{BEAMLINE}-ES-SiD:dxp1:PresetMode") # set mode
self.__status = PV(f"{BEAMLINE}-ES-SiD:mca1.ACQG") # 0 done, 1 acquire
self._preset_mode = PV(f"{BEAMLINE}-ES-SiD:dxp1:PresetMode") # set mode
self._status = PV(f"{BEAMLINE}-ES-SiD:mca1.ACQG") # 0 done, 1 acquire
self.__real_time = PV(f"{BEAMLINE}-ES-SiD:mca1.PRTM") # float
self.__live_time = PV(f"{BEAMLINE}-ES-SiD:mca1.PLTM") # float
self.__elapsed_real_time = PV(f"{BEAMLINE}-ES-SiD:mca1.ERTM") # float
self.__elapsed_live_time = PV(f"{BEAMLINE}-ES-SiD:mca1.ELTM") # float
self.__elapsed_trigger_live_time = PV(f"{BEAMLINE}-ES-SiD:dxp1:ElapsedTriggerLiveTime")
self._real_time = PV(f"{BEAMLINE}-ES-SiD:mca1.PRTM") # float
self._live_time = PV(f"{BEAMLINE}-ES-SiD:mca1.PLTM") # float
self._elapsed_real_time = PV(f"{BEAMLINE}-ES-SiD:mca1.ERTM") # float
self._elapsed_live_time = PV(f"{BEAMLINE}-ES-SiD:mca1.ELTM") # float
self._elapsed_trigger_live_time = PV(f"{BEAMLINE}-ES-SiD:dxp1:ElapsedTriggerLiveTime")
self.__instant_dead_time = PV(f"{BEAMLINE}-ES-SiD:mca1.IDTIM")
self.__average_dead_time = PV(f"{BEAMLINE}-ES-SiD:mca1.DTIM")
self._instant_dead_time = PV(f"{BEAMLINE}-ES-SiD:mca1.IDTIM")
self._average_dead_time = PV(f"{BEAMLINE}-ES-SiD:mca1.DTIM")
self.__poll_time_rbv = PV(f"{BEAMLINE}-ES-SiD:PollTime_RBV")
self.__poll_time = PV(f"{BEAMLINE}-ES-SiD:PollTime")
self._poll_time_rbv = PV(f"{BEAMLINE}-ES-SiD:PollTime_RBV")
self._poll_time = PV(f"{BEAMLINE}-ES-SiD:PollTime")
self.__mca = PV(f"{BEAMLINE}-ES-SiD:mca1")
self.__data = PV(f"{BEAMLINE}-ES-SiD:mca1.VAL")
self.__background = PV(f"{BEAMLINE}-ES-SiD:mca1.BG")
self._mca = PV(f"{BEAMLINE}-ES-SiD:mca1")
self._data = PV(f"{BEAMLINE}-ES-SiD:mca1.VAL")
self._background = PV(f"{BEAMLINE}-ES-SiD:mca1.BG")
self.__saveFile = PV(f"{BEAMLINE}-ES-SiD:SaveSystemFile")
self.__saveFile_name = PV(f"{BEAMLINE}-ES-SiD:SaveSystem")
self.__saveFile_rbv = PV(f"{BEAMLINE}-ES-SiD:SaveSystem_RBV") # 1 ssave, 0 done
self.__roi_1 = self.get_roi(BEAMLINE, 1) # currently SiEsc
self.__roi_2 = self.get_roi(BEAMLINE, 2) # currently MnKa
self._saveFile = PV(f"{BEAMLINE}-ES-SiD:SaveSystemFile")
self._saveFile_name = PV(f"{BEAMLINE}-ES-SiD:SaveSystem")
self._saveFile_rbv = PV(f"{BEAMLINE}-ES-SiD:SaveSystem_RBV") # 1 ssave, 0 done
self._roi_1 = self.get_roi(BEAMLINE, 1) # currently SiEsc
self._roi_2 = self.get_roi(BEAMLINE, 2) # currently MnKa
self.__calibration_offset = PV(f"{BEAMLINE}-ES-SiD:mca1.CALO")
self.__calibration_slope = PV(f"{BEAMLINE}-ES-SiD:mca1.CALS")
self._calibration_offset = PV(f"{BEAMLINE}-ES-SiD:mca1.CALO")
self._calibration_slope = PV(f"{BEAMLINE}-ES-SiD:mca1.CALS")
def get_roi(self, bl, roi_num: int):
if roi_num not in [1, 2]:
@@ -52,15 +52,15 @@ class Fluorimeter(object):
def start_acquisition(self, erase: bool = False):
if erase:
self.__erase_and_start.put(1)
self._erase_and_start.put(1)
else:
self.__start.put(1)
self._start.put(1)
def stop_acquisition(self):
self.__stop.put(1)
self._stop.put(1)
def erase(self):
self.__erase.put(1)
self._erase.put(1)
def poll_data(self, timeout_s: float = 30.0):
timeout = time.time() + timeout_s
@@ -68,17 +68,17 @@ class Fluorimeter(object):
if time.time() > timeout:
self.stop_acquisition()
raise TimeoutError("timeout waiting for data to be acquired.")
self.__mca.get()
self._mca.get()
poll(0.1)
def get_current_data(self):
return self.__data.get()
return self._data.get()
def get_current_background(self):
return self.__background.get()
return self._background.get()
def check_status(self):
return self.__status.get()
return self._status.get()
def check_status_done(self):
if self.check_status() == 0:
@@ -107,18 +107,18 @@ class Fluorimeter(object):
if mode not in [0, 1, 2, 3, 4]:
raise ValueError(f"Invalid preset mode: {mode}")
try:
self.__preset_mode.put(mode)
self._preset_mode.put(mode)
except Exception as e:
logger.error(f"Error setting preset mode to {mode}: {e}")
def save_file(self, filename: str | None = None, timeout_s: float = 30.0):
if not filename:
filename = f"KETEK_{time.strftime('%Y%m%d_%H%M%S')}.txt"
self.__saveFile_name.put(filename)
self._saveFile_name.put(filename)
try:
self.__saveFile.put(1)
self._saveFile.put(1)
timeout = time.time() + timeout_s
while self.__saveFile_rbv.get() == 1:
while self._saveFile_rbv.get() == 1:
if time.time() > timeout:
raise TimeoutError("Timed out waiting for save to complete")
poll(0.1)
@@ -127,47 +127,47 @@ class Fluorimeter(object):
@property
def real_time(self):
return self.__real_time.get()
return self._real_time.get()
@real_time.setter
def real_time(self, value):
self.__real_time.put(value)
self._real_time.put(value)
@property
def live_time(self):
return self.__live_time.get()
return self._live_time.get()
@live_time.setter
def live_time(self, value):
self.__live_time.put(value)
self._live_time.put(value)
def elapsed_real_time(self):
return self.__elapsed_real_time.get()
return self._elapsed_real_time.get()
def elapsed_live_time(self):
return self.__elapsed_live_time.get()
return self._elapsed_live_time.get()
def elapsed_trigger_live_time(self):
return self.__elapsed_trigger_live_time.get()
return self._elapsed_trigger_live_time.get()
def instant_dead_time(self):
return self.__instant_dead_time.get()
return self._instant_dead_time.get()
def average_dead_time(self):
return self.__average_dead_time.get()
return self._average_dead_time.get()
@property
def slope(self):
return self.__calibration_slope.get()
return self._calibration_slope.get()
@property
def offset(self):
return self.__calibration_offset.get()
return self._calibration_offset.get()
@property
def poll_time(self):
return self.__poll_time_rbv.get()
return self._poll_time_rbv.get()
@poll_time.setter
def poll_time(self, value):
self.__poll_time.put(value)
self._poll_time.put(value)
+30 -26
View File
@@ -9,6 +9,8 @@ from aarecommon.models.beamline import MXBeamline
from aarecommon.models.models import DAQStatusModel, FluorescenceSpectrumOutputModel
from aarecommon.models.raster_grid import RasterGridRequest
from aarecommon.models.rotation_scan import RotationScanRequest
from jfjoch_client.api.default_api import DefaultApi
from jfjoch_client.api_client import ApiClient
class ScanTypeEnum(Enum):
@@ -22,16 +24,18 @@ class ScanTypeEnum(Enum):
class JFJochWrapper:
def __init__(self, bl: MXBeamline):
self.__simulated = bl == MXBeamline.SIMULATED
self.__url = get_jfjoch_url(bl)
self._simulated = bl == MXBeamline.SIMULATED
self._url = get_jfjoch_url(bl)
if self.__url == "simulated":
self.__client = None
self.__api = None
if self._url == "simulated":
from unittest.mock import MagicMock
self._client = MagicMock(spec=ApiClient)
self._api = MagicMock(spec=DefaultApi)
return
self.__client = jfjoch_client.ApiClient(jfjoch_client.Configuration(host=self.__url))
self.__api = jfjoch_client.DefaultApi(self.__client)
self._client = jfjoch_client.ApiClient(jfjoch_client.Configuration(host=self._url))
self._api = jfjoch_client.DefaultApi(self._client)
self.cancel()
# if not self.is_idle():
# self.initialize()
@@ -56,13 +60,13 @@ class JFJochWrapper:
message,
operation=operation,
endpoint=endpoint,
base_url=self.__url,
base_url=self._url,
status_code=self._extract_status_code(error),
) from error
def initialize(self):
try:
self.__api.initialize_post()
self._api.initialize_post()
except Exception as e:
self._raise_jfjoch_error(
"JFJoch initialize failed", error=e, operation="POST", endpoint="initialize_post"
@@ -70,17 +74,17 @@ class JFJochWrapper:
def cancel(self):
try:
self.__api.cancel_post()
self._api.cancel_post()
except Exception as e:
self._raise_jfjoch_error(
"JFJoch cancel failed", error=e, operation="POST", endpoint="cancel_post"
)
def is_idle(self) -> bool:
status = self.__api.status_get()
status = self._api.status_get()
return status.state == "Idle"
def __format_dataset_settings(
def _format_dataset_settings(
self,
r: RasterGridRequest | RotationScanRequest,
s: DAQStatusModel,
@@ -192,7 +196,7 @@ class JFJochWrapper:
return dataset_settings
def __start_scan(
def _start_scan(
self,
scan_type: ScanTypeEnum,
r: RasterGridRequest | RotationScanRequest,
@@ -200,9 +204,9 @@ class JFJochWrapper:
f: FluorescenceSpectrumOutputModel | None = None,
async_start: bool = True,
):
dataset_settings = self.__format_dataset_settings(r, s, f, async_start=async_start)
dataset_settings = self._format_dataset_settings(r, s, f, async_start=async_start)
try:
self.__api.start_post(dataset_settings=dataset_settings)
self._api.start_post(dataset_settings=dataset_settings)
except Exception as e:
self._raise_jfjoch_error(
f"JFJoch data collection failed to initialize for {scan_type.value} scan with exception: {e}",
@@ -219,18 +223,18 @@ class JFJochWrapper:
async_start: bool = True,
) -> None:
if r.screening:
self.__start_scan(ScanTypeEnum.SCREENING, r, s, f, async_start=async_start)
self._start_scan(ScanTypeEnum.SCREENING, r, s, f, async_start=async_start)
else:
self.__start_scan(ScanTypeEnum.ROTATION, r, s, f, async_start=async_start)
self._start_scan(ScanTypeEnum.ROTATION, r, s, f, async_start=async_start)
def measure_raster(self, r: RasterGridRequest, s: DAQStatusModel, async_start: bool = True):
self.__start_scan(ScanTypeEnum.RASTER, r, s, async_start=async_start)
self._start_scan(ScanTypeEnum.RASTER, r, s, async_start=async_start)
def wait_till_running(self, timeout: int | float = 60):
if self.__simulated:
if self._simulated:
return None
try:
self.__api.wait_until_running_post_with_http_info(timeout=math.ceil(timeout))
self._api.wait_until_running_post_with_http_info(timeout=math.ceil(timeout))
return True
except Exception as e:
self._raise_jfjoch_error(
@@ -241,11 +245,11 @@ class JFJochWrapper:
)
def wait_till_done(self, timeout: int | float) -> jfjoch_client.models.ScanResult | None:
if self.__simulated:
if self._simulated:
return None
try:
self.__api.wait_till_done_post_with_http_info(timeout=math.ceil(timeout))
return self.__api.result_scan_get()
self._api.wait_till_done_post_with_http_info(timeout=math.ceil(timeout))
return self._api.result_scan_get()
except Exception as e:
self._raise_jfjoch_error(
f"JFJoch wait till done retrieval returned an exception: {e}",
@@ -256,7 +260,7 @@ class JFJochWrapper:
def detector(self) -> jfjoch_client.models.DetectorListElement:
try:
detector_list = self.__api.config_select_detector_get()
detector_list = self._api.config_select_detector_get()
except Exception as e:
self._raise_jfjoch_error(
f"JFJoch detector configuration retrieval failed: {e}",
@@ -270,7 +274,7 @@ class JFJochWrapper:
"JFJoch returned no configured detectors",
operation="GET",
endpoint="config_select_detector_get",
base_url=self.__url,
base_url=self._url,
status_code=None,
)
@@ -301,7 +305,7 @@ class JFJochWrapper:
last_error = None
for _ in range(3):
try:
return self.__api.image_buffer_image_jpeg_get(
return self._api.image_buffer_image_jpeg_get(
id=image_id,
show_spots=show_spots,
show_res_est=show_res_est,
+6 -6
View File
@@ -24,11 +24,11 @@ class PssState:
self._simulated = beamline == MXBeamline.SIMULATED
BEAMLINE = beamline.value.upper()
if self._simulated:
self.__prohibited = None
self.__alarm = None
self._prohibited = None
self._alarm = None
else:
self.__prohibited = PV(f"{BEAMLINE}-EH1-PSYS:PROHIBITED-STATE")
self.__alarm = PV(f"{BEAMLINE}-EH1-PSYS:ALARM-STATE")
self._prohibited = PV(f"{BEAMLINE}-EH1-PSYS:PROHIBITED-STATE")
self._alarm = PV(f"{BEAMLINE}-EH1-PSYS:ALARM-STATE")
def is_prohibited(self) -> bool:
"""True when the hutch is interlocked so the robot is allowed to move.
@@ -39,7 +39,7 @@ class PssState:
"""
if self._simulated:
return True
value = self.__prohibited.get()
value = self._prohibited.get()
if value is None:
logger.warning(
"PROHIBITED-STATE PV returned no value; treating hutch as not prohibited"
@@ -55,7 +55,7 @@ class PssState:
"""True when ``ALARM-STATE`` is non-zero (a safety alarm is active)."""
if self._simulated:
return False
value = self.__alarm.get()
value = self._alarm.get()
if value is None:
return False
if isinstance(value, str):
+29 -29
View File
@@ -21,31 +21,31 @@ class Smargon(object):
def __init__(self, bl: MXBeamline):
if bl == MXBeamline.X06DA:
self.__simulated = False
self.__base = cfg_get("daq.hardware.smargon_url", "http://x06da-smargopolo.psi.ch:3000")
self._simulated = False
self._base = cfg_get("daq.hardware.smargon_url", "http://x06da-smargopolo.psi.ch:3000")
elif bl == MXBeamline.X10SA:
self.__simulated = False
self.__base = cfg_get("daq.hardware.smargon_url", "http://x10sa-smargopolo.psi.ch:3000")
self._simulated = False
self._base = cfg_get("daq.hardware.smargon_url", "http://x10sa-smargopolo.psi.ch:3000")
elif bl == MXBeamline.X06SA:
raise NotImplementedError("Not implemented smargon url for X06SA")
elif bl == MXBeamline.SIMULATED:
self.__simulated = True
self._simulated = True
else:
raise Exception("unknown beamline")
self.__pos = self.SMARGON_HOME
self.__pos_aero = self.AERO_HOME
self._pos = self.SMARGON_HOME
self._pos_aero = self.AERO_HOME
def gonget(self, thing: str) -> dict:
"""issue a GET for some API component on the smargopolo server
short hand for goniometer get"""
cmd = f"{self.__base}/{thing}"
cmd = f"{self._base}/{thing}"
try:
r = requests.get(cmd, timeout=2.0)
except requests.exceptions.RequestException as e:
raise SmargonCommunicationError(
f"Smargon GET failed for '{thing}'",
endpoint=thing,
base_url=self.__base,
base_url=self._base,
operation="GET",
) from e
@@ -53,7 +53,7 @@ class Smargon(object):
raise SmargonCommunicationError(
f"Smargon GET returned HTTP {r.status_code} for '{thing}': {r.reason}",
endpoint=thing,
base_url=self.__base,
base_url=self._base,
operation="GET",
status_code=r.status_code,
)
@@ -64,7 +64,7 @@ class Smargon(object):
raise SmargonCommunicationError(
f"Smargon GET returned invalid JSON for '{thing}'",
endpoint=thing,
base_url=self.__base,
base_url=self._base,
operation="GET",
status_code=r.status_code,
) from e
@@ -72,14 +72,14 @@ class Smargon(object):
def gonput(self, thing: str):
"""issue a PUT command for some API component on the smargopolo server
short hand for goniometer put"""
cmd = f"{self.__base}/{thing}"
cmd = f"{self._base}/{thing}"
try:
r = requests.put(cmd, timeout=2.0)
except requests.exceptions.RequestException as e:
raise SmargonCommunicationError(
f"Smargon PUT failed for '{thing}'",
endpoint=thing,
base_url=self.__base,
base_url=self._base,
operation="PUT",
) from e
@@ -87,7 +87,7 @@ class Smargon(object):
raise SmargonCommunicationError(
f"Smargon PUT returned HTTP {r.status_code} for '{thing}': {r.reason}",
endpoint=thing,
base_url=self.__base,
base_url=self._base,
operation="PUT",
status_code=r.status_code,
)
@@ -104,7 +104,7 @@ class Smargon(object):
@mode.setter
def mode(self, mode: SmargonMode):
if self.__simulated:
if self._simulated:
return
self.gonput(f"mode?mode={mode}")
@@ -122,21 +122,21 @@ class Smargon(object):
return True
def enable_correction(self):
if self.__simulated:
if self._simulated:
return
self.gonput("corr_type?corr_type=1")
def disable_correction(self):
if self.__simulated:
if self._simulated:
return
self.gonput("corr_type?corr_type=0")
@property
def readback(self) -> SmargonCoordinate:
if self.__simulated:
return self.__pos
if self._simulated:
return self._pos
scs = self.gonget("readbackSCS")
return SmargonCoordinate(
@@ -147,8 +147,8 @@ class Smargon(object):
@property
def readback_aerotech(self) -> AerotechCoordinate:
if self.__simulated:
return self.__pos_aero
if self._simulated:
return self._pos_aero
acs = self.gonget("readbackAEROTECH")
return AerotechCoordinate(
@@ -157,8 +157,8 @@ class Smargon(object):
@property
def target(self) -> SmargonCoordinate:
if self.__simulated:
return self.__pos
if self._simulated:
return self._pos
scs = self.gonget("targetSCS") # targetAEROTECH, #targetOMEGA
return SmargonCoordinate(
@@ -169,8 +169,8 @@ class Smargon(object):
@target.setter
def target(self, coord: SmargonCoordinate):
if self.__simulated:
self.__pos = coord
if self._simulated:
self._pos = coord
return
target_string = ""
@@ -187,8 +187,8 @@ class Smargon(object):
@property
def target_aerotech(self) -> AerotechCoordinate:
if self.__simulated:
return self.__pos_aero
if self._simulated:
return self._pos_aero
acs = self.gonget("targetAEROTECH") # targetAEROTECH, #targetOMEGA
return AerotechCoordinate(
@@ -197,8 +197,8 @@ class Smargon(object):
@target_aerotech.setter
def target_aerotech(self, coord: AerotechCoordinate):
if self.__simulated:
self.__pos_aero = coord
if self._simulated:
self._pos_aero = coord
return
target_string = ""
+1 -1
View File
@@ -57,7 +57,7 @@ class TellClient:
"""High-level Tell robot API using a pluggable backend"""
def __init__(self, bl: MXBeamline, backend: TellBackend | None = None):
self.__beamline = bl
self._beamline = bl
self.backend = backend or PShellTellBackend(bl)
self._aborted = False
+50 -50
View File
@@ -34,51 +34,51 @@ class ZMQCameraClient:
timeout_ms: Receive timeout in milliseconds
"""
if beamline == MXBeamline.X06DA:
self.__simulated = False
self.__zmq_url = "tcp://x06da-pserv-01:9089"
self._simulated = False
self._zmq_url = "tcp://x06da-pserv-01:9089"
elif beamline == MXBeamline.X10SA:
self.__simulated = False
self.__zmq_url = "tcp://x10sa-spark-01:9091"
self._simulated = False
self._zmq_url = "tcp://x10sa-spark-01:9091"
elif beamline == MXBeamline.SIMULATED:
self.__simulated = True
self.__zmq_url = None
self._simulated = True
self._zmq_url = None
else:
raise Exception("unknown beamline")
self.__timeout_ms = timeout_ms
self.__context: Optional[zmq.Context] = None
self.__socket: Optional[zmq.Socket] = None
self.__last_image: Optional[np.ndarray] = None
self.__last_fetch_time: float = 0.0
self.__connected = False
self._timeout_ms = timeout_ms
self._context: Optional[zmq.Context] = None
self._socket: Optional[zmq.Socket] = None
self._last_image: Optional[np.ndarray] = None
self._last_fetch_time: float = 0.0
self._connected = False
def _ensure_connected(self) -> bool:
"""Ensure the ZMQ socket is connected. Returns True if successful."""
if self.__connected and self.__socket is not None:
if self._connected and self._socket is not None:
return True
if self.__simulated or self.__zmq_url is None:
if self._simulated or self._zmq_url is None:
return False
try:
if self.__context is None:
self.__context = zmq.Context()
if self._context is None:
self._context = zmq.Context()
if self.__socket is not None:
self.__socket.close()
if self._socket is not None:
self._socket.close()
self.__socket = self.__context.socket(zmq.SUB)
self.__socket.setsockopt(zmq.SUBSCRIBE, b"")
self.__socket.setsockopt(zmq.RCVTIMEO, self.__timeout_ms)
self._socket = self._context.socket(zmq.SUB)
self._socket.setsockopt(zmq.SUBSCRIBE, b"")
self._socket.setsockopt(zmq.RCVTIMEO, self._timeout_ms)
# Discard old messages, keep only the latest
self.__socket.setsockopt(zmq.CONFLATE, 1)
self.__socket.connect(self.__zmq_url)
self.__connected = True
logger.debug(f"ZMQ camera connected to {self.__zmq_url}")
self._socket.setsockopt(zmq.CONFLATE, 1)
self._socket.connect(self._zmq_url)
self._connected = True
logger.debug(f"ZMQ camera connected to {self._zmq_url}")
return True
except Exception as e:
logger.warning(f"Failed to connect ZMQ camera to {self.__zmq_url}: {e}")
self.__connected = False
logger.warning(f"Failed to connect ZMQ camera to {self._zmq_url}: {e}")
self._connected = False
return False
def get_image(self, gray: bool = False) -> Optional[np.ndarray]:
@@ -91,18 +91,18 @@ class ZMQCameraClient:
Returns:
numpy array with the image, or None if unavailable.
"""
if self.__simulated:
if self._simulated:
return None
if not self._ensure_connected():
return None
try:
r = self.__socket.recv_multipart()
r = self._socket.recv_multipart()
if len(r) < 2:
logger.debug("ZMQ camera: incomplete message received")
return self.__last_image
return self._last_image
data = r[-1]
header = None
@@ -125,60 +125,60 @@ class ZMQCameraClient:
if gray:
gray_img = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
self.__last_image = gray_img
self._last_image = gray_img
return gray_img
else:
self.__last_image = rgb
self._last_image = rgb
return rgb
else:
logger.debug("ZMQ camera: unexpected image format")
return self.__last_image
return self._last_image
except zmq.Again:
logger.debug("ZMQ camera: timeout waiting for frame")
return self.__last_image
return self._last_image
except Exception as e:
logger.warning(f"ZMQ camera error: {e}")
self.__connected = False
return self.__last_image
self._connected = False
return self._last_image
def is_available(self) -> bool:
"""Check if ZMQ camera is available and responding."""
if self.__simulated:
if self._simulated:
return False
if not self._ensure_connected():
return False
try:
old_timeout = self.__socket.getsockopt(zmq.RCVTIMEO)
self.__socket.setsockopt(zmq.RCVTIMEO, 500)
old_timeout = self._socket.getsockopt(zmq.RCVTIMEO)
self._socket.setsockopt(zmq.RCVTIMEO, 500)
try:
self.__socket.recv_multipart()
self.__socket.setsockopt(zmq.RCVTIMEO, old_timeout)
self._socket.recv_multipart()
self._socket.setsockopt(zmq.RCVTIMEO, old_timeout)
return True
except zmq.Again:
self.__socket.setsockopt(zmq.RCVTIMEO, old_timeout)
self._socket.setsockopt(zmq.RCVTIMEO, old_timeout)
return False
except Exception:
return False
def close(self):
"""Close the ZMQ connection."""
if self.__socket:
if self._socket:
try:
self.__socket.close()
self._socket.close()
except Exception:
pass
self.__socket = None
if self.__context:
self._socket = None
if self._context:
try:
self.__context.term()
self._context.term()
except Exception:
pass
self.__context = None
self.__connected = False
self._context = None
self._connected = False
@property
def url(self) -> str:
return self.__zmq_url if self.__zmq_url else "simulated"
return self._zmq_url if self._zmq_url else "simulated"
+36 -36
View File
@@ -111,11 +111,11 @@ class MainWindow(QMainWindow):
self._use_legacy_theme_action = None
self._use_portrait_theme_action = None
self.__base_url = base_url
self.__token = token
self.__mounting = False
self.__samcam_feed_banner_active = False
self.__samcam_feed_banner_message = "Sample camera feed unavailable"
self._base_url = base_url
self._token = token
self._mounting = False
self._samcam_feed_banner_active = False
self._samcam_feed_banner_message = "Sample camera feed unavailable"
self._automation_critical_banner_active = False
self._dev_help_dialog = None
@@ -168,7 +168,7 @@ class MainWindow(QMainWindow):
)
payload = jwt.decode(token_str, options={"verify_signature": False})
self.__decoded_token = TokenData(**payload)
self._decoded_token = TokenData(**payload)
except Exception as e:
logger.error(f"Failed to decode authentication token: {e}", exc_info=True)
QMessageBox.critical(
@@ -242,7 +242,7 @@ class MainWindow(QMainWindow):
self.beamline_state_panel = BeamlineStatePanel(parent=self.left_column)
self._beamline_state_panel_enabled = bool(
self.__decoded_token.staff or self._show_beamline_state_panel_for_users
self._decoded_token.staff or self._show_beamline_state_panel_for_users
)
self.left_column_layout.addWidget(self.data_collection)
@@ -349,7 +349,7 @@ class MainWindow(QMainWindow):
self.beamline_controls_scroll = NoWheelScrollArea(top_widget)
self.beamline = BeamlineControls(
self.beamline_controls_scroll, staff=self.__decoded_token.staff
self.beamline_controls_scroll, staff=self._decoded_token.staff
)
top_widget_layout.addWidget(self.beamline_controls_scroll)
self.beamline_controls_scroll.setWidget(self.beamline)
@@ -360,7 +360,7 @@ class MainWindow(QMainWindow):
self.tell_samples = TellSamplePanel(samples=SampleShortInfoList(s=[]))
self.ref_tools_panel = ReferenceToolsPanel(samples=SampleShortInfoList(s=[]))
self.job_list_panel = SampleQueuePanel(show_user=self.__decoded_token.staff)
self.job_list_panel = SampleQueuePanel(show_user=self._decoded_token.staff)
self.compact_automation_panel.play_pause_clicked.connect(self.job_list_panel.run)
self.compact_automation_panel.skip_clicked.connect(self.job_list_panel.skip_current_sample)
@@ -384,7 +384,7 @@ class MainWindow(QMainWindow):
self.ref_tools_dock.setAllowedAreas(Qt.DockWidgetArea.BottomDockWidgetArea)
self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.ref_tools_dock)
self.tabifyDockWidget(self.ref_tools_dock, self.tell_samples_dock)
if self.__decoded_token.staff:
if self._decoded_token.staff:
self.ref_tools_dock.show()
else:
self.ref_tools_dock.hide()
@@ -549,10 +549,10 @@ class MainWindow(QMainWindow):
event_bus=self._tutorial_event_bus,
)
self.status_bar = StatusBar(self.__decoded_token, parent=self)
self.status_bar = StatusBar(self._decoded_token, parent=self)
self.setStatusBar(self.status_bar)
self.daq = DAQWorker(base_url=self.__base_url, token=self.__token)
self.daq = DAQWorker(base_url=self._base_url, token=self._token)
self.installEventFilter(self)
@@ -594,7 +594,7 @@ class MainWindow(QMainWindow):
self.status_bar.baton_request_refused.connect(self._refuse_baton_request)
self.daq.spreadsheet.connect(self.tell_samples.new_sample_list)
if self.__decoded_token.staff:
if self._decoded_token.staff:
self.daq.reference_tools.connect(self.ref_tools_panel.new_list)
self.beamline.samcam.changed.connect(self.daq.samcam_settings)
@@ -620,7 +620,7 @@ class MainWindow(QMainWindow):
self.beamline.illumination_panel.front_light.connect(self.daq.front_light)
self.beamline.illumination_panel.back_light.connect(self.daq.back_light)
if self.__decoded_token.staff:
if self._decoded_token.staff:
self.beamline.monochromator_panel.mono_pitch_scan.connect(self.daq.mono_pitch_scan)
self.beamline.monochromator_panel.change_energy.connect(self.daq.change_energy)
self.beamline.abr_tweak.abr_tweak.connect(self.daq.abr_tweak)
@@ -806,7 +806,7 @@ class MainWindow(QMainWindow):
if self.prediction_thread is not None:
self.daq.update.connect(self.prediction_thread.update_daq_status)
if self.__decoded_token.staff:
if self._decoded_token.staff:
self.daq.update.connect(self.beamline.monochromator_panel.update_daq_status)
self.daq.update.connect(self.beamline.beam_size.update_daq_status)
self.daq.update.connect(self.beamline.beam_center.update_daq_status)
@@ -990,17 +990,17 @@ class MainWindow(QMainWindow):
self._show_samcam_feed_banner(message or "Sample camera feed unavailable")
def _show_samcam_feed_banner(self, message: str) -> None:
self.__samcam_feed_banner_message = message
self._samcam_feed_banner_message = message
self._show_runtime_notification(
title="Sample camera", message=message, level="warning", sticky=True
)
self.__samcam_feed_banner_active = True
self._samcam_feed_banner_active = True
def _clear_samcam_feed_banner(self) -> None:
if not self.__samcam_feed_banner_active:
if not self._samcam_feed_banner_active:
return
self._clear_runtime_notification()
self.__samcam_feed_banner_active = False
self._samcam_feed_banner_active = False
def _stop_axis_camera_threads(self) -> None:
for attr_name in ("beamline_camera_thread", "gonio_camera_thread"):
@@ -1093,7 +1093,7 @@ class MainWindow(QMainWindow):
self.prediction_metrics_dock.setVisible(False)
self.log_dock.setVisible(False)
if self.__decoded_token.staff:
if self._decoded_token.staff:
self.ref_tools_dock.setVisible(False)
self.collection_controls_scroll.setVisible(False)
@@ -1113,7 +1113,7 @@ class MainWindow(QMainWindow):
self.collection_controls_scroll.setVisible(self._pre_automation_left_column_visible)
self.beamline_controls_scroll.setVisible(self._pre_automation_right_column_visible)
if self.__decoded_token.staff:
if self._decoded_token.staff:
self.ref_tools_dock.setVisible(self._pre_automation_ref_tools_visible)
self._in_compact_automation_view = False
@@ -1141,8 +1141,8 @@ class MainWindow(QMainWindow):
# ── Camera: scale-to-fit + hide legend ─────────────────────────────
self.portrait_sample_camera.set_show_overlay_legend(False)
try:
self.portrait_sample_camera._SampleCameraImageLabel__autoscale = True
self.portrait_sample_camera._SampleCameraImageLabel__scaling()
self.portrait_sample_camera._autoscale = True
self.portrait_sample_camera._scaling()
except Exception:
pass
@@ -1413,7 +1413,7 @@ class MainWindow(QMainWindow):
self.tell_samples_dock.visibilityChanged.connect(show_samples_action.setChecked)
view_menu.addAction(show_samples_action)
if self.__decoded_token.staff:
if self._decoded_token.staff:
show_reference_tools_action = QAction("Show Reference Tools", self)
show_reference_tools_action.setCheckable(True)
show_reference_tools_action.setChecked(True)
@@ -1549,7 +1549,7 @@ class MainWindow(QMainWindow):
refresh_axis_cameras_action.triggered.connect(self.refresh_axis_cameras)
help_menu.addAction(refresh_axis_cameras_action)
if self.__decoded_token.staff:
if self._decoded_token.staff:
local_contact_action = QAction("Local Contact", self)
local_contact_action.triggered.connect(self.show_local_contact)
help_menu.addAction(local_contact_action)
@@ -1596,7 +1596,7 @@ class MainWindow(QMainWindow):
self.prediction_metrics_dock.setVisible(False)
self.log_dock.setVisible(False)
if self.__decoded_token.staff:
if self._decoded_token.staff:
self.ref_tools_dock.setVisible(True)
self.ref_tools_dock.raise_()
else:
@@ -1619,7 +1619,7 @@ class MainWindow(QMainWindow):
if self._dev_help_dialog is None:
self._dev_help_dialog = DeveloperHelpDialog(
daq=self.daq,
is_staff=bool(getattr(self.__decoded_token, "staff", False)),
is_staff=bool(getattr(self._decoded_token, "staff", False)),
parent=self,
)
self._dev_help_dialog.refresh()
@@ -1912,7 +1912,7 @@ class MainWindow(QMainWindow):
# 4. Surface recovery UI
try:
if bool(getattr(self.__decoded_token, "staff", False)):
if bool(getattr(self._decoded_token, "staff", False)):
if is_detector_failure:
QMessageBox.critical(
self,
@@ -1958,7 +1958,7 @@ class MainWindow(QMainWindow):
logger.error(f"Failed to surface recovery UI after critical failure: {e}")
def show_beamline_recovery(self) -> None:
if not bool(getattr(self.__decoded_token, "staff", False)):
if not bool(getattr(self._decoded_token, "staff", False)):
return
if self._beamline_recovery_dialog is None:
self._beamline_recovery_dialog = BeamlineRecoveryDialog(daq=self.daq, parent=self)
@@ -1967,7 +1967,7 @@ class MainWindow(QMainWindow):
self._beamline_recovery_dialog.activateWindow()
def show_local_contact(self, tab_name: str = "Status") -> None:
if not bool(getattr(self.__decoded_token, "staff", False)):
if not bool(getattr(self._decoded_token, "staff", False)):
return
if self._local_contact_dialog is None:
self._local_contact_dialog = LocalContactDialog(daq=self.daq, parent=self)
@@ -2024,7 +2024,7 @@ class MainWindow(QMainWindow):
self._refresh_compact_queue_preview()
current_session = int(getattr(self.__decoded_token, "session", -1))
current_session = int(getattr(self._decoded_token, "session", -1))
for gui in getattr(s, "open_guis", []) or []:
try:
if int(gui.session) == current_session and bool(gui.close_requested):
@@ -2040,11 +2040,11 @@ class MainWindow(QMainWindow):
if self._remote_close_deadline_ts is not None:
self._clear_remote_close_request()
if not self.__mounting and s.state == BeamlineStateEnum.RobotSampleExchange:
self.__mounting = True
if not self._mounting and s.state == BeamlineStateEnum.RobotSampleExchange:
self._mounting = True
self.video_tab.setCurrentWidget(self.beamline_combined_panel)
elif self.__mounting and s.state != BeamlineStateEnum.RobotSampleExchange:
self.__mounting = False
elif self._mounting and s.state != BeamlineStateEnum.RobotSampleExchange:
self._mounting = False
self.video_tab.setCurrentWidget(self.sample_camera)
# ========== BATON DIALOG HANDLING ==========
@@ -2403,7 +2403,7 @@ class MainWindow(QMainWindow):
self._last_interaction_report_ts = now
try:
self.daq.report_gui_interaction(int(self.__decoded_token.session))
self.daq.report_gui_interaction(int(self._decoded_token.session))
except Exception as e:
logger.debug(f"Failed to report GUI interaction: {e}")
+3 -3
View File
@@ -30,7 +30,7 @@ class SampleQueueSpreadsheet(QAbstractTableModel):
super().__init__(parent)
if samples is None:
samples = []
self.__running = False
self._running = False
self._show_user = bool(show_user)
self.samples: list[SampleShortInfo] = samples
self.header = (
@@ -54,7 +54,7 @@ class SampleQueueSpreadsheet(QAbstractTableModel):
return Qt.AlignmentFlag.AlignCenter
elif role == Qt.ItemDataRole.BackgroundRole:
if index.row() == 0:
if self.__running:
if self._running:
return QBrush(QColor(255, 102, 0))
else:
return QBrush(QColor(114, 159, 207))
@@ -113,7 +113,7 @@ class SampleQueueSpreadsheet(QAbstractTableModel):
return True
def set_running(self, running: bool):
self.__running = running
self._running = running
self.layoutChanged.emit()
def remove_sample(self, db_id: int):
+37 -37
View File
@@ -57,12 +57,12 @@ class UserSampleSpreadsheet(QAbstractTableModel):
]
self.current_sample = current_sample
self.current_puck = current_puck
self.__sort_col = 3
self.__sort_order = Qt.SortOrder.AscendingOrder
self._sort_col = 3
self._sort_order = Qt.SortOrder.AscendingOrder
self.__filters: dict[int, str] = {}
self.__filter_col: int | None = 5
self.__filter_value: str | None = None
self._filters: dict[int, str] = {}
self._filter_col: int | None = 5
self._filter_value: str | None = None
self.current_pgroup: str | None = None
self.show_all_pgroups: bool = False
@@ -77,20 +77,20 @@ class UserSampleSpreadsheet(QAbstractTableModel):
self.endResetModel()
def rowCount(self, parent=None):
return len(self.__sorted_samples)
return len(self._sorted_samples)
def columnCount(self, parent=None):
return len(self.header)
def data(self, index, role=None):
if role == Qt.ItemDataRole.DisplayRole:
return get_entry(self.__sorted_samples[index.row()], index.column())
return get_entry(self._sorted_samples[index.row()], index.column())
elif role == Qt.ItemDataRole.TextAlignmentRole: # Align text to center
return Qt.AlignmentFlag.AlignCenter
elif role == Qt.ItemDataRole.BackgroundRole:
if self.__sorted_samples[index.row()].db_id == self.current_sample:
if self._sorted_samples[index.row()].db_id == self.current_sample:
return QBrush(QColor(114, 159, 207)) # darker blue
if self.__sorted_samples[index.row()].puck_name == self.current_puck:
if self._sorted_samples[index.row()].puck_name == self.current_puck:
return QBrush(QColor(216, 228, 253)) # light blue
return QBrush(QColor(255, 255, 255)) # White
return None # For other roles, return None
@@ -118,39 +118,39 @@ class UserSampleSpreadsheet(QAbstractTableModel):
def sort(self, column, order):
self.layoutAboutToBeChanged.emit()
self.__sort_order = order
self.__sort_col = column
self._sort_order = order
self._sort_col = column
self._sort()
self.layoutChanged.emit()
def _sort(self):
filtered = self._apply_filter(self.samples)
if self.__sort_col == 3:
self.__sorted_samples = sorted(
if self._sort_col == 3:
self._sorted_samples = sorted(
filtered,
key=lambda row: row.loc_str_sort(),
reverse=(
self.__sort_order == Qt.SortOrder.DescendingOrder
self._sort_order == Qt.SortOrder.DescendingOrder
), # Reverse for descending order
)
else:
# Sort the samples based on the specified column and order
self.__sorted_samples = sorted(
self._sorted_samples = sorted(
filtered,
key=lambda row: get_entry(row, self.__sort_col),
key=lambda row: get_entry(row, self._sort_col),
reverse=(
self.__sort_order == Qt.SortOrder.DescendingOrder
self._sort_order == Qt.SortOrder.DescendingOrder
), # Reverse for descending order
)
def _apply_filter(self, rows: list[SampleShortInfo]) -> list[SampleShortInfo]:
# Default filter by User using current p-group if no explicit filter set
filters: dict[int, str] = {
col: v for col, v in (self.__filters or {}).items() if (v or "").strip()
col: v for col, v in (self._filters or {}).items() if (v or "").strip()
}
if self.__filter_col is not None and (self.__filter_value or "").strip():
filters[self.__filter_col] = self.__filter_value
if self._filter_col is not None and (self._filter_value or "").strip():
filters[self._filter_col] = self._filter_value
if 5 not in filters and self.current_pgroup and not self.show_all_pgroups:
filters[5] = self.current_pgroup
@@ -183,13 +183,13 @@ class UserSampleSpreadsheet(QAbstractTableModel):
sample_data = SampleShortInfoList(s=[])
for i in sorted(set(index.row() for index in indexes)):
sample_data.s.append(self.__sorted_samples[i])
sample_data.s.append(self._sorted_samples[i])
mime_data.setText(sample_data.model_dump_json())
return mime_data
def get_id(self, row: int) -> SampleShortInfo:
return self.__sorted_samples[row]
return self._sorted_samples[row]
def set_filter(self, field: str, text: str | None):
try:
@@ -197,22 +197,22 @@ class UserSampleSpreadsheet(QAbstractTableModel):
except ValueError:
col = None
self.layoutAboutToBeChanged.emit()
self.__filter_col = col if text and text.strip() and col is not None else None
self.__filter_value = text.strip() if text else None
self._filter_col = col if text and text.strip() and col is not None else None
self._filter_value = text.strip() if text else None
# mirror into multi-filter map
if col is not None:
if text and text.strip():
self.__filters[col] = text.strip()
self._filters[col] = text.strip()
else:
self.__filters.pop(col, None)
self._filters.pop(col, None)
self._sort()
self.layoutChanged.emit()
def clear_filter(self):
self.layoutAboutToBeChanged.emit()
self.__filter_col = None
self.__filter_value = None
self.__filters.clear()
self._filter_col = None
self._filter_value = None
self._filters.clear()
self._sort()
self.layoutChanged.emit()
@@ -225,9 +225,9 @@ class UserSampleSpreadsheet(QAbstractTableModel):
def set_column_filter(self, column: int, text: str | None):
self.layoutAboutToBeChanged.emit()
if text and text.strip():
self.__filters[column] = text.strip()
self._filters[column] = text.strip()
else:
self.__filters.pop(column, None)
self._filters.pop(column, None)
self._sort()
self.layoutChanged.emit()
@@ -243,11 +243,11 @@ class UserSampleSpreadsheet(QAbstractTableModel):
def unique_values_for_column(self, column: int, limit: int = 200) -> list[str]:
"""Get unique values for a column from currently filtered samples (excluding this column's filter)."""
# Get currently filtered samples, but exclude the filter for this column
temp_filter = self.__filters.pop(column, None)
temp_filter = self._filters.pop(column, None)
filtered_samples = self._apply_filter(self.samples)
# Restore the filter
if temp_filter is not None:
self.__filters[column] = temp_filter
self._filters[column] = temp_filter
seen: set[str] = set()
out: list[str] = []
@@ -283,10 +283,10 @@ class UserSampleSpreadsheet(QAbstractTableModel):
def suggested_prefixes_for_sample_name(self, limit: int = 200) -> list[str]:
"""Get sample name prefixes from currently filtered samples (excluding column 0 filter)."""
# Get currently filtered samples, excluding the sample name filter
temp_filter = self.__filters.pop(0, None)
temp_filter = self._filters.pop(0, None)
filtered_samples = self._apply_filter(self.samples)
if temp_filter is not None:
self.__filters[0] = temp_filter
self._filters[0] = temp_filter
rx = re.compile(r"^([A-Za-z]+)")
counts: dict[str, int] = {}
@@ -307,10 +307,10 @@ class UserSampleSpreadsheet(QAbstractTableModel):
def suggested_prefixes_for_location(self, limit: int = 200) -> tuple[list[str], list[str]]:
"""Get location prefixes from currently filtered samples (excluding column 3 filter)."""
# Get currently filtered samples, excluding the location filter
temp_filter = self.__filters.pop(3, None)
temp_filter = self._filters.pop(3, None)
filtered_samples = self._apply_filter(self.samples)
if temp_filter is not None:
self.__filters[3] = temp_filter
self._filters[3] = temp_filter
seg_seen: set[str] = set()
segpos_seen: set[str] = set()
+20 -20
View File
@@ -17,7 +17,7 @@ class AbrTweakButtons(QWidget):
def __init__(self, step_mm, parent=None):
super().__init__(parent)
self.__step_mm = step_mm
self._step_mm = step_mm
grid_layout = QGridLayout(self)
grid_layout.setColumnStretch(0, 1)
grid_layout.setColumnStretch(1, 0)
@@ -70,16 +70,16 @@ class AbrTweakButtons(QWidget):
self.abr_tweak.emit(
AerotechCoordinate(
at_mm=Coordinate(
x=self.__step_mm * payload["x"],
y=self.__step_mm * payload["y"],
z=self.__step_mm * payload["z"],
x=self._step_mm * payload["x"],
y=self._step_mm * payload["y"],
z=self._step_mm * payload["z"],
)
)
)
@Slot(float)
def set_step(self, val_um: float):
self.__step_mm = val_um / 1000.0
self._step_mm = val_um / 1000.0
class AbrTweakWidget(QWidget):
@@ -97,15 +97,15 @@ class AbrTweakWidget(QWidget):
grid_layout.addWidget(TitleLabel("ABR meas. pos.", self), 0, 0, 1, 3)
self.__abr_buttons = AbrTweakButtons(DEFAULT_ABR_STEP_UM / 1000, parent=self)
grid_layout.addWidget(self.__abr_buttons, 1, 0, 1, 4)
self.__abr_buttons.abr_tweak.connect(self.abr_button_pressed)
self._abr_buttons = AbrTweakButtons(DEFAULT_ABR_STEP_UM / 1000, parent=self)
grid_layout.addWidget(self._abr_buttons, 1, 0, 1, 4)
self._abr_buttons.abr_tweak.connect(self.abr_button_pressed)
grid_layout.addWidget(QLabel("Step", parent=self), 2, 0)
self.__step_um = NumberLineEdit(1, 1000, DEFAULT_ABR_STEP_UM, 0, parent=self)
grid_layout.addWidget(self.__step_um, 2, 1)
self._step_um = NumberLineEdit(1, 1000, DEFAULT_ABR_STEP_UM, 0, parent=self)
grid_layout.addWidget(self._step_um, 2, 1)
grid_layout.addWidget(QLabel("μm", parent=self), 2, 2)
self.__step_um.newValue.connect(self.__abr_buttons.set_step)
self._step_um.newValue.connect(self._abr_buttons.set_step)
save_button = QPushButton("Save ABR pos.")
grid_layout.addWidget(save_button, 3, 0, 1, 3)
@@ -129,19 +129,19 @@ class AbrTweakWidget(QWidget):
@Slot(DAQStatusModel)
def update_daq_status(self, s: DAQStatusModel):
self.__abr_buttons.gmx_label.setText(f"{s.geom.aerotech_meas.x:.3f}")
self._abr_buttons.gmx_label.setText(f"{s.geom.aerotech_meas.x:.3f}")
if abs(s.geom.aerotech.x) >= 0.001:
self.__abr_buttons.gmx_label.setStyleSheet("color: rgb(255, 0, 0);")
self._abr_buttons.gmx_label.setStyleSheet("color: rgb(255, 0, 0);")
else:
self.__abr_buttons.gmx_label.setStyleSheet("color: rgb(0, 0, 0);")
self.__abr_buttons.gmy_label.setText(f"{s.geom.aerotech_meas.y:.3f}")
self._abr_buttons.gmx_label.setStyleSheet("color: rgb(0, 0, 0);")
self._abr_buttons.gmy_label.setText(f"{s.geom.aerotech_meas.y:.3f}")
if abs(s.geom.aerotech.y) >= 0.001:
self.__abr_buttons.gmy_label.setStyleSheet("color: rgb(255, 0, 0);")
self._abr_buttons.gmy_label.setStyleSheet("color: rgb(255, 0, 0);")
else:
self.__abr_buttons.gmy_label.setStyleSheet("color: rgb(0, 0, 0);")
self._abr_buttons.gmy_label.setStyleSheet("color: rgb(0, 0, 0);")
self.__abr_buttons.gmz_label.setText(f"{s.geom.aerotech_meas.z:.3f}")
self._abr_buttons.gmz_label.setText(f"{s.geom.aerotech_meas.z:.3f}")
if abs(s.geom.aerotech.z) >= 0.001:
self.__abr_buttons.gmz_label.setStyleSheet("color: rgb(255, 0, 0);")
self._abr_buttons.gmz_label.setStyleSheet("color: rgb(255, 0, 0);")
else:
self.__abr_buttons.gmz_label.setStyleSheet("color: rgb(0, 0, 0);")
self._abr_buttons.gmz_label.setStyleSheet("color: rgb(0, 0, 0);")
+12 -12
View File
@@ -33,23 +33,23 @@ class DataCollectionSettings(QFrame):
self.file_path_panel = FilePathPanel(self)
v_layout.addWidget(self.file_path_panel)
self.__tab_widget = QTabWidget()
self._tab_widget = QTabWidget()
self.raster = RasterDataCollectionPanel(
parent=self, raster_mgr=raster_mgr, diffraction=diffraction
)
self.__tab_widget.addTab(self.raster, "Raster scan")
self._tab_widget.addTab(self.raster, "Raster scan")
self.screening = RotationDataCollectionPanel(parent=self, diffraction=diffraction)
self.__tab_widget.addTab(self.screening, "Rotation")
self._tab_widget.addTab(self.screening, "Rotation")
self.simple = SimpleRotationSettingsPanel(parent=self)
self.__tab_widget.addTab(self.simple, "Simple")
self._tab_widget.addTab(self.simple, "Simple")
self.fluo = FluorescenceDataCollectionPanel(parent=self)
self.__tab_widget.addTab(self.fluo, "XRF")
self._tab_widget.addTab(self.fluo, "XRF")
v_layout.addWidget(self.__tab_widget)
v_layout.addWidget(self._tab_widget)
v_layout.addStretch()
abort_button = QPushButton("Abort measurement", parent=self)
@@ -65,13 +65,13 @@ class DataCollectionSettings(QFrame):
self.file_path_panel.path_updated.connect(raster_mgr.update_filename)
self.file_path_panel.path_updated.connect(self.screening.update_filename)
self.file_path_panel.path_updated.connect(self.simple.update_filename)
self.__sample_id = None
self._sample_id = None
self.__tab_widget.currentChanged.connect(self._on_tab_changed)
self._tab_widget.currentChanged.connect(self._on_tab_changed)
@Slot()
def switch_to_raster(self):
self.__tab_widget.setCurrentIndex(0)
self._tab_widget.setCurrentIndex(0)
@Slot()
def cancel_button_clicked(self):
@@ -82,9 +82,9 @@ class DataCollectionSettings(QFrame):
self.raster.update_daq_status(s)
self.screening.update_daq_status(s)
self.simple.update_daq_status(s)
if s.sample is not None and s.sample.db_id != self.__sample_id:
self.__sample_id = s.sample.db_id
self.__tab_widget.setCurrentIndex(0)
if s.sample is not None and s.sample.db_id != self._sample_id:
self._sample_id = s.sample.db_id
self._tab_widget.setCurrentIndex(0)
@Slot(int)
def _on_tab_changed(self, idx: int):
+39 -39
View File
@@ -23,18 +23,18 @@ class FilePathPanel(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
grid_layout = QGridLayout(self)
self.__sample_name = "sample"
self.__sample_id = -1
self.__dewar_pos = "None"
self.__puck_name = "Manual"
self.__puck_pos = 0
self.__curr_pgroup = "p11206"
self.__beamline = mx_beamline().value.lower()
self._sample_name = "sample"
self._sample_id = -1
self._dewar_pos = "None"
self._puck_name = "Manual"
self._puck_pos = 0
self._curr_pgroup = "p11206"
self._beamline = mx_beamline().value.lower()
self.__filename = ""
self.__scan_kind = "raster" # default: "rotation" | "screening" | "raster"
self._filename = ""
self._scan_kind = "raster" # default: "rotation" | "screening" | "raster"
self.__formatted_date = datetime.now().strftime("%Y%m%d")
self._formatted_date = datetime.now().strftime("%Y%m%d")
grid_layout.addWidget(TitleLabel("Dataset path", self), 0, 0, 1, 2)
@@ -86,22 +86,22 @@ class FilePathPanel(QWidget):
def _expand_macros(self, base: str, rn: int) -> str:
name = f"{base}_{rn:03d}"
name = name.replace("{date}", self.__formatted_date)
name = name.replace("{sample}", self.__sample_name)
name = name.replace("{CrystalName}", self.__sample_name)
name = name.replace("{puck}", self.__puck_name)
name = name.replace("{position}", f"{self.__puck_pos:02d}")
name = name.replace("{sample_id}", f"{self.__sample_id}")
name = name.replace("{beamline}", f"{self.__beamline}")
name = name.replace("{date}", self._formatted_date)
name = name.replace("{sample}", self._sample_name)
name = name.replace("{CrystalName}", self._sample_name)
name = name.replace("{puck}", self._puck_name)
name = name.replace("{position}", f"{self._puck_pos:02d}")
name = name.replace("{sample_id}", f"{self._sample_id}")
name = name.replace("{beamline}", f"{self._beamline}")
name = name.replace(
"{prefix}", f"{self.__puck_name}/{self.__puck_pos:02d}/{self.__sample_name}"
"{prefix}", f"{self._puck_name}/{self._puck_pos:02d}/{self._sample_name}"
)
return name
def _effective_dataset_base(self, base_no_run: str) -> str:
root = Path("/sls/mx/data") / self.__curr_pgroup / "raw"
root = Path("/sls/mx/data") / self._curr_pgroup / "raw"
p = Path(base_no_run)
match self.__scan_kind:
match self._scan_kind:
case "rotation":
p = Path("data") / p
case "screening":
@@ -150,16 +150,16 @@ class FilePathPanel(QWidget):
self.run_number_edit.blockSignals(False)
# Store the GUIs base (without applying scan-kind transforms) for wiring into requests later
self.__filename = self._expand_macros(base, rn)
self._filename = self._expand_macros(base, rn)
# Preview label shows the effective path (what will be written)
effective = self._effective_dataset_base(self.__filename)
effective = self._effective_dataset_base(self._filename)
exists = os.path.exists(f"{effective}_master.h5") or os.path.exists(effective)
self.file_name_label.setText(effective + "_master.h5")
self.file_name_label.setStyleSheet(
"color: rgb(200, 0, 0);" if exists else "color: rgb(0, 0, 0);"
)
self.path_updated.emit(self.__filename)
self.path_updated.emit(self._filename)
@Slot()
def increment_run_number(self):
@@ -184,44 +184,44 @@ class FilePathPanel(QWidget):
@property
def filename(self) -> str:
return self.__filename
return self._filename
@Slot(SampleShortInfo)
def update_sample(self, sample: SampleShortInfo):
if sample is None:
self.__sample_id = -1
self.__sample_name = "sample"
self.__dewar_pos = "None"
self.__puck_name = "Manual"
self.__puck_pos = 99
self.directory_edit.setText(f"{self.__formatted_date}/test")
self._sample_id = -1
self._sample_name = "sample"
self._dewar_pos = "None"
self._puck_name = "Manual"
self._puck_pos = 99
self.directory_edit.setText(f"{self._formatted_date}/test")
else:
self.__sample_id = sample.db_id
self.__sample_name = sample.sample_name
self.__dewar_pos = sample.loc_str()
self.__puck_name = sample.puck_name
self.__puck_pos = sample.pin
self._sample_id = sample.db_id
self._sample_name = sample.sample_name
self._dewar_pos = sample.loc_str()
self._puck_name = sample.puck_name
self._puck_pos = sample.pin
self.run_number_edit.setValue(1)
if sample.aaredb_params is not None and sample.aaredb_params.directory is not None:
self.directory_edit.setText(f"{sample.aaredb_params.directory}")
elif sample.location is not None:
self.directory_edit.setText(
f"{self.__formatted_date}/{self.__puck_name}/{self.__puck_pos:02d}"
f"{self._formatted_date}/{self._puck_name}/{self._puck_pos:02d}"
)
else:
self.directory_edit.setText(f"{self.__formatted_date}/manual/{self.__sample_name}")
self.directory_edit.setText(f"{self._formatted_date}/manual/{self._sample_name}")
self.update_filename()
@Slot(DAQStatusModel)
def update_daq_status(self, status: DAQStatusModel):
if status.session.current_pgroup:
self.__curr_pgroup = status.session.current_pgroup
self._curr_pgroup = status.session.current_pgroup
@Slot(str)
def set_scan_kind(self, kind: str):
# kind in {"rotation","screening","raster"}
self.__scan_kind = kind
self._scan_kind = kind
self.update_filename()
def effective_path_for_base(self, base_with_run: str) -> str:
+7 -7
View File
@@ -13,16 +13,16 @@ class ManualSamplePanel(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.__sample_name = "Lyso"
self._sample_name = "Lyso"
self.__pgroup = "p16371"
self._pgroup = "p16371"
grid_layout = QGridLayout(self)
grid_layout.addWidget(TitleLabel("Manual sample", self), 0, 0, 1, 3)
grid_layout.addWidget(QLabel("Sample"), 1, 0)
self._text_name = QLineEdit(self.__sample_name)
self._text_name = QLineEdit(self._sample_name)
grid_layout.addWidget(self._text_name, 1, 1)
self._text_name.textChanged.connect(self._name_changed)
self._text_name.setStyleSheet("background-color: rgb(255, 255, 255);")
@@ -95,7 +95,7 @@ class ManualSamplePanel(QWidget):
@Slot(str)
def _name_changed(self, s: str):
self.__sample_name = s
self._sample_name = s
@Slot()
def _add_clicked(self):
@@ -111,14 +111,14 @@ class ManualSamplePanel(QWidget):
db_id=-1,
puck_name="",
dewar_name="",
sample_name=self.__sample_name,
sample_name=self._sample_name,
run_number=1, # can't be -1. So have defaulted to 1 for now.
pin=1,
aaredb_params=data_processing,
user=self.__pgroup,
user=self._pgroup,
)
self.sample_manual.emit(s)
@Slot(DAQStatusModel)
def update_daq_status(self, s: DAQStatusModel):
self.__pgroup = s.session.current_pgroup
self._pgroup = s.session.current_pgroup
+2 -2
View File
@@ -25,7 +25,7 @@ class OmegaPanel(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.__omega = 0
self._omega = 0
grid_layout = QGridLayout(self)
@@ -69,4 +69,4 @@ class OmegaPanel(QWidget):
@Slot(DAQStatusModel)
def update_daq_status(self, s: DAQStatusModel):
self.edit.edit.update_value(s.geom.omega_deg)
self.__omega = s.geom.omega_deg
self._omega = s.geom.omega_deg
+1 -1
View File
@@ -726,7 +726,7 @@ class PortraitModePanel(QWidget):
return
# Still busy, wait
if getattr(self._job_list_panel, "_SampleQueuePanel__busy", False):
if getattr(self._job_list_panel, "_busy", False):
return
# Safe to restart
+31 -33
View File
@@ -42,17 +42,15 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
self._previous_sample_was_none_raster = True
self.__n_x = raster_mgr.active_grid.n_x
self.__n_y = raster_mgr.active_grid.n_y
self.__size_x = raster_mgr.active_grid.grid_size_mm.x * 1000.0
self.__size_y = raster_mgr.active_grid.grid_size_mm.y * 1000.0
self.__total_time = raster_mgr.active_grid.exp_time_s * self.__n_x * self.__n_y
self._n_x = raster_mgr.active_grid.n_x
self._n_y = raster_mgr.active_grid.n_y
self._size_x = raster_mgr.active_grid.grid_size_mm.x * 1000.0
self._size_y = raster_mgr.active_grid.grid_size_mm.y * 1000.0
self._total_time = raster_mgr.active_grid.exp_time_s * self._n_x * self._n_y
self._layout.addWidget(QLabel("Grid element size", parent=self), 3, 0)
self.width_enter = DbOverrideLineEdit(
5, 100, default=self.__size_x, decimals=0, parent=self
)
self.width_enter = DbOverrideLineEdit(5, 100, default=self._size_x, decimals=0, parent=self)
self.width_enter.valueChanged.connect(self.grid_size)
self._register_override_field(self.width_enter)
self._layout.addWidget(self.width_enter, 3, 1)
@@ -60,7 +58,7 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
self._layout.addWidget(QLabel(" x ", parent=self), 3, 2)
self.height_enter = DbOverrideLineEdit(
5, 100, default=self.__size_y, decimals=0, parent=self
5, 100, default=self._size_y, decimals=0, parent=self
)
self.height_enter.valueChanged.connect(self.grid_size)
self._register_override_field(self.height_enter)
@@ -138,7 +136,7 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
self._layout.addItem(horizontal_spacer, 10, 0, 1, 5)
self._layout.addWidget(QLabel("Measurement time", parent=self), 11, 0)
self.total_time = QLabel(f"{self.__total_time} min 0 s")
self.total_time = QLabel(f"{self._total_time} min 0 s")
self.total_time.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
self._layout.addWidget(self.total_time, 11, 1, 1, 3)
@@ -158,9 +156,9 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
@Slot(float)
def grid_size(self, _: float):
self.__size_x = self.width_enter.value
self.__size_y = self.height_enter.value
self.grid_size_updated.emit(self.__size_x / 1000.0, self.__size_y / 1000.0)
self._size_x = self.width_enter.value
self._size_y = self.height_enter.value
self.grid_size_updated.emit(self._size_x / 1000.0, self._size_y / 1000.0)
self.update_grid_scan_size()
@Slot(float)
@@ -170,10 +168,10 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
@Slot(int, int, float, float)
def grid_scan_size_change(self, n_x: int, n_y: int, size_x_mm: float, size_y_mm: float):
self.__size_x = size_x_mm * 1000.0
self.__size_y = size_y_mm * 1000.0
self.__n_x = n_x
self.__n_y = n_y
self._size_x = size_x_mm * 1000.0
self._size_y = size_y_mm * 1000.0
self._n_x = n_x
self._n_y = n_y
self.calculate_total_time()
self.update_grid_scan_size()
@@ -193,17 +191,17 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
self._previous_sample_was_none_raster = True
elif s.sample is not None:
self._previous_sample_was_none_raster = False
self.__beamline_state = s.state
self._beamline_state = s.state
def update_grid_scan_size(self):
# The live grid geometry is the "database"/system value for the element
# size; a user override persists and can be reverted via the toggle.
self.width_enter.set_db_value(self.__size_x)
self.height_enter.set_db_value(self.__size_y)
self.n_x_label.setText(str(self.__n_x))
self.size_x_label.setText(f"{self.__size_x * self.__n_x:.1f}")
self.n_y_label.setText(str(self.__n_y))
self.size_y_label.setText(f"{self.__size_y * self.__n_y:.1f}")
self.width_enter.set_db_value(self._size_x)
self.height_enter.set_db_value(self._size_y)
self.n_x_label.setText(str(self._n_x))
self.size_x_label.setText(f"{self._size_x * self._n_x:.1f}")
self.n_y_label.setText(str(self._n_y))
self.size_y_label.setText(f"{self._size_y * self._n_y:.1f}")
self.update_total_time_label()
try:
self.exp_time_s(self.image_time_enter.value)
@@ -222,26 +220,26 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
]
def update_total_time_label(self):
mins = int(self.__total_time // 60)
secs = int(round(self.__total_time % 60))
mins = int(self._total_time // 60)
secs = int(round(self._total_time % 60))
if secs == 60:
mins += 1
secs = 0
self.total_time.setText(f"{mins} min {secs} s")
def calculate_total_time(self):
if self.__n_x <= 0 or self.__n_y <= 0 or self.image_time_enter.value < 0:
self.__total_time = 0.0
if self._n_x <= 0 or self._n_y <= 0 or self.image_time_enter.value < 0:
self._total_time = 0.0
else:
self.__total_time = self.__n_x * self.__n_y * self.image_time_enter.value * 1.3
self._total_time = self._n_x * self._n_y * self.image_time_enter.value * 1.3
# 30% buffer added
# Show minutes
self.update_total_time_label()
@Slot()
def _on_evaluate_clicked(self):
if self.__beamline_state != BeamlineStateEnum.SampleAlignment:
logger.error(f"Beamline state {self.__beamline_state} is not Sample Alignment")
if self._beamline_state != BeamlineStateEnum.SampleAlignment:
logger.error(f"Beamline state {self._beamline_state} is not Sample Alignment")
QMessageBox.critical(None, "Error", "Beamline state is not Sample Alignment")
return
if self.check_before_run(scan_kind="raster"):
@@ -249,8 +247,8 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
@Slot()
def _on_evaluate_auto_clicked(self):
if self.__beamline_state != BeamlineStateEnum.SampleAlignment:
logger.error(f"Beamline state {self.__beamline_state} is not Sample Alignment")
if self._beamline_state != BeamlineStateEnum.SampleAlignment:
logger.error(f"Beamline state {self._beamline_state} is not Sample Alignment")
QMessageBox.critical(None, "Error", "Beamline state is not Sample Alignment")
return
if self.check_before_run(scan_kind="raster"):
+24 -24
View File
@@ -57,16 +57,16 @@ class ReferenceToolsModel(QAbstractTableModel):
"Rotation count",
"Screening count",
]
self.__sort_col = 0
self.__sort_order = Qt.SortOrder.AscendingOrder
self.__sorted_samples: list[SampleShortInfo] = []
self._sort_col = 0
self._sort_order = Qt.SortOrder.AscendingOrder
self._sorted_samples: list[SampleShortInfo] = []
if self.samples:
self._sort()
else:
self.__sorted_samples = []
self._sorted_samples = []
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
return len(self.__sorted_samples)
return len(self._sorted_samples)
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
return len(self.header)
@@ -75,15 +75,15 @@ class ReferenceToolsModel(QAbstractTableModel):
if not index.isValid():
return None
if index.row() >= len(self.__sorted_samples):
if index.row() >= len(self._sorted_samples):
return None
if role == Qt.ItemDataRole.DisplayRole:
return get_entry(self.__sorted_samples[index.row()], index.column())
return get_entry(self._sorted_samples[index.row()], index.column())
elif role == Qt.ItemDataRole.TextAlignmentRole:
return Qt.AlignmentFlag.AlignCenter
elif role == Qt.ItemDataRole.BackgroundRole:
if self.__sorted_samples[index.row()].db_id == self.current_reference:
if self._sorted_samples[index.row()].db_id == self.current_reference:
return QBrush(QColor(114, 159, 207)) # darker blue
return QBrush(QColor(255, 255, 255)) # white
@@ -103,54 +103,54 @@ class ReferenceToolsModel(QAbstractTableModel):
if self.samples: # Only sort if we have samples
self._sort()
else:
self.__sorted_samples = [] # Clear sorted samples if no data
self._sorted_samples = [] # Clear sorted samples if no data
finally:
self.endResetModel()
def sort(self, column, order):
self.layoutAboutToBeChanged.emit()
self.__sort_order = order
self.__sort_col = column
self._sort_order = order
self._sort_col = column
self._sort()
self.layoutChanged.emit()
def _sort(self):
"""Sort samples, using special handling for location column."""
if not self.samples:
self.__sorted_samples = []
self._sorted_samples = []
return
if self.__sort_col == 0:
if self._sort_col == 0:
# Special sorting for location (Position column)
self.__sorted_samples = sorted(
self._sorted_samples = sorted(
self.samples,
key=lambda row: row.loc_str_sort(),
reverse=(self.__sort_order == Qt.SortOrder.DescendingOrder),
reverse=(self._sort_order == Qt.SortOrder.DescendingOrder),
)
elif self.__sort_col == 2:
elif self._sort_col == 2:
# Numeric sort for Mount count; place None last on ascending, first on descending
none_sentinel = (
float("inf") if self.__sort_order == Qt.SortOrder.AscendingOrder else float("-inf")
float("inf") if self._sort_order == Qt.SortOrder.AscendingOrder else float("-inf")
)
self.__sorted_samples = sorted(
self._sorted_samples = sorted(
self.samples,
key=lambda row: (
row.mount_count if isinstance(row.mount_count, (int, float)) else none_sentinel
),
reverse=(self.__sort_order == Qt.SortOrder.DescendingOrder),
reverse=(self._sort_order == Qt.SortOrder.DescendingOrder),
)
else:
# String sort with empty fallback
self.__sorted_samples = sorted(
self._sorted_samples = sorted(
self.samples,
key=lambda row: get_entry(row, self.__sort_col) or "",
reverse=(self.__sort_order == Qt.SortOrder.DescendingOrder),
key=lambda row: get_entry(row, self._sort_col) or "",
reverse=(self._sort_order == Qt.SortOrder.DescendingOrder),
)
def get_item(self, row: int) -> Optional[SampleShortInfo]:
"""Get sample at the given row index."""
if 0 <= row < len(self.__sorted_samples):
return self.__sorted_samples[row]
if 0 <= row < len(self._sorted_samples):
return self._sorted_samples[row]
return None
def update_current_reference(self, reference_id: int | None):
+21 -21
View File
@@ -48,13 +48,13 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
),
)
self.__beamline_state = None
self.__curr_pgroup = "p11206"
self._beamline_state = None
self._curr_pgroup = "p11206"
self._filename = ""
self.__omega = 0
self.__dose_mgy = 0
self.__total_time = 0.0
self._omega = 0
self._dose_mgy = 0
self._total_time = 0.0
self._layout.addWidget(QLabel("Start angle", parent=self), 3, 0)
self.start_angle = NumberLineEdit(-720, 720.0, 0.0, decimals=3, parent=self)
@@ -139,7 +139,7 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
self._register_override_field(self.image_time_enter)
self._layout.addWidget(QLabel("Total measurement time", parent=self), 13, 0)
self.total_time = QLabel(f"{self.__total_time} min 0 s")
self.total_time = QLabel(f"{self._total_time} min 0 s")
self.total_time.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
self._layout.addWidget(self.total_time, 13, 1, 1, 3)
@@ -150,7 +150,7 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
self.calculate_measurement_time()
self._layout.addWidget(QLabel("Dose", parent=self), 14, 0)
self.dose = QLabel(f"{self.__dose_mgy}")
self.dose = QLabel(f"{self._dose_mgy}")
self.dose.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
self._layout.addWidget(self.dose, 14, 1, 1, 3)
self._layout.addWidget(QLabel("MGy", parent=self), 14, 4)
@@ -167,8 +167,8 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
@Slot()
def run_screening(self):
if self.__beamline_state != BeamlineStateEnum.SampleAlignment:
logger.error(f"Beamline state {self.__beamline_state} is not Sample Alignment")
if self._beamline_state != BeamlineStateEnum.SampleAlignment:
logger.error(f"Beamline state {self._beamline_state} is not Sample Alignment")
QMessageBox.critical(None, "Error", "Beamline state is not Sample Alignment")
return
if not self.check_before_run(scan_kind="screening"):
@@ -210,8 +210,8 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
@Slot()
def run_measurement(self):
if self.__beamline_state != BeamlineStateEnum.SampleAlignment:
logger.error(f"Beamline state {self.__beamline_state} is not Sample Alignment")
if self._beamline_state != BeamlineStateEnum.SampleAlignment:
logger.error(f"Beamline state {self._beamline_state} is not Sample Alignment")
QMessageBox.critical(None, "Error", "Beamline state is not Sample Alignment")
return
if not self.check_before_run(scan_kind="rotation"):
@@ -243,8 +243,8 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
return round(total_angle / image_angle)
def update_total_time_label(self):
mins = int(self.__total_time // 60)
secs = int(round(self.__total_time % 60))
mins = int(self._total_time // 60)
secs = int(round(self._total_time % 60))
if secs == 60:
mins += 1
secs = 0
@@ -256,10 +256,10 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
or self.total_angle.value <= 0
or self.image_time_enter.value < 0
):
self.__total_time = 0.0
self._total_time = 0.0
self.update_total_time_label()
return
self.__total_time = (
self._total_time = (
self.total_angle.value / self.image_angle.value
) * self.image_time_enter.value
self.update_total_time_label()
@@ -271,7 +271,7 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
@Slot(DAQStatusModel)
def update_daq_status(self, s: DAQStatusModel):
super().update_daq_status(s)
self.__omega = s.geom.omega_deg
self._omega = s.geom.omega_deg
can_edit = getattr(self, "_can_edit_params", False)
for w in (
@@ -289,20 +289,20 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
self.calculate_measurement_time()
if s.session.current_pgroup:
self.__curr_pgroup = s.session.current_pgroup
self._curr_pgroup = s.session.current_pgroup
lambda_a = s.diffraction.wavelength_angstrom
kdose = 2000 / (lambda_a * lambda_a)
beam_area = s.geom.beam_size_mm.x * s.geom.beam_size_mm.y * 1e6
time = self.image_number() * self.image_time_enter.value
self.__dose_mgy = (time * s.bl.flux_ph_s * self._transmission) / (beam_area * kdose)
self.dose.setText(f"{(self.__dose_mgy / 1e6):.1f}")
self.__beamline_state = s.state
self._dose_mgy = (time * s.bl.flux_ph_s * self._transmission) / (beam_area * kdose)
self.dose.setText(f"{(self._dose_mgy / 1e6):.1f}")
self._beamline_state = s.state
@Slot()
def update_omega_start(self):
if not self.start_angle.isReadOnly():
self.start_angle.force_update_value(self.__omega)
self.start_angle.force_update_value(self._omega)
def get_parameter_mappings(self):
"""Return rotation-specific parameter mappings."""
+5 -5
View File
@@ -46,7 +46,7 @@ class SamcamPanel(QWidget):
self.exposure_spinbox.setStyleSheet("QDoubleSpinBox { background-color: white; }")
self.exposure_spinbox.setDecimals(3)
self.exposure_spinbox.valueChanged.connect(self.__changed)
self.exposure_spinbox.valueChanged.connect(self._changed)
exposure_layout.addWidget(exposure_label)
exposure_layout.addWidget(self.exposure_spinbox)
@@ -58,7 +58,7 @@ class SamcamPanel(QWidget):
self.gain_spinbox.setSingleStep(1)
self.gain_spinbox.setDecimals(1)
self.gain_spinbox.setStyleSheet("QDoubleSpinBox { background-color: white; }")
self.gain_spinbox.valueChanged.connect(self.__changed)
self.gain_spinbox.valueChanged.connect(self._changed)
gain_layout.addWidget(gain_label)
gain_layout.addWidget(self.gain_spinbox)
@@ -84,7 +84,7 @@ class SamcamPanel(QWidget):
screenshot_message_layout.addWidget(self.screenshot_message_edit)
self.screenshot_button = QPushButton("Take screenshot")
self.screenshot_button.clicked.connect(self.__request_screenshot)
self.screenshot_button.clicked.connect(self._request_screenshot)
# Show detections checkbox
detections_layout = QHBoxLayout()
@@ -160,14 +160,14 @@ class SamcamPanel(QWidget):
layout.addLayout(target_color_layout)
self.setLayout(layout)
def __changed(self):
def _changed(self):
self.changed.emit(
SampleCameraSettings(
gain=self.gain_spinbox.value(), exposure=self.exposure_spinbox.value()
)
)
def __request_screenshot(self):
def _request_screenshot(self):
self.screenshot_requested.emit(
self.screenshot_filename_edit.text(), self.screenshot_message_edit.text()
)
+55 -55
View File
@@ -41,12 +41,12 @@ class SampleQueuePanel(QFrame):
self, parent=None, samples: SampleShortInfoList | None = None, show_user: bool = False
):
super().__init__(parent)
self.__baton_holder = None
self.__busy = None
self.__beamline_state = None
self.__pause = True
self.__set_to_pause = False
self.__warning_msg_box = None
self._baton_holder = None
self._busy = None
self._beamline_state = None
self._pause = True
self._set_to_pause = False
self._warning_msg_box = None
self._current_db_id: int | None = None
self._step_through = False
self.ring_current = None
@@ -55,10 +55,10 @@ class SampleQueuePanel(QFrame):
self._tell_connected = True
self.loop_restart_requested = None
self.__recovery_timer = QTimer(self)
self.__recovery_timer.setSingleShot(True)
self.__recovery_timer.setInterval(600 * 1000) # 10 minutes
self.__recovery_timer.timeout.connect(self.resume_automation)
self._recovery_timer = QTimer(self)
self._recovery_timer.setSingleShot(True)
self._recovery_timer.setInterval(600 * 1000) # 10 minutes
self._recovery_timer.timeout.connect(self.resume_automation)
self.setFrameShape(QFrame.Shape.StyledPanel)
self.setFrameShadow(QFrame.Shadow.Raised)
@@ -125,7 +125,7 @@ class SampleQueuePanel(QFrame):
def _samples_left_in_queue(self) -> int:
total = len(self.table_model.samples)
if self.__pause or total == 0:
if self._pause or total == 0:
return total
return max(0, total - 1)
@@ -181,7 +181,7 @@ class SampleQueuePanel(QFrame):
self.table_model.remove_sample(current.db_id)
self._current_db_id = None
if not self.__pause and len(self.table_model.samples) > 0:
if not self._pause and len(self.table_model.samples) > 0:
next_item = self.table_model.samples[0]
if self._step_through:
self.pause_automation(set_id_to_None=False)
@@ -229,11 +229,11 @@ class SampleQueuePanel(QFrame):
return len(self._bad_conditions()) == 0
def resume_automation(self):
if self.__warning_msg_box:
self.__warning_msg_box.done(0)
self.__warning_msg_box = None
if self._warning_msg_box:
self._warning_msg_box.done(0)
self._warning_msg_box = None
if self.__pause and len(self.table_model.samples) > 0:
if self._pause and len(self.table_model.samples) > 0:
logger.info("Resuming automation after timeout")
self.run()
@@ -242,34 +242,34 @@ class SampleQueuePanel(QFrame):
self.resume_automation()
def show_warning_recovery_dialog(self):
self.__warning_msg_box = QMessageBox(self)
self.__warning_msg_box.setIcon(QMessageBox.Icon.Warning)
self.__warning_msg_box.setWindowTitle("TELL Warning")
self.__warning_msg_box.setText("TELL reported a warning. Please see console for details.")
self.__warning_msg_box.setInformativeText(
self._warning_msg_box = QMessageBox(self)
self._warning_msg_box.setIcon(QMessageBox.Icon.Warning)
self._warning_msg_box.setWindowTitle("TELL Warning")
self._warning_msg_box.setText("TELL reported a warning. Please see console for details.")
self._warning_msg_box.setInformativeText(
"Automation paused for 10 minutes to allow sufficient dry time."
"\nClick 'Continue Now' to resume immediately, or wait for auto-resume."
)
continue_btn = self.__warning_msg_box.addButton(
continue_btn = self._warning_msg_box.addButton(
"Continue Now", QMessageBox.ButtonRole.AcceptRole
)
self.__warning_msg_box.setWindowModality(Qt.WindowModality.NonModal)
self._warning_msg_box.setWindowModality(Qt.WindowModality.NonModal)
continue_btn.clicked.connect(self.manual_resume_from_warning)
self.__warning_msg_box.show()
self._warning_msg_box.show()
def show_error_dialog(self, title: str, msg: str, info: str | None = None):
self.__warning_msg_box = QMessageBox(self)
self.__warning_msg_box.setIcon(QMessageBox.Icon.Warning)
self.__warning_msg_box.setWindowTitle(title)
self.__warning_msg_box.setText(msg)
self._warning_msg_box = QMessageBox(self)
self._warning_msg_box.setIcon(QMessageBox.Icon.Warning)
self._warning_msg_box.setWindowTitle(title)
self._warning_msg_box.setText(msg)
if info:
self.__warning_msg_box.setInformativeText(info)
self.__warning_msg_box.setWindowModality(Qt.WindowModality.NonModal)
self.__warning_msg_box.show()
self._warning_msg_box.setInformativeText(info)
self._warning_msg_box.setWindowModality(Qt.WindowModality.NonModal)
self._warning_msg_box.show()
def is_running(self):
return not self.__pause
return not self._pause
def _finish_empty_queue(self):
self.pause_automation()
@@ -290,14 +290,14 @@ class SampleQueuePanel(QFrame):
self.unmount.emit()
def run(self):
self.__recovery_timer.stop()
self._recovery_timer.stop()
# Pausing must ALWAYS be possible, regardless of beamline state, busy
# flag, baton or maintenance. Only starting/resuming is gated on
# conditions, so handle the pause toggle first and return immediately.
if not self.__pause:
self.__set_to_pause = True
self.__pause = True
if not self._pause:
self._set_to_pause = True
self._pause = True
self.table_model.set_running(False)
self.play_button.setText("▶ Run")
self._emit_samples_in_queue_changed()
@@ -307,11 +307,11 @@ class SampleQueuePanel(QFrame):
# --- Starting/resuming: require the beamline to be in a good state. ---
checks_enabled = self._checks_enabled()
if checks_enabled:
if self.__warning_msg_box:
self.__warning_msg_box.done(0)
self.__warning_msg_box = None
if self._warning_msg_box:
self._warning_msg_box.done(0)
self._warning_msg_box = None
if self.__beamline_state != BeamlineStateEnum.SampleAlignment:
if self._beamline_state != BeamlineStateEnum.SampleAlignment:
self.show_error_dialog(
title="wrong beamline state",
msg="Cannot run automation while beamline is not in sample alignment state",
@@ -319,8 +319,8 @@ class SampleQueuePanel(QFrame):
)
return
if self.__busy:
logger.error(f"Cannot run automation while beamline is busy. Busy flag = {self.__busy}")
if self._busy:
logger.error(f"Cannot run automation while beamline is busy. Busy flag = {self._busy}")
self.show_error_dialog(
title="Beamline is busy",
msg="Cannot run automation while beamline is busy",
@@ -328,7 +328,7 @@ class SampleQueuePanel(QFrame):
)
return
if self.__baton_holder is SessionsStateEnum.Vacant:
if self._baton_holder is SessionsStateEnum.Vacant:
self.show_error_dialog(
title="Session is vacant",
msg="Starting automation while session is vacant is not currently implemented",
@@ -337,7 +337,7 @@ class SampleQueuePanel(QFrame):
)
return
elif self.__baton_holder is not SessionsStateEnum.OwnedByYou:
elif self._baton_holder is not SessionsStateEnum.OwnedByYou:
self.show_error_dialog(
title="You do not hold the baton",
msg="You do not hold the baton.",
@@ -347,7 +347,7 @@ class SampleQueuePanel(QFrame):
)
return
if self.__beamline_state is BeamlineStateEnum.Maintenance:
if self._beamline_state is BeamlineStateEnum.Maintenance:
self.show_error_dialog(
title="Maintenance mode",
msg="Cannot run automation while beamline is in maintenance mode",
@@ -371,8 +371,8 @@ class SampleQueuePanel(QFrame):
return
self.table_model.set_running(True)
self.__set_to_pause = False
self.__pause = False
self._set_to_pause = False
self._pause = False
self.play_button.setText("⏸ Pause")
current = self.table_model.samples[0]
self._current_db_id = current.db_id
@@ -400,13 +400,13 @@ class SampleQueuePanel(QFrame):
self._experiment_shutter_state = s.bl.exp_shutter_open
self._door_prohibited = getattr(s.bl, "pss_prohibited", None)
self._tell_connected = getattr(s, "tell_connected", True)
self.__beamline_state = s.state
self.__busy = s.busy
self.__baton_holder = s.session.session
self._beamline_state = s.state
self._busy = s.busy
self._baton_holder = s.session.session
def pause_automation(self, set_id_to_None: bool = True):
self.table_model.set_running(False)
self.__pause = True
self._pause = True
self.play_button.setText("▶ Run")
if set_id_to_None:
self._current_db_id = None
@@ -427,7 +427,7 @@ class SampleQueuePanel(QFrame):
if conditions_auto_check(self, msg, self._conditions_ok):
logger.debug("Conditions recovered or user chose to continue; resuming automation")
self.table_model.set_running(True)
self.__pause = False
self._pause = False
self.play_button.setText("⏸ Pause")
self._emit_samples_in_queue_changed()
else:
@@ -440,7 +440,7 @@ class SampleQueuePanel(QFrame):
self.table_model.remove_sample(db_id)
self._current_db_id = None
if not self.__pause and len(self.table_model.samples) > 0:
if not self._pause and len(self.table_model.samples) > 0:
next_item = self.table_model.samples[0]
if self._step_through:
self.pause_automation(set_id_to_None=False)
@@ -457,7 +457,7 @@ class SampleQueuePanel(QFrame):
self._current_db_id = None
self.pause_automation()
logger.warning("TELL Warning: Pausing for 10 minutes.")
self.__recovery_timer.start()
self._recovery_timer.start()
self.show_warning_recovery_dialog()
elif reply == "Critical":
@@ -485,7 +485,7 @@ class SampleQueuePanel(QFrame):
else:
self.table_model.remove_sample(db_id)
self._current_db_id = None
if not self.__pause and len(self.table_model.samples) > 0:
if not self._pause and len(self.table_model.samples) > 0:
next_item = self.table_model.samples[0]
if self._step_through:
self.pause_automation(set_id_to_None=False)
+6 -6
View File
@@ -35,7 +35,7 @@ class ScanSettingsPanel(QWidget):
parent=None,
):
super().__init__(parent)
self.__diffraction = diffraction
self._diffraction = diffraction
self._transmission = default_transmission
self._dtz = default_dtz
@@ -167,10 +167,10 @@ class ScanSettingsPanel(QWidget):
def update_daq_status(self, s: DAQStatusModel):
self.dtz_enter.update_limits(s.bl.dtz_min, s.bl.dtz_max)
self.high_res_enter.update_limits(
self.__diffraction.resolution_angstrom(s.bl.dtz_min),
self.__diffraction.resolution_angstrom(s.bl.dtz_max),
self._diffraction.resolution_angstrom(s.bl.dtz_min),
self._diffraction.resolution_angstrom(s.bl.dtz_max),
)
self.__diffraction = s.diffraction
self._diffraction = s.diffraction
self._ring_current = s.bl.ring_current_mA
self._experiment_shutter_state = s.bl.exp_shutter_open
self._door_prohibited = getattr(s.bl, "pss_prohibited", None)
@@ -214,10 +214,10 @@ class ScanSettingsPanel(QWidget):
self._high_res = float(self.high_res_enter.value)
def _dtz_to_res(self, dtz: float) -> float:
return self.__diffraction.resolution_angstrom(dtz)
return self._diffraction.resolution_angstrom(dtz)
def _res_to_dtz(self, res: float) -> float:
dtz = self.__diffraction.calc_dtz_mm(res)
dtz = self._diffraction.calc_dtz_mm(res)
return self.MIN_DTZ if dtz < self.MIN_DTZ else dtz
@Slot(float)
+6 -6
View File
@@ -53,8 +53,8 @@ class SmargonPanel(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.__geom: SampleGeometryModel | None = None
self.__step = 100
self._geom: SampleGeometryModel | None = None
self._step = 100
grid_layout = QGridLayout(self)
@@ -89,7 +89,7 @@ class SmargonPanel(QWidget):
@Slot(float)
def step_changed(self, f: float):
self.__step = f
self._step = f
@Slot()
def home(self):
@@ -108,13 +108,13 @@ class SmargonPanel(QWidget):
def update_daq_status(self, s: DAQStatusModel):
self.chi_enter.update_value(s.geom.smargon.chi_deg)
self.phi_enter.update_value(s.geom.smargon.phi_deg)
self.__geom = s.geom
self._geom = s.geom
@Slot(Coordinate)
def smargon_rel(self, c: Coordinate):
if self.__geom is None:
if self._geom is None:
return
self.smargon.emit(
SmargonCoordinate(sh_mm=self.__geom.beamline_to_smargon(c * self.__step / 1000.0))
SmargonCoordinate(sh_mm=self._geom.beamline_to_smargon(c * self._step / 1000.0))
)
+12 -12
View File
@@ -33,9 +33,9 @@ class SimpleRotationSettingsPanel(QWidget):
self._filename = ""
self.dtz = 120
self.image_time_s = 0.1
self.__d = None
self._d = None
self._temperature = 100
self.__omega = 0
self._omega = 0
self._wilson_b = None
self.flux_ph_s = None
self.total_time_s = 0
@@ -211,12 +211,12 @@ class SimpleRotationSettingsPanel(QWidget):
@Slot(DAQStatusModel)
def update_daq_status(self, s: DAQStatusModel):
self.__d = s
self._d = s
# TODO only update best_res after raster finished otherwise ask to update. or have toggle to overwrite with user value
# TODO only take best res from flat face scan
# TODO identify flat face!!!!
best_res = s.last_best_res
self.__omega = s.geom.omega_deg
self._omega = s.geom.omega_deg
if best_res is not None:
# clamp to control limits and update field; this will also trigger recalculation
@@ -274,7 +274,7 @@ class SimpleRotationSettingsPanel(QWidget):
@Slot()
def update_omega_start(self):
self.start_angle_enter.update_value(self.__omega)
self.start_angle_enter.update_value(self._omega)
def update_total_time_label(self):
mins = int(self.total_time_s // 60)
@@ -285,7 +285,7 @@ class SimpleRotationSettingsPanel(QWidget):
self.total_time.setText(f"{mins} min {secs} s")
def update_calculated_labels(self):
if self.__d is None:
if self._d is None:
return
flux = 2.5e11 # TODO link flux
# TODO add start angle
@@ -301,10 +301,10 @@ class SimpleRotationSettingsPanel(QWidget):
self.target_res_label.setText(f"{d_tar:.2f}")
Kdose = 2000 / (self.__d.diffraction.wavelength_angstrom**2)
Kdose = 2000 / (self._d.diffraction.wavelength_angstrom**2)
beam_size_um_y = self.__d.geom.beam_size_mm.y * 1000
beam_size_um_x = self.__d.geom.beam_size_mm.x * 1000
beam_size_um_y = self._d.geom.beam_size_mm.y * 1000
beam_size_um_x = self._d.geom.beam_size_mm.x * 1000
self.dose_rate_MGy_s = (flux / (beam_size_um_x * beam_size_um_y * Kdose)) / 1e6
if (
self.xtal_y is None
@@ -348,7 +348,7 @@ class SimpleRotationSettingsPanel(QWidget):
else:
self.transmission = 1.0
self.dtz = self.__d.diffraction.calc_dtz_mm(d_tar)
self.dtz = self._d.diffraction.calc_dtz_mm(d_tar)
if self.dtz < 108:
self.dtz = 108
@@ -357,9 +357,9 @@ class SimpleRotationSettingsPanel(QWidget):
if self.dtz <= 0.0:
self.dtz_label.setText("""<span style="color: red ; ">-</span>""")
elif self.dtz < self.__d.bl.dtz_min:
elif self.dtz < self._d.bl.dtz_min:
self.dtz_label.setText(f"""<span style="color: red ; ">{self.dtz:.2f}</span>""")
self.dtz = self.__d.bl.dtz_min
self.dtz = self._d.bl.dtz_min
else:
self.dtz_label.setText(f"{self.dtz:.2f}")
+2 -2
View File
@@ -246,5 +246,5 @@ class TellSamplePanel(QFrame):
self.curr_sample_label.setText(base_text)
if status.session.current_pgroup is not None:
self.__current_pgroup = status.session.current_pgroup
self.table_model.set_default_user_filter(self.__current_pgroup)
self._current_pgroup = status.session.current_pgroup
self.table_model.set_default_user_filter(self._current_pgroup)
+10 -10
View File
@@ -12,10 +12,10 @@ class ZoomPanel(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.__zoom = 1.0
self._zoom = 1.0
grid_layout = QGridLayout(self)
self.__zoom_settings = [
self._zoom_settings = [
{"name": "1.0x", "value": 1},
{"name": "2.0x", "value": 280},
{"name": "3.5x", "value": 500},
@@ -26,13 +26,13 @@ class ZoomPanel(QWidget):
grid_layout.addWidget(TitleLabel("Zoom", self), 0, 0, 1, 2)
i = 2
self.__buttons = []
for z in self.__zoom_settings:
self._buttons = []
for z in self._zoom_settings:
button = ButtonWithPayload(z["name"], {"zoom": z["value"]})
grid_layout.addWidget(button, int(i / 2), int(i % 2))
i += 1
button.pressed.connect(self.set_zoom)
self.__buttons.append(button)
self._buttons.append(button)
@Slot(dict)
def set_zoom(self, payload: dict):
@@ -40,9 +40,9 @@ class ZoomPanel(QWidget):
@Slot(DAQStatusModel)
def update_daq_status(self, status: DAQStatusModel):
self.__zoom = status.bl.zoom
for z in self.__zoom_settings:
if z["value"] == self.__zoom:
self.__buttons[self.__zoom_settings.index(z)].setStyleSheet("font-weight: bold;")
self._zoom = status.bl.zoom
for z in self._zoom_settings:
if z["value"] == self._zoom:
self._buttons[self._zoom_settings.index(z)].setStyleSheet("font-weight: bold;")
else:
self.__buttons[self.__zoom_settings.index(z)].setStyleSheet("")
self._buttons[self._zoom_settings.index(z)].setStyleSheet("")
+131 -133
View File
@@ -3,7 +3,7 @@ from enum import Enum
from typing import List, Tuple
import numpy as np
from aarecommon.config.beamline import get_jfjoch_url, mx_beamline, cfg_get
from aarecommon.config.beamline import cfg_get, get_jfjoch_url, mx_beamline
from aarecommon.config.logger import setup_logger
from aarecommon.math.coordinate import Coordinate, SmargonCoordinate
from aarecommon.math.find_xtal import compute_crystal_score_array
@@ -121,25 +121,25 @@ class RasterGridManager(QObject):
def __init__(self, geom: SampleGeometryModel):
super().__init__()
self.__sample_id = -1
self.__beamline = mx_beamline()
self.__detector_url = get_jfjoch_url(self.__beamline)
self._sample_id = -1
self._beamline = mx_beamline()
self._detector_url = get_jfjoch_url(self._beamline)
self.__beam_size_mm = geom.beam_size_mm
self.__geom = geom
self._beam_size_mm = geom.beam_size_mm
self._geom = geom
self.__metric = RasterGridMetric.RASTER_SCORE
self._metric = RasterGridMetric.RASTER_SCORE
self.__loaded_image_prefix = None
self.__loaded_image_index = None
self._loaded_image_prefix = None
self._loaded_image_index = None
self.__start_point: QPointF = QPointF(0, 0)
self.__active_grid: RasterGridRequest = RasterGridRequest(
self._start_point: QPointF = QPointF(0, 0)
self._active_grid: RasterGridRequest = RasterGridRequest(
n_x=0,
n_y=0,
smargon_top_left=self.__geom.smargon,
grid_size_mm=Coordinate(x=0.8 * self.__beam_size_mm.x, y=0.8 * self.__beam_size_mm.y),
omega_deg=self.__geom.omega_deg,
smargon_top_left=self._geom.smargon,
grid_size_mm=Coordinate(x=0.8 * self._beam_size_mm.x, y=0.8 * self._beam_size_mm.y),
omega_deg=self._geom.omega_deg,
exp_time_s=cfg_get(
"daq.data_collection_settings.default_raster_settings.exp_time_s", 0.01
),
@@ -148,24 +148,24 @@ class RasterGridManager(QObject):
),
dtz=cfg_get("daq.data_collection_settings.default_raster_settings.dtz", 200.0),
)
self.__completed_grids: List[CompletedRasterGridElem] = []
self._completed_grids: List[CompletedRasterGridElem] = []
# Cache of pre-rendered heatmap bitmaps, keyed by (id(grid_elem), metric).
# Each entry is (QImage, backing ndarray); the ndarray must be kept alive
# because QImage shares its buffer without copying. Rebuilt only when the
# data or metric changes, not on every repaint (sample move / zoom).
self.__heatmap_cache: dict[tuple[int, "RasterGridMetric"], tuple[QImage, np.ndarray]] = {}
self._heatmap_cache: dict[tuple[int, "RasterGridMetric"], tuple[QImage, np.ndarray]] = {}
@property
def active_grid(self) -> RasterGridRequest:
return self.__active_grid
return self._active_grid
def _is_grid_visible(self, grid: RasterGridRequest):
if (
grid.visible
and abs(normalize_angle(grid.omega_deg - self.__geom.omega_deg)) < 0.2
and abs(grid.smargon_top_left.phi_deg - self.__geom.smargon.phi_deg) < 0.2
and abs(grid.smargon_top_left.chi_deg - self.__geom.smargon.chi_deg) < 0.2
and abs(normalize_angle(grid.omega_deg - self._geom.omega_deg)) < 0.2
and abs(grid.smargon_top_left.phi_deg - self._geom.smargon.phi_deg) < 0.2
and abs(grid.smargon_top_left.chi_deg - self._geom.smargon.chi_deg) < 0.2
and grid.n_x > 0
and grid.n_y > 0
):
@@ -178,15 +178,15 @@ class RasterGridManager(QObject):
if not self._is_grid_visible(grid):
return None
cell = grid.grid_size_pxl(self.__geom)
cell = grid.grid_size_pxl(self._geom)
cell_w = float(cell.x)
cell_h = float(cell.y)
if cell_w <= 0 or cell_h <= 0:
return None
start_anchor = grid.start_pxl(self.__geom)
start_picture = self.__geom.sample_to_picture(start_anchor)
start_anchor = grid.start_pxl(self._geom)
start_picture = self._geom.sample_to_picture(start_anchor)
return float(start_picture.x), float(start_picture.y), cell_w, cell_h
@@ -219,91 +219,91 @@ class RasterGridManager(QObject):
@Slot(RasterGridRequest)
def update_active_grid_request(self, grid: RasterGridRequest):
self.__active_grid.smargon_top_left = grid.smargon_top_left
self.__active_grid.omega_deg = grid.omega_deg
self.__active_grid.grid_size_mm = grid.grid_size_mm
self.__active_grid.n_x = grid.n_x
self.__active_grid.n_y = grid.n_y
self._active_grid.smargon_top_left = grid.smargon_top_left
self._active_grid.omega_deg = grid.omega_deg
self._active_grid.grid_size_mm = grid.grid_size_mm
self._active_grid.n_x = grid.n_x
self._active_grid.n_y = grid.n_y
self.grid_scan_size_changed.emit(
self.__active_grid.n_x,
self.__active_grid.n_y,
self.__active_grid.grid_size_mm.x,
self.__active_grid.grid_size_mm.y,
self._active_grid.n_x,
self._active_grid.n_y,
self._active_grid.grid_size_mm.x,
self._active_grid.grid_size_mm.y,
)
@Slot(DAQStatusModel)
def update_daq_status(self, s: DAQStatusModel):
if s.sample is not None and s.sample.db_id != self.__sample_id:
self.__sample_id = s.sample.db_id
if s.sample is not None and s.sample.db_id != self._sample_id:
self._sample_id = s.sample.db_id
self.clear_completed_grids()
self.clear_active_grid()
if self.__beam_size_mm != s.geom.beam_size_mm:
self.__beam_size_mm = s.geom.beam_size_mm
self.update_grid_size(0.8 * self.__beam_size_mm.x, 0.8 * self.__beam_size_mm.y)
self.__geom = s.geom
if self._beam_size_mm != s.geom.beam_size_mm:
self._beam_size_mm = s.geom.beam_size_mm
self.update_grid_size(0.8 * self._beam_size_mm.x, 0.8 * self._beam_size_mm.y)
self._geom = s.geom
def resize_active_grid(self, end_point: QPointF):
self.update_active_grid(self.__start_point, end_point)
self.update_active_grid(self._start_point, end_point)
def update_active_grid(self, start: QPointF, end: QPointF):
delta_x = (end.x() - start.x()) * self.__geom.pixel_in_mm
delta_y = (end.y() - start.y()) * self.__geom.pixel_in_mm
delta_x = (end.x() - start.x()) * self._geom.pixel_in_mm
delta_y = (end.y() - start.y()) * self._geom.pixel_in_mm
self.__active_grid.n_x = round(abs(delta_x / self.__active_grid.grid_size_mm.x))
self.__active_grid.n_y = round(abs(delta_y / self.__active_grid.grid_size_mm.y))
self._active_grid.n_x = round(abs(delta_x / self._active_grid.grid_size_mm.x))
self._active_grid.n_y = round(abs(delta_y / self._active_grid.grid_size_mm.y))
c = self.__geom.picture_to_sample(Coordinate(x=start.x(), y=start.y()))
c = self._geom.picture_to_sample(Coordinate(x=start.x(), y=start.y()))
if delta_x < 0:
c.x -= self.__active_grid.n_x * self.__active_grid.grid_size_mm.x
c.x -= self._active_grid.n_x * self._active_grid.grid_size_mm.x
if delta_y < 0:
c.y -= self.__active_grid.n_y * self.__active_grid.grid_size_mm.y
c.y -= self._active_grid.n_y * self._active_grid.grid_size_mm.y
if delta_x < 0 or delta_y < 0:
c_pic = self.__geom.sample_to_picture(c)
self.__start_point = QPointF(c_pic.x, c_pic.y)
c_pic = self._geom.sample_to_picture(c)
self._start_point = QPointF(c_pic.x, c_pic.y)
else:
self.__start_point = start
self._start_point = start
self.__active_grid.smargon_top_left = SmargonCoordinate(
sh_mm=self.__geom.beamline_to_smargon(c),
phi_deg=self.__geom.smargon.phi_deg,
chi_deg=self.__geom.smargon.chi_deg,
self._active_grid.smargon_top_left = SmargonCoordinate(
sh_mm=self._geom.beamline_to_smargon(c),
phi_deg=self._geom.smargon.phi_deg,
chi_deg=self._geom.smargon.chi_deg,
)
self.__active_grid.omega_deg = self.__geom.omega_deg
self._active_grid.omega_deg = self._geom.omega_deg
self.grid_scan_size_changed.emit(
self.__active_grid.n_x,
self.__active_grid.n_y,
self.__active_grid.grid_size_mm.x,
self.__active_grid.grid_size_mm.y,
self._active_grid.n_x,
self._active_grid.n_y,
self._active_grid.grid_size_mm.x,
self._active_grid.grid_size_mm.y,
)
def move_active_grid(self, delta: QPointF):
if not self._is_grid_visible(self.__active_grid):
if not self._is_grid_visible(self._active_grid):
return
delta_pxl = Coordinate(x=delta.x(), y=delta.y())
delta_mm = self.__geom.smargon_nudge(delta_pxl * self.__geom.pixel_in_mm)
delta_mm = self._geom.smargon_nudge(delta_pxl * self._geom.pixel_in_mm)
self.__active_grid.smargon_top_left = SmargonCoordinate(
sh_mm=self.__active_grid.smargon_top_left.sh_mm + delta_mm,
phi_deg=self.__geom.smargon.phi_deg,
chi_deg=self.__geom.smargon.chi_deg,
self._active_grid.smargon_top_left = SmargonCoordinate(
sh_mm=self._active_grid.smargon_top_left.sh_mm + delta_mm,
phi_deg=self._geom.smargon.phi_deg,
chi_deg=self._geom.smargon.chi_deg,
)
@Slot()
def clear_active_grid(self):
self.__active_grid.n_x = 0
self.__active_grid.n_y = 0
self._active_grid.n_x = 0
self._active_grid.n_y = 0
self.grid_scan_size_changed.emit(
0, 0, self.__active_grid.grid_size_mm.x, self.__active_grid.grid_size_mm.y
0, 0, self._active_grid.grid_size_mm.x, self._active_grid.grid_size_mm.y
)
def get_grid_coord(self, grid: RasterGridRequest, point: QPointF) -> Tuple[int, int]:
point_bl = self.__geom.picture_to_sample(Coordinate(x=point.x(), y=point.y()))
point_bl = self._geom.picture_to_sample(Coordinate(x=point.x(), y=point.y()))
delta = point_bl - self.__geom.smargon_to_beamline(grid.smargon_top_left.sh_mm)
delta = point_bl - self._geom.smargon_to_beamline(grid.smargon_top_left.sh_mm)
elem_x = grid.grid_size_mm.x
elem_y = grid.grid_size_mm.y
@@ -315,7 +315,7 @@ class RasterGridManager(QObject):
@Slot(QPointF)
def load_image(self, point: QPointF):
for grid in self.__completed_grids:
for grid in self._completed_grids:
if self._is_grid_visible(grid.request):
n_x = grid.request.n_x
n_y = grid.request.n_y
@@ -325,11 +325,11 @@ class RasterGridManager(QObject):
cell = cell_y * n_x + cell_x
if (
self.__loaded_image_prefix != grid.result.file_prefix
or self.__loaded_image_index != grid.result.images[cell].number
self._loaded_image_prefix != grid.result.file_prefix
or self._loaded_image_index != grid.result.images[cell].number
):
self.__loaded_image_prefix = grid.result.file_prefix
self.__loaded_image_index = grid.result.images[cell].number
self._loaded_image_prefix = grid.result.file_prefix
self._loaded_image_index = grid.result.images[cell].number
logger.debug(
f"Load {grid.result.file_prefix} {grid.result.images[cell].number}"
)
@@ -339,22 +339,20 @@ class RasterGridManager(QObject):
# TODO if not in the same PGroup, user can stream from last run only but never load from a file.
# If loading a prior run, this shoudl throw an error,
# if user in same pgroup, load will always be fine.
# self.image_selected.emit(self.__detector_url, grid.result.images[cell].number)
# self.image_selected.emit(self._detector_url, grid.result.images[cell].number)
def is_part_of_active_grid(self, point: QPointF) -> bool:
if not self._is_grid_visible(self.__active_grid):
if not self._is_grid_visible(self._active_grid):
return False
point_bl = self.__geom.picture_to_sample(Coordinate(x=point.x(), y=point.y()))
point_bl = self._geom.picture_to_sample(Coordinate(x=point.x(), y=point.y()))
delta = point_bl - self.__geom.smargon_to_beamline(
self.__active_grid.smargon_top_left.sh_mm
)
return (0 <= delta.x < self.__active_grid.n_x * self.__active_grid.grid_size_mm.x) and (
0 <= delta.y < self.__active_grid.n_y * self.__active_grid.grid_size_mm.y
delta = point_bl - self._geom.smargon_to_beamline(self._active_grid.smargon_top_left.sh_mm)
return (0 <= delta.x < self._active_grid.n_x * self._active_grid.grid_size_mm.x) and (
0 <= delta.y < self._active_grid.n_y * self._active_grid.grid_size_mm.y
)
def is_part_of_completed_grid(self, point: QPointF) -> str | None:
for grid in self.__completed_grids:
for grid in self._completed_grids:
if self._is_grid_visible(grid.request):
n_x = grid.request.n_x
n_y = grid.request.n_y
@@ -432,25 +430,25 @@ class RasterGridManager(QObject):
if (grid_size_mm_x <= 0) or (grid_size_mm_y <= 0):
raise ValueError("Grid size must be positive")
self.__active_grid.n_x = round(
self.__active_grid.n_x * self.__active_grid.grid_size_mm.x / grid_size_mm_x
self._active_grid.n_x = round(
self._active_grid.n_x * self._active_grid.grid_size_mm.x / grid_size_mm_x
)
self.__active_grid.n_y = round(
self.__active_grid.n_y * self.__active_grid.grid_size_mm.y / grid_size_mm_y
self._active_grid.n_y = round(
self._active_grid.n_y * self._active_grid.grid_size_mm.y / grid_size_mm_y
)
self.__active_grid.grid_size_mm = Coordinate(x=grid_size_mm_x, y=grid_size_mm_y)
self._active_grid.grid_size_mm = Coordinate(x=grid_size_mm_x, y=grid_size_mm_y)
self.grid_scan_size_changed.emit(
self.__active_grid.n_x,
self.__active_grid.n_y,
self.__active_grid.grid_size_mm.x,
self.__active_grid.grid_size_mm.y,
self._active_grid.n_x,
self._active_grid.n_y,
self._active_grid.grid_size_mm.x,
self._active_grid.grid_size_mm.y,
)
@Slot()
def run_grid_scan(self):
ag = self.__active_grid
ag = self._active_grid
grid = RasterGridRequest(
dtz=ag.dtz,
transmission=ag.transmission,
@@ -476,7 +474,7 @@ class RasterGridManager(QObject):
@Slot()
def run_grid_scan_auto(self):
ag = self.__active_grid
ag = self._active_grid
grid = RasterGridRequest(
dtz=ag.dtz,
transmission=ag.transmission,
@@ -506,10 +504,10 @@ class RasterGridManager(QObject):
if visible_rect.isEmpty():
visible_rect = QRectF()
self._draw_grid(painter, self.__active_grid, None, alpha, visible_rect)
self._draw_grid(painter, self._active_grid, None, alpha, visible_rect)
for i in self.__completed_grids:
match self.__metric:
for i in self._completed_grids:
match self._metric:
case RasterGridMetric.SPOTS_LOW_RES:
v = [obj.spots_low_res for obj in i.result.images]
case RasterGridMetric.SPOTS:
@@ -536,12 +534,12 @@ class RasterGridManager(QObject):
v = [self.spot_ice_ratio(obj) for obj in i.result.images]
case RasterGridMetric.SPOTS_INDEXED:
v = [obj.spots_indexed for obj in i.result.images]
self._draw_completed_heatmap(painter, i, v, alpha, visible_rect, (id(i), self.__metric))
self._draw_completed_heatmap(painter, i, v, alpha, visible_rect, (id(i), self._metric))
def _invalidate_heatmap_cache(self) -> None:
"""Drop all cached heatmap bitmaps. Call whenever the completed-grid data
set or the active metric changes; cheap to rebuild lazily on next paint."""
self.__heatmap_cache.clear()
self._heatmap_cache.clear()
def _heatmap_image(
self, cache_key: tuple, grid: RasterGridRequest, values: List[float] | List[int]
@@ -550,7 +548,7 @@ class RasterGridManager(QObject):
on a cache miss. One pixel per cell; colours baked at full opacity with
the alpha channel encoding validity (overlay transparency is applied at
blit time via painter opacity, so it never invalidates the cache)."""
cached = self.__heatmap_cache.get(cache_key)
cached = self._heatmap_cache.get(cache_key)
if cached is not None:
return cached[0]
@@ -558,7 +556,7 @@ class RasterGridManager(QObject):
if built is None:
return None
self.__heatmap_cache[cache_key] = built
self._heatmap_cache[cache_key] = built
return built[0]
def _build_heatmap_image(
@@ -875,28 +873,28 @@ class RasterGridManager(QObject):
@Slot(float)
def update_exposure_time(self, exp_time_s: float):
self.__active_grid.exp_time_s = exp_time_s
self._active_grid.exp_time_s = exp_time_s
@Slot(float)
def update_transmission(self, transmission: float):
self.__active_grid.transmission = transmission
self._active_grid.transmission = transmission
@Slot(float)
def update_dtz(self, dtz: float):
self.__active_grid.dtz = dtz
self._active_grid.dtz = dtz
@Slot(str)
def update_filename(self, filename: str):
self.__active_grid.file_prefix = filename
self._active_grid.file_prefix = filename
@Slot(RasterGridMetric)
def metric(self, r: RasterGridMetric):
self.__metric = r
self._metric = r
self._invalidate_heatmap_cache()
@Slot()
def clear_completed_grids(self):
self.__completed_grids.clear()
self._completed_grids.clear()
self._invalidate_heatmap_cache()
self.completed_grid_updated.emit()
@@ -904,50 +902,50 @@ class RasterGridManager(QObject):
def grid_scan_completed(self, raster_list: CompletedRasterGrid):
last_raster = None
for raster in raster_list.r:
self.__completed_grids.append(raster)
self._completed_grids.append(raster)
last_raster = raster
self.__active_grid.n_x = 0
self.__active_grid.n_y = 0
self._active_grid.n_x = 0
self._active_grid.n_y = 0
self._invalidate_heatmap_cache()
if last_raster is not None and last_raster.result.file_prefix is not None:
com = last_raster.centre_of_mass
logger.info(f"COM: {com}")
cell = com.max_image if com else 0
self.image_selected.emit(self.__detector_url, cell)
self.image_selected.emit(self._detector_url, cell)
self.completed_grid_updated.emit()
@Slot(int, bool)
def completed_grid_scan_toggle_visible(self, row: int, visible: bool):
if 0 <= row < len(self.__completed_grids):
self.__completed_grids[row].request.visible = visible
if 0 <= row < len(self._completed_grids):
self._completed_grids[row].request.visible = visible
self.completed_grid_updated.emit()
@Slot(int)
def completed_grid_scan_delete(self, row: int):
if 0 <= row < len(self.__completed_grids):
del self.__completed_grids[row]
if 0 <= row < len(self._completed_grids):
del self._completed_grids[row]
self._invalidate_heatmap_cache()
self.completed_grid_updated.emit()
@Slot(int)
def completed_grid_scan_goto(self, row: int):
if 0 <= row < len(self.__completed_grids):
self.omega.emit(self.__completed_grids[row].request.omega_deg)
if 0 <= row < len(self._completed_grids):
self.omega.emit(self._completed_grids[row].request.omega_deg)
self.smargon.emit(
SmargonCoordinate(
phi_deg=self.__completed_grids[row].request.smargon_top_left.phi_deg,
chi_deg=self.__completed_grids[row].request.smargon_top_left.chi_deg,
phi_deg=self._completed_grids[row].request.smargon_top_left.phi_deg,
chi_deg=self._completed_grids[row].request.smargon_top_left.chi_deg,
)
)
@Slot(int)
def completed_grid_redo(self, row: int):
if 0 <= row < len(self.__completed_grids):
src = self.__completed_grids[row].request
self.__active_grid.n_x = src.n_x
self.__active_grid.n_y = src.n_y
self.__active_grid.grid_size_mm = Coordinate(x=src.grid_size_mm.x, y=src.grid_size_mm.y)
self.__active_grid.smargon_top_left = SmargonCoordinate(
if 0 <= row < len(self._completed_grids):
src = self._completed_grids[row].request
self._active_grid.n_x = src.n_x
self._active_grid.n_y = src.n_y
self._active_grid.grid_size_mm = Coordinate(x=src.grid_size_mm.x, y=src.grid_size_mm.y)
self._active_grid.smargon_top_left = SmargonCoordinate(
sh_mm=Coordinate(
x=src.smargon_top_left.sh_mm.x,
y=src.smargon_top_left.sh_mm.y,
@@ -956,18 +954,18 @@ class RasterGridManager(QObject):
phi_deg=src.smargon_top_left.phi_deg,
chi_deg=src.smargon_top_left.chi_deg,
)
self.__active_grid.omega_deg = src.omega_deg
self._active_grid.omega_deg = src.omega_deg
self.__active_grid.visible = True
self.__completed_grids[row].request.visible = False
self._active_grid.visible = True
self._completed_grids[row].request.visible = False
self.grid_scan_size_changed.emit(
self.__active_grid.n_x,
self.__active_grid.n_y,
self.__active_grid.grid_size_mm.x,
self.__active_grid.grid_size_mm.y,
self._active_grid.n_x,
self._active_grid.n_y,
self._active_grid.grid_size_mm.x,
self._active_grid.grid_size_mm.y,
)
self.completed_grid_updated.emit()
def get_completed_grids(self) -> List[CompletedRasterGridElem]:
return self.__completed_grids
return self._completed_grids
@@ -8,10 +8,10 @@ class RotationScanManager(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self.__beamline = mx_beamline()
self.__detector_url = get_jfjoch_url(self.__beamline)
self._beamline = mx_beamline()
self._detector_url = get_jfjoch_url(self._beamline)
@Slot(CompletedRotationScan)
def scan_completed(self, r: CompletedRotationScan):
if r.result.file_prefix is not None:
self.file_ready.emit(self.__detector_url, 0)
self.file_ready.emit(self._detector_url, 0)
@@ -7,11 +7,11 @@ class SampleMountLogic(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self.__sample = None
self._sample = None
@Slot(DAQStatusModel)
def update_daq_status(self, s: DAQStatusModel):
if s.sample != self.__sample:
if s.sample != self._sample:
self.sample_changed.emit(s.sample)
self.__sample = s.sample
self._sample = s.sample
+57 -57
View File
@@ -21,68 +21,68 @@ class SampleCameraThread(QThread):
def __init__(self, zmq_url: str, parent=None):
super().__init__(parent)
context = zmq.Context()
self.__socket = context.socket(zmq.SUB)
self.__socket.setsockopt(zmq.SUBSCRIBE, b"")
self.__socket.setsockopt(zmq.RCVTIMEO, 500)
self.__socket.connect(zmq_url)
self._socket = context.socket(zmq.SUB)
self._socket.setsockopt(zmq.SUBSCRIBE, b"")
self._socket.setsockopt(zmq.RCVTIMEO, 500)
self._socket.connect(zmq_url)
self.running = True
self.__measure_focus = False
self.__focus_mask = None
self.__beam_x = 0
self.__beam_y = 0
self.__last_beam_pos = None
self.__radius = 40
self._measure_focus = False
self._focus_mask = None
self._beam_x = 0
self._beam_y = 0
self._last_beam_pos = None
self._radius = 40
self.__fps_window_start = time.perf_counter()
self.__fps_frame_count = 0
self.__fps_emit_period_s = 0.5
self.__last_frame_time = None
self.__no_frame_timeout_s = 5.0
self._fps_window_start = time.perf_counter()
self._fps_frame_count = 0
self._fps_emit_period_s = 0.5
self._last_frame_time = None
self._no_frame_timeout_s = 5.0
self.__camera_available = False
self.__last_camera_error: str | None = None
self._camera_available = False
self._last_camera_error: str | None = None
def __set_camera_available(self, available: bool, error: str | None = None) -> None:
if available != self.__camera_available:
self.__camera_available = available
def _set_camera_available(self, available: bool, error: str | None = None) -> None:
if available != self._camera_available:
self._camera_available = available
self.camera_availability_changed.emit(available)
if error is not None and error != self.__last_camera_error:
self.__last_camera_error = error
if error is not None and error != self._last_camera_error:
self._last_camera_error = error
self.camera_error.emit(error)
if available:
self.__last_camera_error = None
self._last_camera_error = None
@Slot(DAQStatusModel)
def update_daq_status(self, s: DAQStatusModel):
self.__beam_x = s.geom.beam_location_pxl.x
self.__beam_y = s.geom.beam_location_pxl.y
if (self.__beam_x, self.__beam_y) != self.__last_beam_pos:
self.__focus_mask = None # Invalidate cache
self.__last_beam_pos = (self.__beam_x, self.__beam_y)
self._beam_x = s.geom.beam_location_pxl.x
self._beam_y = s.geom.beam_location_pxl.y
if (self._beam_x, self._beam_y) != self._last_beam_pos:
self._focus_mask = None # Invalidate cache
self._last_beam_pos = (self._beam_x, self._beam_y)
@Slot(bool)
def enable_focus_measurement(self, enabled: bool = True):
"""Enable or disable focus measurement."""
self.__measure_focus = enabled
self._measure_focus = enabled
def run(self):
while self.running:
try:
r = self.__socket.recv_multipart()
r = self._socket.recv_multipart()
now = time.perf_counter()
self.__last_frame_time = now
self.__fps_frame_count += 1
self._last_frame_time = now
self._fps_frame_count += 1
elapsed = now - self.__fps_window_start
if elapsed >= self.__fps_emit_period_s:
fps = self.__fps_frame_count / elapsed if elapsed > 0 else 0.0
elapsed = now - self._fps_window_start
if elapsed >= self._fps_emit_period_s:
fps = self._fps_frame_count / elapsed if elapsed > 0 else 0.0
self.fps_measure.emit(float(fps))
self.__fps_window_start = now
self.__fps_frame_count = 0
self._fps_window_start = now
self._fps_frame_count = 0
if len(r) < 2:
continue
@@ -106,7 +106,7 @@ class SampleCameraThread(QThread):
encoded = np.frombuffer(data, dtype=np.uint8)
bgr = cv2.imdecode(encoded, cv2.IMREAD_COLOR)
if bgr is None:
self.__set_camera_available(
self._set_camera_available(
False, "Sample camera feed unavailable: failed to decode JPEG frame"
)
continue
@@ -116,24 +116,24 @@ class SampleCameraThread(QThread):
raw = np.frombuffer(data, np.uint8).reshape((h, w))
rgb = cv2.cvtColor(raw, cv2.COLOR_BAYER_GB2RGB)
else:
self.__set_camera_available(
self._set_camera_available(
False,
f"Sample camera feed unavailable: unsupported frame header {header}",
)
continue
self.__set_camera_available(True)
self._set_camera_available(True)
if self.__measure_focus:
if self._measure_focus:
gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
if self.__focus_mask is None or self.__focus_mask.shape != gray.shape:
if self._focus_mask is None or self._focus_mask.shape != gray.shape:
height, width = gray.shape
y, x = np.ogrid[:height, :width]
self.__focus_mask = (x - self.__beam_x) ** 2 + (
y - self.__beam_y
) ** 2 <= self.__radius**2
self._focus_mask = (x - self._beam_x) ** 2 + (
y - self._beam_y
) ** 2 <= self._radius**2
sharpness = focus_measure_edges(gray, self.__focus_mask)
sharpness = focus_measure_edges(gray, self._focus_mask)
self.focus_measure.emit(sharpness)
qimage = QImage(
@@ -141,33 +141,33 @@ class SampleCameraThread(QThread):
).copy()
self.camera_image.emit(QPixmap.fromImage(qimage))
else:
self.__set_camera_available(
self._set_camera_available(
False, "Sample camera feed unavailable: no frame header in zmq stream"
)
except zmq.Again: # Timeout occurred
now = time.perf_counter()
elapsed = now - self.__fps_window_start
if elapsed >= self.__fps_emit_period_s:
elapsed = now - self._fps_window_start
if elapsed >= self._fps_emit_period_s:
no_frames_long = (
self.__last_frame_time is None
or (now - self.__last_frame_time) >= self.__no_frame_timeout_s
self._last_frame_time is None
or (now - self._last_frame_time) >= self._no_frame_timeout_s
)
self.fps_measure.emit(float("nan") if no_frames_long else 0.0)
self.__fps_window_start = now
self.__fps_frame_count = 0
self._fps_window_start = now
self._fps_frame_count = 0
if no_frames_long:
self.__set_camera_available(False, "Sample camera feed unavailable")
self._set_camera_available(False, "Sample camera feed unavailable")
continue # Check self.running again
except Exception as e:
self.__set_camera_available(False, f"Sample camera feed unavailable: {e}")
self._set_camera_available(False, f"Sample camera feed unavailable: {e}")
self.running = False
def stop(self):
self.running = False
if self.__socket:
self.__socket.close()
if self._socket:
self._socket.close()
if self.isRunning():
self.quit()
+191 -191
View File
@@ -142,15 +142,15 @@ class DAQWorker(QObject):
"""
super().__init__(parent)
self._active_status_error_key = None
self.__token = token
self.__base_url = base_url
self.__net_manager = QNetworkAccessManager()
self.__net_manager.sslErrors.connect(self._handle_ssl_errors)
self.__timer = QTimer()
self.__timer.setInterval(500)
self.__timer.timeout.connect(self.regular_update)
self.__timer.start()
self.__counter = 0
self._token = token
self._base_url = base_url
self._net_manager = QNetworkAccessManager()
self._net_manager.sslErrors.connect(self._handle_ssl_errors)
self._timer = QTimer()
self._timer.setInterval(500)
self._timer.timeout.connect(self.regular_update)
self._timer.start()
self._counter = 0
self._automation_progress_buffer = ""
self._cleanup_done = False
@@ -215,7 +215,7 @@ class DAQWorker(QObject):
self._local_contact_metadata_poll_enabled = True
self._local_contact_metadata_error: str | None = None
if self.__base_url is not None:
if self._base_url is not None:
self.start_face_detection_stream()
self.start_baton_stream()
self.start_automation_progress_stream()
@@ -333,17 +333,17 @@ class DAQWorker(QObject):
"""
Periodically triggered slot to update spreadsheet and request status from server.
"""
if self.__counter % SPREADHSEET_FREQUENCY == 0:
if self._counter % SPREADHSEET_FREQUENCY == 0:
self.load_spreadsheet()
self.load_reference_tools()
self.__counter = (self.__counter + 1) % SPREADHSEET_FREQUENCY
self._counter = (self._counter + 1) % SPREADHSEET_FREQUENCY
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:
if self._base_url is None:
return
now = time.monotonic()
@@ -351,16 +351,16 @@ class DAQWorker(QObject):
return
self._last_status_request_ts = now
request = QNetworkRequest(QUrl(f"{self.__base_url}/status"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
reply = self.__net_manager.get(request)
request = QNetworkRequest(QUrl(f"{self._base_url}/status"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
reply = self._net_manager.get(request)
reply.finished.connect(lambda: self.handle_status_response(reply))
def server_about_info(self) -> tuple[str, str]:
version_request = QNetworkRequest(QUrl(f"{self.__base_url}/about/running_version"))
version_reply = self.__net_manager.get(version_request)
file_request = QNetworkRequest(QUrl(f"{self.__base_url}/about/server_file_path"))
file_reply = self.__net_manager.get(file_request)
version_request = QNetworkRequest(QUrl(f"{self._base_url}/about/running_version"))
version_reply = self._net_manager.get(version_request)
file_request = QNetworkRequest(QUrl(f"{self._base_url}/about/server_file_path"))
file_reply = self._net_manager.get(file_request)
version = (
str(version_reply.readAll()) if version_reply.waitForReadyRead(500) else "Not connected"
)
@@ -734,15 +734,15 @@ class DAQWorker(QObject):
url: The relative URL (endpoint).
body: The request body string.
"""
if self.__base_url is None:
if self._base_url is None:
logger.info(f"POST /{url}: {body}")
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/{url}"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
request = QNetworkRequest(QUrl(f"{self._base_url}/{url}"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
if str:
request.setRawHeader(b"Content-Type", b"application/json")
reply = self.__net_manager.post(request, QByteArray(body.encode("utf-8")))
reply = self._net_manager.post(request, QByteArray(body.encode("utf-8")))
reply.finished.connect(lambda: self.handle_req_response(reply))
def generic_put(self, url: str, body: str = ""):
@@ -753,14 +753,14 @@ class DAQWorker(QObject):
url: The relative URL (endpoint).
body: The request body string.
"""
if self.__base_url is None:
if self._base_url is None:
logger.info(f"PUT /{url}: {body}")
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/{url}"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
request = QNetworkRequest(QUrl(f"{self._base_url}/{url}"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
if str:
request.setRawHeader(b"Content-Type", b"application/json")
reply = self.__net_manager.put(request, QByteArray(body.encode("utf-8")))
reply = self._net_manager.put(request, QByteArray(body.encode("utf-8")))
reply.finished.connect(lambda: self.handle_req_response(reply))
def generic_delete(self, url: str):
@@ -770,13 +770,13 @@ class DAQWorker(QObject):
Args:
url: The relative URL (endpoint).
"""
if self.__base_url is None:
if self._base_url is None:
logger.info(f"DELETE /{url}")
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/{url}"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
reply = self.__net_manager.deleteResource(request)
request = QNetworkRequest(QUrl(f"{self._base_url}/{url}"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
reply = self._net_manager.deleteResource(request)
reply.finished.connect(lambda: self.handle_req_response(reply))
@Slot(float)
@@ -869,45 +869,45 @@ class DAQWorker(QObject):
@Slot(str)
def free_beamline(self, confirmation_code: str):
if self.__base_url is None:
if self._base_url is None:
logger.info("POST /state/free_beamline")
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/state/free_beamline"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
request = QNetworkRequest(QUrl(f"{self._base_url}/state/free_beamline"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Content-Type", b"application/json")
body = json.dumps({"confirmation_code": confirmation_code})
reply = self.__net_manager.post(request, QByteArray(body.encode("utf-8")))
reply = self._net_manager.post(request, QByteArray(body.encode("utf-8")))
reply.finished.connect(
lambda: self._handle_recovery_action_response(reply, "Beamline busy flag cleared.")
)
@Slot(str)
def take_over_beamline(self, confirmation_code: str):
if self.__base_url is None:
if self._base_url is None:
logger.info("POST /access/take_over_beamline")
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/access/take_over_beamline"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
request = QNetworkRequest(QUrl(f"{self._base_url}/access/take_over_beamline"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Content-Type", b"application/json")
body = json.dumps({"confirmation_code": confirmation_code})
reply = self.__net_manager.post(request, QByteArray(body.encode("utf-8")))
reply = self._net_manager.post(request, QByteArray(body.encode("utf-8")))
reply.finished.connect(
lambda: self._handle_recovery_action_response(reply, "Beamline session taken over.")
)
@Slot(str)
def recover_beamline(self, confirmation_code: str):
if self.__base_url is None:
if self._base_url is None:
logger.info("POST /recovery/recover_beamline")
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/recovery/recover_beamline"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
request = QNetworkRequest(QUrl(f"{self._base_url}/recovery/recover_beamline"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Content-Type", b"application/json")
body = json.dumps({"confirmation_code": confirmation_code})
reply = self.__net_manager.post(request, QByteArray(body.encode("utf-8")))
reply = self._net_manager.post(request, QByteArray(body.encode("utf-8")))
reply.finished.connect(
lambda: self._handle_recovery_action_response(
reply, "Beamline recovered to Maintenance."
@@ -916,15 +916,15 @@ class DAQWorker(QObject):
@Slot(str)
def recovery_unmount_sample(self, confirmation_code: str):
if self.__base_url is None:
if self._base_url is None:
logger.info("POST /recovery/unmount_sample")
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/recovery/unmount_sample"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
request = QNetworkRequest(QUrl(f"{self._base_url}/recovery/unmount_sample"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Content-Type", b"application/json")
body = json.dumps({"confirmation_code": confirmation_code})
reply = self.__net_manager.post(request, QByteArray(body.encode("utf-8")))
reply = self._net_manager.post(request, QByteArray(body.encode("utf-8")))
reply.finished.connect(
lambda: self._handle_recovery_action_response(reply, "Recovery unmount completed.")
)
@@ -960,13 +960,13 @@ class DAQWorker(QObject):
@Slot()
def get_all_pgroups(self):
logger.debug("PUT /access/all_pgroups")
if self.__base_url is None:
if self._base_url is None:
self.staff_pgroups_loaded.emit([])
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/access/all_pgroups"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
request = QNetworkRequest(QUrl(f"{self._base_url}/access/all_pgroups"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Content-Type", b"application/json")
reply = self.__net_manager.put(request, QByteArray(b""))
reply = self._net_manager.put(request, QByteArray(b""))
reply.finished.connect(lambda: self._handle_all_pgroups_response(reply))
@Slot(SampleCameraSettings)
@@ -1032,11 +1032,11 @@ class DAQWorker(QObject):
"""
self.run_number_incremented.emit()
request = QNetworkRequest(QUrl(f"{self.__base_url}/scan/rotation"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
request = QNetworkRequest(QUrl(f"{self._base_url}/scan/rotation"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Content-Type", b"application/json")
body = r.model_dump_json()
reply = self.__net_manager.post(request, QByteArray(body.encode("utf-8")))
reply = self._net_manager.post(request, QByteArray(body.encode("utf-8")))
reply.finished.connect(lambda: self.handle_rotation_scan_response(reply))
def handle_raster_scan_response(self, reply: QNetworkReply):
@@ -1086,7 +1086,7 @@ class DAQWorker(QObject):
r: The RasterGridRequest object.
"""
self.run_number_incremented.emit()
if self.__base_url is None:
if self._base_url is None:
logger.info(f"POST /scan/raster: {r.model_dump_json()}")
image_number = r.get_image_number()
new_copy = copy.deepcopy(r)
@@ -1113,11 +1113,11 @@ class DAQWorker(QObject):
self.raster_scan_completed.emit(reply)
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/scan/raster?auto_center=false"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
request = QNetworkRequest(QUrl(f"{self._base_url}/scan/raster?auto_center=false"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Content-Type", b"application/json")
body = r.model_dump_json()
reply = self.__net_manager.post(request, QByteArray(body.encode("utf-8")))
reply = self._net_manager.post(request, QByteArray(body.encode("utf-8")))
reply.finished.connect(lambda: self.handle_raster_scan_response(reply))
@Slot(RasterGridRequest)
@@ -1129,7 +1129,7 @@ class DAQWorker(QObject):
r: The RasterGridRequest object.
"""
self.run_number_incremented.emit()
if self.__base_url is None:
if self._base_url is None:
logger.info(f"POST /scan/raster: {r.model_dump_json()}")
image_number = r.get_image_number()
new_copy = copy.deepcopy(r)
@@ -1155,32 +1155,32 @@ class DAQWorker(QObject):
self.raster_scan_completed.emit(reply)
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/scan/raster?auto_center=true"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
request = QNetworkRequest(QUrl(f"{self._base_url}/scan/raster?auto_center=true"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Content-Type", b"application/json")
body = r.model_dump_json()
reply = self.__net_manager.post(request, QByteArray(body.encode("utf-8")))
reply = self._net_manager.post(request, QByteArray(body.encode("utf-8")))
reply.finished.connect(lambda: self.handle_raster_scan_response(reply))
@Slot()
def load_spreadsheet(self):
if self.__base_url is None:
if self._base_url is None:
logger.info("GET /sample/spreadsheet")
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/sample/spreadsheet"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
reply = self.__net_manager.get(request)
request = QNetworkRequest(QUrl(f"{self._base_url}/sample/spreadsheet"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
reply = self._net_manager.get(request)
reply.finished.connect(lambda: self.handle_spreadsheet_response(reply))
@Slot()
def load_reference_tools(self):
if self.__base_url is None:
if self._base_url is None:
logger.info("GET /sample/reference_tools")
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/sample/reference_tools"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
reply = self.__net_manager.get(request)
request = QNetworkRequest(QUrl(f"{self._base_url}/sample/reference_tools"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
reply = self._net_manager.get(request)
reply.finished.connect(lambda: self.handle_reference_tools_response(reply))
def _emit_manual_collection_critical_failure(self, msg: str) -> None:
@@ -1372,20 +1372,20 @@ class DAQWorker(QObject):
Args:
s: The SampleShortInfo object.
"""
if self.__base_url is None:
if self._base_url is None:
logger.info(f"POST /scan/auto: {s.model_dump_json()}")
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/scan/auto"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
request = QNetworkRequest(QUrl(f"{self._base_url}/scan/auto"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Content-Type", b"application/json")
body = s.model_dump_json()
reply = self.__net_manager.post(request, QByteArray(body.encode("utf-8")))
reply = self._net_manager.post(request, QByteArray(body.encode("utf-8")))
reply.finished.connect(lambda: self.handle_auto_scan_response(reply, s.db_id))
@Slot(SimpleScanParameters)
def smart_params(self, p: SimpleScanParameters):
if self.__base_url is None:
if self._base_url is None:
logger.info(f"POST /scan/smart_params: {p.model_dump_json()}")
return
self.generic_post("scan/smart_params", p.model_dump_json())
@@ -1420,26 +1420,26 @@ class DAQWorker(QObject):
@Slot()
def resync_sample(self):
if self.__base_url is None:
if self._base_url is None:
logger.info("POST /sample/resync")
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/sample/resync"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
request = QNetworkRequest(QUrl(f"{self._base_url}/sample/resync"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Content-Type", b"application/json")
reply = self.__net_manager.post(request, QByteArray(b""))
reply = self._net_manager.post(request, QByteArray(b""))
reply.finished.connect(lambda: self._handle_sample_resync_response(reply))
@Slot()
def resync_local_contact_detector_metadata(self):
if self.__base_url is None:
if self._base_url is None:
logger.info("POST /local_contact/resync/detector_metadata")
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/local_contact/resync/detector_metadata"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
request = QNetworkRequest(QUrl(f"{self._base_url}/local_contact/resync/detector_metadata"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Content-Type", b"application/json")
reply = self.__net_manager.post(request, QByteArray(b""))
reply = self._net_manager.post(request, QByteArray(b""))
reply.finished.connect(lambda: self._handle_detector_metadata_resync_response(reply))
@Slot(float)
@@ -1486,7 +1486,7 @@ class DAQWorker(QObject):
if not self._local_contact_metadata_poll_enabled:
return
if self.__base_url is None:
if self._base_url is None:
self.local_contact_simulation_state_loaded.emit(
{
"bec": False,
@@ -1498,9 +1498,9 @@ class DAQWorker(QObject):
)
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/local_contact/simulation_state"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
reply = self.__net_manager.get(request)
request = QNetworkRequest(QUrl(f"{self._base_url}/local_contact/simulation_state"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
reply = self._net_manager.get(request)
reply.finished.connect(lambda: self._handle_local_contact_simulation_state_response(reply))
def _handle_local_contact_simulation_state_response(self, reply: QNetworkReply):
@@ -1523,13 +1523,13 @@ class DAQWorker(QObject):
if not self._local_contact_metadata_poll_enabled:
return
if self.__base_url is None:
if self._base_url is None:
self.local_contact_device_state_loaded.emit({})
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/local_contact/device_state"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
reply = self.__net_manager.get(request)
request = QNetworkRequest(QUrl(f"{self._base_url}/local_contact/device_state"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
reply = self._net_manager.get(request)
reply.finished.connect(lambda: self._handle_local_contact_device_state_response(reply))
def _handle_local_contact_device_state_response(self, reply: QNetworkReply):
@@ -1552,13 +1552,13 @@ class DAQWorker(QObject):
if not self._local_contact_metadata_poll_enabled:
return
if self.__base_url is None:
if self._base_url is None:
self.local_contact_links_loaded.emit({})
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/local_contact/links"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
reply = self.__net_manager.get(request)
request = QNetworkRequest(QUrl(f"{self._base_url}/local_contact/links"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
reply = self._net_manager.get(request)
reply.finished.connect(lambda: self._handle_local_contact_links_response(reply))
def _handle_local_contact_links_response(self, reply: QNetworkReply):
@@ -1578,7 +1578,7 @@ class DAQWorker(QObject):
@Slot()
def load_local_contact_config(self):
if self.__base_url is None:
if self._base_url is None:
self.local_contact_config_loaded.emit(
{
"mount_to_center_sleep_s": 0.0,
@@ -1588,9 +1588,9 @@ class DAQWorker(QObject):
)
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/local_contact/config"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
reply = self.__net_manager.get(request)
request = QNetworkRequest(QUrl(f"{self._base_url}/local_contact/config"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
reply = self._net_manager.get(request)
reply.finished.connect(lambda: self._handle_local_contact_config_response(reply))
def _handle_local_contact_config_response(self, reply: QNetworkReply):
@@ -1610,15 +1610,15 @@ class DAQWorker(QObject):
@Slot(dict)
def set_local_contact_config(self, payload: dict):
if self.__base_url is None:
if self._base_url is None:
self.local_contact_config_saved.emit(payload)
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/local_contact/config"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
request = QNetworkRequest(QUrl(f"{self._base_url}/local_contact/config"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Content-Type", b"application/json")
body = QByteArray(json.dumps(payload).encode("utf-8"))
reply = self.__net_manager.put(request, body)
reply = self._net_manager.put(request, body)
reply.finished.connect(lambda: self._handle_set_local_contact_config_response(reply))
def _handle_set_local_contact_config_response(self, reply: QNetworkReply):
@@ -1651,14 +1651,14 @@ class DAQWorker(QObject):
@Slot()
def bec_list_all_user_macros(self):
if self.__base_url is None:
if self._base_url is None:
logger.info("GET /bec/user_macros")
self.bec_user_macros_loaded.emit([])
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/bec/user_macros"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
reply = self.__net_manager.get(request)
request = QNetworkRequest(QUrl(f"{self._base_url}/bec/user_macros"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
reply = self._net_manager.get(request)
reply.finished.connect(lambda: self._handle_bec_user_macros_response(reply))
def _handle_bec_user_macros_response(self, reply: QNetworkReply):
@@ -1674,14 +1674,14 @@ class DAQWorker(QObject):
@Slot()
def bec_list_all_devices(self):
if self.__base_url is None:
if self._base_url is None:
logger.info("GET /bec/devices")
self.bec_devices_loaded.emit([])
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/bec/devices"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
reply = self.__net_manager.get(request)
request = QNetworkRequest(QUrl(f"{self._base_url}/bec/devices"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
reply = self._net_manager.get(request)
reply.finished.connect(lambda: self._handle_bec_devices_response(reply))
def _handle_bec_devices_response(self, reply: QNetworkReply):
@@ -1763,13 +1763,13 @@ class DAQWorker(QObject):
"""
Request an ML-based bounding box for the current sample.
"""
if self.__base_url is None:
if self._base_url is None:
logger.info("POST /alc/ml_bounding_box")
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/alc/ml_bounding_box"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
reply = self.__net_manager.post(request, QByteArray(b""))
request = QNetworkRequest(QUrl(f"{self._base_url}/alc/ml_bounding_box"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
reply = self._net_manager.post(request, QByteArray(b""))
reply.finished.connect(lambda: self.handle_ml_box_response(reply))
def _handle_face_detection_response(self, reply: QNetworkReply):
@@ -1818,7 +1818,7 @@ class DAQWorker(QObject):
)
return
if self.__base_url is not None:
if self._base_url is not None:
QTimer.singleShot(1000, self.start_face_detection_stream)
@staticmethod
@@ -1990,11 +1990,11 @@ class DAQWorker(QObject):
)
return
if self.__base_url is not None:
if self._base_url is not None:
QTimer.singleShot(1000, self.start_automation_progress_stream)
def start_automation_progress_stream(self):
if self.__base_url is None:
if self._base_url is None:
return
if self._automation_progress_stream_reply is not None:
@@ -2005,15 +2005,15 @@ class DAQWorker(QObject):
self._automation_progress_buffer = ""
request = QNetworkRequest(QUrl(f"{self.__base_url}/sse/automation_progress"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
reply = self.__net_manager.get(request)
request = QNetworkRequest(QUrl(f"{self._base_url}/sse/automation_progress"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
reply = self._net_manager.get(request)
reply.readyRead.connect(lambda: self._read_automation_progress_stream(reply))
reply.finished.connect(self._restart_automation_progress_stream)
self._automation_progress_stream_reply = reply
def start_face_detection_stream(self):
if self.__base_url is None:
if self._base_url is None:
return
if self._face_detection_stream_reply is not None:
@@ -2022,9 +2022,9 @@ class DAQWorker(QObject):
if self._face_detection_stream_blocked_403:
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/sse/face_detection"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
reply = self.__net_manager.get(request)
request = QNetworkRequest(QUrl(f"{self._base_url}/sse/face_detection"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
reply = self._net_manager.get(request)
reply.readyRead.connect(lambda: self._read_face_detection_stream(reply))
reply.finished.connect(self._restart_face_detection_stream)
self._face_detection_stream_reply = reply
@@ -2038,16 +2038,16 @@ class DAQWorker(QObject):
steps: Number of rotation steps.
step_size: Degrees per step.
"""
if self.__base_url is None:
if self._base_url is None:
logger.info(f"POST /face_detection/run?steps={steps}&step_size={step_size}")
return
request = QNetworkRequest(
QUrl(f"{self.__base_url}/face_detection/run?steps={steps}&step_size={step_size}")
QUrl(f"{self._base_url}/face_detection/run?steps={steps}&step_size={step_size}")
)
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Content-Type", b"application/json")
reply = self.__net_manager.post(request, QByteArray(b""))
reply = self._net_manager.post(request, QByteArray(b""))
reply.finished.connect(lambda: self._handle_face_detection_response(reply))
@Slot()
@@ -2070,9 +2070,9 @@ class DAQWorker(QObject):
data = json.loads(response_data) if response_data else []
if emit_status:
# fetch status and bkg in parallel (simple sequential here)
status_req = QNetworkRequest(QUrl(f"{self.__base_url}/fluorimeter/status"))
status_req.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
status_reply = self.__net_manager.get(status_req)
status_req = QNetworkRequest(QUrl(f"{self._base_url}/fluorimeter/status"))
status_req.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
status_reply = self._net_manager.get(status_req)
status_reply.finished.connect(
lambda: self._handle_fluorimeter_status_and_emit(data, status_reply)
)
@@ -2086,9 +2086,9 @@ class DAQWorker(QObject):
try:
s_payload = self.handle_response(status_reply)
s = int(s_payload) if s_payload not in ("", "null") else -1
b_req = QNetworkRequest(QUrl(f"{self.__base_url}/fluorimeter/background"))
b_req.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
b_reply = self.__net_manager.get(b_req)
b_req = QNetworkRequest(QUrl(f"{self._base_url}/fluorimeter/background"))
b_req.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
b_reply = self._net_manager.get(b_req)
b_reply.finished.connect(lambda: self._emit_fluorimeter_with_bkg(data, s, b_reply))
except Exception as e:
logger.error(f"Fluorimeter status error: {e}")
@@ -2107,13 +2107,13 @@ class DAQWorker(QObject):
@Slot()
def fluorimeter_spectrum(self, f: FluorescenceSpectrumParameterModel):
if self.__base_url is None:
if self._base_url is None:
return
req = QNetworkRequest(QUrl(f"{self.__base_url}/fluorimeter/spectrum"))
req.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
req = QNetworkRequest(QUrl(f"{self._base_url}/fluorimeter/spectrum"))
req.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
req.setRawHeader(b"Content-Type", b"application/json")
body = f.model_dump_json()
reply = self.__net_manager.post(req, QByteArray(body.encode("utf-8")))
reply = self._net_manager.post(req, QByteArray(body.encode("utf-8")))
reply.finished.connect(lambda: self._handle_fluorimeter_spectrum(reply))
def _handle_fluorimeter_spectrum(self, reply: QNetworkReply):
@@ -2127,20 +2127,20 @@ class DAQWorker(QObject):
@Slot()
def fluorimeter_request_snapshot(self):
if self.__base_url is None:
if self._base_url is None:
return
req = QNetworkRequest(QUrl(f"{self.__base_url}/fluorimeter/data"))
req.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
reply = self.__net_manager.get(req)
req = QNetworkRequest(QUrl(f"{self._base_url}/fluorimeter/data"))
req.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
reply = self._net_manager.get(req)
reply.finished.connect(lambda: self._handle_fluorimeter_data(reply, emit_status=True))
# Optional: SSE listener for live updates
def start_fluorimeter_stream(self):
if self.__base_url is None:
if self._base_url is None:
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/sse/fluorimeter"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
reply = self.__net_manager.get(request)
request = QNetworkRequest(QUrl(f"{self._base_url}/sse/fluorimeter"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
reply = self._net_manager.get(request)
reply.readyRead.connect(lambda: self._read_fluorimeter_stream(reply))
reply.finished.connect(lambda: reply.deleteLater())
@@ -2186,19 +2186,19 @@ class DAQWorker(QObject):
Server default is grouped. We flatten grouped payloads for existing UI.
"""
if self.__base_url is None:
if self._base_url is None:
self.error_codes_loaded.emit(export_error_codes())
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/meta/error-codes"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
reply = self.__net_manager.get(request)
request = QNetworkRequest(QUrl(f"{self._base_url}/meta/error-codes"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
reply = self._net_manager.get(request)
reply.finished.connect(lambda: self._handle_error_codes_response(reply))
def _retry_error_codes_legacy(self) -> None:
request = QNetworkRequest(QUrl(f"{self.__base_url}/meta/error-codes/flat"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
reply = self.__net_manager.get(request)
request = QNetworkRequest(QUrl(f"{self._base_url}/meta/error-codes/flat"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
reply = self._net_manager.get(request)
reply.finished.connect(lambda: self._handle_error_codes_response(reply))
@Slot(QNetworkReply)
@@ -2228,7 +2228,7 @@ class DAQWorker(QObject):
@Slot(str, str)
def send_screenshot_db(self, filename: str = "", message: str = ""):
if self.__base_url is None:
if self._base_url is None:
logger.info(f"POST /samcam/send_screenshot_db?filename={filename}&message={message}")
return
@@ -2248,22 +2248,22 @@ class DAQWorker(QObject):
def start_baton_stream(self):
"""Start SSE stream for baton status updates."""
if self.__base_url is None:
if self._base_url is None:
return
if self._baton_stream_reply is not None:
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/sse/baton"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
reply = self.__net_manager.get(request)
request = QNetworkRequest(QUrl(f"{self._base_url}/sse/baton"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
reply = self._net_manager.get(request)
reply.readyRead.connect(lambda: self._read_baton_stream(reply))
reply.finished.connect(self._restart_baton_stream)
self._baton_stream_reply = reply
def _restart_baton_stream(self):
self._baton_stream_reply = None
if self.__base_url is not None:
if self._base_url is not None:
QTimer.singleShot(1000, self.start_baton_stream)
def _read_baton_stream(self, reply: QNetworkReply):
@@ -2307,14 +2307,14 @@ class DAQWorker(QObject):
"""
Request the baton to gain write access to the beamline.
"""
if self.__base_url is None:
if self._base_url is None:
logger.info("POST /baton/request")
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/baton/request"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
request = QNetworkRequest(QUrl(f"{self._base_url}/baton/request"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Content-Type", b"application/json")
reply = self.__net_manager.post(request, QByteArray(b""))
reply = self._net_manager.post(request, QByteArray(b""))
reply.finished.connect(lambda: self._handle_baton_request_response(reply))
def _handle_baton_request_response(self, reply: QNetworkReply):
@@ -2346,16 +2346,16 @@ class DAQWorker(QObject):
Args:
accept: True to grant the baton, False to refuse.
"""
if self.__base_url is None:
if self._base_url is None:
logger.info(f"POST /baton/respond?accept={accept}")
return
request = QNetworkRequest(
QUrl(f"{self.__base_url}/baton/respond?accept={str(accept).lower()}")
QUrl(f"{self._base_url}/baton/respond?accept={str(accept).lower()}")
)
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Content-Type", b"application/json")
reply = self.__net_manager.post(request, QByteArray(b""))
reply = self._net_manager.post(request, QByteArray(b""))
reply.finished.connect(lambda: self._handle_baton_response_result(reply))
def _handle_baton_response_result(self, reply: QNetworkReply):
@@ -2386,12 +2386,12 @@ class DAQWorker(QObject):
@Slot()
def check_baton_timeout(self):
"""Poll to check if timeout has been reached."""
if self.__base_url is None:
if self._base_url is None:
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/baton/check_timeout"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
reply = self.__net_manager.get(request)
request = QNetworkRequest(QUrl(f"{self._base_url}/baton/check_timeout"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
reply = self._net_manager.get(request)
reply.finished.connect(lambda: self._handle_baton_timeout_response(reply))
def _handle_baton_timeout_response(self, reply: QNetworkReply):
@@ -2448,18 +2448,18 @@ class DAQWorker(QObject):
@Slot()
def load_gui_sessions(self):
if self.__base_url is None:
if self._base_url is None:
self.gui_sessions_loaded.emit([])
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/admin/gui_sessions"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
reply = self.__net_manager.get(request)
request = QNetworkRequest(QUrl(f"{self._base_url}/admin/gui_sessions"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
reply = self._net_manager.get(request)
reply.finished.connect(lambda: self._handle_gui_sessions_response(reply))
@Slot(int, int)
def request_gui_close(self, session_id: int, grace_seconds: int = 60):
if self.__base_url is None:
if self._base_url is None:
logger.info(
f"POST /admin/gui_sessions/{session_id}/request_close?grace_seconds={grace_seconds}"
)
@@ -2467,23 +2467,23 @@ class DAQWorker(QObject):
request = QNetworkRequest(
QUrl(
f"{self.__base_url}/admin/gui_sessions/{session_id}/request_close?grace_seconds={grace_seconds}"
f"{self._base_url}/admin/gui_sessions/{session_id}/request_close?grace_seconds={grace_seconds}"
)
)
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Content-Type", b"application/json")
reply = self.__net_manager.post(request, QByteArray(b""))
reply = self._net_manager.post(request, QByteArray(b""))
reply.finished.connect(lambda: self._handle_gui_session_mutation_response(reply))
@Slot(int)
def force_remove_gui_session(self, session_id: int):
if self.__base_url is None:
if self._base_url is None:
logger.info(f"DELETE /admin/gui_sessions/{session_id}")
return
request = QNetworkRequest(QUrl(f"{self.__base_url}/admin/gui_sessions/{session_id}"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
reply = self.__net_manager.deleteResource(request)
request = QNetworkRequest(QUrl(f"{self._base_url}/admin/gui_sessions/{session_id}"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
reply = self._net_manager.deleteResource(request)
reply.finished.connect(lambda: self._handle_gui_session_mutation_response(reply))
def _handle_gui_session_mutation_response(self, reply: QNetworkReply):
@@ -2502,15 +2502,15 @@ class DAQWorker(QObject):
@Slot(int)
def report_gui_interaction(self, session_id: int):
if self.__base_url is None:
if self._base_url is None:
return
request = QNetworkRequest(
QUrl(f"{self.__base_url}/admin/gui_sessions/{session_id}/interaction")
QUrl(f"{self._base_url}/admin/gui_sessions/{session_id}/interaction")
)
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Content-Type", b"application/json")
reply = self.__net_manager.post(request, QByteArray(b""))
reply = self._net_manager.post(request, QByteArray(b""))
reply.finished.connect(lambda: reply.deleteLater())
def cleanup(self) -> None:
@@ -2525,8 +2525,8 @@ class DAQWorker(QObject):
logger.warning(f"Failed to stop _baton_timeout_timer: {e}")
try:
if hasattr(self, "_DAQWorker__timer") and self.__timer is not None:
self.__timer.stop()
if hasattr(self, "_timer") and self._timer is not None:
self._timer.stop()
except Exception as e:
logger.warning(f"Failed to stop __timer: {e}")
+19 -19
View File
@@ -7,20 +7,20 @@ class JFJochDBusClient(QObject):
def __init__(self):
super().__init__()
self.__session_bus = QDBusConnection.sessionBus()
self.__interface = None
self.__dbus_available = False
self._session_bus = QDBusConnection.sessionBus()
self._interface = None
self._dbus_available = False
self.__beamline = mx_beamline()
self._beamline = mx_beamline()
try:
self.__detector_url = get_jfjoch_url(self.__beamline)
self._detector_url = get_jfjoch_url(self._beamline)
except ValueError:
self.__detector_url = None
self._detector_url = None
try:
self.__session_bus = QDBusConnection.sessionBus()
if self.__session_bus.isConnected():
self.__dbus_available = True
self._session_bus = QDBusConnection.sessionBus()
if self._session_bus.isConnected():
self._dbus_available = True
print("D-Bus session bus connected")
else:
print("D-Bus not available")
@@ -30,23 +30,23 @@ class JFJochDBusClient(QObject):
def _ensure_interface(self):
"""Ensure we have a valid interface, creating one if needed."""
if not self.__dbus_available:
if not self._dbus_available:
print("D-Bus not available, cannot connect to JFJoch viewer")
return False
if self.__interface is None or not self.__interface.isValid():
if not self.__session_bus.isConnected():
if self._interface is None or not self._interface.isValid():
if not self._session_bus.isConnected():
print("Session bus not connected")
return False
self.__interface = QDBusInterface(
self._interface = QDBusInterface(
"ch.psi.jfjoch_viewer", # Service name
"/", # Object path
"ch.psi.jfjoch_viewer", # Interface name
self.__session_bus,
self._session_bus,
)
if not self.__interface.isValid():
if not self._interface.isValid():
print("Service 'ch.psi.jfjoch_viewer' not available")
return False
@@ -54,7 +54,7 @@ class JFJochDBusClient(QObject):
@Slot(str, int)
def load_image(self, name: str, number: int):
if not self.__dbus_available:
if not self._dbus_available:
print("D-Bus not available, cannot load image in JFJoch viewer")
return
@@ -64,13 +64,13 @@ class JFJochDBusClient(QObject):
path = "/sls/mx/data/" + name + "_master.h5"
print(f"Load dataset: {path} image: {number}")
if self._ensure_interface():
self.__interface.call("LoadFile", path, number, 1)
self._interface.call("LoadFile", path, number, 1)
@Slot()
def load_online(self):
if not self.__dbus_available:
if not self._dbus_available:
print("D-Bus not available, cannot load online data in JFJoch viewer")
return
if self._ensure_interface():
self.__interface.call("LoadFile", self.__detector_url, -1, 1)
self._interface.call("LoadFile", self._detector_url, -1, 1)
File diff suppressed because it is too large Load Diff
+11 -13
View File
@@ -15,9 +15,9 @@ class LoginDialog(QDialog):
self.setWindowTitle("User Authentication")
self.setMinimumWidth(400)
self.setStyleSheet("background-color: rgb(216, 228, 253);")
self.__base_url = base_url
self.__reply = None
self.__network_manager = None
self._base_url = base_url
self._reply = None
self._network_manager = None
# Layout
layout = QVBoxLayout(self)
@@ -44,14 +44,14 @@ class LoginDialog(QDialog):
@Slot()
def authenticate(self):
if self.__base_url is None:
if self._base_url is None:
token_data = TokenData(
sub=self.name_entry.text(), staff=True, session=15, pgroups=["p16371", "p22233"]
)
self.token = jwt.encode(token_data.model_dump(), "ABC123")
self.accept()
self.__network_manager = QNetworkAccessManager(self)
request = QNetworkRequest(QUrl(f"{self.__base_url}/token"))
self._network_manager = QNetworkAccessManager(self)
request = QNetworkRequest(QUrl(f"{self._base_url}/token"))
request.setHeader(
QNetworkRequest.KnownHeaders.ContentTypeHeader, "application/x-www-form-urlencoded"
)
@@ -60,17 +60,15 @@ class LoginDialog(QDialog):
payload.addQueryItem("username", f"{self.name_entry.text()}")
payload.addQueryItem("password", "")
payload_string = payload.toString()
self.__reply = self.__network_manager.post(
request, QByteArray.fromStdString(payload_string)
)
self.__reply.finished.connect(self.handle_token_response)
self._reply = self._network_manager.post(request, QByteArray.fromStdString(payload_string))
self._reply.finished.connect(self.handle_token_response)
@Slot()
def handle_token_response(self):
if self.__reply is not None and self.__reply.error() == QNetworkReply.NetworkError.NoError:
response_data = self.__reply.readAll().data()
if self._reply is not None and self._reply.error() == QNetworkReply.NetworkError.NoError:
response_data = self._reply.readAll().data()
response_json = json.loads(response_data.decode("utf-8"))
if "access_token" in response_json:
self.token = response_json["access_token"]
self.__reply.deleteLater()
self._reply.deleteLater()
self.accept()
+10 -10
View File
@@ -1,11 +1,11 @@
from PySide6.QtCore import Signal, Slot
from PySide6.QtWidgets import (
QTableWidget,
QHBoxLayout,
QHeaderView,
QPushButton,
QTableWidget,
QTableWidgetItem,
QWidget,
QHBoxLayout,
QPushButton,
)
from aare.gui.scan_logic.raster_grid_manager import RasterGridManager
@@ -19,7 +19,7 @@ class RasterGridTable(QTableWidget):
def __init__(self, raster_mgr: RasterGridManager, parent=None):
super().__init__(parent)
self.__raster_mgr = raster_mgr
self._raster_mgr = raster_mgr
self.setColumnCount(5)
self.setHorizontalHeaderLabels(["Omega", "Chi", "Phi", "Size (WxH)", "Actions"])
@@ -36,14 +36,14 @@ class RasterGridTable(QTableWidget):
self.setMinimumHeight(100)
# Connect to raster manager signals
self.__raster_mgr.completed_grid_updated.connect(self.refresh_table)
self.copy_clicked.connect(self.__raster_mgr.completed_grid_redo)
self.delete_clicked.connect(self.__raster_mgr.completed_grid_scan_delete)
self.visible_clicked.connect(self.__raster_mgr.completed_grid_scan_toggle_visible)
self.goto_clicked.connect(self.__raster_mgr.completed_grid_scan_goto)
self._raster_mgr.completed_grid_updated.connect(self.refresh_table)
self.copy_clicked.connect(self._raster_mgr.completed_grid_redo)
self.delete_clicked.connect(self._raster_mgr.completed_grid_scan_delete)
self.visible_clicked.connect(self._raster_mgr.completed_grid_scan_toggle_visible)
self.goto_clicked.connect(self._raster_mgr.completed_grid_scan_goto)
def add_rows(self):
for r in self.__raster_mgr.get_completed_grids():
for r in self._raster_mgr.get_completed_grids():
req = r.request
row_position = self.rowCount()
+28 -30
View File
@@ -40,10 +40,10 @@ class StatusBar(QStatusBar):
def __init__(self, token: TokenData, parent=None):
super().__init__(parent)
self.__decoded_token = token
self.__status = None
self.__is_staff = self.__decoded_token.staff
self.__allowed_pgroups = self.__decoded_token.pgroups
self._decoded_token = token
self._status = None
self._is_staff = self._decoded_token.staff
self._allowed_pgroups = self._decoded_token.pgroups
self._message_clear_timer = QTimer(self)
self._message_clear_timer.setSingleShot(True)
@@ -134,7 +134,7 @@ class StatusBar(QStatusBar):
@Slot(DAQStatusModel)
def update_daq_status(self, status: DAQStatusModel):
try:
self.__status = status
self._status = status
if status.bl.flux_ph_s is None:
self.flux.set_value("0")
else:
@@ -300,10 +300,10 @@ class StatusBar(QStatusBar):
def _update_session_display(self):
"""Update session label based on current status."""
if self.__status is None:
if self._status is None:
return
session_state = self.__status.session.session
session_state = self._status.session.session
# Base text
if session_state == SessionsStateEnum.OwnedByYou:
@@ -327,8 +327,8 @@ class StatusBar(QStatusBar):
def show_session_menu(self):
menu = QMenu(self)
is_busy = self.__status and self.__status.busy
session_state = self.__status.session.session if self.__status else SessionsStateEnum.Vacant
is_busy = self._status and self._status.busy
session_state = self._status.session.session if self._status else SessionsStateEnum.Vacant
# Determine if we are the holder or waiting for baton
is_yours = session_state in (
SessionsStateEnum.OwnedByYou,
@@ -362,7 +362,7 @@ class StatusBar(QStatusBar):
# Already have a pending request - show cancel option
action_cancel = menu.addAction("Cancel Request")
action_cancel.triggered.connect(self._on_cancel_request_clicked)
elif self.__is_staff:
elif self._is_staff:
# Staff can always grab (override)
action_grab = menu.addAction("Grab (Override)")
action_grab.setEnabled(not is_busy) # Still respect busy for safety
@@ -395,11 +395,11 @@ class StatusBar(QStatusBar):
# --- END SESSION (cleanup) ---
action_end = menu.addAction("End Session")
action_end.setEnabled(bool(is_yours or self.__is_staff))
action_end.setEnabled(bool(is_yours or self._is_staff))
action_end.triggered.connect(self.end_session_clicked)
# --- STAFF: FORCE GRAB (emergency) ---
if self.__is_staff and is_other:
if self._is_staff and is_other:
menu.addSeparator()
action_force = menu.addAction("⚠️ Force Take Over")
action_force.triggered.connect(self._on_force_session_clicked)
@@ -411,18 +411,16 @@ class StatusBar(QStatusBar):
menu.exec()
def show_pgroup_menu(self):
in_curr = self.__status
in_curr = self._status
logger.info(f"in_curr is {in_curr}")
if not in_curr:
print(f"is status None?: {self.__status}")
print(f"is status None?: {self._status}")
allow_pgroup_menu = self.__is_staff or in_curr
allow_pgroup_menu = self._is_staff or in_curr
owned_by_you = (
self.__status and self.__status.session.session == SessionsStateEnum.OwnedByYou
)
owned_by_you = self._status and self._status.session.session == SessionsStateEnum.OwnedByYou
if allow_pgroup_menu:
logger.info("showing pgroup menu")
@@ -455,13 +453,13 @@ class StatusBar(QStatusBar):
menu.exec()
def show_state_menu(self):
if self.__status is None:
if self._status is None:
return
logger.info(self.__status.state)
logger.info(self._status.state)
menu = QMenu(self)
if self.__status.busy or self.__status.state in [BeamlineStateEnum.Moving]:
if self._status.busy or self._status.state in [BeamlineStateEnum.Moving]:
return
action_0 = menu.addAction("Available transitions")
@@ -472,7 +470,7 @@ class StatusBar(QStatusBar):
action_0.setEnabled(False)
menu.addSeparator()
if self.__status.state in [
if self._status.state in [
BeamlineStateEnum.RobotSampleExchange,
BeamlineStateEnum.SampleExchange,
BeamlineStateEnum.DewarTransfer,
@@ -483,14 +481,14 @@ class StatusBar(QStatusBar):
]:
action_1 = menu.addAction("Sample alignment")
action_1.triggered.connect(self.sa)
elif self.__status.state in [BeamlineStateEnum.SampleAlignment]:
elif self._status.state in [BeamlineStateEnum.SampleAlignment]:
action_2 = menu.addAction("Manual sample exchange")
action_2.triggered.connect(self.se)
action_3 = menu.addAction("Dewar transfer")
action_3.triggered.connect(self.dl)
action_4 = menu.addAction("Beam location")
action_4.triggered.connect(self.beam_location)
elif self.__status.state in [BeamlineStateEnum.Maintenance]:
elif self._status.state in [BeamlineStateEnum.Maintenance]:
action_2 = menu.addAction("Manual sample exchange")
action_2.triggered.connect(self.se)
@@ -520,12 +518,12 @@ class StatusBar(QStatusBar):
self._show_pgroup_after_baton_grant()
def show_change_dialog(self):
logger.debug(self.__decoded_token.pgroups)
curr = self.__status.session.current_pgroup
logger.debug(self._decoded_token.pgroups)
curr = self._status.session.current_pgroup
pgroups = [
str(p) for p in (self.__allowed_pgroups or []) if p is not None and str(p).strip()
str(p) for p in (self._allowed_pgroups or []) if p is not None and str(p).strip()
]
if self.__is_staff:
if self._is_staff:
def _on_loaded(lst: list):
try:
@@ -543,7 +541,7 @@ class StatusBar(QStatusBar):
pass
self.staff_pgroups_loaded.connect(_on_loaded)
self.__list_staff_pgroups()
self._list_staff_pgroups()
return
self._generate_pgroup_dialogue(curr=curr, pgroups=pgroups)
@@ -586,7 +584,7 @@ class StatusBar(QStatusBar):
def open_shutter_clicked(self):
self.open_shutter.emit()
def __list_staff_pgroups(self):
def _list_staff_pgroups(self):
self.get_all_pgroups.emit()
return
+5 -5
View File
@@ -1,4 +1,4 @@
from PySide6.QtCore import Signal, Qt
from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import QLabel
@@ -7,16 +7,16 @@ class ValueLabel(QLabel):
def __init__(self, text: str, unit: str = "", parent=None):
super().__init__(parent)
self.__descr = text
self.__unit = unit
self._descr = text
self._unit = unit
def set_value(self, s: str, color: str | None = None):
if color is not None:
self.setText(
f"""{self.__descr}: <span style="color: {color};"><b>{s}</b> {self.__unit} </span>"""
f"""{self._descr}: <span style="color: {color};"><b>{s}</b> {self._unit} </span>"""
)
else:
self.setText(f"{self.__descr}: <b>{s}</b> {self.__unit}")
self.setText(f"{self._descr}: <b>{s}</b> {self._unit}")
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
@@ -15,9 +15,9 @@ from aare.daq.daq import AareDAQ
def test_execute_loop_centering_rejects_invalid_sample():
daq = object.__new__(AareDAQ)
daq._AareDAQ__set_state = MagicMock()
daq._AareDAQ__devs = MagicMock()
daq._AareDAQ__aare = MagicMock()
daq._set_state = MagicMock()
daq._devs = MagicMock()
daq._aare = MagicMock()
daq._handle_operation_error = MagicMock()
daq._create_loop_centering_service = MagicMock()
@@ -29,9 +29,9 @@ def test_execute_loop_centering_rejects_invalid_sample():
def test_execute_loop_centering_handles_service_failure():
daq = object.__new__(AareDAQ)
daq._AareDAQ__set_state = MagicMock()
daq._AareDAQ__devs = types.SimpleNamespace(lamp_light=None)
daq._AareDAQ__aare = MagicMock()
daq._set_state = MagicMock()
daq._devs = types.SimpleNamespace(lamp_light=None)
daq._aare = MagicMock()
daq._handle_operation_error = MagicMock()
daq.save_screenshot_db = MagicMock()
@@ -60,9 +60,9 @@ def test_execute_loop_centering_handles_service_failure():
def test_execute_loop_centering_handles_service_success():
daq = object.__new__(AareDAQ)
daq._AareDAQ__set_state = MagicMock()
daq._AareDAQ__devs = MagicMock()
daq._AareDAQ__aare = MagicMock()
daq._set_state = MagicMock()
daq._devs = MagicMock()
daq._aare = MagicMock()
daq._handle_operation_error = MagicMock()
daq.save_screenshot_db = MagicMock()
+1 -1
View File
@@ -76,7 +76,7 @@ def geom_model():
def test_aare_wrapper_init(mock_grid, mock_proc, mock_sample, mock_tell, mock_api, mock_bl):
wrapper = AareWrapper(bl=mock_bl)
mock_api.assert_called_once()
assert wrapper._AareWrapper__bl == mock_bl
assert wrapper._bl == mock_bl
@patch("aareDB.ApiClient")
+6 -5
View File
@@ -49,7 +49,7 @@ def staff_token_data():
def test_create_access_token(token_data):
with patch("aare.daq.auth.SECRET_KEY", "test_secret"):
with patch("aare.daq.auth.jwt_key", return_value="test_secret"):
token = create_access_token(token_data)
assert isinstance(token, str)
payload = jwt.decode(token, "test_secret", algorithms=["HS256"])
@@ -62,7 +62,7 @@ def test_authenticate_user(mock_cfg):
patch("pwd.getpwnam") as mock_pwd,
patch("os.getgrouplist") as mock_groups,
patch("grp.getgrgid") as mock_grp,
patch("aare.daq.auth.SECRET_KEY", "test_secret"),
patch("aare.daq.auth.jwt_key", return_value="test_secret"),
):
mock_pwd.return_value.pw_name = "testuser"
mock_pwd.return_value.pw_gid = 1000
@@ -88,7 +88,7 @@ def test_authenticate_user(mock_cfg):
def test_parse_token():
with patch("aare.daq.auth.SECRET_KEY", "test_secret"):
with patch("aare.daq.auth.jwt_key", return_value="test_secret"):
token = jwt.encode(
{"sub": "user", "pgroups": [], "session": 1, "staff": False}, "test_secret"
)
@@ -97,8 +97,9 @@ def test_parse_token():
def test_parse_token_invalid():
with pytest.raises(AuthenticationException):
parse_token("invalid.token.here")
with patch("aare.daq.auth.jwt_key", return_value="test_secret"):
with pytest.raises(AuthenticationException):
parse_token("invalid.token.here")
def test_check_jwt_ro(mock_cfg, token_data):
@@ -31,8 +31,8 @@ class _FakeRedis:
def _make_config_with_fake_redis() -> BeamlineConfig:
cfg = BeamlineConfig.__new__(BeamlineConfig)
cfg._BeamlineConfig__bl = "testbeamline"
cfg._BeamlineConfig__client = _FakeRedis()
cfg._bl = "testbeamline"
cfg._client = _FakeRedis()
return cfg
+3 -3
View File
@@ -12,7 +12,7 @@ def test_execute_face_detection_reports_failure(monkeypatch):
calls = {"set_state": [], "handle_error": []}
monkeypatch.setattr(daq, "_AareDAQ__set_state", lambda state: calls["set_state"].append(state))
monkeypatch.setattr(daq, "_set_state", lambda state: calls["set_state"].append(state))
monkeypatch.setattr(
daq,
"_create_face_detection_service",
@@ -49,7 +49,7 @@ def test_execute_face_detection_can_skip_error_reporting(monkeypatch):
handle_error_calls = []
monkeypatch.setattr(daq, "_AareDAQ__set_state", lambda state: None)
monkeypatch.setattr(daq, "_set_state", lambda state: None)
monkeypatch.setattr(
daq,
"_create_face_detection_service",
@@ -77,7 +77,7 @@ def test_public_face_detection_uses_execute_face_detection(monkeypatch):
daq = object.__new__(AareDAQ)
cfg = types.SimpleNamespace(try_set_busy=lambda timeout=360: None, state_busy=False)
setattr(daq, "_AareDAQ__cfg", cfg)
setattr(daq, "_cfg", cfg)
setattr(
daq,
+1 -1
View File
@@ -25,7 +25,7 @@ def mlbox(mock_wrapper):
def test_init_x10sa(mock_wrapper):
ml = MlBox(MXBeamline.X10SA, wrapper=mock_wrapper)
assert ml._MlBox__beamline == MXBeamline.X10SA
assert ml._beamline == MXBeamline.X10SA
def test_predict_uses_injected_wrapper():
+28 -32
View File
@@ -29,10 +29,10 @@ def _make_sample(sample_id: int, name: str) -> SampleShortInfo:
def _make_daq(previous_sample: SampleShortInfo | None) -> AareDAQ:
daq = object.__new__(AareDAQ)
daq._AareDAQ__cfg = SimpleNamespace(current_sample=previous_sample)
daq._AareDAQ__aare = MagicMock()
daq._AareDAQ__devs = MagicMock()
daq._AareDAQ__set_state = MagicMock()
daq._cfg = SimpleNamespace(current_sample=previous_sample)
daq._aare = MagicMock()
daq._devs = MagicMock()
daq._set_state = MagicMock()
daq._handle_operation_error = MagicMock()
daq.save_screenshot_db = MagicMock()
daq.sync_current_sample_from_tell = MagicMock(return_value=previous_sample)
@@ -43,8 +43,8 @@ def _make_daq(previous_sample: SampleShortInfo | None) -> AareDAQ:
@pytest.fixture
def bare_daq():
daq = object.__new__(AareDAQ)
daq._AareDAQ__cfg = SimpleNamespace()
daq._AareDAQ__devs = SimpleNamespace()
daq._cfg = SimpleNamespace()
daq._devs = SimpleNamespace()
return daq
@@ -52,8 +52,8 @@ def test_create_mounting_service_builds_expected_context(mock_logger, bare_daq):
service = bare_daq._create_mounting_service()
assert isinstance(service.ctx, MountingContext)
assert service.ctx.deps.cfg is bare_daq._AareDAQ__cfg
assert service.ctx.deps.devs is bare_daq._AareDAQ__devs
assert service.ctx.deps.cfg is bare_daq._cfg
assert service.ctx.deps.devs is bare_daq._devs
assert service.ctx.settings.mount_position == ABR_POS_MOUNT
@@ -95,7 +95,7 @@ def test_execute_mount_and_prepare_success_uses_mounting_result_fields():
assert result is True
send_calls = daq._AareDAQ__aare.send_sample_event.call_args_list
send_calls = daq._aare.send_sample_event.call_args_list
assert send_calls[0].args[0] == previous_sample.db_id
assert send_calls[0].args[1] == SampleEventType.UNMOUNTING
assert send_calls[1].args[0] == target_sample.db_id
@@ -108,10 +108,8 @@ def test_execute_mount_and_prepare_success_uses_mounting_result_fields():
daq.save_screenshot_db.assert_called_once_with(
target_sample.db_id, f"{target_sample.db_id}_mounted"
)
assert (
daq._AareDAQ__set_state.call_args_list[0].args[0] == BeamlineStateEnum.RobotSampleExchange
)
assert daq._AareDAQ__set_state.call_args_list[-1].args[0] == BeamlineStateEnum.SampleAlignment
assert daq._set_state.call_args_list[0].args[0] == BeamlineStateEnum.RobotSampleExchange
assert daq._set_state.call_args_list[-1].args[0] == BeamlineStateEnum.SampleAlignment
def test_execute_mount_and_prepare_marks_previous_sample_unmounted_when_mount_fails_after_auto_unmount():
@@ -132,9 +130,9 @@ def test_execute_mount_and_prepare_marks_previous_sample_unmounted_when_mount_fa
result = daq._execute_mount_and_prepare(target_sample)
assert result is False
assert daq._AareDAQ__cfg.current_sample is None
assert daq._cfg.current_sample is None
send_calls = daq._AareDAQ__aare.send_sample_event.call_args_list
send_calls = daq._aare.send_sample_event.call_args_list
assert send_calls[0].args[0] == previous_sample.db_id
assert send_calls[0].args[1] == SampleEventType.UNMOUNTING
@@ -150,7 +148,7 @@ def test_execute_mount_and_prepare_marks_previous_sample_unmounted_when_mount_fa
assert daq._handle_operation_error.call_args.kwargs["sample"] == target_sample
assert daq._handle_operation_error.call_args.kwargs["event_type"] == SampleEventType.MOUNTFAILED
assert daq._AareDAQ__set_state.call_args_list[-1].args[0] == BeamlineStateEnum.SampleAlignment
assert daq._set_state.call_args_list[-1].args[0] == BeamlineStateEnum.SampleAlignment
def test_execute_mount_and_prepare_does_not_mark_previous_sample_unmounted_when_not_confirmed():
@@ -171,9 +169,9 @@ def test_execute_mount_and_prepare_does_not_mark_previous_sample_unmounted_when_
result = daq._execute_mount_and_prepare(target_sample)
assert result is False
assert daq._AareDAQ__cfg.current_sample == previous_sample
assert daq._cfg.current_sample == previous_sample
send_calls = daq._AareDAQ__aare.send_sample_event.call_args_list
send_calls = daq._aare.send_sample_event.call_args_list
assert len(send_calls) == 2
assert send_calls[0].args[0] == previous_sample.db_id
assert send_calls[0].args[1] == SampleEventType.UNMOUNTING
@@ -201,7 +199,7 @@ def test_execute_mount_and_prepare_unmount_success_uses_unmount_operation():
assert result is True
send_calls = daq._AareDAQ__aare.send_sample_event.call_args_list
send_calls = daq._aare.send_sample_event.call_args_list
assert len(send_calls) == 2
assert send_calls[0].args[0] == previous_sample.db_id
assert send_calls[0].args[1] == SampleEventType.UNMOUNTING
@@ -209,10 +207,8 @@ def test_execute_mount_and_prepare_unmount_success_uses_unmount_operation():
assert send_calls[1].args[1] == SampleEventType.UNMOUNTED
daq.save_screenshot_db.assert_not_called()
assert (
daq._AareDAQ__set_state.call_args_list[0].args[0] == BeamlineStateEnum.RobotSampleExchange
)
assert daq._AareDAQ__set_state.call_args_list[-1].args[0] == BeamlineStateEnum.SampleAlignment
assert daq._set_state.call_args_list[0].args[0] == BeamlineStateEnum.RobotSampleExchange
assert daq._set_state.call_args_list[-1].args[0] == BeamlineStateEnum.SampleAlignment
def test_raise_if_critical_jfjoch_detector_error_preserves_jfjoch_exception_family():
@@ -279,9 +275,9 @@ def test_raise_if_critical_jfjoch_detector_error_ignores_non_critical_jfjoch_err
def test_create_loop_centering_service_uses_shared_screenshot_service():
daq = object.__new__(AareDAQ)
daq._AareDAQ__cfg = SimpleNamespace()
daq._AareDAQ__devs = SimpleNamespace()
daq._AareDAQ__mlbox = SimpleNamespace(predict_all_best=MagicMock())
daq._cfg = SimpleNamespace()
daq._devs = SimpleNamespace()
daq._mlbox = SimpleNamespace(predict_all_best=MagicMock())
daq._screenshot_service = MagicMock(spec=ScreenshotService)
daq._append_smargon_trace = MagicMock()
@@ -296,12 +292,12 @@ def test_create_loop_centering_service_uses_shared_screenshot_service():
def test_create_raster_service_uses_shared_screenshot_service():
daq = object.__new__(AareDAQ)
daq._AareDAQ__cfg = SimpleNamespace()
daq._AareDAQ__devs = SimpleNamespace()
daq._AareDAQ__mlbox = SimpleNamespace()
daq._AareDAQ__jfjoch = SimpleNamespace()
daq._AareDAQ__aare = SimpleNamespace()
daq._AareDAQ__set_state = MagicMock()
daq._cfg = SimpleNamespace()
daq._devs = SimpleNamespace()
daq._mlbox = SimpleNamespace()
daq._jfjoch = SimpleNamespace()
daq._aare = SimpleNamespace()
daq._set_state = MagicMock()
daq._screenshot_service = MagicMock(spec=ScreenshotService)
type(daq).sample = property(lambda self: None)
+8 -8
View File
@@ -10,11 +10,11 @@ from aare.daq.spreadsheetupdater import get_ws_headers, on_message, set_spreadsh
@pytest.fixture
def mock_config():
with patch("aare.daq.spreadsheetupdater.config") as mock:
mock._BeamlineConfig__bl = "X10SA"
mock._BeamlineConfig__client = MagicMock()
mock._bl = "X10SA"
mock._client = MagicMock()
# Mocking private attributes access which the code uses
mock._BeamlineConfig__client = mock._BeamlineConfig__client
mock._BeamlineConfig__bl = mock._BeamlineConfig__bl
mock._client = mock._client
mock._bl = mock._bl
yield mock
@@ -34,7 +34,7 @@ def test_set_spreadsheet_in_redis(mock_config):
data = {"test": "data"}
with patch("aare.daq.spreadsheetupdater.config") as mock_cfg_internal:
mock_client = MagicMock()
mock_cfg_internal._BeamlineConfig__client = mock_client
mock_cfg_internal._client = mock_client
mock_cfg_internal.client = mock_client
set_spreadsheet_in_redis(data)
@@ -88,7 +88,7 @@ def test_on_message_success(mock_config):
with patch("aare.daq.spreadsheetupdater.PuckWithTellPosition", side_effect=mock_pucks):
on_message(None, message)
calls = mock_config._BeamlineConfig__client.set.call_args_list
calls = mock_config._client.set.call_args_list
written_keys = [call.args[0] for call in calls]
assert "X10SA:sample_spreadsheet" in written_keys
@@ -119,9 +119,9 @@ def test_on_message_empty_ref(mock_config):
on_message(None, message)
ref_key = "X10SA:reference-tools"
mock_config._BeamlineConfig__client.delete.assert_called_with(ref_key)
mock_config._client.delete.assert_called_with(ref_key)
def test_on_message_invalid_json(mock_config):
on_message(None, "invalid json")
mock_config._BeamlineConfig__client.set.assert_not_called()
mock_config._client.set.assert_not_called()
+6 -5
View File
@@ -1,5 +1,6 @@
import json
from unittest.mock import MagicMock, patch
from aare.daq import tellupdater
@@ -107,13 +108,13 @@ def test_record_tell_event_writes_history_to_redis():
tellupdater.tell_event_history.clear()
mock_config = MagicMock()
mock_config._BeamlineConfig__bl = "x10sa"
mock_config._bl = "x10sa"
with patch("aare.daq.tellupdater.config", mock_config):
tellupdater.record_tell_event("Motion Task", "dry")
mock_config._BeamlineConfig__client.set.assert_called_once()
redis_key, redis_value = mock_config._BeamlineConfig__client.set.call_args.args
mock_config._client.set.assert_called_once()
redis_key, redis_value = mock_config._client.set.call_args.args
assert redis_key == "x10sa:tell_events"
payload = json.loads(redis_value)
@@ -128,13 +129,13 @@ def test_record_tell_event_keeps_last_25_events():
tellupdater.tell_event_history.clear()
mock_config = MagicMock()
mock_config._BeamlineConfig__bl = "x10sa"
mock_config._bl = "x10sa"
with patch("aare.daq.tellupdater.config", mock_config):
for idx in range(30):
tellupdater.record_tell_event("Motion Sync", f"event-{idx}")
redis_key, redis_value = mock_config._BeamlineConfig__client.set.call_args.args
redis_key, redis_value = mock_config._client.set.call_args.args
assert redis_key == "x10sa:tell_events"
payload = json.loads(redis_value)
+5 -5
View File
@@ -26,8 +26,8 @@ def aerotech_controller(mock_aerotech_api):
):
controller = AerotechController(MXBeamline.X10SA)
controller._AerotechController__api = mock_aerotech_api
controller._AerotechController__simulated = False
controller._api = mock_aerotech_api
controller._simulated = False
return controller
@@ -36,13 +36,13 @@ def test_init_x10sa(mock_aerotech_api):
"aare.devices.aerotech.cfg_get", return_value="http://mx-x10sa-queue-01.psi.ch:5234"
):
controller = AerotechController(MXBeamline.X10SA)
assert controller._AerotechController__base == "http://mx-x10sa-queue-01.psi.ch:5234"
assert controller._AerotechController__simulated is False
assert controller._base == "http://mx-x10sa-queue-01.psi.ch:5234"
assert controller._simulated is False
def test_init_simulated():
controller = AerotechController(MXBeamline.SIMULATED)
assert controller._AerotechController__simulated is True
assert controller._simulated is True
def test_cancel(aerotech_controller, mock_aerotech_api):
@@ -18,7 +18,7 @@ def test_shutter_init(mock_pv):
@patch("aare.devices.experimental_hutch_shutter.PV")
def test_shutter_state(mock_pv):
# Mocking self.__state
# Mocking self._state
mock_state_pv = MagicMock()
mock_pv.side_effect = [MagicMock(), MagicMock(), mock_state_pv]
+3 -3
View File
@@ -89,17 +89,17 @@ def create_mock_daq_status():
def test_init_x10sa(mock_jfjoch_client):
wrapper = JFJochWrapper(MXBeamline.X10SA)
assert wrapper._JFJochWrapper__url == "http://sls-gpu-002:8080"
assert wrapper._url == "http://sls-gpu-002:8080"
def test_init_x06da(mock_jfjoch_client):
wrapper = JFJochWrapper(MXBeamline.X06DA)
assert wrapper._JFJochWrapper__url == "http://sls-gpu-001:8080"
assert wrapper._url == "http://sls-gpu-001:8080"
def test_init_simulated(mock_jfjoch_client):
wrapper = JFJochWrapper(MXBeamline.SIMULATED)
assert wrapper._JFJochWrapper__simulated is True
assert wrapper._simulated is True
def test_init_unknown():
+8 -8
View File
@@ -30,7 +30,7 @@ def camera_thread(mock_zmq, qtbot):
def test_init(camera_thread):
assert camera_thread.running is True
assert camera_thread._SampleCameraThread__camera_available is False
assert camera_thread._camera_available is False
def test_update_daq_status(camera_thread):
@@ -38,15 +38,15 @@ def test_update_daq_status(camera_thread):
s.geom.beam_location_pxl.x = 100
s.geom.beam_location_pxl.y = 200
camera_thread.update_daq_status(s)
assert camera_thread._SampleCameraThread__beam_x == 100
assert camera_thread._SampleCameraThread__beam_y == 200
assert camera_thread._beam_x == 100
assert camera_thread._beam_y == 200
def test_enable_focus_measurement(camera_thread):
camera_thread.enable_focus_measurement(True)
assert camera_thread._SampleCameraThread__measure_focus is True
assert camera_thread._measure_focus is True
camera_thread.enable_focus_measurement(False)
assert camera_thread._SampleCameraThread__measure_focus is False
assert camera_thread._measure_focus is False
def test_run_success(camera_thread, mock_zmq, qtbot):
@@ -63,7 +63,7 @@ def test_run_success(camera_thread, mock_zmq, qtbot):
with qtbot.waitSignal(camera_thread.camera_image, timeout=5000):
camera_thread.run()
assert camera_thread._SampleCameraThread__camera_available is True
assert camera_thread._camera_available is True
def test_run_bayer_success(camera_thread, mock_zmq, qtbot):
@@ -80,7 +80,7 @@ def test_run_bayer_success(camera_thread, mock_zmq, qtbot):
with qtbot.waitSignal(camera_thread.camera_image, timeout=5000):
camera_thread.run()
assert camera_thread._SampleCameraThread__camera_available is True
assert camera_thread._camera_available is True
def test_run_zmq_timeout(camera_thread, mock_zmq, qtbot):
@@ -89,7 +89,7 @@ def test_run_zmq_timeout(camera_thread, mock_zmq, qtbot):
raise zmq.Again()
mock_zmq.recv_multipart.side_effect = side_effect
camera_thread._SampleCameraThread__fps_emit_period_s = 0.0
camera_thread._fps_emit_period_s = 0.0
with qtbot.waitSignal(camera_thread.fps_measure, timeout=5000):
try:
@@ -0,0 +1,50 @@
import importlib
from unittest.mock import patch
import pytest
from aare.beamline_dispatch.beamline_dispatch import get_beamline_dispatch
from aare.beamline_dispatch.simulated import SimulatedDispatch
from aare.beamline_dispatch.x06da.beamline_dispatch import X06daDispatch
from aare.beamline_dispatch.x06sa import X06saDispatch
from aare.beamline_dispatch.x10sa import X10saDispatch
def test_dispatch_cannot_be_instantiated_without_env():
with patch.dict("os.environ", {}, clear=True):
with pytest.raises(ValueError) as e:
_ = get_beamline_dispatch()
assert e.match("Please set the BEAMLINE environment variable")
def test_simulated_dispatch_can_be_instantiated():
with patch.dict("os.environ", {"BEAMLINE": "SIMULATED"}):
dispatch = get_beamline_dispatch()
assert isinstance(dispatch, SimulatedDispatch)
@pytest.mark.skipif(importlib.util.find_spec("pxi_bec") is None, reason="run only for pxi flavour")
def test_pxi_dispatch_can_be_instantiated():
with patch.dict("os.environ", {"BEAMLINE": "X06SA"}):
with pytest.raises(TypeError) as e:
_ = get_beamline_dispatch()
assert e.match("Can't instantiate abstract class")
@pytest.mark.skipif(
importlib.util.find_spec("pxii_bec") is None, reason="run only for pxii flavour"
)
def test_pxii_dispatch_can_be_instantiated():
with patch.dict("os.environ", {"BEAMLINE": "X10SA"}):
dispatch = get_beamline_dispatch()
assert isinstance(dispatch, X10saDispatch)
@pytest.mark.skipif(
importlib.util.find_spec("pxiii_bec") is None, reason="run only for pxiii flavour"
)
def test_pxiii_dispatch_can_be_instantiated():
with patch.dict("os.environ", {"BEAMLINE": "X06DA"}):
with pytest.raises(TypeError) as e:
_ = get_beamline_dispatch()
assert e.match("Can't instantiate abstract class")