ci: add better linting #121
+77
-7
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
import numpy as np
|
||||
import cv2
|
||||
import numpy as np
|
||||
from scipy.optimize import curve_fit
|
||||
|
||||
|
||||
|
||||
+22
-24
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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,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"]
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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):
|
||||
|
||||
@@ -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
@@ -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
@@ -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")
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()&"))
|
||||
|
||||
Executable → Regular
+4
-12
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import numpy as np
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from aare.daq.autofocus import calculate_focus_measure
|
||||
|
||||
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user