ci: add better linting #121

Merged
perl_d merged 6 commits from ci/add_better_linting into main 2026-08-03 14:35:39 +02:00
116 changed files with 1142 additions and 926 deletions
+77 -7
View File
@@ -16,7 +16,9 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: https://github.com/actions/checkout@v5
with:
fetch-depth: 0
- name: Setup
uses: ./.gitea/actions/install
@@ -30,16 +32,32 @@ jobs:
source .venv/bin/activate
ruff format --check
- name: Lint
run: |
source .venv/bin/activate
ruff check
- name: Checkout pyright diff plugin
uses: https://github.com/actions/checkout@v5
with:
repository: mx/diff_quality_basedpyright
path: diff_quality_basedpyright
- name: Typecheck Diff
run: |
source .venv/bin/activate
uv pip install -e diff_quality_basedpyright
diff-quality --violations=basedpyright
test:
runs-on: ubuntu-latest
needs: "lint"
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13"]
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: https://github.com/actions/checkout@v5
- name: Setup
uses: ./.gitea/actions/install
@@ -58,14 +76,13 @@ jobs:
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
uses: https://github.com/actions/checkout@v5
- name: Setup
uses: ./.gitea/actions/install
@@ -92,11 +109,10 @@ jobs:
test-with-coverage:
runs-on: ubuntu-latest
needs: ["lint", "test", "test-with-beamline-plugins"]
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: https://github.com/actions/checkout@v5
- name: Setup
uses: ./.gitea/actions/install
@@ -112,3 +128,57 @@ jobs:
run: |
source .venv/bin/activate
pytest --cov=aare --cov-config=./pyproject.toml --cov-branch --cov-report=xml --no-cov-on-fail ./tests/unit
- name: Build coverage summary
run: |
source .venv/bin/activate
{
echo "<!-- coverage-report -->"
echo "### Coverage report"
echo
echo "**Total line + branch coverage: $(coverage report --format=total)%**"
echo
echo "<details><summary>Per-file breakdown</summary>"
echo
coverage report --format=markdown
echo
echo "</details>"
} > coverage-summary.md
# Only the rendered summary is uploaded. coverage.xml (~1 MB) is rejected
# with an HTML 403 by the proxy in front of gitea.psi.ch.
- name: Upload coverage
uses: https://github.com/actions/upload-artifact@v3
with:
name: coverage
path: coverage-summary.md
if-no-files-found: error
coverage-analysis:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
needs: ["test-with-coverage"]
if: github.event_name == 'pull_request'
steps:
- name: Download coverage
uses: https://github.com/actions/download-artifact@v3
with:
name: coverage
- name: Find Comment
uses: https://github.com/peter-evans/find-comment@v3
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: "github-actions[bot]"
body-includes: "<!-- coverage-report -->"
- name: Create or update comment
uses: https://github.com/peter-evans/create-or-update-comment@v5
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body-path: coverage-summary.md
edit-mode: replace
+46 -20
View File
@@ -7,24 +7,24 @@ requires-python = ">=3.11"
dependencies = [
"uv",
"gunicorn",
"aarecommon>=0.2.0",
"pydantic==2.11.4",
"numpy==2.2.5",
"aarecommon>=0.2.2",
"pydantic>=2.11",
"numpy",
"jfjoch_client==1.0.0rc146",
"pyJWT==2.10.1",
"pyzmq==26.4.0",
"opencv-python-headless==4.11.0.86",
"pyJWT",
"pyzmq",
"opencv-python-headless",
"PySide6==6.9.0",
"requests==2.32.4",
"pyepics==3.5.8",
"redis==6.2.0",
"python-redis-lock==4.0.0",
"fastapi==0.115.13",
"requests",
"pyepics~=3.5",
"redis",
"python-redis-lock",
"fastapi",
"uvicorn",
"aaredb>=0.2",
"python_multipart==0.0.20",
"websocket-client==1.8.0",
"sseclient==0.0.27", #Was sseclient-py==1.8.0 but this was broken???
"python_multipart",
"websocket-client",
"sseclient",
"psi-pshell==2.1.0",
"bec_lib>=3.130.3",
"bec-ipython-client>=3.130.3",
@@ -36,18 +36,36 @@ dependencies = [
[project.optional-dependencies]
test = [
# "pytest==9.0.3",
"pytest-cov==7.1.0",
"pytest-mock==3.14.0",
"pytest-qt==4.4.0",
"pytest-asyncio==0.25.3",
"basedpyright",
"pytest",
"pytest-cov",
"pytest-mock",
"pytest-qt",
"pytest-asyncio",
"pytest-timeout",
"ruff==0.15.*"
"ruff>=0.15"
]
docs = [
"zensical"
]
[lint]
ignore = ["F401", "F541", "W503", "W504"]
[tool.uv]
# Transitive deps (aarecommon -> aarelc_infer) ask for the non-headless
# `opencv-python`. It unpacks into the same `cv2/` directory as the
# `opencv-python-headless` we depend on, so whichever installs last wins for the
# overlapping files and you get a mixed install -- the Python half of one wheel
# next to the native half of the other. That surfaces as
# `AttributeError: partially initialized module 'cv2' has no attribute
# 'gapi_wip_gst_GStreamerPipeline'` on `import cv2`, and it bites in the
# test-with-beamline-plugins job because installing the plugins re-resolves and
# flips the install order. A marker that never matches drops the requirement,
# leaving headless as the only cv2 provider.
override-dependencies = ["opencv-python; sys_platform == 'nonexistent'"]
[tool.uv.sources]
aaredb = { index = "psi"}
@@ -81,5 +99,13 @@ multi_line_output = 3
[tool.ruff]
line-length = 100
[tool.ruff.lint]
ignore = [
# Timestamps here are local beamline wall-clock and are stored/displayed as
# naive ISO strings; adding a tzinfo would change what aaredb and the GUI see.
"DTZ005",
"DTZ006",
]
[tool.ruff.format]
skip-magic-trailing-comma = true
+22 -25
View File
@@ -1,11 +1,14 @@
import json
import threading
import time
from typing import Any, Dict
from typing import Any
import cv2
import numpy as np
import zmq
from aarecommon.config.logger import setup_logger
logger = setup_logger("aareDAQ")
class ImageStatsReceiver:
@@ -33,7 +36,7 @@ class ImageStatsReceiver:
self.connection_attempts = 0
self.last_message_time = None
def calculate_projections(self, image: np.ndarray) -> Dict[str, Any]:
def calculate_projections(self, image: np.ndarray) -> dict[str, Any]:
"""Calculate X and Y projections of the image"""
# Convert to grayscale if color image
if len(image.shape) == 3:
@@ -99,7 +102,7 @@ class ImageStatsReceiver:
"y_coords": list(range(len(y_projection))),
}
def calculate_radial_integration(self, image: np.ndarray, num_bins: int = 50) -> Dict[str, Any]:
def calculate_radial_integration(self, image: np.ndarray, num_bins: int = 50) -> dict[str, Any]:
"""Calculate radial integration of the image"""
# Convert to grayscale if color image
if len(image.shape) == 3:
@@ -153,7 +156,7 @@ class ImageStatsReceiver:
"num_bins": num_bins,
}
def calculate_image_stats(self, image: np.ndarray) -> Dict[str, Any]:
def calculate_image_stats(self, image: np.ndarray) -> dict[str, Any]:
"""Calculate comprehensive statistics for an image"""
image_float = image.astype(np.float64)
@@ -229,8 +232,8 @@ class ImageStatsReceiver:
except zmq.Again:
return None
except Exception as e:
print(f"Error processing ZMQ message: {e}")
except Exception:
logger.exception("Error processing ZMQ message")
return None
def receiver_thread(self):
@@ -250,7 +253,7 @@ class ImageStatsReceiver:
else:
print(f"[{time.strftime('%H:%M:%S')}] Waiting for image data...")
def print_formatted_stats(self, stats: Dict[str, Any]):
def print_formatted_stats(self, stats: dict[str, Any]):
"""Print formatted statistics in a compact terminal format"""
timestamp = time.strftime("%H:%M:%S", time.localtime(stats["timestamp"]))
@@ -295,7 +298,7 @@ class ImageStatsReceiver:
)
# Optional: Print per-channel stats if available
if any(k.startswith("mean_ch") for k in stats.keys()):
if any(k.startswith("mean_ch") for k in stats):
channels = []
i = 0
while f"mean_ch{i}" in stats:
@@ -327,7 +330,7 @@ class ImageStatsReceiver:
}
return None
def save_radial_profile_to_file(self, filename: str = None):
def save_radial_profile_to_file(self, filename: str | None = None):
"""Save the current radial profile to a file"""
if filename is None:
filename = f"radial_profile_{int(time.time())}.txt"
@@ -343,13 +346,13 @@ class ImageStatsReceiver:
f.write(f"# Center: {radial['center']}\n")
f.write("# Radius(pixels)\tMean_Intensity\tStd_Intensity\tPixel_Count\n")
for i in range(len(radial["r_centers"])):
f.write(
f"{radial['r_centers'][i]:.2f}\t"
f"{radial['radial_profile'][i]:.2f}\t"
f"{radial['radial_std'][i]:.2f}\t"
f"{radial['pixel_counts'][i]}\n"
)
f.writelines(
f"{radial['r_centers'][i]:.2f}\t"
f"{radial['radial_profile'][i]:.2f}\t"
f"{radial['radial_std'][i]:.2f}\t"
f"{radial['pixel_counts'][i]}\n"
for i in range(len(radial["r_centers"]))
)
print(f"Radial profile saved to {filename}")
return filename
@@ -392,8 +395,8 @@ def start_image_stats_receiver(zmq_url: str = "tcp://localhost:5555"):
stats_receiver = ImageStatsReceiver(zmq_url)
stats_receiver.start()
print(f"Started image statistics monitoring on {zmq_url}")
except Exception as e:
print(f"Failed to start image stats receiver: {e}")
except Exception:
logger.exception("Failed to start image stats receiver")
stats_receiver = None
@@ -408,8 +411,6 @@ def stop_image_stats_receiver():
def get_latest_image_stats():
"""Get the latest image statistics"""
global stats_receiver
if stats_receiver and stats_receiver.latest_stats:
with stats_receiver.stats_lock:
return stats_receiver.latest_stats.copy()
@@ -418,17 +419,13 @@ def get_latest_image_stats():
def get_radial_profile():
"""Get just the radial profile data"""
global stats_receiver
if stats_receiver:
return stats_receiver.get_radial_profile_summary()
return None
def save_current_radial_profile(filename: str = None):
def save_current_radial_profile(filename: str | None = None):
"""Save the current radial profile to a file"""
global stats_receiver
if stats_receiver:
return stats_receiver.save_radial_profile_to_file(filename)
return None
+10 -9
View File
@@ -1,19 +1,20 @@
import math
import sys
from typing import ClassVar
from PySide6.QtCore import QPointF, QRectF, Qt
from PySide6.QtGui import QColor, QFont, QFontMetrics, QLinearGradient, QPainter, QPen
from PySide6.QtWidgets import (
QApplication,
QWidget,
QVBoxLayout,
QFrame,
QHBoxLayout,
QLabel,
QFrame,
QPushButton,
QScrollArea,
QStackedWidget,
QVBoxLayout,
QWidget,
)
from PySide6.QtCore import Qt, QPointF, QRectF
from PySide6.QtGui import QPainter, QColor, QPen, QLinearGradient, QFont, QFontMetrics
import sys
import math
# ---------------------------------------------------------------------------
# Colour palette
@@ -114,7 +115,7 @@ class OrbitWidget(QWidget):
# LED step indicator
# ---------------------------------------------------------------------------
class LEDStages(QWidget):
STEPS = ["Mount", "Centre", "Raster", "Collect"]
STEPS: ClassVar[list[str]] = ["Mount", "Centre", "Raster", "Collect"]
def __init__(self, active_step: int = 1, parent=None):
"""
+4 -4
View File
@@ -13,12 +13,12 @@ class PShellClient:
def _get_response(self, response, is_json=True):
if response.status_code != 200:
raise Exception(response.text)
raise RuntimeError(response.text)
return json.loads(response.text) if is_json else response.text
def _get_binary_response(self, response):
if response.status_code != 200:
raise Exception(response.text)
raise RuntimeError(response.text)
return response.raw.read()
def get_version(self):
@@ -292,8 +292,8 @@ class PShellClient:
def print_logs(self):
for log_line in self.get_logs():
print("%s %s %-20s %-8s %s" % tuple(log_line))
print("{} {} {:<20} {:<8} {}".format(*log_line))
def print_devices(self):
for log_line in self.get_devices():
print("%-16s %-32s %-10s %-32s %s" % tuple(log_line))
print("{:<16} {:<32} {:<10} {:<32} {}".format(*log_line))
+6 -5
View File
@@ -1,9 +1,10 @@
# python
import sys
import argparse
from typing import Any, Dict, Tuple
import yaml
import sys
from typing import Any
import redis
import yaml
def decode_bulk(value: Any):
@@ -19,7 +20,7 @@ def decode_bulk(value: Any):
return value
def fetch_key(r: redis.Redis, key: bytes) -> Tuple[str, Any]:
def fetch_key(r: redis.Redis, key: bytes) -> tuple[str, Any]:
decode_bulk(key)
t = r.type(key)
if isinstance(t, bytes):
@@ -66,7 +67,7 @@ def dump_redis_to_yaml(
):
r = redis.Redis(host=host, port=port, db=db, password=password)
dump: Dict[str, Any] = {"meta": {"host": host, "port": port, "db": db}, "data": {}}
dump: dict[str, Any] = {"meta": {"host": host, "port": port, "db": db}, "data": {}}
cursor = 0
while True:
@@ -6,7 +6,7 @@ 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(
raise RuntimeError(
"JWT_AAREDAQ_KEY environment variable not set, cannot guarantee safe authentication."
)
return key
+24 -23
View File
@@ -2,7 +2,6 @@ import datetime
import io
import json
import os
from typing import List, Optional
import aareDB
import cv2
@@ -76,7 +75,7 @@ class AareWrapper:
self._key_file = configuration.key_file
@log_timing(logger, "AareDB call")
def set_pucks_beamline(self, input_list: List[PuckLoadedInfo]):
def set_pucks_beamline(self, input_list: list[PuckLoadedInfo]):
o = []
for i in input_list:
@@ -98,12 +97,12 @@ class AareWrapper:
try:
s.db_id = self._sample_api.insert_sample(manual_sample).id
except Exception as e:
logger.error(f"Error inserting sample: {e}")
except Exception:
logger.exception("Error inserting sample")
@log_timing(logger, "AareDB call")
def send_sample_event(
self, sample_id: StrictInt, event_type: SampleEventType, comment: Optional[str] = None
self, sample_id: StrictInt, event_type: SampleEventType, comment: str | None = None
) -> None:
if sample_id is None or sample_id < 0:
if sample_id is None:
@@ -118,12 +117,12 @@ class AareWrapper:
sample_id=sample_id,
sample_event_create=SampleEventCreate(event_type=event_type, comment=comment),
)
except Exception as e:
logger.error(f"Error sending sample event {event_type!s} to db: {e}")
except Exception:
logger.exception(f"Error sending sample event {event_type!s} to db")
@log_timing(logger, "AareDB call")
def upload_image(
self, sample_id: int, filename: str, bgr_image: np.ndarray, message: Optional[str] = None
self, sample_id: int, filename: str, bgr_image: np.ndarray, message: str | None = None
):
_, buffer = cv2.imencode(".jpg", bgr_image)
jpeg_bytes = io.BytesIO(buffer)
@@ -145,7 +144,7 @@ class AareWrapper:
logger.debug(f"Response status code: {response.status_code}")
@log_timing(logger, "AareDB call")
def upload_jpg(self, sample_id: int, filename: str, jpg_image, message: Optional[str] = None):
def upload_jpg(self, sample_id: int, filename: str, jpg_image, message: str | None = None):
logger.debug(f"jppg_image of type: {type(jpg_image)}")
url = f"{self._host}/protected_router/sample_runner/{sample_id}/upload-images"
headers = {
@@ -165,7 +164,7 @@ class AareWrapper:
@log_timing(logger, "AareDB call")
def create_rotation_run(
self, s: Optional[SampleShortInfo], r: RotationScanRequest, d: DAQStatusModel
self, s: SampleShortInfo | None, r: RotationScanRequest, d: DAQStatusModel
):
if s is None:
return
@@ -233,12 +232,12 @@ class AareWrapper:
sample_id=s.db_id, experiment_parameters_create=experiment_params_payload
)
# logger.debug("Experiment parameters created:", response)
except Exception as e:
logger.error(e)
except Exception:
logger.exception("Error creating experiment parameters for rotation run")
@log_timing(logger, "AareDB call")
def create_gridscan_run(
self, s: Optional[SampleShortInfo], r: RasterGridRequest, d: DAQStatusModel
self, s: SampleShortInfo | None, r: RasterGridRequest, d: DAQStatusModel
):
if s is None:
return
@@ -296,16 +295,16 @@ class AareWrapper:
)
# logger.info("Experiment parameters created:", response)
except Exception as e:
logger.debug(e)
logger.debug(e, exc_info=True)
@log_timing(logger, "AareDB call")
def ingest_gridscan(
self,
sample: Optional[SampleShortInfo],
sample: SampleShortInfo | None,
raster_result: ScanResult,
raster_request: RasterGridRequest,
geom: SampleGeometryModel,
com: Optional[CenterOfMassModel],
com: CenterOfMassModel | None,
beam_mark_pxl: tuple[float, float],
):
@@ -339,11 +338,11 @@ class AareWrapper:
def format_gridscan_payload(
self,
sample: Optional[SampleShortInfo],
sample: SampleShortInfo | None,
raster_result: ScanResult,
raster_request: RasterGridRequest,
geom: SampleGeometryModel,
com: Optional[CenterOfMassModel],
com: CenterOfMassModel | None,
beam_mark_pxl: tuple[float, float],
) -> RasterPayloadModel | None:
@@ -377,7 +376,9 @@ class AareWrapper:
for img in raster_result.images
]
except Exception as e:
logger.warning(f"raster score computation failed, sending null score: {e}")
logger.warning(
f"raster score computation failed, sending null score: {e}", exc_info=True
)
score = None
payload = RasterPayloadModel(
@@ -398,12 +399,12 @@ class AareWrapper:
except Exception as e:
logger.error(e)
raise e
raise
@log_timing(logger, "AareDB call")
def ingest_scan(
self,
sample: Optional[SampleShortInfo],
sample: SampleShortInfo | None,
result: ScanResult,
geom: SampleGeometryModel,
beam_mark_pxl: tuple[float, float],
@@ -437,7 +438,7 @@ class AareWrapper:
def format_scan_payload(
self,
sample: Optional[SampleShortInfo],
sample: SampleShortInfo | None,
result: ScanResult,
geom: SampleGeometryModel,
beam_mark_pxl: tuple[float, float],
@@ -455,4 +456,4 @@ class AareWrapper:
except Exception as e:
logger.error(e)
raise e
raise
+3 -2
View File
@@ -5,7 +5,6 @@ import pwd
import time
import uuid
from datetime import UTC, datetime, timedelta
from typing import List
import jwt
from aarecommon.errors.exception_handler import (
@@ -38,7 +37,7 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
class TokenData(BaseModel):
sub: str # Username
pgroups: List[str]
pgroups: list[str]
session: int
staff: bool = False
@@ -136,6 +135,7 @@ def check_jwt_rw(cfg: BeamlineConfig, data: TokenData) -> None:
cfg.try_extend_active_session(data.session, SESSION_EXPIRE_SECONDS)
except Exception:
# In case something is wrong but you are holder (maybe redis expiry?)
logger.debug("Could not extend the active session; claiming it instead", exc_info=True)
cfg.try_set_active_session(data.session, SESSION_EXPIRE_SECONDS)
@@ -159,6 +159,7 @@ def check_jwt_staff(cfg: BeamlineConfig, data: TokenData) -> None:
try:
cfg.try_extend_active_session(data.session, SESSION_EXPIRE_SECONDS)
except Exception:
logger.debug("Could not extend the active session; claiming it instead", exc_info=True)
cfg.try_set_active_session(data.session, SESSION_EXPIRE_SECONDS)
+1 -1
View File
@@ -1,5 +1,5 @@
import numpy as np
import cv2
import numpy as np
from scipy.optimize import curve_fit
+22 -24
View File
@@ -4,7 +4,6 @@ import json
import time
from dataclasses import asdict, is_dataclass
from datetime import datetime
from typing import List, Tuple
import numpy as np
import redis
@@ -148,6 +147,7 @@ class BeamlineConfig:
try:
ttl = int(self._client.ttl(self._gui_session_key(session)))
except Exception:
logger.debug("Could not read the GUI session TTL", exc_info=True)
return None
if ttl > 0:
@@ -161,7 +161,7 @@ class BeamlineConfig:
try:
return OpenGuiSessionInfo(**json.loads(raw))
except Exception:
logger.warning(f"Failed to parse GUI session info for session {session}")
logger.warning(f"Failed to parse GUI session info for session {session}", exc_info=True)
return None
def touch_gui_session(
@@ -263,7 +263,7 @@ class BeamlineConfig:
holder_session = holder.session if holder is not None else None
sessions: list[OpenGuiSessionInfo] = []
for session_id in sorted((int(s) for s in session_ids)):
for session_id in sorted(int(s) for s in session_ids):
payload = self._read_gui_session(session_id)
if payload is not None:
payload.holds_baton = payload.session == holder_session
@@ -348,7 +348,7 @@ class BeamlineConfig:
if active is None:
self._client.set(f"{self._bl}:active_session", session)
elif active != session:
raise Exception(
raise RuntimeError(
"There is already active session with different id. Try again later."
)
self._client.expire(f"{self._bl}:active_session", expiry_sec)
@@ -358,7 +358,7 @@ class BeamlineConfig:
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.")
raise RuntimeError("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)
@@ -372,7 +372,7 @@ class BeamlineConfig:
if current_ttl is None or current_ttl < 0 or current_ttl < expiry_sec:
self._client.expire(key, expiry_sec)
else:
raise Exception(
raise RuntimeError(
"There is already active session with different id. Try again later."
)
@@ -402,6 +402,7 @@ class BeamlineConfig:
try:
return BatonHolderInfo(**json.loads(tmp))
except Exception:
logger.debug("Could not parse the baton holder info", exc_info=True)
return None
@baton_holder.setter
@@ -420,6 +421,7 @@ class BeamlineConfig:
try:
return BatonRequest(**json.loads(tmp))
except Exception:
logger.debug("Could not parse the pending baton request", exc_info=True)
return None
def set_pending_baton_request(
@@ -445,6 +447,7 @@ class BeamlineConfig:
try:
return BatonTransferQueue(**json.loads(tmp))
except Exception:
logger.debug("Could not parse the queued baton transfer", exc_info=True)
return None
@queued_baton_transfer.setter
@@ -457,12 +460,9 @@ class BeamlineConfig:
def can_transfer_baton_now(self) -> bool:
"""Check if baton can be transferred (beamline not mid-operation)."""
# Can't transfer while beamline is busy
if self.state_busy:
return False
# Add automation queue check here when you implement it
# if self.automation_queue_running:
# return False
return True
# return not (self.state_busy or self.automation_queue_running)
return not self.state_busy
def execute_baton_transfer(
self,
@@ -522,9 +522,7 @@ class BeamlineConfig:
@property
def commissioning_mode(self) -> bool:
tmp = self._client.get(f"{self._bl}:commissioning_mode")
if tmp is None:
return False
return True
return tmp is not None
@commissioning_mode.setter
def commissioning_mode(self, commisioning_mode: bool) -> None:
@@ -548,7 +546,7 @@ class BeamlineConfig:
curr_state = self.state
if curr_state != target:
self.state_busy = False
raise Exception("Beamline is not in a proper state")
raise RuntimeError("Beamline is not in a proper state")
def start_moving(
self, target: BeamlineStateEnum, timeout: int | None = None
@@ -635,7 +633,7 @@ class BeamlineConfig:
return float(np.log(lens_factor / (b * target_pixel_in_mm)) / a)
@property
def beam_center(self) -> Tuple[float, float]:
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")
if tmp_x:
@@ -649,7 +647,7 @@ class BeamlineConfig:
return val_x, val_y
@beam_center.setter
def beam_center(self, data: Tuple[float, float]):
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])
@@ -721,7 +719,7 @@ class BeamlineConfig:
data_dict = json.loads(tmp)
return SampleShortInfoList(**data_dict)
def spreadsheet_pgroup(self, pgroups: List[str]) -> SampleShortInfoList:
def spreadsheet_pgroup(self, pgroups: list[str]) -> SampleShortInfoList:
sample = self.spreadsheet
sample.s = list(filter(lambda x: x.user in pgroups, sample.s))
return sample
@@ -820,7 +818,7 @@ class BeamlineConfig:
def zoom_settings(self) -> ZoomModel:
mode = self.zoom_mode
if not mode or not isinstance(mode, ZoomModeEnum):
raise Exception("incorrect zoom settings mode used")
raise ValueError("incorrect zoom settings mode used")
tmp = self._client.get(f"{self._bl}:{self.zoom_setting_string(mode)}")
if tmp is None:
return zoom_manager(mode, self._mxb)
@@ -831,7 +829,7 @@ class BeamlineConfig:
def zoom_settings(self, data: ZoomModel):
mode = self.zoom_mode
if not mode or not isinstance(mode, ZoomModeEnum):
raise Exception("incorrect zoom settings mode used")
raise ValueError("incorrect zoom settings mode used")
self._client.set(f"{self._bl}:{self.zoom_setting_string(mode)}", data.model_dump_json())
def save_zoom_camera_setting(
@@ -1013,8 +1011,8 @@ class BeamlineConfig:
try:
data_dict = json.loads(tmp)
return SimpleScanParameters(**data_dict)
except Exception as e:
logger.error(f"failed to parse auto_params from redis: {e}; raw={tmp}")
except Exception:
logger.exception(f"failed to parse auto_params from redis; raw={tmp}")
return None
@auto_params.setter
@@ -1184,7 +1182,7 @@ class BeamlineConfig:
payload = json.loads(str(raw))
return payload if isinstance(payload, dict) else {}
except Exception as e:
logger.warning(f"Failed to read detector metadata from Redis: {e}")
logger.warning(f"Failed to read detector metadata from Redis: {e}", exc_info=True)
return {}
def set_detector_metadata(self, payload: dict) -> dict:
@@ -1255,7 +1253,7 @@ class BeamlineConfig:
return LocalContactConfigModel.model_validate_json(str(raw_value))
except Exception as e:
logger.warning(f"Failed to read Local Contact config from Redis: {e}")
logger.warning(f"Failed to read Local Contact config from Redis: {e}", exc_info=True)
return default
def set_local_contact_config(
+62 -70
View File
@@ -2,10 +2,10 @@ import copy
import json
import secrets
import time
from datetime import datetime, timezone
from collections.abc import Callable
from datetime import UTC, datetime
from math import ceil
from pathlib import Path
from typing import Callable, List, Optional, Tuple
from aarecommon.config.beamline import cfg_get
from aarecommon.config.logger import setup_logger
@@ -583,7 +583,7 @@ class AareDAQ:
parsed = json.loads(str(raw_value))
return parsed if isinstance(parsed, list) else []
except Exception as e:
logger.debug(f"Failed to read tell_events from Redis: {e}")
logger.debug(f"Failed to read tell_events from Redis: {e}", exc_info=True)
return []
@staticmethod
@@ -609,11 +609,14 @@ class AareDAQ:
return True
if (
tell_state.last_event_class == "Motion Sync"
and tell_state.last_event_value == "Sample put on Puck"
(
tell_state.last_event_class == "Motion Sync"
and tell_state.last_event_value == "Sample put on Puck"
)
and state_ts is not None
and state_ts >= started_at
):
if state_ts is not None and state_ts >= started_at:
return True
return True
for event in reversed(self._get_tell_events_from_redis()):
if event.get("class") == "Motion Sync" and event.get("event") == "Sample put on Puck":
@@ -637,7 +640,7 @@ class AareDAQ:
try:
self._face_detection_progress_cb(payload)
except Exception as e:
logger.warning(f"Failed to emit face detection progress: {e}")
logger.warning(f"Failed to emit face detection progress: {e}", exc_info=True)
def _emit_automation_progress(self, progress: AutomationProgress) -> None:
if self._automation_progress_cb is None:
@@ -645,7 +648,7 @@ class AareDAQ:
try:
self._automation_progress_cb(progress)
except Exception as e:
logger.warning(f"Failed to emit automation progress: {e}")
logger.warning(f"Failed to emit automation progress: {e}", exc_info=True)
def _record_best_effort_step_failure(
self,
@@ -943,10 +946,10 @@ class AareDAQ:
def _handle_operation_error(
self,
operation: DAQOperation,
sample: Optional[SampleShortInfo],
sample: SampleShortInfo | None,
error: Exception,
event_type: SampleEventType = SampleEventType.FAILED,
additional_comment: Optional[str] = None,
additional_comment: str | None = None,
) -> None:
"""
Centralized databse maessage error handling for all operations.
@@ -1006,7 +1009,7 @@ class AareDAQ:
True if successful, False otherwise
"""
previous_sample = None
mount_started_at = datetime.now(timezone.utc)
mount_started_at = datetime.now(UTC)
self._last_mount_error_message = ""
try:
@@ -1018,7 +1021,8 @@ class AareDAQ:
)
except Exception as sync_error:
logger.warning(
f"Failed to reconcile previous sample from TELL before mount: {sync_error}"
f"Failed to reconcile previous sample from TELL before mount: {sync_error}",
exc_info=True,
)
if previous_sample is not None and previous_sample.db_id is not None:
@@ -1079,7 +1083,7 @@ class AareDAQ:
except Exception as e:
self._last_mount_error_message = str(e) or "Mount failed"
logger.error(f"Mount failed: {e}")
logger.exception("Mount failed")
previous_sample_unmounted = (
previous_sample is not None
@@ -1151,7 +1155,7 @@ class AareDAQ:
return True
except Exception as e:
logger.error(f"Loop centering failed: {e}")
logger.exception("Loop centering failed")
if result is not None and result.error is not None:
additional_comment = result.comment if result.comment is not None else ""
self._handle_operation_error(
@@ -1169,7 +1173,7 @@ class AareDAQ:
step_size: int = 15,
face_min_ratio: float = 0.3,
report_error: bool = True,
sample: Optional[SampleShortInfo] = None,
sample: SampleShortInfo | None = None,
) -> FaceDetectionResult:
"""
Execute face detection sequence through the face detection service.
@@ -1186,7 +1190,7 @@ class AareDAQ:
sample = self.sample
logger.debug(f"No sample provided, using current sample from DAQ {sample}")
except Exception:
logger.error("Failed to get current sample")
logger.exception("Failed to get current sample")
sample = None
aare = getattr(self, "_aare", None)
@@ -1213,12 +1217,13 @@ class AareDAQ:
return result
except Exception as e:
logger.error(f"Face detection failed: {e}")
logger.exception("Face detection failed")
additional_comment = f"{e}" if e is not None else ""
try:
sample = self.sample
except Exception:
logger.debug("Could not read the current sample for face detection", exc_info=True)
sample = None
if report_error:
@@ -1448,7 +1453,7 @@ class AareDAQ:
f.flush()
except Exception as e:
logger.warning(f"Failed to append smargon trace: {e}")
logger.warning(f"Failed to append smargon trace: {e}", exc_info=True)
def _sample_matches_mounted_address(
self, sample: SampleShortInfo | None, mounted_address
@@ -1555,7 +1560,7 @@ class AareDAQ:
"Cannot explicitly move to busy state",
extra={"target": target, "state": self._cfg.state},
)
raise Exception("Cannot explicitly move to busy state")
raise RuntimeError("Cannot explicitly move to busy state")
start = time.perf_counter()
self._cfg.try_set_busy(timeout=300)
@@ -1583,7 +1588,7 @@ class AareDAQ:
# name = clean_filename(name)
return name
def spreadsheet_params(self) -> tuple[Optional[SimpleScanParameters], str | None]:
def spreadsheet_params(self) -> tuple[SimpleScanParameters | None, str | None]:
file_prefix = None
if self.status.sample is None:
@@ -1622,8 +1627,7 @@ class AareDAQ:
new_res = 1 / ((1 / res) + 0.1)
corrected_dtz = self.diffraction_geometry.calc_dtz_mm(new_res)
logger.debug(f"corrected dtz: {corrected_dtz}")
if corrected_dtz < 108:
corrected_dtz = 108
corrected_dtz = max(corrected_dtz, 108)
params.dtz = round(corrected_dtz)
osc = getattr(aaredb_params, "oscillation", None)
@@ -1655,10 +1659,9 @@ class AareDAQ:
try:
self._devs.aerotech_omega = val
self._cfg.state_busy = False
except Exception as e:
logger.error(f"Omega error: {e}")
except Exception:
logger.exception("Omega move timed out")
self._cfg.state_busy = False
logger.error("Omega move timed out")
else:
self._cfg.state_busy = False
logger.error("Omega has to be between -2000 and 2000 degrees")
@@ -1801,7 +1804,7 @@ class AareDAQ:
curr_sample = self._cfg.current_sample
if curr_sample is not None and curr_sample.location is not None:
raise Exception("Sample from TELL is loaded")
raise RuntimeError("Sample from TELL is loaded")
self._aare.create_manual_sample(target)
self._cfg.current_sample = target
@@ -1834,8 +1837,8 @@ class AareDAQ:
def tell_toggle_blower(self):
try:
self._devs.tell.toggle_blower()
except Exception as e:
logger.error(f"Failed to turn off blower: {e}")
except Exception:
logger.exception("Failed to turn off blower")
def initialise_smargon(self):
self._cfg.try_set_busy(timeout=360)
@@ -1900,7 +1903,7 @@ class AareDAQ:
logger.debug(f"Failed to change mounted sample: {e}")
raise
def list_loaded_pucks(self) -> List[PuckLoadedInfo]:
def list_loaded_pucks(self) -> list[PuckLoadedInfo]:
return []
def _auto_focus(self, settings: AutofocusSettings, settle_time_s: float = 1.0) -> float:
@@ -2014,14 +2017,13 @@ class AareDAQ:
)
self._devs.smargon_wait(timeout=180)
return
def _build_fake_rotation_result(self, request: RotationScanRequest) -> CompletedRotationScan:
start_angle = 0.0
try:
start_angle = float(self.omega)
except Exception:
pass
logger.debug("Could not read omega for the fake rotation result", exc_info=True)
return build_fake_rotation_result(request, start_angle=start_angle)
@@ -2157,10 +2159,8 @@ class AareDAQ:
try:
if self._cfg.state_busy and self._cfg.state != BeamlineStateEnum.Maintenance:
self._set_state(BeamlineStateEnum.SampleAlignment)
except Exception as cleanup_error:
logger.exception(
f"Failed to restore SampleAlignment after rotation: {cleanup_error}"
)
except Exception:
logger.exception("Failed to restore SampleAlignment after rotation")
finally:
self._cfg.state_busy = False
@@ -2222,7 +2222,6 @@ class AareDAQ:
except Exception:
self._cfg.state_busy = False
raise
pass
def mark_beam(self, x_pxl: float, y_pxl: float):
self._cfg.set_busy(BeamlineStateEnum.BeamLocation)
@@ -2255,11 +2254,11 @@ class AareDAQ:
return sample_geom
@property
def beam_center(self) -> Tuple[float, float]:
def beam_center(self) -> tuple[float, float]:
return self._cfg.beam_center
@beam_center.setter
def beam_center(self, val: Tuple[float, float]):
def beam_center(self, val: tuple[float, float]):
self._cfg.beam_center = val
@property
@@ -2329,7 +2328,7 @@ class AareDAQ:
self._cfg.state_busy = False
@log_timing(logger, "Auto loop center")
def auto_loop_center(self, sample: Optional[SampleShortInfo] = None) -> float:
def auto_loop_center(self, sample: SampleShortInfo | None = None) -> float:
"""
Automatically center the loop using ML-based detection.
This performs a multi-step sequence including rotation and centering.
@@ -2415,9 +2414,7 @@ class AareDAQ:
default_message=self._default_screenshot_message(sample.db_id),
)
def send_message_db(
self, db_id: int, event_type: SampleEventType, comment: Optional[str] = None
):
def send_message_db(self, db_id: int, event_type: SampleEventType, comment: str | None = None):
self._aare.send_sample_event(db_id, event_type, comment)
@property
@@ -2510,8 +2507,8 @@ class AareDAQ:
try:
params.dtz = self.diffraction_geometry.calc_dtz_mm(res)
logger.debug(f"dtz: {params.dtz}")
except Exception as e:
logger.error(f"Failed to calculate dtz for resolution {res}: {e}")
except Exception:
logger.exception(f"Failed to calculate dtz for resolution {res}")
# Adjust exposure time based on resolution
if res <= 1.5:
@@ -2524,7 +2521,7 @@ class AareDAQ:
return params
def get_collection_params(self, prefer_smart: bool = False) -> tuple[SimpleScanParameters, str]:
spreadsheet_params, file_prefix = self.spreadsheet_params()
spreadsheet_params, _file_prefix = self.spreadsheet_params()
logger.debug(f"spreadsheet_params: {spreadsheet_params}")
smart_params = self._cfg.auto_params
default_params = SimpleScanParameters(exp_time_s=0.04, dtz=110, incr_omega_deg=0.2)
@@ -2544,7 +2541,7 @@ class AareDAQ:
def _end_operation(
self,
start: float,
operation: Optional[DAQOperation] = DAQOperation.AUTOMATION,
operation: DAQOperation | None = DAQOperation.AUTOMATION,
error: bool = False,
) -> float:
"""
@@ -2603,9 +2600,7 @@ class AareDAQ:
self._emit_automation_progress(progress)
formatted_date = datetime.now().strftime("%Y%m%d")
sample_prefix = "{}/{}/{:02d}/{}".format(
formatted_date, sample.puck_name, sample.pin, sample.sample_name
)
sample_prefix = f"{formatted_date}/{sample.puck_name}/{sample.pin:02d}/{sample.sample_name}"
try:
self._validate_automation_state(context="automation start")
@@ -2697,9 +2692,7 @@ class AareDAQ:
if raster_params.filename is not None:
logger.info(f"Using filename {raster_params.filename}")
sample_prefix = "{filename}/{prefix}".format(
filename=raster_params.filename, prefix=sample.sample_name
)
sample_prefix = f"{raster_params.filename}/{sample.sample_name}"
raster_grid = RasterGridRequest(
exp_time_s=raster_params.exp_time_s,
@@ -2777,9 +2770,7 @@ class AareDAQ:
if params.filename is not None:
logger.info(f"Using filename {params.filename}")
sample_prefix = "{filename}/{prefix}".format(
filename=params.filename, prefix=sample.sample_name
)
sample_prefix = f"{params.filename}/{sample.sample_name}"
rotation_request = RotationScanRequest(
start_omega_deg=start_omega,
@@ -2883,7 +2874,7 @@ class AareDAQ:
logger.error(f"Failed to mount sample: {e}")
if e.critical:
self._end_operation(start, operation=DAQOperation.AUTOMATION, error=True)
raise e
raise
else:
pass
@@ -2909,7 +2900,7 @@ class AareDAQ:
event_type=SampleEventType.FAILED,
)
self._end_operation(start, operation=DAQOperation.AUTOMATION, error=True)
raise Exception(f"Critical Error in automation: {e}") from e
raise RuntimeError(f"Critical Error in automation: {e}") from e
self._record_completed_sample_time(progress, time.time() - sample_started_at)
self._mark_progress_finished(progress, True, "Automation complete")
@@ -2937,7 +2928,7 @@ class AareDAQ:
)
if not self._cfg.state_busy:
raise Exception("Beamline should be busy")
raise RuntimeError("Beamline should be busy")
if target == BeamlineStateEnum.Maintenance:
self._cfg.state = BeamlineStateEnum.Maintenance
@@ -3131,8 +3122,8 @@ class AareDAQ:
raise
except Exception as e:
logger.exception(
f"Exception during state transition : {curr_state} -> {target}."
f"Changing state to maintenance due to error: {e}",
f"Exception during state transition : {curr_state} -> {target}. "
"Changing state to maintenance due to error.",
extra={"from_state": curr_state, "to_state": target},
)
self._cfg.state = BeamlineStateEnum.Maintenance
@@ -3175,7 +3166,8 @@ class AareDAQ:
)
except Exception as e:
logger.warning(
f"Falling back to default diffraction geometry because cached detector metadata is unavailable: {e}"
f"Falling back to default diffraction geometry because cached detector metadata is unavailable: {e}",
exc_info=True,
)
energy = self._devs.energy_kev
dtz = self._devs.dtz
@@ -3246,8 +3238,8 @@ class AareDAQ:
pss_alarm=pss_alarm,
)
except Exception as e:
logger.exception(f"Failed to retrieve beamline status: {e}")
except Exception:
logger.exception("Failed to retrieve beamline status")
raise
def _safe_sample(self) -> tuple[SampleShortInfo | None, bool, str | None]:
@@ -3259,7 +3251,7 @@ class AareDAQ:
except TellCommunicationError as e:
return self._cfg.current_sample, False, str(e)
except Exception as e:
logger.warning(f"error in status sample info call: {e}")
logger.warning(f"error in status sample info call: {e}", exc_info=True)
# Keep status flowing even if Tell code throws something unexpected
return self._cfg.current_sample, False, f"TELL unavailable: {e}"
@@ -3269,7 +3261,7 @@ class AareDAQ:
try:
_ = self._devs.aerotech.status()
except Exception as e:
logger.warning(f"Aerotech error in _aerotech_status: {e}")
logger.warning(f"Aerotech error in _aerotech_status: {e}", exc_info=True)
aerotech_ok = False
aerotech_err = f"Cannot connect to Aerotech: {e}"
return aerotech_ok, aerotech_err
@@ -3299,7 +3291,7 @@ class AareDAQ:
self._devs.exp_shutter.close()
aerotech_connected = False
except Exception as e:
logger.warning(f"Unexpected error in _safe_geom: {e}")
logger.warning(f"Unexpected error in _safe_geom: {e}", exc_info=True)
smargon_error = f"Safe geometry failed: {e}"
aerotech_error = f"Safe geometry failed: {e}"
aerotech_connected = False
@@ -3324,7 +3316,7 @@ class AareDAQ:
return self.beamline_status
except Exception as e:
# TODO add error message to send to GUI to say problem
logger.error(f"Failed to retrieve beamline status: {str(e)}")
logger.error(f"Failed to retrieve beamline status: {e!s}")
raise
def _safe_tell_state(self) -> TellStateModel | None:
@@ -3345,14 +3337,14 @@ class AareDAQ:
return TellStateModel.model_validate_json(str(raw_value))
except Exception as e:
logger.warning(f"Failed to read tell_state from Redis: {e}")
logger.warning(f"Failed to read tell_state from Redis: {e}", exc_info=True)
return None
def _safe_diffraction_geometry(self) -> DiffractionGeometry:
try:
return self.diffraction_geometry
except Exception as e:
logger.warning(f"Failed to retrieve diffraction geomtrey: {str(e)}")
logger.warning(f"Failed to retrieve diffraction geomtrey: {e!s}", exc_info=True)
# Must satisfy pydantic constraints in DiffractionGeometry
return DiffractionGeometry(
energy_keV=12.4,
+3 -1
View File
@@ -115,7 +115,9 @@ class BeamlineDevices:
try:
self.bec_worker.shutdown_client()
except Exception as e:
logger.warning(f"Failed to shutdown previous BEC worker cleanly: {e}")
logger.warning(
f"Failed to shutdown previous BEC worker cleanly: {e}", exc_info=True
)
finally:
beamline = MXBeamline.SIMULATED if simulated else self._beamline
logger.info(f"Restarting BEC worker with simulated={simulated}")
+38 -28
View File
@@ -1,6 +1,6 @@
import time
from collections.abc import Iterable
from dataclasses import dataclass, field
from typing import Iterable, Optional
import cv2
import numpy as np
@@ -92,7 +92,7 @@ class MlBox:
return None
return image
except Exception as e:
logger.warning(f"Failed to decode prediction bundle image: {e}")
logger.warning(f"Failed to decode prediction bundle image: {e}", exc_info=True)
return None
@staticmethod
@@ -106,7 +106,7 @@ class MlBox:
return LatestPredictionModel.model_validate(metadata)
return LatestPredictionModel.model_validate(metadata.model_dump())
except Exception as e:
logger.warning(f"Failed to validate prediction metadata: {e}")
logger.warning(f"Failed to validate prediction metadata: {e}", exc_info=True)
return None
@staticmethod
@@ -126,7 +126,8 @@ class MlBox:
except Exception as e:
last_error = e
logger.warning(
f"Prediction bundle fetch failed on attempt {attempt}/{self.RETRY_COUNT}: {e}"
f"Prediction bundle fetch failed on attempt {attempt}/{self.RETRY_COUNT}: {e}",
exc_info=True,
)
if attempt < self.RETRY_COUNT:
time.sleep(self.RETRY_SLEEP_S)
@@ -156,7 +157,9 @@ class MlBox:
if isinstance(raw, (list, tuple)) and len(raw) >= 2:
return float(raw[0]), float(raw[1])
except Exception as e:
logger.warning(f"Failed to parse target_point from prediction metadata: {e}")
logger.warning(
f"Failed to parse target_point from prediction metadata: {e}", exc_info=True
)
return None
@@ -171,7 +174,9 @@ class MlBox:
if raw_focus is not None:
focus = float(raw_focus)
except Exception as e:
logger.warning(f"Failed to parse focus_score from prediction metadata: {e}")
logger.warning(
f"Failed to parse focus_score from prediction metadata: {e}", exc_info=True
)
return MLBundleMeta(target_point=target_point, focus=focus)
@@ -308,15 +313,13 @@ class MlBox:
def box_filter_overlap(
self, model: MLBoxModel, pin: MLBoxModel, overlap_parameter: float = 0.5
) -> bool:
if self._check_overlap(model, pin) >= overlap_parameter:
return True
return False
return self._check_overlap(model, pin) >= overlap_parameter
def _filter_predictions(
self,
predictions: MLOutputModel,
overlap_with_pin: Optional[float] = None,
confidence_min: Optional[float] = None,
overlap_with_pin: float | None = None,
confidence_min: float | None = None,
):
pin = predictions.get_best_for_class(MLBoxType.PIN)
@@ -330,14 +333,17 @@ class MlBox:
continue
if model.cls == MLBoxType.PIN:
continue
if overlap_with_pin is not None and pin:
if self._check_overlap(model, pin) >= overlap_with_pin:
keys_to_remove.append(key)
if (
overlap_with_pin is not None
and pin
and self._check_overlap(model, pin) >= overlap_with_pin
):
keys_to_remove.append(key)
for k in keys_to_remove:
predictions.boxes.pop(k, None)
@staticmethod
def _best_by_class(results) -> Optional[MLOutputModel]:
def _best_by_class(results) -> MLOutputModel | None:
out = MLOutputModel()
for pred in results:
try:
@@ -350,7 +356,7 @@ class MlBox:
y2 = float(box.get("y2"))
except Exception as e:
logger.debug(f"Failed to parse prediction: {e}")
logger.debug(f"Failed to parse prediction: {e}", exc_info=True)
continue
base_key = MLOutputModel.get_class_str(cls=cls)
@@ -375,7 +381,7 @@ class MlBox:
@staticmethod
def _all_from_prediction_model(
prediction: LatestPredictionModel | None,
) -> Optional[MLOutputModel]:
) -> MLOutputModel | None:
if prediction is None or not getattr(prediction, "boxes", None):
return None
@@ -389,7 +395,7 @@ class MlBox:
x2 = float(det.x2)
y2 = float(det.y2)
except Exception as e:
logger.debug(f"Failed to parse bundle detection: {e}")
logger.debug(f"Failed to parse bundle detection: {e}", exc_info=True)
continue
out.add_box(cls=cls, box_tuple=(x1, y1, x2, y2), conf=conf)
@@ -406,9 +412,9 @@ class MlBox:
@staticmethod
def get_preferred_class_box_with_confidence_threshold(
boxes: MLOutputModel,
preferred_class: Optional[Iterable[int] | int | MLBoxType] = None,
preferred_class: Iterable[int] | int | MLBoxType | None = None,
loop_preference_margin: float = 0.1,
) -> Optional[MLBoxModel]:
) -> MLBoxModel | None:
"""
Get best box, preferring loops over pin even if pin has higher confidence,
unless pin's confidence exceeds loops by the margin.
@@ -466,8 +472,8 @@ class MlBox:
@staticmethod
def get_preferred_class_box(
boxes: MLOutputModel, preferred_class: Optional[Iterable[int] | int | MLBoxType] = None
) -> Optional[MLBoxModel]:
boxes: MLOutputModel, preferred_class: Iterable[int] | int | MLBoxType | None = None
) -> MLBoxModel | None:
if preferred_class is None:
order = (MLBoxType.CRYSTAL, MLBoxType.LOOP_FACE, MLBoxType.LOOP_ALL, MLBoxType.PIN)
else:
@@ -525,8 +531,9 @@ class MlBox:
def predict_best_no_filter(
self, return_image: bool = False, return_bundle_meta: bool = False
) -> (
Optional[MLOutputModel]
| tuple[Optional[MLOutputModel], np.ndarray | None]
MLOutputModel
| None
| tuple[MLOutputModel | None, np.ndarray | None]
| MLBoxPredictionsResult
):
ml_bundle = self._collect_best_bundle()
@@ -553,8 +560,9 @@ class MlBox:
return_image: bool = False,
return_bundle_meta: bool = False,
) -> (
Optional[MLOutputModel]
| tuple[Optional[MLOutputModel], np.ndarray | None]
MLOutputModel
| None
| tuple[MLOutputModel | None, np.ndarray | None]
| MLBoxPredictionsResult
):
ml_bundle = self._collect_best_bundle()
@@ -630,7 +638,7 @@ class MlBox:
overlap_with_pin: float | None = None,
confidence_min: float | None = None,
return_image: bool = False,
) -> Optional[MLBoxModel] | tuple[Optional[MLBoxModel], np.ndarray | None]:
) -> MLBoxModel | None | tuple[MLBoxModel | None, np.ndarray | None]:
"""
Request bounding boxes for the next N bundle fetches and return the best available box.
"""
@@ -659,7 +667,9 @@ class MlBox:
logger.debug(f"Frame {frame_idx + 1}/{n_frames}: no detection")
except Exception as e:
logger.warning(f"Frame {frame_idx + 1}/{n_frames}: prediction error: {e}")
logger.warning(
f"Frame {frame_idx + 1}/{n_frames}: prediction error: {e}", exc_info=True
)
continue
if best_box is None:
@@ -1,7 +1,7 @@
import time
from collections.abc import Callable
from dataclasses import dataclass
from math import ceil, floor
from typing import Callable
from aarecommon.config.beamline import cfg_get
from aarecommon.config.logger_events import (
@@ -73,8 +73,8 @@ def scale_auto_raster_grid(
physical_size_y_mm = n_y * grid_size.y
scale = (image_count / max_images) ** 0.5
scaled_n_x = max(1, int(floor(n_x / scale)))
scaled_n_y = max(1, int(floor(n_y / scale)))
scaled_n_x = max(1, floor(n_x / scale))
scaled_n_y = max(1, floor(n_y / scale))
while scaled_n_x * scaled_n_y > max_images:
if scaled_n_x >= scaled_n_y and scaled_n_x > 1:
@@ -201,9 +201,9 @@ def _box_to_raster_request(
frac_x = float(cfg_get("daq.auto_raster.grid_padding_fraction_x", 0.15))
frac_y_top = float(cfg_get("daq.auto_raster.grid_padding_fraction_y", 0.15))
frac_y_bottom = float(cfg_get("daq.auto_raster.grid_padding_fraction_y_bottom", frac_y_top))
pad_x = max(1, int(ceil(frac_x * n_x)))
pad_y_top = max(1, int(ceil(frac_y_top * n_y)))
pad_y_bottom = max(1, int(ceil(frac_y_bottom * n_y)))
pad_x = max(1, ceil(frac_x * n_x))
pad_y_top = max(1, ceil(frac_y_top * n_y))
pad_y_bottom = max(1, ceil(frac_y_bottom * n_y))
x1 = x1 - pad_x * grid_size.x / geom.pixel_in_mm
y1 = y1 - pad_y_top * grid_size.y / geom.pixel_in_mm
n_x = n_x + 2 * pad_x
@@ -210,7 +210,7 @@ class FaceDetectionService:
return FaceDetectionResult(success=True, payload=payload)
except Exception as e:
self.logger.error(f"error in face detection sequence {e}")
self.logger.exception("error in face detection sequence")
payload = self._emit_empty_result()
return FaceDetectionResult(
success=False, payload=payload, error=e, comment="Face detection sequence failed"
@@ -3,7 +3,6 @@ import math
import statistics
import time
import warnings
from typing import Dict, List, Optional, Tuple
import numpy as np
from aarecommon.config.logger import setup_logger
@@ -13,7 +12,7 @@ logger = setup_logger("aareDAQ")
def box_height_from_tuple(box: tuple[float, float, float, float]) -> float:
x1, y1, x2, y2 = box
_x1, y1, _x2, y2 = box
return abs(y2 - y1)
@@ -24,7 +23,7 @@ def box_area_from_tuple(box: tuple[float, float, float, float]) -> float:
def prepare_samples(
boxes_by_angle: dict[int, tuple[float, float, float, float]], area=False
) -> List[Tuple[float, float]]:
) -> list[tuple[float, float]]:
# angles in degrees -> (theta_rad, height)
samples = []
for deg, box in boxes_by_angle.items():
@@ -37,7 +36,7 @@ def cos_model(theta_deg: float | np.ndarray, A: float, B: float, phi_rad: float,
return A + B * np.cos(C * np.deg2rad(theta_deg) - phi_rad)
def mad_filter(samples: List[Tuple[float, float]], k: float = 3.5) -> List[Tuple[float, float]]:
def mad_filter(samples: list[tuple[float, float]], k: float = 3.5) -> list[tuple[float, float]]:
if not samples:
return samples
ys = [y for _, y in samples]
@@ -54,7 +53,6 @@ def samples_to_json(samples):
}
with open("cos_test.json", "w") as f:
json.dump(output_data, f, indent=2)
return
def fit_metrics(y_true: np.ndarray, y_pred: np.ndarray) -> tuple[float, float, float]:
@@ -67,7 +65,7 @@ def fit_metrics(y_true: np.ndarray, y_pred: np.ndarray) -> tuple[float, float, f
return rmse, mae, r2
def fit_cosine(samples: List[Tuple[float, float]]) -> dict:
def fit_cosine(samples: list[tuple[float, float]]) -> dict:
samples = mad_filter(samples, k=3.5)
if len(samples) < 3:
A = sum(y for _, y in samples) / max(1, len(samples))
@@ -84,6 +82,7 @@ def fit_cosine(samples: List[Tuple[float, float]]) -> dict:
beta, _, _, _ = np.linalg.lstsq(X, ys, rcond=None)
A0, C0, S0 = beta.tolist()
except Exception:
logger.debug("Cosine fit seeding failed; falling back to the mean", exc_info=True)
A0, C0, S0 = float(np.mean(ys)), 0.0, 0.0
B0 = float(math.hypot(C0, S0))
@@ -111,7 +110,7 @@ def fit_cosine(samples: List[Tuple[float, float]]) -> dict:
except Exception as e:
# Fallback to initial
logger.info(f"error in curve fit {e}")
logger.info(f"error in curve fit {e}", exc_info=True)
yhat0 = cos_model(degs, A0, max(0.0, B0), phi0, C0)
rmse0, mae0, r2_0 = fit_metrics(ys, yhat0)
return {
@@ -135,11 +134,9 @@ def get_samples_out(boxes):
return samples_out
def choose_best_fit(
fits_by_name: Dict[str, Dict],
) -> Tuple[Optional[float], Optional[Dict], Optional[str]]:
def choose_best_fit(fits_by_name: dict[str, dict]) -> tuple[float | None, dict | None, str | None]:
def key(entry: Dict):
def key(entry: dict):
params = entry.get("params") or {}
rmse = params.get("rmse")
mae = params.get("mae")
@@ -152,6 +149,7 @@ def choose_best_fit(
logger.info(f"rmse: {rmse}, mae: {mae}, r2: {r2}")
return float(rmse), float(mae), -float(r2)
except Exception:
logger.debug("Could not score a candidate fit", exc_info=True)
return None
best_name = None
@@ -1,17 +1,17 @@
from aare.daq.operations.loop_centering.analyzer import LoopCenteringAnalyzer
from aare.daq.operations.loop_centering.models import (
LoopCenteringContext,
AngleAnalysis,
AttemptSummary,
LoopCenteringContext,
LoopCenteringSettings,
)
from aare.daq.operations.loop_centering.service import LoopCenteringService
from aare.daq.operations.loop_centering.analyzer import LoopCenteringAnalyzer
__all__ = [
"LoopCenteringContext",
"AngleAnalysis",
"AttemptSummary",
"LoopCenteringSettings",
"LoopCenteringService",
"LoopCenteringAnalyzer",
"LoopCenteringContext",
"LoopCenteringService",
"LoopCenteringSettings",
]
@@ -1,5 +1,4 @@
import time
import traceback
from aarecommon.config.logger_events import log_duration
from aarecommon.errors.exception_handler import LoopCenteringFailed
@@ -203,7 +202,5 @@ class LoopCenteringService:
f"Needle: {found_classes_count.get(5, 0)}"
)
self.logger.error(f"ALC exception: {alc_comment}")
self.logger.error(traceback.format_exc())
self.logger.error(f"Error in loop centering: {e}")
self.logger.exception(f"ALC exception: {alc_comment}")
return LoopCenteringResult(success=False, comment=alc_comment, error=e)
+8 -6
View File
@@ -43,7 +43,9 @@ class MountingService:
self.logger.error(
"Goniometer didn't reach position based on magnet position sensor readout"
)
raise Exception("Goniometer is not in position based on magnet position sensor readout")
raise RuntimeError(
"Goniometer is not in position based on magnet position sensor readout"
)
def _handle_consecutive_mount_failure(self) -> None:
count = self.ctx.deps.cfg.increment_mount_failure_streak()
@@ -53,8 +55,8 @@ class MountingService:
self.logger.warning(f"Mount failed {count} times in a row; drying")
try:
self.dry(park=False)
except Exception as e:
self.logger.exception(f"Failed to dry after mount failure: {e}")
except Exception:
self.logger.exception("Failed to dry after mount failure")
if count >= STOP_AFTER_FAIL_COUNT:
self.logger.error(f"Mount failed {count} times in a row, stopping automation")
@@ -62,8 +64,8 @@ class MountingService:
try:
self._unmount_current_sample(timeout=60.0)
self.dry(park=True)
except Exception as e:
self.logger.exception(f"Failed to clean up after repeated mount failure: {e}")
except Exception:
self.logger.exception("Failed to clean up after repeated mount failure")
raise MountingFailed(
f"Mount failed {count} times in a row, stopping automation.", critical=True
@@ -235,7 +237,7 @@ class MountingService:
)
except Exception as e:
self.logger.error(f"Mount failed: {e}")
self.logger.exception("Mount failed")
return MountingResult(
success=False,
mounted_sample=None,
+1 -1
View File
@@ -1,4 +1,4 @@
from aare.daq.operations.raster.models import RasterContext, RasterBoundingBoxResult
from aare.daq.operations.raster.models import RasterBoundingBoxResult, RasterContext
from aare.daq.operations.raster.service import RasterService
__all__ = ["RasterBoundingBoxResult", "RasterContext", "RasterService"]
+7 -5
View File
@@ -121,7 +121,9 @@ class RasterService:
if bits:
comment = f"Raster diffraction ({', '.join(bits)})"
except Exception:
pass
self.logger.debug(
"Could not build the raster diffraction preview comment", exc_info=True
)
self.ctx.deps.aare.upload_jpg(sample_id, filename, diffraction_image, message=comment)
@@ -325,7 +327,7 @@ class RasterService:
padded_height_mm = (
box_height_pxl * geom.pixel_in_mm * (1.0 + 2.0 * y_padding_fraction_each_side)
)
n_y = max(1, int(ceil(padded_height_mm / grid_size_mm.y)))
n_y = max(1, ceil(padded_height_mm / grid_size_mm.y))
self.logger.info(
"Computed second auto-center raster y size from ML box height",
extra=merge_log_context(
@@ -647,9 +649,9 @@ class RasterService:
request=copy.deepcopy(request), result=scan_result, centre_of_mass=com
)
except Exception as e:
except Exception:
self.logger.exception(
f"Failed during raster: {e}",
"Failed during raster",
extra=merge_log_context(
sample_log_context(self.ctx.sample),
raster_request_log_context(request),
@@ -661,7 +663,7 @@ class RasterService:
def execute_auto_center(self, request: RasterGridRequest) -> CompletedRasterGrid | None:
sample = self.ctx.sample
if sample is None:
raise Exception("Sample must be mounted to auto center")
raise RuntimeError("Sample must be mounted to auto center")
old_prefix = request.file_prefix
geom = self.ctx.sample_geometry
+19 -12
View File
@@ -4,8 +4,9 @@ import importlib
import json
import os
import time
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from typing import AsyncGenerator, Optional
from typing import Any, ClassVar
import uvicorn
from aarecommon.config.beamline import mx_beamline
@@ -84,7 +85,10 @@ _automation_progress_state_lock = asyncio.Lock()
class AareUvicornWorker(UvicornWorker):
# CONFIG_KWARGS merged last into uvicorn Config (uvicorn/workers.py:69) →
# keeps our access-log filter + proxy_headers under gunicorn.
CONFIG_KWARGS = {"log_config": get_uvicorn_logging_config(), "proxy_headers": False}
CONFIG_KWARGS: ClassVar[dict[str, Any]] = {
"log_config": get_uvicorn_logging_config(),
"proxy_headers": False,
}
@asynccontextmanager
@@ -109,16 +113,16 @@ async def lifespan(application: FastAPI):
try:
cfg.reset_automation_progress()
except Exception as e:
logger.warning(f"Failed to reset automation progress Redis keys: {e}")
logger.warning(f"Failed to reset automation progress Redis keys: {e}", exc_info=True)
try:
daq.refresh_detector_metadata_cache()
except Exception as e:
logger.warning(f"Initial hardware metadata refresh failed: {e}")
logger.warning(f"Initial hardware metadata refresh failed: {e}", exc_info=True)
# ── Initial TELL sync ──
try:
daq.sync_current_sample_from_tell(force=True)
except Exception as e:
logger.warning(f"Initial sample sync from TELL failed: {e}")
logger.warning(f"Initial sample sync from TELL failed: {e}", exc_info=True)
# ── Wire callbacks ──
daq.set_face_detection_progress_callback(_push_face_detection_progress)
daq.set_automation_progress_callback(_push_automation_progress)
@@ -184,6 +188,7 @@ def _sample_is_mounted() -> bool:
try:
return daq.sample is not None
except Exception:
logger.debug("Could not determine whether a sample is mounted", exc_info=True)
return False
@@ -199,7 +204,7 @@ def _push_face_detection_progress(payload: dict) -> None:
next_seq = int(_face_detection_state.get("seq", 0)) + 1
_face_detection_state = {"seq": next_seq, **payload}
except Exception as e:
logger.warning(f"Failed to update face detection progress: {e}")
logger.warning(f"Failed to update face detection progress: {e}", exc_info=True)
def _get_automation_progress_state() -> dict:
@@ -209,7 +214,7 @@ def _get_automation_progress_state() -> dict:
try:
return cfg.get_automation_progress_state()
except Exception as e:
logger.warning(f"Failed to read automation progress from Redis: {e}")
logger.warning(f"Failed to read automation progress from Redis: {e}", exc_info=True)
return {"seq": 0, "progress": None}
@@ -228,7 +233,9 @@ def _push_automation_progress(progress: AutomationProgress) -> None:
f"current_step={state.get('progress', {}).get('current_step')}"
)
except Exception as e:
logger.warning(f"Failed to update automation progress: {type(e).__name__}: {e}")
logger.warning(
f"Failed to update automation progress: {type(e).__name__}: {e}", exc_info=True
)
async def face_detection_event_stream() -> AsyncGenerator[str, None]:
@@ -2289,8 +2296,8 @@ async def get_all_pgroups(token: str = Depends(oauth2_scheme)):
names.sort(key=lambda d: int(d[1:]))
_all_pgroups_cache[base_path] = (names, now)
return names
except Exception as e:
logger.error(f"Failed to list pgroups: {e}")
except Exception:
logger.exception("Failed to list pgroups")
return []
@@ -2450,7 +2457,7 @@ async def sse_fluorimeter(token: str = Depends(oauth2_scheme)):
daq.fluorimeter_start(erase=False)
except Exception:
# Ignore if already running or start not needed
pass
logger.debug("Fluorimeter already running or start not needed", exc_info=True)
return StreamingResponse(
fluorimeter_stream(),
media_type="text/event-stream",
@@ -2487,7 +2494,7 @@ async def send_screenshot_db(
async def send_message_db(
db_id: int,
event_type: SampleEventType,
comment: Optional[str] = None,
comment: str | None = None,
token: str = Depends(oauth2_scheme),
):
data = auth.parse_token(token)
+4 -5
View File
@@ -173,11 +173,10 @@ def register_exception_handlers(app) -> None:
)
body = _error_body(exc, code_override=code_override)
# TODO: migrate baton to its own error type
if isinstance(exc, UserRightsException):
if "do not hold the baton" in exc.message:
return JSONResponse(
status_code=status, content=body, headers=getattr(exc, "headers", None)
)
if isinstance(exc, UserRightsException) and "do not hold the baton" in exc.message:
return JSONResponse(
status_code=status, content=body, headers=getattr(exc, "headers", None)
)
logger.warning(
"AareAuthError: %s",
body["message"],
+8 -5
View File
@@ -3,11 +3,14 @@ import os
import time
from aarecommon.config.beamline import mx_beamline
from aarecommon.config.logger import setup_logger
from aarecommon.models.models import DewarAddress, SampleShortInfo, SampleShortInfoList
from aareDB.models import PuckWithTellPosition
from aare.daq.config import BeamlineConfig
logger = setup_logger("aareDAQ")
beamline = mx_beamline()
config = BeamlineConfig(bl=beamline)
@@ -105,10 +108,10 @@ def on_message(ws, message):
config._client.delete(ref_key)
print("[REDIS][INFO] Cleared reference tools key:", ref_key)
except Exception:
pass
logger.debug("Could not clear the reference tools key", exc_info=True)
except Exception as exc:
print("[WS][ERROR] Failed to parse or convert message:", exc)
except Exception:
logger.exception("[WS] Failed to parse or convert message")
def on_error(ws, error):
@@ -178,8 +181,8 @@ def main():
print(f"[WS][INFO] Connecting to {WS_URL}...")
ws.run_forever(sslopt=ssl_opt)
except Exception as e:
print(f"[MAIN][ERROR] WebSocket connection failed: {e}")
except Exception:
logger.exception("[MAIN] WebSocket connection failed")
time.sleep(5)
+18 -19
View File
@@ -1,7 +1,7 @@
from __future__ import annotations
import re
from datetime import datetime, timezone
from datetime import UTC, datetime
from aarecommon.models.tell import TellActivityEnum, TellPhaseEnum, TellStateModel
@@ -11,7 +11,7 @@ UNMOUNT_STATUS_RE = re.compile(r"^unmount:\s*")
def _utc_now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
return datetime.now(UTC).isoformat()
def initial_tell_state() -> TellStateModel:
@@ -248,11 +248,7 @@ def advance_tell_state(
if event_name == "Motion Sync":
if value == "Sample get from Gonio":
next_phase = (
TellPhaseEnum.RETURNING_OLD_SAMPLE
if state.operation == "mount"
else TellPhaseEnum.RETURNING_OLD_SAMPLE
)
next_phase = TellPhaseEnum.RETURNING_OLD_SAMPLE
next_operation = "mount" if state.operation == "mount" else "unmount"
return _update_state(
state,
@@ -324,18 +320,21 @@ def advance_tell_state(
mount_error="",
)
if event_name == "Gripper detection":
if value in {"No Pin in Gripper", "Pin still in Gripper", "Pin is lost"}:
return _update_state(
state,
activity=TellActivityEnum.ERROR,
message=value,
event_name=event_name,
event_value=value,
mount_success=False,
mount_error=value,
phase=TellPhaseEnum.FAILED,
)
if event_name == "Gripper detection" and value in {
"No Pin in Gripper",
"Pin still in Gripper",
"Pin is lost",
}:
return _update_state(
state,
activity=TellActivityEnum.ERROR,
message=value,
event_name=event_name,
event_value=value,
mount_success=False,
mount_error=value,
phase=TellPhaseEnum.FAILED,
)
if event_name == "state":
if value == "Ready":
+2 -2
View File
@@ -4,7 +4,7 @@ import ssl
import threading
import time
from collections import deque
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any, cast
import requests
@@ -180,7 +180,7 @@ def set_tell_state_in_redis(state: TellStateModel) -> None:
def record_tell_event(event_name, event_value):
event_record = TellEventRecord(
timestamp=datetime.now(timezone.utc).isoformat(), class_=event_name, event=event_value
timestamp=datetime.now(UTC).isoformat(), class_=event_name, event=event_value
)
latest_tell_events[event_name] = event_value
+2 -2
View File
@@ -266,8 +266,8 @@ def common2dh(devs: BeamlineDevices, cfg: BeamlineConfig):
if devs.tell.get_mounted_sample() is not None:
try:
devs.tell.unmount(wait=True)
except Exception as e:
print(f"Error for unmounting: {e}")
except Exception:
logger.exception("Error for unmounting")
devs.tell.dry(wait_cold=-1, wait=False)
+11 -17
View File
@@ -1,5 +1,3 @@
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
@@ -23,7 +21,7 @@ AEROTECH_HOME = AerotechCoordinate(at_mm=Coordinate(x=0, y=0, z=0), omega_deg=0)
logger = setup_logger("aareDAQ")
class AerotechController(object):
class AerotechController:
def __init__(self, bl: MXBeamline):
if bl == MXBeamline.X06DA:
self._simulated = False
@@ -42,7 +40,7 @@ class AerotechController(object):
self._pos = AEROTECH_HOME
self._vel = 0
else:
raise Exception("unknown beamline")
raise ValueError("unknown beamline")
if not self._simulated:
self._client = ApiClient(Configuration(host=self._base))
@@ -184,11 +182,7 @@ class AerotechController(object):
) from e
def rotation_scan(
self,
rotation_deg: float | int,
time_sec: float | int,
start_pos_deg: float | int,
run_async: bool = False,
self, rotation_deg: float, time_sec: float, start_pos_deg: float, run_async: bool = False
):
payload = RotationRequest(
rotation_deg=rotation_deg,
@@ -212,11 +206,11 @@ class AerotechController(object):
def grid_scan(
self,
grid_elem_count_y: int,
grid_elem_size_y_um: int | float,
time_sec: int | float,
grid_elem_size_x_um: Optional[Union[float, int]] = None,
grid_elem_count_x: Optional[int] = None,
run_async: Optional[bool] = False,
grid_elem_size_y_um: float,
time_sec: float,
grid_elem_size_x_um: float | None = None,
grid_elem_count_x: int | None = None,
run_async: bool | None = False,
):
payload = GridRequest(
grid_elem_count_x=grid_elem_count_x,
@@ -240,9 +234,9 @@ class AerotechController(object):
def screening_scan(
self,
rotation_deg: float | int,
wedge_deg: float | int,
time_sec: float | int,
rotation_deg: float,
wedge_deg: float,
time_sec: float,
steps: int,
run_async: bool = False,
):
+8 -5
View File
@@ -3,6 +3,9 @@ from enum import Enum
import epics
import numpy as np
from aarecommon.config.logger import setup_logger
logger = setup_logger("aareDAQ")
class AutoEnum(Enum):
@@ -21,7 +24,7 @@ class AutoExposureSettings:
exp_max: float = 30000.000
class epicsAD(object):
class epicsAD:
def __init__(self, prefix, cam="cam1:", image="image1:"):
self.img = None
self.monitored = False
@@ -61,11 +64,11 @@ class epicsAD(object):
try:
epics.ca.pend_io()
except Exception as e:
print(f"EPICS error: {e}")
pass
except Exception:
logger.exception("EPICS error")
def _init_auto_exp(self, settings: AutoExposureSettings = AutoExposureSettings()):
def _init_auto_exp(self, settings: AutoExposureSettings | None = None):
settings = settings or AutoExposureSettings()
self.acquire.put(0)
self.aoi_start_x.put(settings.aoi_offset_x)
self.aoi_start_y.put(settings.aoi_offset_y)
+59 -58
View File
@@ -1,6 +1,6 @@
import sys
import time
from enum import Enum
from typing import List, Optional
from aarecommon.config.beamline import cfg_get, mx_beamline
from aarecommon.config.logger import setup_logger
@@ -90,9 +90,9 @@ class BECClientWorker:
self._set_scilog_tags()
try:
self._init_beamline_environment()
except Exception as e:
logger.error(f"Error initialising BEC devices: {e}")
exit(1)
except Exception:
logger.exception("Error initialising BEC devices")
sys.exit(1)
logger.debug(f"simulated is {self.simulated}")
def _init_beamline_environment(self):
@@ -112,14 +112,15 @@ class BECClientWorker:
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}")
logger.exception("Error initialising zoom and ring_current")
self._zoom = None
self.ring_current = None
raise Exception(f"Error initialising BEC devices: {e}")
raise RuntimeError(f"Error initialising BEC devices: {e}") from e
def _raise_bec_error(
self, exc: Exception, *, operation: str, tags: Optional[List[str]] = None
) -> None:
def _bec_error(
self, exc: Exception, *, operation: str, tags: list[str] | None = None
) -> BECCommunicationError:
"""Report a failed BEC operation and build the error; callers ``raise ... from`` it."""
message = f"BEC operation '{operation}' failed: {type(exc).__name__}: {exc}"
# logger.exception(message)
if tags is None:
@@ -134,8 +135,8 @@ class BECClientWorker:
last_alarm = self.client.show_last_alarm()
message += f"\n\nAlarm: {last_alarm}\n\n"
logger.error(f"last_alarm: {last_alarm}")
except Exception as e:
logger.error(f"Couldn't raise BEC alarms: {e}")
except Exception:
logger.exception("Couldn't raise BEC alarms")
try:
self.scilog_msg(
@@ -144,40 +145,40 @@ class BECClientWorker:
error_message=f"Error during '{operation}': {exc}",
tags=tags,
)
except Exception as e:
logger.error(f"Error sending scilog message: {e}")
except Exception:
logger.exception("Error sending scilog message")
if isinstance(exc, AssertionError):
raise BECCommunicationError(
return BECCommunicationError(
f"BEC internal assertion failed during '{operation}'",
operation=operation,
exception=exc,
) from exc
)
raise BECCommunicationError(message, operation=operation, exception=exc) from exc
return BECCommunicationError(message, operation=operation, exception=exc)
def _set_scilog_tags(self, tags: Optional[List[str]] = None):
def _set_scilog_tags(self, tags: list[str] | None = None):
try:
if tags:
self.client.messaging.scilog.set_default_tags(tags)
else:
self.client.messaging.scilog.set_default_tags(["AareDAQ"])
except Exception as e:
logger.error(f"Error setting scilog tags: {e}")
self._raise_bec_error(e, operation="set scilog tags")
logger.exception("Error setting scilog tags")
raise self._bec_error(e, operation="set scilog tags") from e
def scilog_msg(
self,
message: str,
error: bool = False,
warning: bool = False,
error_message: Optional[str] = None,
error_message: str | None = None,
attachments=None,
bold: bool = False,
italic: bool = False,
color: Optional[str] = None,
additonal_text: Optional[List[str]] = None,
tags: Optional[List[str]] = None,
color: str | None = None,
additonal_text: list[str] | None = None,
tags: list[str] | None = None,
):
if color and color not in ["red", "green", "yellow", "blue", "pink"]:
logger.warning("specified color not in allowed list,using default")
@@ -185,7 +186,7 @@ class BECClientWorker:
try:
msg = self.client.messaging.scilog.new()
except Exception as e:
self._raise_bec_error(e, operation="scilog_msg")
raise self._bec_error(e, operation="scilog_msg") from e
try:
msg.add_text(message, bold=bold, italic=italic, color=color)
if error:
@@ -193,32 +194,32 @@ class BECClientWorker:
elif warning:
msg.add_text(error_message, bold=True, color="yellow")
except Exception as e:
logger.error(f"Error adding text: {e}")
logger.exception("Error adding text")
msg.add_text(f"Error adding text: {e}")
try:
if attachments:
for attachment in attachments:
msg.add_attachment(attachment)
except Exception as e:
logger.error(f"Error adding attachment: {e}")
logger.exception("Error adding attachment")
msg.add_text(f"Error adding attachment: {e}")
try:
if additonal_text:
for text in additonal_text:
msg.add_text(text)
except Exception as e:
logger.error(f"Error adding additional text: {e}")
logger.exception("Error adding additional text")
msg.add_text(f"Error adding additional text: {e}")
try:
if tags:
msg.add_tags(tags)
except Exception as e:
logger.error(f"Error setting scilog tags: {e}")
logger.exception("Error setting scilog tags")
msg.add_text(f"Error setting scilog tags: {e}")
try:
msg.send()
except Exception as e:
logger.error(f"Error sending scilog message: {e}")
except Exception:
logger.exception("Error sending scilog message")
def run_macro(self, macro_name: str, *args, queue: str = "default", **kwargs):
if self.simulated:
@@ -227,7 +228,7 @@ class BECClientWorker:
try:
return self.client.proc.run_macro(macro_name, *args, queue=queue)
except Exception as e:
self._raise_bec_error(e, operation=f"run_macro:{macro_name}")
raise self._bec_error(e, operation=f"run_macro:{macro_name}") from e
def run_macro_blocked(self, macro_name: str, *args, queue: str = "default", **kwargs):
if self.simulated:
@@ -240,7 +241,7 @@ class BECClientWorker:
print(status)
return status
except Exception as e:
self._raise_bec_error(e, operation=f"run_macro_blocked:{macro_name}")
raise self._bec_error(e, operation=f"run_macro_blocked:{macro_name}") from e
@log_timing(logger, "BEC move_to")
def move_to(self, state: BeamlineState):
@@ -263,7 +264,7 @@ class BECClientWorker:
)
return False
except Exception as e:
self._raise_bec_error(e, operation=f"planner.move_to:{state.value}")
raise self._bec_error(e, operation=f"planner.move_to:{state.value}") from e
def is_state(self, state: BeamlineState):
if self.simulated:
@@ -298,7 +299,7 @@ class BECClientWorker:
return []
return [str(macro) for macro in raw_macros]
except Exception as e:
self._raise_bec_error(e, operation="list_all_user_macros")
raise self._bec_error(e, operation="list_all_user_macros") from e
def _list_all_macros(self):
result = self.client.macros.list_user_macros()
@@ -313,7 +314,7 @@ class BECClientWorker:
try:
return self._load_user_macros()
except Exception as e:
self._raise_bec_error(e, operation="load_user_macros")
raise self._bec_error(e, operation="load_user_macros") from e
def _load_user_macros(self):
result = self.macros.load_all_user_macros()
@@ -345,9 +346,9 @@ class BECClientWorker:
logger.info(f"Reinitialised BEC planner and position devices using method={method}")
return self.list_position_devices()
except Exception as e:
self._raise_bec_error(
raise self._bec_error(
e, operation=f"reinitialise_planner_and_position_devices:{method}"
)
) from e
def shutdown_client(self):
self.client.shutdown()
@@ -356,7 +357,7 @@ class BECClientWorker:
try:
self.macros.mono_pitch_scan(plot)
except Exception as e:
self._raise_bec_error(e, operation="mono_pitch_scan", tags=["mono_pitch_scan"])
raise self._bec_error(e, operation="mono_pitch_scan", tags=["mono_pitch_scan"]) from e
if self.beamline is MXBeamline.X06DA:
addtional_text = [f"New dcm_pitch position: {self.dev.dcm_pitch.position:5f}"]
else:
@@ -375,18 +376,18 @@ class BECClientWorker:
energy_kev = energy_ev / 1000
return energy_kev
def change_energy(self, value: float | int, plot: bool = False):
def change_energy(self, value: float, plot: bool = False):
current_energy = self.check_current_energy()
logger.info(f"Current energy: {current_energy:.1f} eV")
logger.info(f"Change energy requested: from {current_energy:.1f} to {value:.1f} eV")
try:
self.macros.bl_energy(value, move_gap=False, mono_scan=True, plot=plot)
except Exception as e:
self._raise_bec_error(
raise self._bec_error(
e,
operation=f"Requested energy change from:{current_energy:.1f} to {value} eV",
tags=["energy_change"],
)
) from e
if abs(value - self.check_current_energy()) > 1:
logger.warning(
@@ -420,7 +421,7 @@ class BECClientWorker:
try:
return self.dev.det_z.position
except Exception as e:
self._raise_bec_error(e, operation="get_det_z", tags=["det_z"])
raise self._bec_error(e, operation="get_det_z", tags=["det_z"]) from e
def det_z(self, value: float, timeout: int | None = None):
"""timeout is None or integer in s"""
@@ -430,13 +431,13 @@ class BECClientWorker:
status.wait(timeout=timeout)
return status
except Exception as e:
self._raise_bec_error(e, operation=f"scans.mv:det_z:{value}", tags=["det_z"])
raise self._bec_error(e, operation=f"scans.mv:det_z:{value}", tags=["det_z"]) from e
def get_det_y(self):
try:
return self.dev.det_y.position
except Exception as e:
self._raise_bec_error(e, operation="get_det_z", tags=["det_z"])
raise self._bec_error(e, operation="get_det_z", tags=["det_z"]) from e
def det_y(self, value: float, timeout: int | None = None):
"""timeout is None or integer in s"""
@@ -446,7 +447,7 @@ class BECClientWorker:
status.wait(timeout=timeout)
return status
except Exception as e:
self._raise_bec_error(e, operation=f"scans.mv:det_y:{value}", tags=["det_z"])
raise self._bec_error(e, operation=f"scans.mv:det_y:{value}", tags=["det_z"]) from e
@property
def backlight_brightness(self) -> BrightnessEnum:
@@ -455,11 +456,11 @@ class BECClientWorker:
try:
return BrightnessEnum(self._backlight_brightness.actual)
except Exception as e:
self._raise_bec_error(
raise self._bec_error(
e,
operation="backlight brightness, could not get backlight brightness",
tags=["backlight"],
)
) from e
raise
@backlight_brightness.setter
@@ -470,7 +471,9 @@ class BECClientWorker:
try:
self._backlight_brightness.move(value)
except Exception as e:
self._raise_bec_error(e, operation=f"backlight_brightness:{value}", tags=["backlight"])
raise self._bec_error(
e, operation=f"backlight_brightness:{value}", tags=["backlight"]
) from e
raise
def get_backlight_pos(self) -> BrightnessEnum:
@@ -489,11 +492,11 @@ class BECClientWorker:
target = BrightnessEnum.ON
self.backlight_brightness = target
except Exception as e:
self._raise_bec_error(
raise self._bec_error(
e,
operation="backlight toggle, could not change backlight on/off ",
tags=["backlight"],
)
) from e
def save_current_bs_pos(self):
self.macros.save_current_position(self.dev.bs_z, "safe")
@@ -535,13 +538,12 @@ if __name__ == "__main__":
beamline = mx_beamline()
try:
client = BECClientWorker(beamline)
except Exception as e:
print(f"Error: {e}")
except Exception:
logger.exception("Failed to start BEC client")
try:
client.shutdown_client()
except Exception as e:
import sys
except Exception:
logger.exception("Failed to shut the BEC client down cleanly")
sys.exit(1)
# print(client.get_det_cov(actual=True))
# print(client.is_state(BeamlineState.ROBOT_SAMPLE_EXCHANGE))
@@ -581,10 +583,9 @@ if __name__ == "__main__":
# print(client.backlight_brightness)
# client.scilog_msg("Testing scilog messages with color = yellow and italic", italic=True,
# color="green", warning=False)
except Exception as e:
# client._raise_bec_error(e, operation="send message")
except Exception:
client.shutdown_client()
print(f"Error: {e}")
logger.exception("BEC client smoke test failed")
# try:
# det_value = 980
Executable → Regular
+1 -1
View File
@@ -1,7 +1,7 @@
from enum import Enum
from typing import Any
from aare.devices.set_get_pv import SetGetPV, MoveResult
from aare.devices.set_get_pv import MoveResult, SetGetPV
class EnumPV(SetGetPV):
+5 -5
View File
@@ -7,7 +7,7 @@ from epics import PV, poll
logger = setup_logger("aareaDAQ")
class Fluorimeter(object):
class Fluorimeter:
def __init__(self, beamline: MXBeamline, **kwargs):
BEAMLINE = beamline.value.upper()
@@ -108,8 +108,8 @@ class Fluorimeter(object):
raise ValueError(f"Invalid preset mode: {mode}")
try:
self._preset_mode.put(mode)
except Exception as e:
logger.error(f"Error setting preset mode to {mode}: {e}")
except Exception:
logger.exception(f"Error setting preset mode to {mode}")
def save_file(self, filename: str | None = None, timeout_s: float = 30.0):
if not filename:
@@ -122,8 +122,8 @@ class Fluorimeter(object):
if time.time() > timeout:
raise TimeoutError("Timed out waiting for save to complete")
poll(0.1)
except Exception as e:
logger.error(f"Error saving file {filename}: {e}")
except Exception:
logger.exception(f"Error saving file {filename}")
@property
def real_time(self):
+41 -37
View File
@@ -4,6 +4,7 @@ from enum import Enum
import jfjoch_client
from aarecommon.config.beamline import get_jfjoch_url
from aarecommon.config.logger import setup_logger
from aarecommon.errors.exception_handler import JFJochCommunicationError
from aarecommon.models.beamline import MXBeamline
from aarecommon.models.models import DAQStatusModel, FluorescenceSpectrumOutputModel
@@ -12,6 +13,8 @@ from aarecommon.models.rotation_scan import RotationScanRequest
from jfjoch_client.api.default_api import DefaultApi
from jfjoch_client.api_client import ApiClient
logger = setup_logger("aareDAQ")
class ScanTypeEnum(Enum):
RASTER = "Raster"
@@ -53,32 +56,33 @@ class JFJochWrapper:
return None
def _raise_jfjoch_error(
def _jfjoch_error(
self, message: str, *, error: Exception, operation: str, endpoint: str
) -> None:
raise JFJochCommunicationError(
) -> JFJochCommunicationError:
"""Build the error for a failed JFJoch call; callers ``raise ... from`` it."""
return JFJochCommunicationError(
message,
operation=operation,
endpoint=endpoint,
base_url=self._url,
status_code=self._extract_status_code(error),
) from error
)
def initialize(self):
try:
self._api.initialize_post()
except Exception as e:
self._raise_jfjoch_error(
raise self._jfjoch_error(
"JFJoch initialize failed", error=e, operation="POST", endpoint="initialize_post"
)
) from e
def cancel(self):
try:
self._api.cancel_post()
except Exception as e:
self._raise_jfjoch_error(
raise self._jfjoch_error(
"JFJoch cancel failed", error=e, operation="POST", endpoint="cancel_post"
)
) from e
def is_idle(self) -> bool:
status = self._api.status_get()
@@ -176,23 +180,22 @@ class JFJochWrapper:
)
dataset_settings.xray_fluorescence_spectrum = xrf
if s.sample.aaredb_params:
if s.sample.aaredb_params.unitcell:
unit_cell_db = s.sample.aaredb_params.unitcell
unit_cell_split = unit_cell_db.replace(",", " ").split()
unit_cell_floats = [float(x) for x in unit_cell_split]
unit_cell = jfjoch_client.UnitCell(
a=unit_cell_floats[0],
b=unit_cell_floats[1],
c=unit_cell_floats[2],
alpha=unit_cell_floats[3],
beta=unit_cell_floats[4],
gamma=unit_cell_floats[5],
)
dataset_settings.unit_cell = unit_cell
if s.sample.aaredb_params.spacegroupnumber:
space_group_number = s.sample.aaredb_params.spacegroupnumber
dataset_settings.space_group_number = space_group_number
if s.sample.aaredb_params and s.sample.aaredb_params.unitcell:
unit_cell_db = s.sample.aaredb_params.unitcell
unit_cell_split = unit_cell_db.replace(",", " ").split()
unit_cell_floats = [float(x) for x in unit_cell_split]
unit_cell = jfjoch_client.UnitCell(
a=unit_cell_floats[0],
b=unit_cell_floats[1],
c=unit_cell_floats[2],
alpha=unit_cell_floats[3],
beta=unit_cell_floats[4],
gamma=unit_cell_floats[5],
)
dataset_settings.unit_cell = unit_cell
if s.sample.aaredb_params.spacegroupnumber:
space_group_number = s.sample.aaredb_params.spacegroupnumber
dataset_settings.space_group_number = space_group_number
return dataset_settings
@@ -208,12 +211,12 @@ class JFJochWrapper:
try:
self._api.start_post(dataset_settings=dataset_settings)
except Exception as e:
self._raise_jfjoch_error(
raise self._jfjoch_error(
f"JFJoch data collection failed to initialize for {scan_type.value} scan with exception: {e}",
error=e,
operation="POST",
endpoint="start_post",
)
) from e
def measure_rotation(
self,
@@ -230,44 +233,44 @@ class JFJochWrapper:
def measure_raster(self, r: RasterGridRequest, s: DAQStatusModel, async_start: bool = True):
self._start_scan(ScanTypeEnum.RASTER, r, s, async_start=async_start)
def wait_till_running(self, timeout: int | float = 60):
def wait_till_running(self, timeout: float = 60):
if self._simulated:
return None
try:
self._api.wait_until_running_post_with_http_info(timeout=math.ceil(timeout))
return True
except Exception as e:
self._raise_jfjoch_error(
raise self._jfjoch_error(
f"JFJoch wait until running returned an exception: {e}",
error=e,
operation="POST",
endpoint="wait_until_running_post",
)
) from e
def wait_till_done(self, timeout: int | float) -> jfjoch_client.models.ScanResult | None:
def wait_till_done(self, timeout: float) -> jfjoch_client.models.ScanResult | None:
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()
except Exception as e:
self._raise_jfjoch_error(
raise self._jfjoch_error(
f"JFJoch wait till done retrieval returned an exception: {e}",
error=e,
operation="POST",
endpoint="wait_till_done_post / result_scan_get",
)
) from e
def detector(self) -> jfjoch_client.models.DetectorListElement:
try:
detector_list = self._api.config_select_detector_get()
except Exception as e:
self._raise_jfjoch_error(
raise self._jfjoch_error(
f"JFJoch detector configuration retrieval failed: {e}",
error=e,
operation="GET",
endpoint="config_select_detector_get",
)
) from e
if len(detector_list.detectors) == 0:
raise JFJochCommunicationError(
@@ -281,12 +284,12 @@ class JFJochWrapper:
try:
return detector_list.detectors[detector_list.current_id]
except Exception as e:
self._raise_jfjoch_error(
raise self._jfjoch_error(
"JFJoch returned an invalid selected detector entry",
error=e,
operation="GET",
endpoint="config_select_detector_get",
)
) from e
def take_pedestal(self):
raise NotImplementedError(
@@ -312,6 +315,7 @@ class JFJochWrapper:
show_beam_center=show_beam_center,
)
except Exception as e:
logger.debug("Diffraction image retrieval attempt failed; retrying", exc_info=True)
last_error = e
time.sleep(wait_between_retries_s)
raise last_error
+16 -11
View File
@@ -1,9 +1,13 @@
import re
import time
from typing import Callable, Union, Any
from collections.abc import Callable
from typing import Any
from aarecommon.config.logger import setup_logger
from epics import PV, Motor, poll
logger = setup_logger("aareDAQ")
def wait_for_movement_to_finish(*motors):
"""
@@ -21,12 +25,12 @@ def wait_for_movement_to_finish(*motors):
longest = 0.0
for motor in motors:
time_to_target = motor.readback / motor.slew_speed
longest = time_to_target if time_to_target > longest else longest
longest = max(longest, time_to_target)
timeout = time.time() + 1.5 * longest
done = False
while not done and time.time() < timeout:
done = all([m.done_moving for m in motors])
done = all(m.done_moving for m in motors)
if time.time() > timeout:
print("TIMEOUT waiting for motors to be done moving; current motor positions:")
@@ -102,7 +106,7 @@ def is_epics_type(pv: PV, pv_type: str) -> bool:
def wait_string_condition(
pv: PV, target: Union[str, re.Pattern], *, timeout: float = 60.0, polling: float = 0.1
pv: PV, target: str | re.Pattern, *, timeout: float = 60.0, polling: float = 0.1
):
"""wait until an epics.PV of type string reaches target
:pv: epics.PV
@@ -209,11 +213,12 @@ def wait_motor_position(
raises: TimeoutError if a timeout occurs
"""
if not callable(tester):
raise RuntimeError("argument 'tester' must be a function")
raise TypeError("argument 'tester' must be a function")
try:
move_time = abs(motor.drive - motor.readback) / motor.speed
except Exception:
logger.debug("Could not compute the motor move time; using the 1 s default", exc_info=True)
move_time = 1.0 # in case of unusual motor record
tout = move_time + time.time() + timeout
@@ -230,7 +235,7 @@ def wait_motor_position(
def wait_enum_condition(
pv: PV, value: Union[str, int, re.Pattern], *, timeout: float = 60.0, polling=0.1
pv: PV, value: str | int | re.Pattern, *, timeout: float = 60.0, polling=0.1
):
"""wait until an epics.PV enum reaches value
pv: epics.PV
@@ -249,16 +254,16 @@ def wait_enum_condition(
if not (isinstance(pv, PV) and pv.type.lower().endswith("enum")):
raise AttributeError("argument 'pv' must be an epics.PV of type enum")
if not (isinstance(value, str) or isinstance(value, int) or isinstance(value, re.Pattern)):
raise AttributeError("argument 'value' must be either an int, str, or re.Pattern")
if not (isinstance(value, (str, int, re.Pattern))):
raise TypeError("argument 'value' must be either an int, str, or re.Pattern")
if type(value) is int:
tester = lambda pv: value == pv.get() # noqa: E731
tester = lambda pv: value == pv.get()
elif type(value) is str:
tester = lambda pv: str(value) == pv.get(as_string=True).lower() # noqa: E731
tester = lambda pv: str(value) == pv.get(as_string=True).lower()
value = str(value).lower() # it's already a str :-/
elif isinstance(value, re.Pattern):
tester = lambda pv: value.match(pv.get(as_string=True)) # noqa: E731
tester = lambda pv: value.match(pv.get(as_string=True))
else:
raise AttributeError("argument 'value' must be either an int, str, or re.Pattern")
+9 -7
View File
@@ -1,22 +1,24 @@
from __future__ import annotations
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from typing import Any, Mapping, Optional, Union, Callable
from typing import Any
from epics import PV
from aare.devices.mx_lib import pv_wait
RawValue = Union[str, float, int]
ResolverValue = Union[
RawValue,
tuple[Callable[..., RawValue], tuple[Any, ...]], # (func, args) pattern you already use
]
RawValue = str | float | int
ResolverValue = (
RawValue
| tuple[Callable[..., RawValue], tuple[Any, ...]] # (func, args) pattern you already use
)
@dataclass
class MoveResult:
target: RawValue
name: Optional[str] = None
name: str | None = None
class SetGetPV:
+9 -9
View File
@@ -15,7 +15,7 @@ class SmargonMode(Enum):
ERROR = 99
class Smargon(object):
class Smargon:
SMARGON_HOME = SmargonCoordinate(sh_mm=Coordinate(x=0, y=0, z=18), phi_deg=0, chi_deg=0)
AERO_HOME = AerotechCoordinate(x=0, y=0, z=0, omega=0)
@@ -31,7 +31,7 @@ class Smargon(object):
elif bl == MXBeamline.SIMULATED:
self._simulated = True
else:
raise Exception("unknown beamline")
raise ValueError("unknown beamline")
self._pos = self.SMARGON_HOME
self._pos_aero = self.AERO_HOME
@@ -175,13 +175,13 @@ class Smargon(object):
target_string = ""
if coord.sh_mm is not None:
target_string += "&SHX={:.5f}&SHY={:.5f}&SHZ={:.5f}".format(
coord.sh_mm.x, coord.sh_mm.y, coord.sh_mm.z
target_string += (
f"&SHX={coord.sh_mm.x:.5f}&SHY={coord.sh_mm.y:.5f}&SHZ={coord.sh_mm.z:.5f}"
)
if coord.chi_deg is not None:
target_string += "&CHI={:.5f}".format(coord.chi_deg)
target_string += f"&CHI={coord.chi_deg:.5f}"
if coord.phi_deg is not None:
target_string += "&PHI={:.5f}".format(coord.phi_deg)
target_string += f"&PHI={coord.phi_deg:.5f}"
if target_string:
self.gonput(f"targetSCS?{target_string}")
@@ -203,11 +203,11 @@ class Smargon(object):
target_string = ""
if coord.at_mm is not None:
target_string += "&GMX={:.5f}&GMY={:.5f}&GMZ={:.5f}".format(
coord.at_mm.x, coord.at_mm.y, coord.at_mm.z
target_string += (
f"&GMX={coord.at_mm.x:.5f}&GMY={coord.at_mm.y:.5f}&GMZ={coord.at_mm.z:.5f}"
)
if coord.omega_deg is not None:
target_string += "&GMU={:.5f}".format(coord.omega_deg)
target_string += f"&GMU={coord.omega_deg:.5f}"
if target_string:
self.gonput(f"targetAEROTECH?{target_string}")
+3 -2
View File
@@ -1,7 +1,8 @@
import json
import re
import time
from typing import Any, Callable, Protocol
from collections.abc import Callable
from typing import Any, Protocol
from urllib.parse import urlparse
import requests
@@ -325,7 +326,7 @@ class SimTellBackend:
}
)
except Exception:
logger.warning("Failed to load simulated samples info")
logger.warning("Failed to load simulated samples info", exc_info=True)
def abort(self) -> None:
self._state = "Ready"
+16 -14
View File
@@ -1,8 +1,8 @@
import ast
import json
import re
from datetime import UTC
from enum import Enum
from typing import List
from aarecommon.config.logger import setup_logger
from aarecommon.errors.exception_handler import (
@@ -94,7 +94,7 @@ class TellClient:
raise TellConnectionException("Tell reconnecting")
elif state == "Closing":
raise TellConnectionException("Tell is disconnecting")
raise Exception("Invalid state: " + str(state))
raise RuntimeError("Invalid state: " + str(state))
def set_in_mount_position(self, value):
"""tells the robot that the beamlien is safe and to set the in mount position flag allowing mounting
@@ -142,6 +142,7 @@ class TellClient:
if not self.is_remote_mode():
reasons.append("TELL is not in remote mode")
except Exception as e:
logger.debug("Failed to check TELL remote mode", exc_info=True)
reasons.append(f"failed to check remote mode: {e}")
if reasons:
@@ -158,13 +159,13 @@ class TellClient:
except Exception as e:
raise TellCommunicationError(
f"Mount can't start: failed to check door status: {e}", operation="mount_precheck"
)
) from e
if not door_closed:
raise TellCommunicationError(
"Mount can't start: TELL doors are open", operation="mount_precheck"
)
def set_samples_info(self, info: List[PuckWithTellPosition]):
def set_samples_info(self, info: list[PuckWithTellPosition]):
"""sets the samples in the robot dewar based on the given list of PuckWithTellPosition objects
and runs set_sample_info in the background"""
@@ -205,9 +206,8 @@ class TellClient:
result = self.get_result(self._last_cmd_id)
logger.debug(f"getting result for command {self._last_cmd_id}: {result}")
status = result["status"]
if "completed" != status:
if "removed" != status:
raise MountingFailed(f"{msg} {result}")
if "completed" != status and "removed" != status:
raise MountingFailed(f"{msg} {result}")
return f"{msg} {result}"
def estimate_mounting_time(self, segment) -> int:
@@ -233,6 +233,7 @@ class TellClient:
needs_drying = mount_needs_drying + unmount_needs_drying
return needs_cooling * 30 + needs_drying * 120
except Exception:
logger.debug("Could not estimate the mounting time", exc_info=True)
return 0
def mount(
@@ -358,7 +359,7 @@ class TellClient:
except TellCommunicationError:
raise
except Exception as e:
logger.error(f"Exception occurred: {e}")
logger.exception("Exception occurred")
raise TellCommunicationError(
message=f"Error during mount {segment}{puck}-{sample}: {e}", critical=True
)
@@ -452,7 +453,7 @@ class TellClient:
# return eval(status)
return ast.literal_eval(status)
def get_detected_pucks(self) -> List[PuckLoadedInfo]:
def get_detected_pucks(self) -> list[PuckLoadedInfo]:
j = json.loads(self.backend.eval("get_pucks_info()&"))
output = []
@@ -474,6 +475,7 @@ class TellClient:
try:
offset = float(self.backend.eval("get_pin_offset()&"))
except Exception:
logger.debug("Could not read the pin offset; assuming 0", exc_info=True)
offset = 0.0
return offset
@@ -482,7 +484,7 @@ class TellClient:
return float(current)
def set_current(self, current: float) -> float:
self.backend.eval("smart_magnet.set_current({:.1f})&".format(current))
self.backend.eval(f"smart_magnet.set_current({current:.1f})&")
current = self.backend.eval("smart_magnet.get_current_rb()&")
return float(current)
@@ -549,7 +551,7 @@ class TellClient:
raise SmartMagnetFaultException
except Exception as e:
logger.error(f"check_smart_magnet_mounted failed: {e}")
raise e
raise
def make_tell_client(bl: MXBeamline) -> TellClient:
@@ -561,7 +563,7 @@ def make_tell_client(bl: MXBeamline) -> TellClient:
if __name__ == "__main__":
from datetime import datetime, timezone
from datetime import datetime
from aarecommon.config.beamline import mx_beamline
@@ -574,8 +576,8 @@ if __name__ == "__main__":
ts = float(tell_client.get_setting("dry_timestamp"))
print("dry timestape: ", ts)
past = datetime.fromtimestamp(ts, tz=timezone.utc)
now = datetime.now(timezone.utc)
past = datetime.fromtimestamp(ts, tz=UTC)
now = datetime.now(UTC)
seconds_ago = int((now - past).total_seconds())
print(seconds_ago)
print("door closer :", tell_client.backend.eval("is_door_closed()&"))
+4 -12
View File
@@ -7,10 +7,10 @@ def wait_position(motor, target, tolerance=None, timeout=60.0):
position = motor.readback
if isinstance(position, float) and tolerance is None:
tst = "%f == %s"
ltst = lambda x, y, z: x == y # noqa: E731
ltst = lambda x, y, z: x == y
elif isinstance(position, float) and tolerance is not None:
tst = "abs(%f - %f) < %f"
ltst = lambda x, y, z: abs(x - y) < z # noqa: E731
ltst = lambda x, y, z: abs(x - y) < z
elif isinstance(position, (bytes, str)):
tst = "'%s' == '%s'"
if isinstance(position, bytes):
@@ -37,19 +37,11 @@ def wait_position(motor, target, tolerance=None, timeout=60.0):
n = n + 1
if n > 20:
n = 0
print(
"waiting_position test: %s (%s, %s, %s)"
% (tst, str(motor.readback), str(target), str(tolerance))
)
print(f"waiting_position test: {tst} ({motor.readback!s}, {target!s}, {tolerance!s})")
timeisup = timeout < time.time()
condition = ltst(motor.readback, target, tolerance)
if timeisup:
msg = "Timeout when waiting for %s to reach %s with tolerance %s. Device was at: %s" % (
motor,
str(target),
str(tolerance),
str(motor.readback),
)
msg = f"Timeout when waiting for {motor} to reach {target!s} with tolerance {tolerance!s}. Device was at: {motor.readback!s}"
print(msg)
raise RuntimeError(msg)
Executable → Regular
+11 -10
View File
@@ -5,7 +5,6 @@ with fallback to area_detector if ZMQ is unavailable.
"""
import json
from typing import Optional
import cv2
import numpy as np
@@ -43,12 +42,12 @@ class ZMQCameraClient:
self._simulated = True
self._zmq_url = None
else:
raise Exception("unknown beamline")
raise ValueError("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._context: zmq.Context | None = None
self._socket: zmq.Socket | None = None
self._last_image: np.ndarray | None = None
self._last_fetch_time: float = 0.0
self._connected = False
@@ -77,11 +76,11 @@ class ZMQCameraClient:
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}")
logger.warning(f"Failed to connect ZMQ camera to {self._zmq_url}: {e}", exc_info=True)
self._connected = False
return False
def get_image(self, gray: bool = False) -> Optional[np.ndarray]:
def get_image(self, gray: bool = False) -> np.ndarray | None:
"""
Fetch the latest image from the ZMQ stream.
@@ -114,6 +113,7 @@ class ZMQCameraClient:
header = decoded
break
except Exception:
logger.debug("Skipping an unparsable ZMQ message part", exc_info=True)
continue
if header and header.get("type") == "uint8":
@@ -138,7 +138,7 @@ class ZMQCameraClient:
logger.debug("ZMQ camera: timeout waiting for frame")
return self._last_image
except Exception as e:
logger.warning(f"ZMQ camera error: {e}")
logger.warning(f"ZMQ camera error: {e}", exc_info=True)
self._connected = False
return self._last_image
@@ -161,6 +161,7 @@ class ZMQCameraClient:
self._socket.setsockopt(zmq.RCVTIMEO, old_timeout)
return False
except Exception:
logger.debug("Camera ZMQ socket is not available", exc_info=True)
return False
def close(self):
@@ -169,13 +170,13 @@ class ZMQCameraClient:
try:
self._socket.close()
except Exception:
pass
logger.debug("Error closing the camera ZMQ socket", exc_info=True)
self._socket = None
if self._context:
try:
self._context.term()
except Exception:
pass
logger.debug("Error terminating the camera ZMQ context", exc_info=True)
self._context = None
self._connected = False
+8 -1
View File
@@ -4,16 +4,23 @@ import importlib
import sys
from typing import TYPE_CHECKING
from aarecommon.config.logger import setup_logger
from aare.gui.constants import LOGGER_NAME
logger = setup_logger(LOGGER_NAME)
if TYPE_CHECKING:
from aare.gui.threads.daq_worker import DAQWorker
def about_text(client: "DAQWorker"):
def about_text(client: DAQWorker):
from aare.gui import gui
try:
version = importlib.metadata.version("aaredaq")
except Exception:
logger.debug("Could not resolve the installed package version", exc_info=True)
version = "Not found - package is not installed into the environment."
server_version, server_file_path = client.server_about_info()
+1
View File
@@ -27,6 +27,7 @@ def auth(base_url: str | None, cert_path: str | None) -> str:
capture_output=True,
text=True,
timeout=120.0,
check=False,
)
except subprocess.TimeoutExpired as e:
logger.error(f"Token request curl timed out: {e}")
+6 -9
View File
@@ -1,6 +1,5 @@
import os
import sys
import traceback
from aarecommon.config.beamline import cfg_get, mx_beamline
from aarecommon.config.logger import setup_logger
@@ -25,8 +24,8 @@ def main():
banner_path = os.path.join(basedir, "graphics/aare_banner.png")
splash_pix = QtGui.QPixmap(banner_path)
splash = LoadingSplashScreen(splash_pix)
except Exception as e:
logger.error(f"Failed to load resources for splash screen: {e}")
except Exception:
logger.exception("Failed to load resources for splash screen")
sys.exit(1)
try:
# define application
@@ -168,12 +167,11 @@ def main():
splash.set_progress(80, "Authentication successful...")
except Exception as e:
splash.finish(None)
logger.error(f"Cannot connect to AareDAQ server. Exiting. {e}")
logger.error(traceback.format_exc())
logger.exception("Cannot connect to AareDAQ server. Exiting.")
QMessageBox.critical(
None,
"Authentication Error",
f"Cannot connect to AareDAQ server:\n{str(e)}\n\n Please check the server is running and your network connection.",
f"Cannot connect to AareDAQ server:\n{e!s}\n\n Please check the server is running and your network connection.",
)
sys.exit(1)
@@ -196,15 +194,14 @@ def main():
except Exception as e:
splash.finish(None)
logger.error(f"Error starting GUI: {e}")
logger.error(f"Traceback: {traceback.format_exc()}")
logger.exception("Error starting GUI")
QMessageBox.critical(
None,
"Fatal Error",
f"An error occurred during startup. See console for details."
f"\nPlease check the server is running and your network connection."
f"\n\n{str(e)}\n\n",
f"\n\n{e!s}\n\n",
)
sys.exit(1)
+1 -1
View File
@@ -16,5 +16,5 @@ class QtLogHandler(logging.Handler):
try:
msg = self.format(record)
self.emitter.message.emit(msg)
except Exception:
except Exception: # noqa: BLE001 - logging here would recurse into this handler
self.handleError(record)
+43 -43
View File
@@ -45,7 +45,7 @@ from aare.gui.panels.fluorescence_panel import FluorescencePanel
from aare.gui.panels.local_contact_panel import LocalContactDialog
# panels
from aare.gui.panels.LogPanel import LogDock
from aare.gui.panels.log_panel import LogDock
from aare.gui.panels.loop_centering_panel import LoopCenteringPanel
from aare.gui.panels.manual_sample_panel import ManualSamplePanel
from aare.gui.panels.portrait_mode import PortraitModePanel
@@ -169,8 +169,8 @@ class MainWindow(QMainWindow):
payload = jwt.decode(token_str, options={"verify_signature": False})
self._decoded_token = TokenData(**payload)
except Exception as e:
logger.error(f"Failed to decode authentication token: {e}", exc_info=True)
except Exception:
logger.exception("Failed to decode authentication token")
QMessageBox.critical(
None,
"Authentication Error",
@@ -934,7 +934,7 @@ class MainWindow(QMainWindow):
if hasattr(self, "content_stack") and hasattr(self, "_standard_main_page"):
self.content_stack.setCurrentWidget(self._standard_main_page)
except Exception as e:
logger.warning(f"Failed to return to main view during shutdown: {e}")
logger.warning(f"Failed to return to main view during shutdown: {e}", exc_info=True)
def _restore_samcam_overlay_settings(self) -> None:
settings = QSettings("PSI", "AareGUI")
@@ -1011,7 +1011,7 @@ class MainWindow(QMainWindow):
try:
thread.stop()
except Exception as e:
logger.warning(f"Failed to stop {attr_name}: {e}")
logger.warning(f"Failed to stop {attr_name}: {e}", exc_info=True)
setattr(self, attr_name, None)
@@ -1028,6 +1028,7 @@ class MainWindow(QMainWindow):
try:
activity_name = tell_state.activity.display_name()
except Exception:
logger.debug("Could not derive the axis busy text from status", exc_info=True)
activity_name = activity_value.capitalize() if activity_value else "Busy"
return f"TELL {activity_name}".upper()
@@ -1144,7 +1145,7 @@ class MainWindow(QMainWindow):
self.portrait_sample_camera._autoscale = True
self.portrait_sample_camera._scaling()
except Exception:
pass
logger.debug("Could not autoscale the portrait-mode camera", exc_info=True)
# ── Hide all chrome that contributes to window width ────────────────
if self.status_bar is not None:
@@ -1222,7 +1223,7 @@ class MainWindow(QMainWindow):
settings.get("show_overlay_legend", True)
)
except Exception:
pass
logger.debug("Could not restore the camera overlay legend", exc_info=True)
if hasattr(self, "_pre_portrait_geometry") and self._pre_portrait_geometry:
self.restoreGeometry(self._pre_portrait_geometry)
@@ -1742,14 +1743,14 @@ class MainWindow(QMainWindow):
try:
if self.job_list_panel is not None and self.job_list_panel.is_running():
self.job_list_panel.pause_automation()
except Exception as e:
logger.error(f"Failed to pause automation after operation failure: {e}")
except Exception:
logger.exception("Failed to pause automation after operation failure")
try:
show = QMessageBox.critical if critical else QMessageBox.warning
show(self, title, message)
except Exception as e:
logger.error(f"Failed to show operation failure popup: {e}")
except Exception:
logger.exception("Failed to show operation failure popup")
def _hutch_blocks_mount(self) -> str | None:
"""Reason the hutch PSS currently blocks a mount, or None if OK.
@@ -1780,8 +1781,8 @@ class MainWindow(QMainWindow):
logger.warning(f"Manual mount blocked by hutch PSS: {reason}")
try:
QMessageBox.critical(self, "Mounting Failed", reason)
except Exception as e:
logger.error(f"Failed to show mount-blocked popup: {e}")
except Exception:
logger.exception("Failed to show mount-blocked popup")
return
self.daq.mount(sample, reference)
@@ -1792,8 +1793,8 @@ class MainWindow(QMainWindow):
logger.warning(f"Manual unmount blocked by hutch PSS: {reason}")
try:
QMessageBox.critical(self, "Unmounting Failed", reason)
except Exception as e:
logger.error(f"Failed to show unmount-blocked popup: {e}")
except Exception:
logger.exception("Failed to show unmount-blocked popup")
return
self.daq.unmount()
@@ -1837,8 +1838,8 @@ class MainWindow(QMainWindow):
f"Details:\n{message}"
),
)
except Exception as e:
logger.error(f"Failed to show manual collection detector popup: {e}")
except Exception:
logger.exception("Failed to show manual collection detector popup")
@Slot(str)
def _on_automation_critical_failure(self, message: str) -> None:
@@ -1864,8 +1865,8 @@ class MainWindow(QMainWindow):
self.job_list_panel.pause_automation()
if self.daq is not None:
self.daq.send_status_request()
except Exception as e:
logger.error(f"Failed to pause automation queue after critical failure: {e}")
except Exception:
logger.exception("Failed to pause automation queue after critical failure")
# 2. Mark the automation progress widget as finished-with-error so
# _is_automation_active() returns False and idle/close timers behave.
@@ -1903,8 +1904,8 @@ class MainWindow(QMainWindow):
progress.finished = True
progress.success = False
self.automation_progress_panel.set_progress(progress)
except Exception as e:
logger.error(f"Failed to update automation progress after critical failure: {e}")
except Exception:
logger.exception("Failed to update automation progress after critical failure")
self._show_runtime_notification(
title="Automation paused", message=message, level="error", sticky=True
@@ -1954,8 +1955,8 @@ class MainWindow(QMainWindow):
"Please contact your local contact to recover the beamline."
),
)
except Exception as e:
logger.error(f"Failed to surface recovery UI after critical failure: {e}")
except Exception:
logger.exception("Failed to surface recovery UI after critical failure")
def show_beamline_recovery(self) -> None:
if not bool(getattr(self._decoded_token, "staff", False)):
@@ -2035,6 +2036,7 @@ class MainWindow(QMainWindow):
)
break
except Exception:
logger.debug("Skipping an unreadable GUI session entry", exc_info=True)
continue
else:
if self._remote_close_deadline_ts is not None:
@@ -2290,7 +2292,7 @@ class MainWindow(QMainWindow):
try:
self._return_to_main_view_for_shutdown()
except Exception as e:
logger.warning(f"Failed to restore main view before close: {e}")
logger.warning(f"Failed to restore main view before close: {e}", exc_info=True)
try:
# TODO put all setting related handling into state_manager
@@ -2300,18 +2302,18 @@ class MainWindow(QMainWindow):
self._save_theme_settings()
except Exception as e:
logger.warning(f"Failed to save main window state: {e}")
logger.warning(f"Failed to save main window state: {e}", exc_info=True)
# End session before closing so the backend removes this GUI from Redis immediately
try:
self.daq.end_session_on_close()
except Exception as e:
logger.warning(f"Failed to end session on close: {e}")
logger.warning(f"Failed to end session on close: {e}", exc_info=True)
try:
self.cleanup()
except Exception as e:
logger.warning(f"Cleanup during closeEvent failed: {e}")
logger.warning(f"Cleanup during closeEvent failed: {e}", exc_info=True)
super().closeEvent(event)
@@ -2322,7 +2324,7 @@ class MainWindow(QMainWindow):
try:
self._return_to_main_view_for_shutdown()
except Exception as e:
logger.warning(f"Failed to restore main view during cleanup: {e}")
logger.warning(f"Failed to restore main view during cleanup: {e}", exc_info=True)
self._cleanup_done = True
@@ -2330,19 +2332,19 @@ class MainWindow(QMainWindow):
if hasattr(self, "_samcam_source_timer") and self._samcam_source_timer is not None:
self._samcam_source_timer.stop()
except Exception as e:
logger.warning(f"Failed to stop _samcam_source_timer: {e}")
logger.warning(f"Failed to stop _samcam_source_timer: {e}", exc_info=True)
try:
if hasattr(self, "_idle_timer") and self._idle_timer is not None:
self._idle_timer.stop()
except Exception as e:
logger.warning(f"Failed to stop _idle_timer: {e}")
logger.warning(f"Failed to stop _idle_timer: {e}", exc_info=True)
try:
if hasattr(self, "_remote_close_timer") and self._remote_close_timer is not None:
self._remote_close_timer.stop()
except Exception as e:
logger.warning(f"Failed to stop _remote_close_timer: {e}")
logger.warning(f"Failed to stop _remote_close_timer: {e}", exc_info=True)
try:
if (
@@ -2351,13 +2353,13 @@ class MainWindow(QMainWindow):
):
self._axis_camera_refresh_timer.stop()
except Exception as e:
logger.warning(f"Failed to stop _axis_camera_refresh_timer: {e}")
logger.warning(f"Failed to stop _axis_camera_refresh_timer: {e}", exc_info=True)
try:
if hasattr(self, "daq") and self.daq is not None:
self.daq.cleanup()
except Exception as e:
logger.warning(f"Failed to clean up DAQ worker: {e}")
logger.warning(f"Failed to clean up DAQ worker: {e}", exc_info=True)
self._stop_axis_camera_threads()
@@ -2371,7 +2373,7 @@ class MainWindow(QMainWindow):
try:
thread.stop()
except Exception as e:
logger.warning(f"Failed to stop {attr_name}: {e}")
logger.warning(f"Failed to stop {attr_name}: {e}", exc_info=True)
setattr(self, attr_name, None)
@@ -2380,14 +2382,14 @@ class MainWindow(QMainWindow):
if hasattr(self, "job_list_panel") and self.job_list_panel is not None:
return bool(self.job_list_panel.is_running())
except Exception:
pass
logger.debug("Could not read the job list panel running state", exc_info=True)
try:
progress = getattr(self.automation_progress_panel, "_progress", None)
if progress is not None and not bool(getattr(progress, "finished", False)):
return True
except Exception:
pass
logger.debug("Could not read the automation progress state", exc_info=True)
return False
@@ -2405,14 +2407,14 @@ class MainWindow(QMainWindow):
try:
self.daq.report_gui_interaction(int(self._decoded_token.session))
except Exception as e:
logger.debug(f"Failed to report GUI interaction: {e}")
logger.debug(f"Failed to report GUI interaction: {e}", exc_info=True)
def _mark_user_interaction(self) -> None:
self._refresh_idle_activity(report_backend=True)
def eventFilter(self, obj, event):
try:
if event is not None and event.type() in {
if event.type() in {
QEvent.Type.MouseButtonPress,
QEvent.Type.MouseButtonRelease,
QEvent.Type.MouseMove,
@@ -2425,7 +2427,7 @@ class MainWindow(QMainWindow):
}:
self._mark_user_interaction()
except Exception as e:
logger.debug(f"GUI interaction event filter error: {e}")
logger.debug(f"GUI interaction event filter error: {e}", exc_info=True)
return super().eventFilter(obj, event)
def _start_remote_close_countdown(
@@ -2454,16 +2456,14 @@ class MainWindow(QMainWindow):
status = self._latest_daq_status
if status is not None and bool(status.busy):
return False
if self._is_automation_active():
return False
return True
return not self._is_automation_active()
@Slot()
def _check_remote_close_deadline(self) -> None:
if self._remote_close_deadline_ts is None:
return
remaining = int(round(self._remote_close_deadline_ts - time.time()))
remaining = round(self._remote_close_deadline_ts - time.time())
if remaining > 0:
if self._remote_close_banner_active:
self.alert_banner_secondary.show_message(
+10 -4
View File
@@ -1,6 +1,12 @@
import json
from aarecommon.config.logger import setup_logger
from PySide6.QtCore import QSettings
from aare.gui.constants import LOGGER_NAME
logger = setup_logger(LOGGER_NAME)
class UIStateManager:
def __init__(self, organization="PSI", app="AareGUI"):
@@ -24,8 +30,8 @@ class UIStateManager:
data = model.to_list()
self.settings.setValue(key, json.dumps(data))
except Exception as e:
print(f"Failed to save model {key}: {e}")
except Exception:
logger.warning(f"Failed to save model {key}", exc_info=True)
def restore_model(self, key: str, model):
try:
@@ -37,8 +43,8 @@ class UIStateManager:
model.from_list(data)
except Exception as e:
print(f"Failed to restore model {key}: {e}")
except Exception:
logger.warning(f"Failed to restore model {key}", exc_info=True)
def save_value(self, key: str, value):
self.settings.setValue(key, value)
+8 -5
View File
@@ -1,7 +1,12 @@
from aarecommon.config.logger import setup_logger
from aarecommon.models.models import SampleShortInfo, SampleShortInfoList
from PySide6.QtCore import QAbstractTableModel, Qt
from PySide6.QtGui import QBrush, QColor
from aare.gui.constants import LOGGER_NAME
logger = setup_logger(LOGGER_NAME)
def get_entry(sample: SampleShortInfo, column: int, *, show_user: bool = False):
if show_user:
@@ -78,9 +83,7 @@ class SampleQueueSpreadsheet(QAbstractTableModel):
return ["text/plain"]
def canDropMimeData(self, data, action, row, column, parent):
if data.hasText():
return True
return False
return bool(data.hasText())
def dropMimeData(self, data, action, row, column, parent):
if not self.canDropMimeData(data, action, row, column, parent):
@@ -107,8 +110,8 @@ class SampleQueueSpreadsheet(QAbstractTableModel):
updated_samples.insert(updated_row, sample)
self.samples = updated_samples
self.endResetModel()
except Exception as e:
print(e)
except Exception:
logger.warning("Failed to handle dropped sample queue rows", exc_info=True)
return False
return True
+7 -1
View File
@@ -1,9 +1,14 @@
import re
from aarecommon.config.logger import setup_logger
from aarecommon.models.models import SampleShortInfo, SampleShortInfoList
from PySide6.QtCore import QAbstractTableModel, QMimeData, Qt
from PySide6.QtGui import QBrush, QColor
from aare.gui.constants import LOGGER_NAME
logger = setup_logger(LOGGER_NAME)
def get_entry(sample: SampleShortInfo, column: int):
if column == 0:
@@ -182,7 +187,7 @@ class UserSampleSpreadsheet(QAbstractTableModel):
sample_data = SampleShortInfoList(s=[])
for i in sorted(set(index.row() for index in indexes)):
for i in sorted({index.row() for index in indexes}):
sample_data.s.append(self._sorted_samples[i])
mime_data.setText(sample_data.model_dump_json())
@@ -275,6 +280,7 @@ class UserSampleSpreadsheet(QAbstractTableModel):
)
)
except Exception:
logger.debug("Falling back to a plain sort for the column values", exc_info=True)
out.sort()
else:
out.sort()
+2 -2
View File
@@ -147,8 +147,8 @@ class AutomationProgressWidget(QWidget):
if seconds is None or seconds <= 0:
return "0m 00s"
if seconds < 60:
return f"0m {int(round(seconds)):02d}s"
minutes, secs = divmod(int(round(seconds)), 60)
return f"0m {round(seconds):02d}s"
minutes, secs = divmod(round(seconds), 60)
if minutes < 60:
return f"{minutes}m {secs:02d}s"
hours, minutes = divmod(minutes, 60)
+1 -1
View File
@@ -1,5 +1,5 @@
from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton
from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget
from aare.gui.widgets.busy_overlay import BusyOverlayStyle
from aare.gui.widgets.video_image import VideoGraphicsView
@@ -1,5 +1,6 @@
from __future__ import annotations
from aarecommon.config.logger import setup_logger
from aarecommon.models.models import DAQStatusModel
from PySide6.QtCore import Slot
from PySide6.QtWidgets import (
@@ -14,8 +15,11 @@ from PySide6.QtWidgets import (
QWidget,
)
from aare.gui.constants import LOGGER_NAME
from aare.gui.threads.daq_worker import DAQWorker
logger = setup_logger(LOGGER_NAME)
class RecoveryPanel(QWidget):
def __init__(self, *, daq: DAQWorker, parent=None):
@@ -161,12 +165,14 @@ class RecoveryPanel(QWidget):
try:
return self._last_status is not None and self._last_status.sample is not None
except Exception:
logger.debug("Could not determine whether a sample is mounted", exc_info=True)
return False
def _beamline_appears_busy(self) -> bool:
try:
return self._last_status is not None and bool(self._last_status.busy)
except Exception:
logger.debug("Could not determine whether the beamline is busy", exc_info=True)
return False
def _refresh_buttons(self) -> None:
+7 -3
View File
@@ -3,9 +3,8 @@ from __future__ import annotations
import json
import logging
import time
from typing import Dict
from aarecommon.config.logger import attach_to_logger, find_existing_formatter
from aarecommon.config.logger import attach_to_logger, find_existing_formatter, setup_logger
from aarecommon.errors.codes import error_code_help
from aarecommon.models.models import OpenGuiSessionInfo
from PySide6.QtCore import Qt, QUrl, Slot
@@ -30,9 +29,12 @@ from PySide6.QtWidgets import (
QWidget,
)
from aare.gui.constants import LOGGER_NAME
from aare.gui.log import QtLogEmitter, QtLogHandler
from aare.gui.threads.daq_worker import DAQWorker
logger = setup_logger(LOGGER_NAME)
class DeveloperHelpDialog(QDialog):
def __init__(self, *, daq: DAQWorker, is_staff: bool, parent=None):
@@ -40,7 +42,7 @@ class DeveloperHelpDialog(QDialog):
self._daq = daq
self._is_staff = bool(is_staff)
self._codes: Dict[str, str] = {}
self._codes: dict[str, str] = {}
self._last_payload: dict = {}
self._freeze_payload: bool = False
self._always_highlight_last_error: bool = True
@@ -390,6 +392,7 @@ class DeveloperHelpDialog(QDialog):
try:
pretty = json.dumps(p or {}, indent=2, sort_keys=True, default=str)
except Exception:
logger.debug("Could not pretty-print the error payload", exc_info=True)
pretty = str(p)
blocks.append(f"#{i}\n{pretty}")
self._payloads_text.setPlainText("\n\n".join(blocks) if blocks else "(none captured yet)")
@@ -596,6 +599,7 @@ class DeveloperHelpDialog(QDialog):
try:
return int(value)
except Exception:
logger.debug("Could not read the selected GUI session id", exc_info=True)
return None
@Slot()
@@ -70,6 +70,7 @@ class FaceDetectionPanel(QWidget):
self.face_detection_button.setEnabled(False)
self.face_detection.emit(int(self.steps), int(self.step_size))
except Exception as e:
logger.warning("Face detection run failed", exc_info=True)
self._manual_run_requested = False
self.status_lbl.setText(f"Error: {e}")
self.face_detection_button.setEnabled(True)
+9 -8
View File
@@ -112,7 +112,7 @@ class FluorescencePanel(QWidget):
self._update_vline(pt.x())
return False
except Exception as e:
logger.debug(f"eventFilter error: {e}")
logger.debug(f"eventFilter error: {e}", exc_info=True)
return False
def _show_context_menu(self, pos):
@@ -153,10 +153,10 @@ class FluorescencePanel(QWidget):
for x, y in zip(x_vals, y_vals):
writer.writerow([f"{x:.6f}", f"{y:.6f}"])
logger.info(f"Spectrum saved to {path}")
except Exception as e:
logger.error(f"Failed to save CSV: {e}")
except Exception as e:
logger.error(f"Context menu error: {e}")
except Exception:
logger.exception("Failed to save CSV")
except Exception:
logger.exception("Context menu error")
def _update_vline(self, x_val: float):
# Draw vertical line spanning current Y axis range at x_val
@@ -172,7 +172,7 @@ class FluorescencePanel(QWidget):
self._vline.replace(1, x_val, ymax)
self._vline.setVisible(True)
except Exception as e:
logger.debug(f"vline error: {e}")
logger.debug(f"vline error: {e}", exc_info=True)
def _nearest_index(self, x_val: float) -> int:
n = self.series.count()
@@ -213,6 +213,7 @@ class FluorescencePanel(QWidget):
else:
self.avg_dead_label.setText("Average dead time: -")
except Exception:
logger.debug("Could not compute the average dead time", exc_info=True)
self.avg_dead_label.setText("Average dead time: -")
# Default peak label
@@ -251,8 +252,8 @@ class FluorescencePanel(QWidget):
ymax = ymin + 1.0
self.axis_x.setRange(xmin, xmax)
self.axis_y.setRange(ymin, ymax)
except Exception as e:
logger.error(f"Update plot error: {e}")
except Exception:
logger.exception("Update plot error")
@Slot(DAQStatusModel)
def update_daq_status(self, s: DAQStatusModel):
+2 -1
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
from collections.abc import Callable
from typing import ClassVar
from aarecommon.config.logger import setup_logger
from aarecommon.models.models import DAQStatusModel
@@ -44,7 +45,7 @@ class LocalContactPanel(QFrame):
TAB_DETECTOR = "Detector"
TAB_CONFIG = "Config"
DEVICE_TITLES = {
DEVICE_TITLES: ClassVar[dict[str, str]] = {
"bec": "BEC",
"detector": "Detector",
"tell": "TELL",
@@ -169,7 +169,9 @@ class LogDock(QDockWidget):
def __init__(self, title="Log", parent=None):
super().__init__(title, parent)
self.setAllowedAreas(
Qt.BottomDockWidgetArea | Qt.RightDockWidgetArea | Qt.LeftDockWidgetArea
Qt.DockWidgetArea.BottomDockWidgetArea
| Qt.DockWidgetArea.RightDockWidgetArea
| Qt.DockWidgetArea.LeftDockWidgetArea
)
self.container = QWidget(self)
+1 -1
View File
@@ -1,4 +1,4 @@
from PySide6.QtWidgets import QWidget, QGridLayout, QPushButton
from PySide6.QtWidgets import QGridLayout, QPushButton, QWidget
from aare.gui.widgets.title_label import TitleLabel
+4 -2
View File
@@ -1,5 +1,7 @@
from __future__ import annotations
from typing import ClassVar
from aarecommon.config.logger import setup_logger
from aarecommon.models.automation import AutomationProgress, StepStatus, WorkflowStateKind
from PySide6.QtCore import QPointF, QRectF, Qt, QTimer, Signal, Slot
@@ -39,10 +41,10 @@ ACTIVE_STEP = "#FFFFFF"
# LED step indicator
# ---------------------------------------------------------------------------
class LEDStages(QWidget):
STEPS = ["Mount", "Centre", "Raster", "Collect"]
STEPS: ClassVar[list[str]] = ["Mount", "Centre", "Raster", "Collect"]
# WorkflowStateKind → LED index
_KIND_TO_INDEX: dict[WorkflowStateKind, int] = {
_KIND_TO_INDEX: ClassVar[dict[WorkflowStateKind, int]] = {
WorkflowStateKind.MOUNT: 0,
WorkflowStateKind.LOOP_CENTRE: 1,
WorkflowStateKind.RASTER: 2,
@@ -13,6 +13,7 @@ import csv
import time
from collections import deque
from dataclasses import dataclass, field
from typing import ClassVar
from aarecommon.config.logger import setup_logger
from aarecommon.models.models import MLBoxType
@@ -110,8 +111,8 @@ def get_class_color(class_name: str) -> str:
class ConfidenceHistogramWidget(QWidget):
"""Real-time histogram of prediction confidence scores."""
BINS = [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]
BIN_COLORS = ["#d62728", "#ff7f0e", "#ffbb78", "#98df8a", "#2ca02c"]
BINS: ClassVar[list[float]] = [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]
BIN_COLORS: ClassVar[list[str]] = ["#d62728", "#ff7f0e", "#ffbb78", "#98df8a", "#2ca02c"]
def __init__(self, parent=None):
super().__init__(parent)
@@ -810,5 +811,5 @@ class PredictionMetricsPanel(QWidget):
logger.info(f"Exported {len(self._history)} frames to {path}")
except Exception as e:
logger.error(f"Failed to export prediction metrics: {e}")
except Exception:
logger.exception("Failed to export prediction metrics")
@@ -221,7 +221,7 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
def update_total_time_label(self):
mins = int(self._total_time // 60)
secs = int(round(self._total_time % 60))
secs = round(self._total_time % 60)
if secs == 60:
mins += 1
secs = 0
+6 -6
View File
@@ -1,5 +1,4 @@
# reference_tools_panel.py
from typing import Optional
from aarecommon.config.logger import setup_logger
from aarecommon.models.models import DAQStatusModel, SampleShortInfo, SampleShortInfoList
@@ -41,7 +40,7 @@ def get_entry(sample: SampleShortInfo, column: int):
class ReferenceToolsModel(QAbstractTableModel):
def __init__(
self,
rows: Optional[list[SampleShortInfo]] | None = None,
rows: list[SampleShortInfo] | None = None,
parent=None,
current_reference: int | None = None,
):
@@ -65,10 +64,10 @@ class ReferenceToolsModel(QAbstractTableModel):
else:
self._sorted_samples = []
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
def rowCount(self, parent: QModelIndex | None = None) -> int:
return len(self._sorted_samples)
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
def columnCount(self, parent: QModelIndex | None = None) -> int:
return len(self.header)
def data(self, index: QModelIndex, role=None):
@@ -147,7 +146,7 @@ class ReferenceToolsModel(QAbstractTableModel):
reverse=(self._sort_order == Qt.SortOrder.DescendingOrder),
)
def get_item(self, row: int) -> Optional[SampleShortInfo]:
def get_item(self, row: int) -> SampleShortInfo | None:
"""Get sample at the given row index."""
if 0 <= row < len(self._sorted_samples):
return self._sorted_samples[row]
@@ -227,7 +226,7 @@ class ReferenceToolsPanel(QFrame):
self.table_view.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.table_view.setSelectionMode(QTableView.SelectionMode.SingleSelection)
def _selected_item(self) -> Optional[SampleShortInfo]:
def _selected_item(self) -> SampleShortInfo | None:
idx = self.table_view.currentIndex()
if not idx.isValid():
return None
@@ -280,4 +279,5 @@ class ReferenceToolsPanel(QFrame):
f"Current sample: <b>{sample.sample_name} ({sample.location.segment}{sample.location.pos}-{sample.pin})</b>"
)
except Exception as e:
logger.debug("Could not update the current sample label", exc_info=True)
self.curr_sample_label.setText(f"Confusing information :/ {e}")
@@ -244,7 +244,7 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
def update_total_time_label(self):
mins = int(self._total_time // 60)
secs = int(round(self._total_time % 60))
secs = round(self._total_time % 60)
if secs == 60:
mins += 1
secs = 0
+2 -2
View File
@@ -279,8 +279,8 @@ class SampleQueuePanel(QFrame):
try:
if self.loop_restart_requested():
return
except Exception as e:
logger.exception(e)
except Exception:
logger.exception("Loop restart request failed")
if self.park_and_dry_when_cleared.isChecked():
logger.info("Automation queue empty; parking and drying TELL.")
+1 -1
View File
@@ -218,7 +218,7 @@ class ScanSettingsPanel(QWidget):
def _res_to_dtz(self, res: float) -> float:
dtz = self._diffraction.calc_dtz_mm(res)
return self.MIN_DTZ if dtz < self.MIN_DTZ else dtz
return max(dtz, self.MIN_DTZ)
@Slot(float)
def _on_dtz_value_changed(self, v: float):
+7 -1
View File
@@ -5,9 +5,10 @@ from math import sqrt
from pathlib import Path
import numpy as np
from aarecommon.config.logger import setup_logger
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
from PySide6.QtCore import QSettings, QTimer, Qt
from PySide6.QtCore import QSettings, Qt, QTimer
from PySide6.QtWidgets import (
QCheckBox,
QComboBox,
@@ -23,6 +24,10 @@ from PySide6.QtWidgets import (
QWidget,
)
from aare.gui.constants import LOGGER_NAME
logger = setup_logger(LOGGER_NAME)
class SmargonTracePanel(QWidget):
HOME_X_MM = 0.0
@@ -274,6 +279,7 @@ class SmargonTracePanel(QWidget):
)
except Exception as e:
logger.warning(f"Failed to load the smargon trace from {csv_path}", exc_info=True)
self._last_rows = []
self._last_distances = []
self._last_lengths = []
+3 -4
View File
@@ -278,7 +278,7 @@ class SimpleRotationSettingsPanel(QWidget):
def update_total_time_label(self):
mins = int(self.total_time_s // 60)
secs = int(round(self.total_time_s % 60))
secs = round(self.total_time_s % 60)
if secs == 60:
mins += 1
secs = 0
@@ -349,8 +349,7 @@ class SimpleRotationSettingsPanel(QWidget):
self.transmission = 1.0
self.dtz = self._d.diffraction.calc_dtz_mm(d_tar)
if self.dtz < 108:
self.dtz = 108
self.dtz = max(self.dtz, 108)
self.transmission_label.setText(f"{self.transmission * 100:.1f}")
self.image_time_label.setText(f"{self.image_time_s:.4f}")
@@ -364,7 +363,7 @@ class SimpleRotationSettingsPanel(QWidget):
self.dtz_label.setText(f"{self.dtz:.2f}")
self.parameters = SimpleScanParameters(
dtz=int(round(self.dtz)),
dtz=round(self.dtz),
exp_time_s=self.image_time_s,
start_omega_deg=self.start_angle_enter.value,
incr_omega_deg=image_angle,
@@ -1402,11 +1402,10 @@ class TargetStabilityPanel(QWidget):
@staticmethod
def _coerce_target_point(raw) -> tuple[float, float] | None:
try:
if isinstance(raw, dict):
if "x" in raw and "y" in raw:
return float(raw["x"]), float(raw["y"])
if isinstance(raw, dict) and "x" in raw and "y" in raw:
return float(raw["x"]), float(raw["y"])
if isinstance(raw, (list, tuple)) and len(raw) >= 2:
return float(raw[0]), float(raw[1])
except Exception as e:
logger.warning(f"Failed to parse target point {raw}: {e}")
logger.warning(f"Failed to parse target point {raw}: {e}", exc_info=True)
return None
+1
View File
@@ -236,6 +236,7 @@ class TellSamplePanel(QFrame):
current_puck=sample.puck_name, current_sample=sample.db_id
)
except Exception as e:
logger.debug("Could not build the TELL sample panel text", exc_info=True)
base_text = f"Confusing information :/ {e}"
if tell_details:
+35 -28
View File
@@ -1,6 +1,5 @@
import math
from enum import Enum
from typing import List, Tuple
import numpy as np
from aarecommon.config.beamline import cfg_get, get_jfjoch_url, mx_beamline
@@ -148,13 +147,13 @@ 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:
@@ -162,15 +161,21 @@ class RasterGridManager(QObject):
def _is_grid_visible(self, grid: RasterGridRequest):
if (
grid.smargon_top_left is None
or grid.smargon_top_left.phi_deg is None
or grid.smargon_top_left.chi_deg is None
or self._geom.smargon.phi_deg is None
or self._geom.smargon.chi_deg is None
):
raise ValueError(f"Cannot compute visibility for {grid}.")
return bool(
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 grid.n_x > 0
and grid.n_y > 0
):
return True
return False
)
def _grid_pixel_geometry(
self, grid: RasterGridRequest
@@ -210,10 +215,10 @@ class RasterGridManager(QObject):
if visible_rect is None or visible_rect.isEmpty():
return 0, grid.n_x, 0, grid.n_y
min_x = max(0, int(math.floor((visible_rect.left() - start_x) / cell_w)) - 1)
max_x = min(grid.n_x, int(math.ceil((visible_rect.right() - start_x) / cell_w)) + 1)
min_y = max(0, int(math.floor((visible_rect.top() - start_y) / cell_h)) - 1)
max_y = min(grid.n_y, int(math.ceil((visible_rect.bottom() - start_y) / cell_h)) + 1)
min_x = max(0, math.floor((visible_rect.left() - start_x) / cell_w) - 1)
max_x = min(grid.n_x, math.ceil((visible_rect.right() - start_x) / cell_w) + 1)
min_y = max(0, math.floor((visible_rect.top() - start_y) / cell_h) - 1)
max_y = min(grid.n_y, math.ceil((visible_rect.bottom() - start_y) / cell_h) + 1)
return min_x, max_x, min_y, max_y
@@ -300,7 +305,7 @@ class RasterGridManager(QObject):
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]:
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()))
delta = point_bl - self._geom.smargon_to_beamline(grid.smargon_top_left.sh_mm)
@@ -542,7 +547,7 @@ class RasterGridManager(QObject):
self._heatmap_cache.clear()
def _heatmap_image(
self, cache_key: tuple, grid: RasterGridRequest, values: List[float] | List[int]
self, cache_key: tuple, grid: RasterGridRequest, values: list[float] | list[int]
) -> QImage | None:
"""Return a cached n_x*n_y heatmap bitmap for this grid, building it once
on a cache miss. One pixel per cell; colours baked at full opacity with
@@ -560,7 +565,7 @@ class RasterGridManager(QObject):
return built[0]
def _build_heatmap_image(
self, n_x: int, n_y: int, values: List[float] | List[int]
self, n_x: int, n_y: int, values: list[float] | list[int]
) -> tuple[QImage, np.ndarray] | None:
if n_x <= 0 or n_y <= 0:
return None
@@ -597,7 +602,7 @@ class RasterGridManager(QObject):
self,
painter: QPainter,
elem: CompletedRasterGridElem,
values: List[float] | List[int],
values: list[float] | list[int],
alpha: int,
visible_rect: QRectF | None,
cache_key: tuple,
@@ -648,7 +653,7 @@ class RasterGridManager(QObject):
self,
painter: QPainter,
grid: RasterGridRequest,
values: List[float] | List[int] | None = None,
values: list[float] | list[int] | None = None,
alpha: int = 127,
visible_rect: QRectF | None = None,
):
@@ -657,9 +662,8 @@ class RasterGridManager(QObject):
if alpha < 0 or alpha > 255:
return
if values is None:
if self._draw_active_grid_fast(painter, grid, visible_rect):
return
if values is None and self._draw_active_grid_fast(painter, grid, visible_rect):
return
geo = self._grid_pixel_geometry(grid)
if geo is None:
@@ -677,9 +681,12 @@ class RasterGridManager(QObject):
):
return
if values is not None and (cell_w < 3.0 or cell_h < 3.0):
if self._draw_completed_grid_fast(painter, grid, values, alpha, visible_rect):
return
if (
values is not None
and (cell_w < 3.0 or cell_h < 3.0)
and self._draw_completed_grid_fast(painter, grid, values, alpha, visible_rect)
):
return
painter.save()
painter.setRenderHint(QPainter.RenderHint.Antialiasing, False)
@@ -759,8 +766,8 @@ class RasterGridManager(QObject):
painter.drawRect(bounds)
min_spacing_px = 4.0
stride_x = max(1, int(math.ceil(min_spacing_px / max(cell_w, 1e-9))))
stride_y = max(1, int(math.ceil(min_spacing_px / max(cell_h, 1e-9))))
stride_x = max(1, math.ceil(min_spacing_px / max(cell_w, 1e-9)))
stride_y = max(1, math.ceil(min_spacing_px / max(cell_h, 1e-9)))
min_ix, max_ix, min_iy, max_iy = self._visible_index_range(
grid, visible_rect, start_x, start_y, cell_w, cell_h
@@ -793,7 +800,7 @@ class RasterGridManager(QObject):
self,
painter: QPainter,
grid: RasterGridRequest,
values: List[float] | List[int],
values: list[float] | list[int],
alpha: int,
visible_rect: QRectF | None,
) -> bool:
@@ -823,8 +830,8 @@ class RasterGridManager(QObject):
)
min_fill_px = 3.0
stride_x = max(1, int(math.ceil(min_fill_px / max(cell_w, 1e-9))))
stride_y = max(1, int(math.ceil(min_fill_px / max(cell_h, 1e-9))))
stride_x = max(1, math.ceil(min_fill_px / max(cell_w, 1e-9)))
stride_y = max(1, math.ceil(min_fill_px / max(cell_h, 1e-9)))
painter.save()
painter.setRenderHint(QPainter.RenderHint.Antialiasing, False)
@@ -855,7 +862,7 @@ class RasterGridManager(QObject):
return True
def _block_value(
self, values: List[float] | List[int], grid_nx: int, x0: int, x1: int, y0: int, y1: int
self, values: list[float] | list[int], grid_nx: int, x0: int, x1: int, y0: int, y1: int
) -> float | None:
best = None
for y in range(y0, y1):
@@ -967,5 +974,5 @@ class RasterGridManager(QObject):
)
self.completed_grid_updated.emit()
def get_completed_grids(self) -> List[CompletedRasterGridElem]:
def get_completed_grids(self) -> list[CompletedRasterGridElem]:
return self._completed_grids
+15 -8
View File
@@ -1,9 +1,14 @@
import cv2
import requests
import numpy as np
import requests
from aarecommon.config.logger import setup_logger
from PySide6.QtCore import QThread, Signal
from PySide6.QtGui import QImage
from aare.gui.constants import LOGGER_NAME
logger = setup_logger(LOGGER_NAME)
class VideoThread(QThread):
frame_ready = Signal(QImage)
@@ -76,16 +81,17 @@ class VideoThread(QThread):
buffer = buffer[last_boundary:]
except requests.exceptions.RequestException as e:
self.error_occurred.emit(f"Connection error: {str(e)}")
self.error_occurred.emit(f"Connection error: {e!s}")
except Exception as e:
self.error_occurred.emit(f"Unexpected error: {str(e)}")
logger.warning("Axis video stream failed", exc_info=True)
self.error_occurred.emit(f"Unexpected error: {e!s}")
finally:
self.running = False
if self.session:
try:
self.session.close()
except Exception:
pass
logger.debug("Error closing the Axis camera session", exc_info=True)
self.session = None
def _process_buffer(self, buffer, boundary):
@@ -119,7 +125,7 @@ class VideoThread(QThread):
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# Convert to QImage
height, width, channel = rgb_frame.shape
height, width, _channel = rgb_frame.shape
bytes_per_line = 3 * width
qt_image = QImage(
rgb_frame.data, width, height, bytes_per_line, QImage.Format.Format_RGB888
@@ -131,6 +137,7 @@ class VideoThread(QThread):
except Exception:
# If frame processing fails, use last good frame if available
logger.debug("Frame processing failed; reusing the last good frame", exc_info=True)
if self.last_good_frame is not None:
self.frame_ready.emit(self.last_good_frame)
@@ -143,16 +150,16 @@ class VideoThread(QThread):
try:
session.close()
except Exception:
pass
logger.debug("Error closing the Axis camera session", exc_info=True)
finally:
self.session = None
try:
self.quit()
except Exception:
pass
logger.debug("Error while quitting the Axis video thread", exc_info=True)
try:
self.wait(5000)
except Exception:
pass
logger.debug("Error while waiting for the Axis video thread to finish", exc_info=True)
+9
View File
@@ -4,11 +4,16 @@ import time
import cv2
import numpy as np
import zmq
from aarecommon.config.logger import setup_logger
from aarecommon.math.autofocus import focus_measure_edges
from aarecommon.models.models import DAQStatusModel
from PySide6.QtCore import QThread, Signal, Slot
from PySide6.QtGui import QImage, QPixmap
from aare.gui.constants import LOGGER_NAME
logger = setup_logger(LOGGER_NAME)
class SampleCameraThread(QThread):
# Define a signal to communicate messages from the thread to the main GUI
@@ -97,6 +102,9 @@ class SampleCameraThread(QThread):
header = decoded
break
except Exception:
logger.debug(
"Skipping an unparsable camera ZMQ message part", exc_info=True
)
continue
if header:
@@ -160,6 +168,7 @@ class SampleCameraThread(QThread):
self._set_camera_available(False, "Sample camera feed unavailable")
continue # Check self.running again
except Exception as e:
logger.warning("Sample camera feed unavailable", exc_info=True)
self._set_camera_available(False, f"Sample camera feed unavailable: {e}")
self.running = False
+109 -101
View File
@@ -8,7 +8,7 @@ import time
from collections import deque
from dataclasses import dataclass
from datetime import datetime
from typing import Literal, cast
from typing import ClassVar, Literal, cast
from aarecommon.config.logger import setup_logger
from aarecommon.errors.codes import AareErrorCode, AuthErrorCode, export_error_codes
@@ -352,7 +352,7 @@ class DAQWorker(QObject):
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"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
reply = self._net_manager.get(request)
reply.finished.connect(lambda: self.handle_status_response(reply))
@@ -367,7 +367,7 @@ class DAQWorker(QObject):
file = str(file_reply.readAll()) if file_reply.waitForReadyRead(500) else "Not connected"
return version, file
_SELF_SIGNED_ERRORS = {
_SELF_SIGNED_ERRORS: ClassVar[set[QSslError.SslError]] = {
QSslError.SslError.SelfSignedCertificate,
QSslError.SslError.SelfSignedCertificateInChain,
}
@@ -564,7 +564,7 @@ class DAQWorker(QObject):
if aerotech_detail:
err_msg = aerotech_detail
except Exception:
logger.error(f"Exception reading status response: {e}")
logger.exception("Exception reading status response")
if status == 403:
self._last_status_can_read = False
@@ -592,7 +592,7 @@ class DAQWorker(QObject):
self._last_smargon_connected = None
self._last_aerotech_connected = None
logger.error(f"Exception from status response: {e}")
logger.exception("Exception from status response")
@Slot(QNetworkReply)
def handle_spreadsheet_response(self, reply: QNetworkReply):
@@ -601,7 +601,7 @@ class DAQWorker(QObject):
parsed_response = SampleShortInfoList.model_validate_json(response_data)
self.spreadsheet.emit(parsed_response)
except Exception as e:
logger.error(f"Exception from spreadsheet response: {e}")
logger.exception("Exception from spreadsheet response")
self.http_error.emit(str(e))
@Slot(QNetworkReply)
@@ -611,7 +611,7 @@ class DAQWorker(QObject):
parsed_response = SampleShortInfoList.model_validate_json(response_data)
self.reference_tools.emit(parsed_response)
except Exception as e:
logger.error(f"Exception from reference tools response: {e}")
logger.exception("Exception from reference tools response")
self.http_error.emit(str(e))
def handle_req_response(self, reply: QNetworkReply):
@@ -623,11 +623,12 @@ class DAQWorker(QObject):
try:
raw_body = reply.readAll().data().decode("utf-8")
except Exception:
pass
logger.debug("Could not read the raw reply body", exc_info=True)
try:
url = reply.request().url().toString()
except Exception:
logger.debug("Could not read the request URL", exc_info=True)
url = ""
net_err = reply.error()
@@ -691,7 +692,7 @@ class DAQWorker(QObject):
self.sample_resync_completed.emit(message)
self.send_status_request()
except Exception as e:
logger.error(f"Sample resync failed: {e}")
logger.exception("Sample resync failed")
self.http_error.emit(str(e))
def _handle_detector_metadata_resync_response(self, reply: QNetworkReply):
@@ -705,7 +706,7 @@ class DAQWorker(QObject):
self.send_status_request()
self.load_local_contact_device_state()
except Exception as e:
logger.error(f"Hardware metadata resync failed: {e}")
logger.exception("Hardware metadata resync failed")
self.http_error.emit(str(e))
def _handle_recovery_action_response(self, reply: QNetworkReply, default_message: str):
@@ -718,7 +719,7 @@ class DAQWorker(QObject):
self.recovery_action_completed.emit(message)
self.send_status_request()
except Exception as e:
logger.error(f"Recovery action failed: {e}")
logger.exception("Recovery action failed")
self.http_error.emit(str(e))
def _emit_placeholder_local_contact_action(self, action_name: str) -> None:
@@ -739,7 +740,7 @@ class DAQWorker(QObject):
return
request = QNetworkRequest(QUrl(f"{self._base_url}/{url}"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
if str:
request.setRawHeader(b"Content-Type", b"application/json")
reply = self._net_manager.post(request, QByteArray(body.encode("utf-8")))
@@ -757,7 +758,7 @@ class DAQWorker(QObject):
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.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
if str:
request.setRawHeader(b"Content-Type", b"application/json")
reply = self._net_manager.put(request, QByteArray(body.encode("utf-8")))
@@ -775,7 +776,7 @@ class DAQWorker(QObject):
return
request = QNetworkRequest(QUrl(f"{self._base_url}/{url}"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
reply = self._net_manager.deleteResource(request)
reply.finished.connect(lambda: self.handle_req_response(reply))
@@ -874,7 +875,7 @@ class DAQWorker(QObject):
return
request = QNetworkRequest(QUrl(f"{self._base_url}/state/free_beamline"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
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")))
@@ -889,7 +890,7 @@ class DAQWorker(QObject):
return
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"Authorization", f"Bearer {self._token}".encode())
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")))
@@ -904,7 +905,7 @@ class DAQWorker(QObject):
return
request = QNetworkRequest(QUrl(f"{self._base_url}/recovery/recover_beamline"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
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")))
@@ -921,7 +922,7 @@ class DAQWorker(QObject):
return
request = QNetworkRequest(QUrl(f"{self._base_url}/recovery/unmount_sample"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
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")))
@@ -949,12 +950,12 @@ class DAQWorker(QObject):
arr = json.loads(response_data) if response_data else []
if not isinstance(arr, list):
raise RuntimeError("Invalid all_pgroups payload")
raise TypeError("Invalid all_pgroups payload")
# Ensure list[str]
out = [str(x) for x in arr if isinstance(x, (str, int))]
self.staff_pgroups_loaded.emit(out)
except Exception as e:
logger.error(f"Exception from all_pgroups response: {e}")
logger.exception("Exception from all_pgroups response")
self.http_error.emit(str(e))
@Slot()
@@ -964,7 +965,7 @@ class DAQWorker(QObject):
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.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
request.setRawHeader(b"Content-Type", b"application/json")
reply = self._net_manager.put(request, QByteArray(b""))
reply.finished.connect(lambda: self._handle_all_pgroups_response(reply))
@@ -1017,7 +1018,7 @@ class DAQWorker(QObject):
parsed_response = CompletedRotationScan.model_validate_json(response_data)
self.standard_scan_completed.emit(parsed_response)
except Exception as e:
logger.error(f"Exception from rotation scan response: {e}")
logger.exception("Exception from rotation scan response")
self.http_error.emit(str(e))
finally:
reply.deleteLater()
@@ -1033,7 +1034,7 @@ 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.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
request.setRawHeader(b"Content-Type", b"application/json")
body = r.model_dump_json()
reply = self._net_manager.post(request, QByteArray(body.encode("utf-8")))
@@ -1072,7 +1073,7 @@ class DAQWorker(QObject):
parsed_response = CompletedRasterGrid.model_validate_json(response_data)
self.raster_scan_completed.emit(parsed_response)
except Exception as e:
logger.error(f"Exception from raster scan response: {e}")
logger.exception("Exception from raster scan response")
self.http_error.emit(str(e))
finally:
reply.deleteLater()
@@ -1114,7 +1115,7 @@ class DAQWorker(QObject):
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.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
request.setRawHeader(b"Content-Type", b"application/json")
body = r.model_dump_json()
reply = self._net_manager.post(request, QByteArray(body.encode("utf-8")))
@@ -1156,7 +1157,7 @@ class DAQWorker(QObject):
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.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
request.setRawHeader(b"Content-Type", b"application/json")
body = r.model_dump_json()
reply = self._net_manager.post(request, QByteArray(body.encode("utf-8")))
@@ -1169,7 +1170,7 @@ class DAQWorker(QObject):
return
request = QNetworkRequest(QUrl(f"{self._base_url}/sample/spreadsheet"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
reply = self._net_manager.get(request)
reply.finished.connect(lambda: self.handle_spreadsheet_response(reply))
@@ -1179,7 +1180,7 @@ class DAQWorker(QObject):
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"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
reply = self._net_manager.get(request)
reply.finished.connect(lambda: self.handle_reference_tools_response(reply))
@@ -1242,9 +1243,10 @@ class DAQWorker(QObject):
else:
err_msg = raw_body
except Exception:
logger.debug("Could not parse the error body as JSON", exc_info=True)
err_msg = raw_body
except Exception:
pass
logger.debug("Could not extract error details from the reply body", exc_info=True)
return status, str(err_msg), body_json
@@ -1278,6 +1280,7 @@ class DAQWorker(QObject):
try:
return code in {item.value for item in AuthErrorCode}
except Exception:
logger.debug("Could not classify the reply as an auth error", exc_info=True)
return False
@staticmethod
@@ -1296,6 +1299,7 @@ class DAQWorker(QObject):
try:
status_int = int(status) if status is not None else None
except Exception:
logger.debug("Could not read the reply status code", exc_info=True)
status_int = None
code = None
@@ -1314,14 +1318,11 @@ class DAQWorker(QObject):
):
return True
if (
return bool(
"daq state error" in text
or "must be idle to start measurement" in text
or "must be idle" in text
):
return True
return False
)
def handle_auto_scan_response(self, reply, sample_id: int):
if reply.error() == QNetworkReply.NetworkError.NoError:
@@ -1377,7 +1378,7 @@ class DAQWorker(QObject):
return
request = QNetworkRequest(QUrl(f"{self._base_url}/scan/auto"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
request.setRawHeader(b"Content-Type", b"application/json")
body = s.model_dump_json()
reply = self._net_manager.post(request, QByteArray(body.encode("utf-8")))
@@ -1425,7 +1426,7 @@ class DAQWorker(QObject):
return
request = QNetworkRequest(QUrl(f"{self._base_url}/sample/resync"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
request.setRawHeader(b"Content-Type", b"application/json")
reply = self._net_manager.post(request, QByteArray(b""))
reply.finished.connect(lambda: self._handle_sample_resync_response(reply))
@@ -1437,7 +1438,7 @@ class DAQWorker(QObject):
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.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
request.setRawHeader(b"Content-Type", b"application/json")
reply = self._net_manager.post(request, QByteArray(b""))
reply.finished.connect(lambda: self._handle_detector_metadata_resync_response(reply))
@@ -1477,6 +1478,7 @@ class DAQWorker(QObject):
try:
url = reply.request().url().toString()
except Exception:
logger.debug("Could not read the request URL", exc_info=True)
url = "unknown-url"
return f"{context}\n\nURL: {url}\nHTTP status: {status}\nError: {exc}"
@@ -1499,7 +1501,7 @@ 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"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
reply = self._net_manager.get(request)
reply.finished.connect(lambda: self._handle_local_contact_simulation_state_response(reply))
@@ -1508,9 +1510,10 @@ class DAQWorker(QObject):
response_data = self.handle_response(reply)
payload = json.loads(response_data) if response_data else {}
if not isinstance(payload, dict):
raise RuntimeError("Invalid local contact simulation state payload")
raise TypeError("Invalid local contact simulation state payload")
self.local_contact_simulation_state_loaded.emit(payload)
except Exception as e:
logger.warning("Local Contact simulation state request failed", exc_info=True)
message = self._build_local_contact_error_message(
"Error transferring information from DAQ while loading Local Contact simulation state.",
reply,
@@ -1528,7 +1531,7 @@ class DAQWorker(QObject):
return
request = QNetworkRequest(QUrl(f"{self._base_url}/local_contact/device_state"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
reply = self._net_manager.get(request)
reply.finished.connect(lambda: self._handle_local_contact_device_state_response(reply))
@@ -1537,9 +1540,10 @@ class DAQWorker(QObject):
response_data = self.handle_response(reply)
payload = json.loads(response_data) if response_data else {}
if not isinstance(payload, dict):
raise RuntimeError("Invalid local contact device state payload")
raise TypeError("Invalid local contact device state payload")
self.local_contact_device_state_loaded.emit(payload)
except Exception as e:
logger.warning("Local Contact device state request failed", exc_info=True)
message = self._build_local_contact_error_message(
"Error transferring information from DAQ while loading Local Contact device state.",
reply,
@@ -1557,7 +1561,7 @@ class DAQWorker(QObject):
return
request = QNetworkRequest(QUrl(f"{self._base_url}/local_contact/links"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
reply = self._net_manager.get(request)
reply.finished.connect(lambda: self._handle_local_contact_links_response(reply))
@@ -1566,9 +1570,10 @@ class DAQWorker(QObject):
response_data = self.handle_response(reply)
payload = json.loads(response_data) if response_data else {}
if not isinstance(payload, dict):
raise RuntimeError("Invalid local contact links payload")
raise TypeError("Invalid local contact links payload")
self.local_contact_links_loaded.emit(payload)
except Exception as e:
logger.warning("Local Contact links request failed", exc_info=True)
message = self._build_local_contact_error_message(
"Error transferring information from DAQ while loading Local Contact links.",
reply,
@@ -1589,7 +1594,7 @@ 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"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
reply = self._net_manager.get(request)
reply.finished.connect(lambda: self._handle_local_contact_config_response(reply))
@@ -1598,14 +1603,14 @@ class DAQWorker(QObject):
response_data = self.handle_response(reply)
payload = json.loads(response_data) if response_data else {}
if not isinstance(payload, dict):
raise RuntimeError("Invalid local contact config payload")
raise TypeError("Invalid local contact config payload")
self.local_contact_config_loaded.emit(payload)
except Exception as e:
message = (
"Error transferring information from DAQ while loading Local Contact config.\n\n"
f"{e}"
)
logger.error(message)
logger.exception(message)
self.local_contact_transfer_error.emit(message)
@Slot(dict)
@@ -1615,7 +1620,7 @@ 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"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
request.setRawHeader(b"Content-Type", b"application/json")
body = QByteArray(json.dumps(payload).encode("utf-8"))
reply = self._net_manager.put(request, body)
@@ -1626,7 +1631,7 @@ class DAQWorker(QObject):
response_data = self.handle_response(reply)
payload = json.loads(response_data) if response_data else {}
if not isinstance(payload, dict):
raise RuntimeError("Invalid local contact config response")
raise TypeError("Invalid local contact config response")
self.local_contact_config_saved.emit(payload)
self.local_contact_config_loaded.emit(payload)
self.status_message.emit("Local Contact config saved.", False)
@@ -1634,7 +1639,7 @@ class DAQWorker(QObject):
message = (
f"Error transferring information from DAQ while saving Local Contact config.\n\n{e}"
)
logger.error(message)
logger.exception(message)
self.local_contact_transfer_error.emit(message)
@Slot(str, bool)
@@ -1657,7 +1662,7 @@ class DAQWorker(QObject):
return
request = QNetworkRequest(QUrl(f"{self._base_url}/bec/user_macros"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
reply = self._net_manager.get(request)
reply.finished.connect(lambda: self._handle_bec_user_macros_response(reply))
@@ -1666,10 +1671,10 @@ class DAQWorker(QObject):
response_data = self.handle_response(reply)
payload = json.loads(response_data) if response_data else []
if not isinstance(payload, list):
raise RuntimeError("Invalid BEC user macros payload")
raise TypeError("Invalid BEC user macros payload")
self.bec_user_macros_loaded.emit([str(item) for item in payload])
except Exception as e:
logger.error(f"Failed to list BEC user macros: {e}")
logger.exception("Failed to list BEC user macros")
self.http_error.emit(str(e))
@Slot()
@@ -1680,7 +1685,7 @@ class DAQWorker(QObject):
return
request = QNetworkRequest(QUrl(f"{self._base_url}/bec/devices"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
reply = self._net_manager.get(request)
reply.finished.connect(lambda: self._handle_bec_devices_response(reply))
@@ -1689,10 +1694,10 @@ class DAQWorker(QObject):
response_data = self.handle_response(reply)
payload = json.loads(response_data) if response_data else []
if not isinstance(payload, list):
raise RuntimeError("Invalid BEC devices payload")
raise TypeError("Invalid BEC devices payload")
self.bec_devices_loaded.emit([str(item) for item in payload])
except Exception as e:
logger.error(f"Failed to list BEC devices: {e}")
logger.exception("Failed to list BEC devices")
self.http_error.emit(str(e))
@Slot(str)
@@ -1755,7 +1760,7 @@ class DAQWorker(QObject):
parsed_response = RasterGridRequest.model_validate_json(response_data)
self.raster_generated_by_ml.emit(parsed_response)
except Exception as e:
logger.error(f"Error in ml box: {e}")
logger.exception("Error in ml box")
self.http_error.emit(str(e))
@Slot()
@@ -1768,7 +1773,7 @@ class DAQWorker(QObject):
return
request = QNetworkRequest(QUrl(f"{self._base_url}/alc/ml_bounding_box"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
reply = self._net_manager.post(request, QByteArray(b""))
reply.finished.connect(lambda: self.handle_ml_box_response(reply))
@@ -1783,7 +1788,7 @@ class DAQWorker(QObject):
data = json.loads(payload)
self.face_detection_result.emit(data)
except Exception as e:
logger.error(f"Error in face detection: {e}")
logger.exception("Error in face detection")
self.http_error.emit(str(e))
finally:
reply.deleteLater()
@@ -1797,8 +1802,8 @@ class DAQWorker(QObject):
if payload:
data = json.loads(payload)
self.face_detection_result.emit(data)
except Exception as e:
logger.error(f"Face detection stream parse error: {e}")
except Exception:
logger.exception("Face detection stream parse error")
def _restart_face_detection_stream(self):
reply = self._face_detection_stream_reply
@@ -1809,6 +1814,7 @@ class DAQWorker(QObject):
try:
status = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute)
except Exception:
logger.debug("Could not read the face detection stream status", exc_info=True)
status = None
if status == 403:
@@ -1968,8 +1974,8 @@ class DAQWorker(QObject):
self._automation_progress_buffer += chunk
self._process_automation_progress_buffer()
except Exception as e:
logger.error(f"Automation progress stream parse error: {e}")
except Exception:
logger.exception("Automation progress stream parse error")
def _restart_automation_progress_stream(self):
reply = self._automation_progress_stream_reply
@@ -1981,6 +1987,7 @@ class DAQWorker(QObject):
try:
status = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute)
except Exception:
logger.debug("Could not read the automation progress stream status", exc_info=True)
status = None
if status == 403:
@@ -2006,7 +2013,7 @@ 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"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
reply = self._net_manager.get(request)
reply.readyRead.connect(lambda: self._read_automation_progress_stream(reply))
reply.finished.connect(self._restart_automation_progress_stream)
@@ -2023,7 +2030,7 @@ class DAQWorker(QObject):
return
request = QNetworkRequest(QUrl(f"{self._base_url}/sse/face_detection"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
reply = self._net_manager.get(request)
reply.readyRead.connect(lambda: self._read_face_detection_stream(reply))
reply.finished.connect(self._restart_face_detection_stream)
@@ -2045,7 +2052,7 @@ class DAQWorker(QObject):
request = QNetworkRequest(
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())
request.setRawHeader(b"Content-Type", b"application/json")
reply = self._net_manager.post(request, QByteArray(b""))
reply.finished.connect(lambda: self._handle_face_detection_response(reply))
@@ -2071,7 +2078,7 @@ class DAQWorker(QObject):
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_req.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
status_reply = self._net_manager.get(status_req)
status_reply.finished.connect(
lambda: self._handle_fluorimeter_status_and_emit(data, status_reply)
@@ -2079,7 +2086,7 @@ class DAQWorker(QObject):
else:
self.fluorimeter_update.emit(data, [], -1)
except Exception as e:
logger.error(f"Fluorimeter data error: {e}")
logger.exception("Fluorimeter data error")
self.http_error.emit(str(e))
def _handle_fluorimeter_status_and_emit(self, data, status_reply: QNetworkReply):
@@ -2087,11 +2094,11 @@ class DAQWorker(QObject):
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_req.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
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}")
logger.exception("Fluorimeter status error")
self.http_error.emit(str(e))
def _emit_fluorimeter_with_bkg(self, data, s, b_reply: QNetworkReply):
@@ -2102,7 +2109,7 @@ class DAQWorker(QObject):
bkg = json.loads(bkg_json) if bkg_json else []
self.fluorimeter_update.emit(data, bkg, s)
except Exception as e:
logger.error(f"Fluorimeter background error: {e}")
logger.exception("Fluorimeter background error")
self.http_error.emit(str(e))
@Slot()
@@ -2110,7 +2117,7 @@ class DAQWorker(QObject):
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.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
req.setRawHeader(b"Content-Type", b"application/json")
body = f.model_dump_json()
reply = self._net_manager.post(req, QByteArray(body.encode("utf-8")))
@@ -2122,7 +2129,7 @@ class DAQWorker(QObject):
parsed_response = FluorescenceSpectrumOutputModel.model_validate_json(response_data)
self.fluorimeter_spectrum_update.emit(parsed_response)
except Exception as e:
logger.error(f"Exception from fluorimeter spectrum: {e}")
logger.exception("Exception from fluorimeter spectrum")
self.http_error.emit(str(e))
@Slot()
@@ -2130,7 +2137,7 @@ class DAQWorker(QObject):
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"))
req.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
reply = self._net_manager.get(req)
reply.finished.connect(lambda: self._handle_fluorimeter_data(reply, emit_status=True))
@@ -2139,7 +2146,7 @@ class DAQWorker(QObject):
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"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
reply = self._net_manager.get(request)
reply.readyRead.connect(lambda: self._read_fluorimeter_stream(reply))
reply.finished.connect(lambda: reply.deleteLater())
@@ -2156,8 +2163,8 @@ class DAQWorker(QObject):
bkg = obj.get("background") or []
status = obj.get("status", -1)
self.fluorimeter_update.emit(data, bkg, status)
except Exception as e:
logger.error(f"SSE parse error: {e}")
except Exception:
logger.exception("SSE parse error")
@staticmethod
def _flatten_error_codes_payload(obj: dict) -> dict[str, str]:
@@ -2173,7 +2180,7 @@ class DAQWorker(QObject):
if isinstance(v, dict):
group = str(k)
for kk, vv in v.items():
out[f"{group}.{str(kk)}"] = str(vv)
out[f"{group}.{kk!s}"] = str(vv)
else:
out[str(k)] = str(v)
return out
@@ -2191,13 +2198,13 @@ class DAQWorker(QObject):
return
request = QNetworkRequest(QUrl(f"{self._base_url}/meta/error-codes"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
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"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
reply = self._net_manager.get(request)
reply.finished.connect(lambda: self._handle_error_codes_response(reply))
@@ -2209,6 +2216,7 @@ class DAQWorker(QObject):
try:
url = reply.request().url().toString()
except Exception:
logger.debug("Could not read the request URL", exc_info=True)
url = ""
if int(status) == 404 and url.endswith("/meta/error-codes"):
@@ -2219,11 +2227,11 @@ class DAQWorker(QObject):
payload = self.handle_response(reply)
obj = json.loads(payload) if payload else {}
if not isinstance(obj, dict):
raise RuntimeError("Invalid error-codes payload (expected JSON object)")
raise TypeError("Invalid error-codes payload (expected JSON object)")
out = self._flatten_error_codes_payload(obj)
self.error_codes_loaded.emit(out)
except Exception as e:
logger.error(f"Failed to load error codes: {e}")
logger.exception("Failed to load error codes")
self.http_error.emit(str(e))
@Slot(str, str)
@@ -2255,7 +2263,7 @@ class DAQWorker(QObject):
return
request = QNetworkRequest(QUrl(f"{self._base_url}/sse/baton"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
reply = self._net_manager.get(request)
reply.readyRead.connect(lambda: self._read_baton_stream(reply))
reply.finished.connect(self._restart_baton_stream)
@@ -2299,8 +2307,8 @@ class DAQWorker(QObject):
self._last_baton_status = status
self.baton_status_changed.emit(status)
except Exception as e:
logger.error(f"Baton stream parse error: {e}")
except Exception:
logger.exception("Baton stream parse error")
@Slot()
def request_baton(self):
@@ -2312,7 +2320,7 @@ class DAQWorker(QObject):
return
request = QNetworkRequest(QUrl(f"{self._base_url}/baton/request"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
request.setRawHeader(b"Content-Type", b"application/json")
reply = self._net_manager.post(request, QByteArray(b""))
reply.finished.connect(lambda: self._handle_baton_request_response(reply))
@@ -2335,7 +2343,7 @@ class DAQWorker(QObject):
elif result.get("error"):
self.status_message.emit(result.get("message", "Request failed"), True)
except Exception as e:
logger.error(f"Baton request failed: {e}")
logger.exception("Baton request failed")
self.http_error.emit(str(e))
@Slot(bool)
@@ -2353,7 +2361,7 @@ class DAQWorker(QObject):
request = QNetworkRequest(
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())
request.setRawHeader(b"Content-Type", b"application/json")
reply = self._net_manager.post(request, QByteArray(b""))
reply.finished.connect(lambda: self._handle_baton_response_result(reply))
@@ -2370,7 +2378,7 @@ class DAQWorker(QObject):
self.start_baton_stream()
self._restart_blocked_sse_streams_if_access_restored()
except Exception as e:
logger.error(f"Baton response failed: {e}")
logger.exception("Baton response failed")
self.http_error.emit(str(e))
@Slot()
@@ -2390,7 +2398,7 @@ class DAQWorker(QObject):
return
request = QNetworkRequest(QUrl(f"{self._base_url}/baton/check_timeout"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
reply = self._net_manager.get(request)
reply.finished.connect(lambda: self._handle_baton_timeout_response(reply))
@@ -2405,7 +2413,7 @@ class DAQWorker(QObject):
self.start_baton_stream()
self._restart_blocked_sse_streams_if_access_restored()
except Exception as e:
logger.error(f"Baton timeout check failed: {e}")
logger.exception("Baton timeout check failed")
self.http_error.emit(str(e))
def release_baton_on_close(self):
@@ -2434,7 +2442,7 @@ class DAQWorker(QObject):
logger.info("End session requested on GUI close")
except Exception as e:
logger.warning(f"Error ending session on close: {e}")
logger.warning(f"Error ending session on close: {e}", exc_info=True)
def _handle_gui_sessions_response(self, reply: QNetworkReply):
try:
@@ -2443,7 +2451,7 @@ class DAQWorker(QObject):
sessions = [OpenGuiSessionInfo.model_validate(item) for item in payload]
self.gui_sessions_loaded.emit(sessions)
except Exception as e:
logger.error(f"Failed to load GUI sessions: {e}")
logger.exception("Failed to load GUI sessions")
self.http_error.emit(str(e))
@Slot()
@@ -2453,7 +2461,7 @@ class DAQWorker(QObject):
return
request = QNetworkRequest(QUrl(f"{self._base_url}/admin/gui_sessions"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
reply = self._net_manager.get(request)
reply.finished.connect(lambda: self._handle_gui_sessions_response(reply))
@@ -2470,7 +2478,7 @@ class DAQWorker(QObject):
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())
request.setRawHeader(b"Content-Type", b"application/json")
reply = self._net_manager.post(request, QByteArray(b""))
reply.finished.connect(lambda: self._handle_gui_session_mutation_response(reply))
@@ -2482,7 +2490,7 @@ class DAQWorker(QObject):
return
request = QNetworkRequest(QUrl(f"{self._base_url}/admin/gui_sessions/{session_id}"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode("utf-8"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
reply = self._net_manager.deleteResource(request)
reply.finished.connect(lambda: self._handle_gui_session_mutation_response(reply))
@@ -2496,7 +2504,7 @@ class DAQWorker(QObject):
_ = self.handle_response(reply)
self.load_gui_sessions()
except Exception as e:
logger.error(f"GUI session mutation failed: {e}")
logger.exception("GUI session mutation failed")
self.http_error.emit(str(e))
self.load_gui_sessions()
@@ -2508,7 +2516,7 @@ class DAQWorker(QObject):
request = QNetworkRequest(
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())
request.setRawHeader(b"Content-Type", b"application/json")
reply = self._net_manager.post(request, QByteArray(b""))
reply.finished.connect(lambda: reply.deleteLater())
@@ -2522,13 +2530,13 @@ class DAQWorker(QObject):
if hasattr(self, "_baton_timeout_timer") and self._baton_timeout_timer is not None:
self._baton_timeout_timer.stop()
except Exception as e:
logger.warning(f"Failed to stop _baton_timeout_timer: {e}")
logger.warning(f"Failed to stop _baton_timeout_timer: {e}", exc_info=True)
try:
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}")
logger.warning(f"Failed to stop __timer: {e}", exc_info=True)
for attr_name in (
"_baton_stream_reply",
@@ -2542,12 +2550,12 @@ class DAQWorker(QObject):
try:
reply.abort()
except Exception as e:
logger.warning(f"Failed to abort {attr_name}: {e}")
logger.warning(f"Failed to abort {attr_name}: {e}", exc_info=True)
try:
reply.deleteLater()
except Exception as e:
logger.warning(f"Failed to delete {attr_name}: {e}")
logger.warning(f"Failed to delete {attr_name}: {e}", exc_info=True)
setattr(self, attr_name, None)
+7 -2
View File
@@ -1,7 +1,12 @@
from aarecommon.config.beamline import get_jfjoch_url, mx_beamline
from aarecommon.config.logger import setup_logger
from PySide6.QtCore import QObject, Slot
from PySide6.QtDBus import QDBusConnection, QDBusInterface
from aare.gui.constants import LOGGER_NAME
logger = setup_logger(LOGGER_NAME)
class JFJochDBusClient(QObject):
def __init__(self):
@@ -25,8 +30,8 @@ class JFJochDBusClient(QObject):
else:
print("D-Bus not available")
except Exception as e:
print(f"D-Bus not available: {e}.")
except Exception:
logger.warning("D-Bus not available", exc_info=True)
def _ensure_interface(self):
"""Ensure we have a valid interface, creating one if needed."""
@@ -91,6 +91,7 @@ class PredictionSubscriber(QThread):
decoded = json.loads(part.decode("utf-8"))
return decoded if isinstance(decoded, dict) else None
except Exception:
logger.debug("Could not parse the prediction payload as JSON", exc_info=True)
return None
def _decode_rgb_image(self, header: dict, data: bytes) -> np.ndarray | None:
@@ -231,13 +232,13 @@ class PredictionSubscriber(QThread):
except Exception as e:
if self.running:
self._set_camera_available(False, f"Sample camera feed unavailable: {e}")
logger.exception(f"PredictionSubscriber error: {e}")
logger.exception("PredictionSubscriber error")
finally:
try:
if self._sock is not None:
self._sock.close(0)
except Exception:
pass
logger.debug("Error closing the prediction ZMQ socket", exc_info=True)
finally:
self._sock = None
@@ -245,7 +246,7 @@ class PredictionSubscriber(QThread):
if self._ctx is not None:
self._ctx.term()
except Exception:
pass
logger.debug("Error terminating the prediction ZMQ context", exc_info=True)
finally:
self._ctx = None
@@ -257,7 +258,7 @@ class PredictionSubscriber(QThread):
if self._sock is not None:
self._sock.close(0)
except Exception:
pass
logger.debug("Error while stopping the prediction subscriber", exc_info=True)
if not self.wait(1500):
logger.warning("PredictionSubscriber did not stop within timeout")
+9 -9
View File
@@ -1,6 +1,7 @@
from typing import Optional, Dict
from PySide6.QtCore import QObject, Signal, Slot, QUrl, QTimer, QByteArray
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply, QSslError
from typing import ClassVar
from PySide6.QtCore import QByteArray, QObject, QTimer, QUrl, Signal, Slot
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest, QSslError
class SSEClient(QObject):
@@ -15,13 +16,13 @@ class SSEClient(QObject):
super().__init__(parent)
self._network_manager = QNetworkAccessManager(self)
self._network_manager.sslErrors.connect(self._handle_ssl_errors)
self._reply: Optional[QNetworkReply] = None
self._reply: QNetworkReply | None = None
self._reconnect_timer = QTimer(self)
self._reconnect_timer.setSingleShot(True)
self._reconnect_timer.timeout.connect(self._attempt_reconnect)
self._url = QUrl()
self._headers: Dict[str, str] = {}
self._headers: dict[str, str] = {}
self._buffer = QByteArray()
self._connected = False
self._reconnect_delay = 1000 # Start with 1 second
@@ -32,7 +33,7 @@ class SSEClient(QObject):
self._current_data = ""
self._current_id = ""
def connect_to_sse(self, url: str, headers: Optional[Dict[str, str]] = None):
def connect_to_sse(self, url: str, headers: dict[str, str] | None = None):
"""Connect to SSE endpoint"""
self._url = QUrl(url)
self._headers = headers or {}
@@ -61,7 +62,7 @@ class SSEClient(QObject):
"""Check if connected to SSE"""
return self._connected and self._reply and self._reply.isOpen()
_SELF_SIGNED_ERRORS = {
_SELF_SIGNED_ERRORS: ClassVar[set[QSslError.SslError]] = {
QSslError.SslError.SelfSignedCertificate,
QSslError.SslError.SelfSignedCertificateInChain,
}
@@ -155,8 +156,7 @@ class SSEClient(QObject):
value = line[colon_index + 1 :]
# Remove leading space from value
if value.startswith(" "):
value = value[1:]
value = value.removeprefix(" ")
if field == "data":
if self._current_data:
@@ -1,5 +1,5 @@
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QDialog, QVBoxLayout, QTextEdit, QDialogButtonBox, QTabWidget, QWidget
from PySide6.QtWidgets import QDialog, QDialogButtonBox, QTabWidget, QTextEdit, QVBoxLayout, QWidget
class ControlsHelpDialog(QDialog):
+18 -12
View File
@@ -4,11 +4,12 @@ import time
from dataclasses import dataclass
from typing import Any
from aarecommon.config.logger import setup_logger
from PySide6.QtCore import (
Property,
QEasingCurve,
QObject,
QPropertyAnimation,
Property,
QRect,
Qt,
QTimer,
@@ -17,6 +18,7 @@ from PySide6.QtCore import (
from PySide6.QtGui import QColor, QPainter, QPen
from PySide6.QtWidgets import QLabel, QPushButton, QWidget
from aare.gui.constants import LOGGER_NAME
from aare.gui.tutorials.tutorial_models import (
StepFlow,
StepStatus,
@@ -36,6 +38,8 @@ from aare.gui.tutorials.tutorial_runtime import (
ensure_step_runtime_state,
)
logger = setup_logger(LOGGER_NAME)
def _now_ms() -> int:
return int(time.time() * 1000)
@@ -169,11 +173,14 @@ class TutorialOverlay(QWidget):
super().keyPressEvent(event)
def mousePressEvent(self, event) -> None:
if self._waiting_for_click and self.current_rect.isValid():
if self.current_rect.contains(event.pos()):
self.highlight_clicked.emit()
event.accept()
return
if (
self._waiting_for_click
and self.current_rect.isValid()
and self.current_rect.contains(event.pos())
):
self.highlight_clicked.emit()
event.accept()
return
super().mousePressEvent(event)
def paintEvent(self, event) -> None:
@@ -460,8 +467,7 @@ class TutorialManager(QObject):
self.tutorial_stopped.emit(scenario_id)
return
if new_index < 0:
new_index = 0
new_index = max(new_index, 0)
self.runtime_state.active_step_index = new_index
step = self.current_scenario.steps[new_index]
@@ -535,6 +541,7 @@ class TutorialManager(QObject):
try:
return self.target_resolver.resolve_target(step.target, self.context)
except Exception:
logger.debug("Could not resolve the tutorial step target", exc_info=True)
return None
def _resolved_target_rect(self, resolved: ResolvedTutorialTarget | None) -> QRect | None:
@@ -688,9 +695,8 @@ class TutorialManager(QObject):
step.skippable
and self.current_scenario is not None
and self.current_scenario.allow_skip
):
if self.runtime_state is not None:
self._advance_to_index(self.runtime_state.active_step_index + 1)
return
) and self.runtime_state is not None:
self._advance_to_index(self.runtime_state.active_step_index + 1)
return
self.overlay.next_button.setEnabled(True)
+3 -3
View File
@@ -51,7 +51,7 @@ class CompletionKind(str, Enum):
class CompletionRule:
kind: CompletionKind
value: Any = None
children: list["CompletionRule"] = field(default_factory=list)
children: list[CompletionRule] = field(default_factory=list)
description: TutorialTextRef | None = None
@@ -121,7 +121,7 @@ class TutorialScenario:
title: TutorialTextRef
description: TutorialTextRef
mode: TutorialMode
steps: list["TutorialStepDefinition"]
steps: list[TutorialStepDefinition]
version: str = "1.0"
tags: set[str] = field(default_factory=set)
@@ -146,6 +146,6 @@ class TutorialContext:
completed_step_ids: list[str] = field(default_factory=list)
state: dict[str, Any] = field(default_factory=dict)
event_log: list["TutorialEvent"] = field(default_factory=list)
event_log: list[TutorialEvent] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict)
@@ -3,9 +3,11 @@ from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Protocol, runtime_checkable
from aarecommon.config.logger import setup_logger
from PySide6.QtCore import QObject, QRect, Signal
from PySide6.QtWidgets import QWidget
from aare.gui.constants import LOGGER_NAME
from aare.gui.tutorials.tutorial_models import (
CompletionKind,
CompletionRule,
@@ -19,6 +21,8 @@ from aare.gui.tutorials.tutorial_models import (
TutorialTextRef,
)
logger = setup_logger(LOGGER_NAME)
@dataclass(slots=True)
class ResolvedTutorialTarget:
@@ -148,6 +152,7 @@ class DictionaryTextResolver:
try:
return template.format(**text.args)
except Exception:
logger.debug("Could not resolve the tutorial text template", exc_info=True)
return template
+1 -1
View File
@@ -112,7 +112,7 @@ class CompactAutomationProgressStrip(QFrame):
def _format_duration(seconds: float | None) -> str:
if seconds is None or seconds <= 0:
return "0m 00s"
total = int(round(seconds))
total = round(seconds)
minutes, secs = divmod(total, 60)
if minutes < 60:
return f"{minutes}m {secs:02d}s"
+6 -7
View File
@@ -1,14 +1,14 @@
from PySide6.QtCore import Qt, Signal, QTimer
from PySide6.QtCore import Qt, QTimer, Signal
from PySide6.QtGui import QFont
from PySide6.QtWidgets import (
QDialog,
QVBoxLayout,
QFrame,
QHBoxLayout,
QLabel,
QPushButton,
QProgressBar,
QFrame,
QPushButton,
QVBoxLayout,
)
from PySide6.QtGui import QFont
class BatonRequestDialog(QDialog):
@@ -317,8 +317,7 @@ class BatonPendingDialog(QDialog):
def _tick(self):
self._remaining -= 1
if self._remaining < 0:
self._remaining = 0
self._remaining = max(self._remaining, 0)
self.progress.setValue(self._remaining)
self.time_label.setText(f"{self._remaining} seconds remaining")
+30 -25
View File
@@ -193,7 +193,7 @@ class SampleCameraImageLabel(QGraphicsView):
self._det_shape = payload.get("shape", None)
self._detections = payload.get("boxes", []) or []
except Exception as e:
logger.debug(f"Exception in update_detections: {e}")
logger.debug(f"Exception in update_detections: {e}", exc_info=True)
self._detections = []
self.update()
@@ -224,7 +224,7 @@ class SampleCameraImageLabel(QGraphicsView):
)
self._last_target_update_ts = now
except Exception as e:
logger.debug(f"Exception in update_target_point: {e}")
logger.debug(f"Exception in update_target_point: {e}", exc_info=True)
self._target_point = None
self._smoothed_target_point = None
self.update()
@@ -405,12 +405,11 @@ class SampleCameraImageLabel(QGraphicsView):
self._state = SampleCameraImageState.RESIZE_RASTER_GRID
else:
self._state = SampleCameraImageState.DRAWING_RASTER_GRID
elif event.button() == Qt.MouseButton.LeftButton:
if (
self._raster_mgr.is_part_of_active_grid(self.start_point)
and not ctrl_override_move
):
self._state = SampleCameraImageState.MOVING_RASTER_GRID
elif event.button() == Qt.MouseButton.LeftButton and (
self._raster_mgr.is_part_of_active_grid(self.start_point)
and not ctrl_override_move
):
self._state = SampleCameraImageState.MOVING_RASTER_GRID
def _update_grid(self):
now = time.monotonic()
@@ -446,13 +445,14 @@ class SampleCameraImageLabel(QGraphicsView):
self.mapToGlobal(event.pos()), f"{mouse_pos.x():.0f}, {mouse_pos.y():.0f} pxl", self
)
if self._state == SampleCameraImageState.IDLE:
if event.modifiers() & Qt.KeyboardModifier.ShiftModifier:
if not self.raster_timer.isActive():
self.load_image.emit(mouse_pos)
self.raster_timer.start(self.raster_timer_interval)
else:
self._pending_load_pos = mouse_pos
if self._state == SampleCameraImageState.IDLE and (
event.modifiers() & Qt.KeyboardModifier.ShiftModifier
):
if not self.raster_timer.isActive():
self.load_image.emit(mouse_pos)
self.raster_timer.start(self.raster_timer_interval)
else:
self._pending_load_pos = mouse_pos
self.end_point = mouse_pos
@@ -613,8 +613,7 @@ class SampleCameraImageLabel(QGraphicsView):
ratio_h = self.viewport().size().height() / self.pixmap_item.boundingRect().height()
ratio = min(ratio_w, ratio_h)
if ratio < 0.1:
ratio = 0.1
ratio = max(ratio, 0.1)
if ratio >= 1.0:
# Don't enable scaling when gain in ratio is < 5% (to avoid back-and-forth)
@@ -736,7 +735,7 @@ class SampleCameraImageLabel(QGraphicsView):
label = str(det.get("label", "")).lower()
conf = det.get("conf", 0.0)
except Exception as e:
logger.debug(f"Error in draw detection {det}: {e}")
logger.debug(f"Error in draw detection {det}: {e}", exc_info=True)
continue
color = color_map.get(label, QColor("magenta"))
@@ -776,7 +775,7 @@ class SampleCameraImageLabel(QGraphicsView):
if isinstance(raw, (list, tuple)) and len(raw) >= 2:
return float(raw[0]), float(raw[1])
except Exception as e:
logger.debug(f"Error parsing target point {raw}: {e}")
logger.debug(f"Error parsing target point {raw}: {e}", exc_info=True)
return None
def _draw_target_point(self, painter: QPainter):
@@ -799,7 +798,10 @@ class SampleCameraImageLabel(QGraphicsView):
img_h, img_w = int(shape[0]), int(shape[1])
tx, ty = self._smoothed_target_point
except Exception as e:
logger.debug(f"Error using smoothed target point {self._smoothed_target_point}: {e}")
logger.debug(
f"Error using smoothed target point {self._smoothed_target_point}: {e}",
exc_info=True,
)
return
sx = pix.width() / float(img_w)
@@ -1122,11 +1124,14 @@ class SampleCameraImageLabel(QGraphicsView):
mouse_view_pos = self.mapFromGlobal(mouse_global_pos)
mouse_scene_pos = self.mapToScene(mouse_view_pos)
if event.key() == Qt.Key.Key_Shift:
if self._state == SampleCameraImageState.IDLE and self._camera_interaction_enabled():
if not self.raster_timer.isActive():
self.load_image.emit(mouse_scene_pos)
self.raster_timer.start(self.raster_timer_interval)
if (
event.key() == Qt.Key.Key_Shift
and self._state == SampleCameraImageState.IDLE
and self._camera_interaction_enabled()
and not self.raster_timer.isActive()
):
self.load_image.emit(mouse_scene_pos)
self.raster_timer.start(self.raster_timer_interval)
super().keyPressEvent(event)
+1 -1
View File
@@ -1,4 +1,4 @@
from PySide6.QtCore import Signal, Qt
from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import QLabel
@@ -1,16 +1,21 @@
from __future__ import annotations
from collections.abc import Iterable
from typing import ClassVar
from aarecommon.config.logger import setup_logger
from aarecommon.models.models import DAQStatusModel
from PySide6.QtCore import Qt, Slot
from PySide6.QtWidgets import QFrame, QGridLayout, QLabel, QSizePolicy, QVBoxLayout
from aare.gui.constants import LOGGER_NAME
from aare.gui.widgets.title_label import TitleLabel
logger = setup_logger(LOGGER_NAME)
class LocalContactStatusWidget(QFrame):
FIELD_TITLES = {
FIELD_TITLES: ClassVar[dict[str, str]] = {
"beamline_state": "Beamline state",
"busy": "Busy",
"sample": "Sample",
@@ -202,6 +207,7 @@ class LocalContactStatusWidget(QFrame):
try:
sample_loc = f" ({status.sample.loc_str()})"
except Exception:
logger.debug("Could not format the sample location", exc_info=True)
sample_loc = ""
if sample_name:
return f"{self._badge('MOUNTED', tone='info')} {sample_name}{sample_loc}"
@@ -351,7 +357,7 @@ class LocalContactStatusWidget(QFrame):
def _refresh(self) -> None:
if self._last_status is None:
for _key, (_title, value) in self._row_widgets.items():
for _title, value in self._row_widgets.values():
value.setText(self._badge("WAITING", tone="neutral"))
return
+2 -2
View File
@@ -114,9 +114,9 @@ def timer_box(
# Auto-accept (as if user pressed Yes)
box.done(QMessageBox.StandardButton.Yes)
stop_timer()
except Exception as e:
except Exception:
# Never let errors in condition_func block UI
logger.error(f"Error in condition_func: {e}")
logger.exception("Error in condition_func")
box.accepted.connect(stop_timer)
box.rejected.connect(stop_timer)
+4 -4
View File
@@ -1,6 +1,6 @@
from PySide6.QtCore import Signal, Slot, Qt
from PySide6.QtCore import Qt, Signal, Slot
from PySide6.QtGui import QDoubleValidator
from PySide6.QtWidgets import QLineEdit, QWidget, QCheckBox, QHBoxLayout
from PySide6.QtWidgets import QCheckBox, QHBoxLayout, QLineEdit, QWidget
class NumberLineEdit(QLineEdit):
@@ -23,7 +23,7 @@ class NumberLineEdit(QLineEdit):
self.setValidator(self.validator)
self.setAlignment(Qt.AlignmentFlag.AlignRight)
self.setToolTip(
"Minimum: {:s}\nMaximum: {:s}".format(self.to_string(min_val), self.to_string(max_val))
f"Minimum: {self.to_string(min_val):s}\nMaximum: {self.to_string(max_val):s}"
)
# Connect the textChanged signal to a custom slot to check validity
@@ -75,7 +75,7 @@ class NumberLineEdit(QLineEdit):
def update_limits(self, min_val: float, max_val: float):
self.validator.setRange(min_val, max_val, self.decimal_count)
self.setToolTip(
"Minimum: {:s}\nMaximum: {:s}".format(self.to_string(min_val), self.to_string(max_val))
f"Minimum: {self.to_string(min_val):s}\nMaximum: {self.to_string(max_val):s}"
)
def validate(self, text) -> bool:
+4 -4
View File
@@ -1,12 +1,12 @@
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QDialog,
QVBoxLayout,
QPushButton,
QLabel,
QComboBox,
QCompleter,
QDialog,
QLabel,
QMessageBox,
QPushButton,
QVBoxLayout,
)
+1 -1
View File
@@ -1,5 +1,5 @@
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QSplashScreen, QProgressBar, QApplication
from PySide6.QtWidgets import QApplication, QProgressBar, QSplashScreen
class LoadingSplashScreen(QSplashScreen):
+5 -7
View File
@@ -230,8 +230,8 @@ class StatusBar(QStatusBar):
html_content_session = f"""Session: {session_flag}"""
self.session_label.setText(html_content_session)
except Exception as e:
logger.error(f"Error updating DAQ status in status bar: {e}")
except Exception:
logger.exception("Error updating DAQ status in status bar")
@Slot(BatonStatus)
def update_baton_status(self, status: BatonStatus):
@@ -263,8 +263,8 @@ class StatusBar(QStatusBar):
logger.info("Baton request no longer incoming, closing local dialog reference")
try:
self._baton_request_dialog.close()
except Exception as e:
logger.error(f"Error closing baton request dialog: {e}")
except Exception:
logger.exception("Error closing baton request dialog")
self._baton_request_dialog = None
def _show_pgroup_after_baton_grant(self) -> None:
@@ -537,8 +537,7 @@ class StatusBar(QStatusBar):
try:
self.staff_pgroups_loaded.disconnect(_on_loaded)
except Exception as e:
logger.debug(f"Error disconnecting: {e}")
pass
logger.debug(f"Error disconnecting: {e}", exc_info=True)
self.staff_pgroups_loaded.connect(_on_loaded)
self._list_staff_pgroups()
@@ -586,7 +585,6 @@ class StatusBar(QStatusBar):
def _list_staff_pgroups(self):
self.get_all_pgroups.emit()
return
def _generate_pgroup_dialogue(self, curr: str | None = None, pgroups: list | None = None):
logger.info(pgroups)
+3 -3
View File
@@ -1,6 +1,6 @@
from PySide6.QtCore import Qt, Slot, QRectF
from PySide6.QtGui import QPainter, QPixmap, QImage, QFont, QColor, QPen, QFontMetrics
from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem
from PySide6.QtCore import QRectF, Qt, Slot
from PySide6.QtGui import QColor, QFont, QFontMetrics, QImage, QPainter, QPen, QPixmap
from PySide6.QtWidgets import QGraphicsPixmapItem, QGraphicsScene, QGraphicsView
from aare.gui.widgets.busy_overlay import BusyOverlayStyle
+3 -3
View File
@@ -50,7 +50,7 @@ def sample_info():
# -------------------------
@pytest.fixture(scope="session")
def server_module():
import aare.daq.server as server
from aare.daq import server
return server
@@ -90,9 +90,9 @@ def client(server_module, mock_backend, auth_token_data):
patch.object(server_module, "mx_beamline", return_value=mock_backend["bl"]),
patch.object(server_module, "BeamlineConfig", return_value=mock_backend["cfg"]),
patch.object(server_module, "AareDAQ", return_value=mock_backend["daq"]),
TestClient(server_module.app) as c,
):
with TestClient(server_module.app) as c:
yield c
yield c
@pytest.fixture
+2 -1
View File
@@ -1,5 +1,6 @@
import pytest
import os
import pytest
from fastapi.testclient import TestClient
# We need to set the environment variable before importing the app
@@ -1,17 +1,18 @@
import numpy as np
import pytest
from aare.daq.operations.face_detection.utils import (
box_height_from_tuple,
box_area_from_tuple,
prepare_samples,
cos_model,
mad_filter,
fit_metrics,
fit_cosine,
get_samples_out,
box_height_from_tuple,
choose_best_fit,
get_flat_face,
chose_best_angle,
cos_model,
fit_cosine,
fit_metrics,
get_flat_face,
get_samples_out,
mad_filter,
prepare_samples,
)
@@ -101,7 +102,7 @@ def test_choose_best_fit():
"Height": {"angle": 45, "params": {"rmse": 0.1, "mae": 0.1, "r2": 0.95}},
"Area": {"angle": 50, "params": {"rmse": 0.05, "mae": 0.05, "r2": 0.98}},
}
angle, fit, name = choose_best_fit(fits)
angle, _fit, name = choose_best_fit(fits)
assert name == "Area"
assert angle == 50.0
@@ -88,7 +88,7 @@ def test_service_succeeds_when_correction_pass_has_valid_target(monkeypatch, con
return AngleAnalysis(
angle_deg=kwargs["angle"],
classes=[MLBoxType.CRYSTAL.value] if kwargs["angle"] == 0 else [],
has_valid_target=True if kwargs["angle"] == 0 else False,
has_valid_target=kwargs["angle"] == 0,
ignore_only=False,
)
return AngleAnalysis(
+9 -7
View File
@@ -130,7 +130,9 @@ def test_create_manual_sample_error(mock_sample, mock_api, mock_bl, sample_info,
wrapper = AareWrapper(bl=mock_bl)
mock_sample.return_value.insert_sample.side_effect = Exception("DB Error")
wrapper.create_manual_sample(sample_info)
assert "Error inserting sample: DB Error" in caplog.text
assert "Error inserting sample" in caplog.text
# the cause now arrives via the logged traceback rather than the message
assert "DB Error" in caplog.text
@patch("aareDB.ApiClient")
@@ -348,25 +350,25 @@ def test_ingest_gridscan_payload_none(mock_api, mock_bl, sample_info, geom_model
# Lines 351-352 in aaredb.py: if payload is None: return
# This happens if format_gridscan_payload returns None.
with patch.object(AareWrapper, "format_gridscan_payload", return_value=None):
wrapper.ingest_gridscan(sample_info, None, None, geom_model, None, (0, 0))
wrapper.ingest_gridscan(sample_info, None, None, geom_model, None, (0, 0)) # type: ignore # intended
with patch.object(AareWrapper, "format_scan_payload", return_value=None):
wrapper.ingest_scan(sample_info, None, geom_model, (0, 0))
wrapper.ingest_scan(sample_info, None, geom_model, (0, 0)) # type: ignore # intended
@patch("aareDB.ApiClient")
def test_format_gridscan_payload_error(mock_api, mock_bl, sample_info, caplog):
wrapper = AareWrapper(bl=mock_bl)
# Passing None for geom_model should trigger an error in smargon_to_picture
with pytest.raises(Exception):
wrapper.format_gridscan_payload(sample_info, None, None, None, None, (0, 0))
with pytest.raises(AttributeError):
wrapper.format_gridscan_payload(sample_info, None, None, None, None, (0, 0)) # type: ignore # intended
assert "NoneType" in caplog.text
@patch("aareDB.ApiClient")
def test_format_scan_payload_error(mock_api, mock_bl, sample_info, caplog):
wrapper = AareWrapper(bl=mock_bl)
with pytest.raises(Exception):
with pytest.raises(AttributeError):
# Passing None for geom should trigger error when accessing geom.beam_size_mm
wrapper.format_scan_payload(sample_info, None, None, (0, 0))
wrapper.format_scan_payload(sample_info, None, None, (0, 0)) # type: ignore # intended
assert "NoneType" in caplog.text
+5 -3
View File
@@ -97,9 +97,11 @@ def test_parse_token():
def test_parse_token_invalid():
with patch("aare.daq.auth.jwt_key", return_value="test_secret"):
with pytest.raises(AuthenticationException):
parse_token("invalid.token.here")
with (
patch("aare.daq.auth.jwt_key", return_value="test_secret"),
pytest.raises(AuthenticationException),
):
parse_token("invalid.token.here")
def test_check_jwt_ro(mock_cfg, token_data):
+2 -1
View File
@@ -1,5 +1,6 @@
import numpy as np
import cv2
import numpy as np
from aare.daq.autofocus import calculate_focus_measure
+3 -3
View File
@@ -1,6 +1,7 @@
import pytest
import numpy as np
from aare.daq.beamcenterfit import beamcenter_fit, Gaussian2Dfit
import pytest
from aare.daq.beamcenterfit import Gaussian2Dfit, beamcenter_fit
def create_synthetic_beam_image(
@@ -50,7 +51,6 @@ def test_beamcenter_fit_no_converge():
# but we want to test the failure path.
# To truly force non-convergence we might need a more extreme case,
# but return None is better than exit() anyway.
pass
def test_beamcenter_fit_no_contours():
+4 -13
View File
@@ -77,20 +77,11 @@ 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, "_cfg", cfg)
daq._cfg = cfg
setattr(
daq,
"_execute_face_detection",
lambda **kwargs: FaceDetectionResult(
success=True,
payload={
"running": False,
"samples": [{"angle": 45}],
"height_fit": {},
"area_fit": {},
},
),
daq._execute_face_detection = lambda **kwargs: FaceDetectionResult(
success=True,
payload={"running": False, "samples": [{"angle": 45}], "height_fit": {}, "area_fit": {}},
)
result = daq.face_detection(steps=7, step_size=30)

Some files were not shown because too many files have changed in this diff Show More