# Conflicts: # common/pyproject.toml # daq/src/aaredaq/spreadsheetupdater.py # gui/pyproject.toml # pyproject.toml # src/aare/daq/tellupdater.py
This commit is contained in:
@@ -1,24 +1,17 @@
|
||||
name: Build and Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
branches:
|
||||
- master
|
||||
- deploy
|
||||
- 'feature/**'
|
||||
- gitea-pages
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- deploy
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
|
||||
env:
|
||||
DEPLOY_BRANCH: gitea-pages
|
||||
PREVIEWS_DIR: previews
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -35,24 +28,8 @@ jobs:
|
||||
|
||||
- name: Build the common wheel
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
cd common/
|
||||
python -m build
|
||||
mv dist/* ../dist
|
||||
|
||||
- name: Build the GUI wheel
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
cd gui/
|
||||
python -m build
|
||||
mv dist/* ../dist
|
||||
|
||||
- name: Build the DAQ wheel
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
cd daq/
|
||||
python -m build
|
||||
mv dist/* ../dist
|
||||
source venv/bin/activate
|
||||
python -m build
|
||||
|
||||
- name: Upload Package to Gitea PyPI
|
||||
if: github.ref == 'refs/heads/master' && contains(github.event.head_commit.message, 'build and publish')
|
||||
@@ -63,146 +40,3 @@ jobs:
|
||||
source venv/bin/activate
|
||||
twine upload --repository-url https://gitea.psi.ch/api/packages/mx/pypi \
|
||||
dist/*
|
||||
|
||||
docs:
|
||||
name: Build and Deploy Docs
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # needed for pushing branches/tags
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Build Sphinx HTML
|
||||
working-directory: daq/docs
|
||||
run: |
|
||||
# ensure on master
|
||||
git fetch origin master:master || true
|
||||
git checkout master || true
|
||||
|
||||
# create venv one level up and activate it
|
||||
python3 -m venv ../venv
|
||||
source ../venv/bin/activate
|
||||
|
||||
# ensure pip and Sphinx are installed in this venv
|
||||
pip install --upgrade pip
|
||||
pip install "Sphinx==8.2.3"
|
||||
pip install myst-parser
|
||||
pip install sphinx_immaterial
|
||||
pip install linkify-it-py
|
||||
|
||||
if [ -f requirements.txt ]; then
|
||||
pip install -r requirements.txt
|
||||
elif [ -f ../docs/requirements.txt ]; then
|
||||
pip install -r ../docs/requirements.txt
|
||||
else
|
||||
# fallback to known packages if requirements file is not present
|
||||
pip install "Sphinx==8.2.3" myst-parser sphinx_immaterial linkify-it-py
|
||||
fi
|
||||
|
||||
|
||||
# install project editable without deps (avoid private packages)
|
||||
pip install -e ../ --no-deps
|
||||
|
||||
# prefer the sphinx-apidoc CLI if available; otherwise try module fallback
|
||||
if command -v sphinx-apidoc > /dev/null 2>&1; then
|
||||
sphinx-apidoc -o modules/ ../src --separate --module-first --force --remove-old
|
||||
else
|
||||
# module fallback: some Sphinx installs expose the module at sphinx.ext.apidoc
|
||||
python -m sphinx.ext.apidoc -o modules/ ../src --separate --module-first --force --remove-old
|
||||
fi
|
||||
|
||||
|
||||
# Build HTML (source is '.' because working-directory is docs)
|
||||
# Run sphinx-build and capture exit code and full output to help debugging if it fails.
|
||||
sphinx-build -b html . _build/html || {
|
||||
echo "=== Sphinx build failed with exit $?: showing build output and tree ==="
|
||||
echo "Contents of docs/ after build attempt:"
|
||||
ls -la
|
||||
echo "Contents of daq/docs/_build (if any):"
|
||||
ls -la _build || true
|
||||
# If sphinx produced a build log file, print it
|
||||
if [ -f sphinx-build.log ]; then
|
||||
echo "---- sphinx-build.log ----"
|
||||
cat sphinx-build.log
|
||||
fi
|
||||
# Exit with non-zero to fail the job (avoid deploying empty site)
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Sanity check: ensure HTML output exists before continuing to deploy
|
||||
if [ ! -d _build/html ] || [ -z "$(ls -A _build/html)" ]; then
|
||||
echo "ERROR: daq/docs/_build/html does not exist or is empty after successful sphinx-build."
|
||||
ls -la _build || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Deploy to gitea-pages branch (Gitea)
|
||||
env:
|
||||
GIT_USER: "Martin Appleby (Gitea)"
|
||||
GIT_EMAIL: "martin.appleby@psi.ch"
|
||||
DEPLOY_TOKEN: ${{ secrets.DOCS_DEPLOY_TOKEN }}
|
||||
run: |
|
||||
set -e
|
||||
git config --global user.name "$GIT_USER"
|
||||
git config --global user.email "$GIT_EMAIL"
|
||||
|
||||
# Deploy strategy: create/update branch gitea-pages and replace its contents
|
||||
# with the generated daq/docs/_build/html. This keeps only the documentation
|
||||
# on the gitea-pages branch.
|
||||
# Confirm build output exists (relative to repository root)
|
||||
BUILD_DIR="daq/docs/_build/html"
|
||||
echo "Checking build output at: $BUILD_DIR"
|
||||
if [ ! -d "$BUILD_DIR" ] || [ -z "$(ls -A "$BUILD_DIR")" ]; then
|
||||
echo "ERROR: $BUILD_DIR does not exist or is empty"
|
||||
echo "Listing docs/:"
|
||||
ls -la docs || true
|
||||
echo "Listing daq/docs/_build:"
|
||||
ls -la daq/docs/_build || true
|
||||
exit 1
|
||||
fi
|
||||
# Work in a temporary clone to avoid touching the runner workspace
|
||||
TMPDIR=$(mktemp -d)
|
||||
REPO_URL="https://$DEPLOY_TOKEN@gitea.psi.ch/${{ github.repository }}"
|
||||
git clone "$REPO_URL" "$TMPDIR"
|
||||
cd "$TMPDIR"
|
||||
|
||||
# Create or switch to orphan branch gitea-pages
|
||||
if git rev-parse --verify --quiet gitea-pages >/dev/null; then
|
||||
git checkout gitea-pages
|
||||
# Make sure working tree matches branch (hard reset)
|
||||
git fetch --prune origin gitea-pages || true
|
||||
git reset --hard origin/gitea-pages || git reset --hard
|
||||
else
|
||||
git checkout --orphan gitea-pages
|
||||
fi
|
||||
# Remove all files tracked in the branch (leave .git)
|
||||
git rm -rf . || true
|
||||
# Also remove untracked files to ensure clean branch state
|
||||
git clean -fdx || true
|
||||
|
||||
# Copy built HTML into the clone root
|
||||
echo "Copying built HTML into branch root..."
|
||||
(cd "${GITHUB_WORKSPACE}/${BUILD_DIR}" && tar -cf - .) | tar -xf - -C "$TMPDIR"
|
||||
|
||||
# Ensure index.html exists in destination as sanity check
|
||||
if [ ! -f "$TMPDIR/index.html" ] && [ ! -f "$TMPDIR/index.htm" ]; then
|
||||
# If site root index is not present, try to check subfolder
|
||||
echo "Warning: no index.html found at branch root after copy. Listing files:"
|
||||
ls -la "$TMPDIR" || true
|
||||
fi
|
||||
|
||||
git add --all
|
||||
if git diff --quiet --cached; then
|
||||
echo "No changes to deploy"
|
||||
exit 0
|
||||
fi
|
||||
COMMIT_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
|
||||
git commit -m "docs: (${COMMIT_DATE}) | auto-update documentation from ${GITHUB_SHA}"
|
||||
git push --force-with-lease origin gitea-pages
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# Changelog
|
||||
## 0.3.0
|
||||
- Merge three repos into one, remove Ultralytics as dependency
|
||||
- **DAQ/Server**:
|
||||
- Workflow assertions to ensure scintillator is down when possible.
|
||||
- Improved aerotech movement and position calculations.
|
||||
- Implementation of camera auto-exposure and zoom-dependent settings.
|
||||
- Added SAS-TT school demo temporary raster function.
|
||||
- Enhanced status polling with robust error handling for `Tell` and `Smargon` communication.
|
||||
- `ml_box` updated with beamline-specific URLs and model class types.
|
||||
- Added separate authentication pathway for staff-only beamline recovery operations.
|
||||
- Improved database message limits for omega (to 2 decimal places).
|
||||
- Added screenshot to database from GUI; face detection sequence and beamline recovery updates.
|
||||
- **GUI**:
|
||||
- Major rework of `predict_subscriber` and prediction stream updates; corrected default ZMQ stream for ML box.
|
||||
- Added FPS counter and sharpness monitoring for sample camera.
|
||||
- Improved camera thread performance and authentication failure handling.
|
||||
- Added X10SA specific camera and prediction configurations.
|
||||
- New `display_status` for user-facing connection/error messages.
|
||||
- Integrated `dev_help` panel with logs and error code search.
|
||||
- Added framework for building GUI tutorials.
|
||||
- Added alert banner for critical disconnection issues (server/Tell).
|
||||
- New screenshot button and controls in sample camera GUI.
|
||||
- Added SSE support for face detection panel updates during automation.
|
||||
- Integrated sample position resync in recovery panel to reduce Tell polling.
|
||||
- **TELL**:
|
||||
- Major refactor: removed redundant code and added simulated client.
|
||||
- Converted wait functions to use inbuilt pshell functionality.
|
||||
- Added error handling and proxy support for connection failures.
|
||||
- Fixed event names for mounting, and added missing events for gripper detection and motion sync.
|
||||
- Refactored SSE connection handling for simplicity and robustness.
|
||||
- **Common & Devices**:
|
||||
- Added centralized `error_codes` and refactored exception handling to use FastAPI handlers.
|
||||
- Expanded device support with new PVs (beamstop, scintillator, collimator, cryojet, etc.).
|
||||
- Cleaned up `mx_lib` wait functions and `my_motor` class; added `clean_filename` helper.
|
||||
- Improved Smargon connection exception handling.
|
||||
- Updated ABR_POS_HOME configuration and added comments for cryo positions.
|
||||
- Added image upload comments and improved authentication error messaging.
|
||||
- **Infrastructure**:
|
||||
- Updated dependencies (added `pshell`) and improved documentation autodeployment workflows.
|
||||
- Refactored package structure and imports.
|
||||
- Fixed `logger_config` file path expansion.
|
||||
|
||||
### 0.2.69
|
||||
- check beamline state before running raster, rotation and automation
|
||||
- lowered polling rate of daq status to improve GUI performance
|
||||
- robot drys then parks after repeated mount failures
|
||||
### 0.2.68:
|
||||
- added check box to numberlineedits to switch between user input and spreadsheet
|
||||
- re-enabled MLprediction
|
||||
- fixed bug in gui where manual sample couldnt be created when run nubmer was negative.
|
||||
- Combined beamline view now split vertically not horizontally
|
||||
- New error handling in DAQ for robot mounting errors
|
||||
- Error box pop ups in GUI when sample gripper detection error occurs
|
||||
- Automation queue pauses if sample gripper detection error occurs multiple times to allow robot dry, if continues stops and wiats for user interverntion
|
||||
- added ingest_scan to send rotation and screening result to db
|
||||
|
||||
### 0.2.67:
|
||||
- added run number to sampleshortinfo
|
||||
- ingest raster result, including beam center to database
|
||||
- aerotech goes to mount postion for manual mount
|
||||
- logger config correctly expands file path for logger location.
|
||||
- smart_rotiation_panel fix - but will be mvoed to DAQ in future update
|
||||
- Fluoresence scan can be saved to CSV with right click
|
||||
- Wait for ready before sending Tell to dry
|
||||
|
||||
### 0.2.66:
|
||||
- moved rastergridingestmodel and centreofmassmodel from models to raster_grid.py
|
||||
- after raster load closest image to new centre
|
||||
@@ -1,25 +0,0 @@
|
||||
[project]
|
||||
name = "aaredaqlib"
|
||||
version = "0.2.72"
|
||||
description = "Libraries shared between AareDAQ and AareGUI"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"pydantic==2.11.4",
|
||||
"numpy==2.2.5",
|
||||
"jfjoch_client==1.0.0rc125",
|
||||
]
|
||||
|
||||
[lint]
|
||||
ignore = ["F401", "F541", "W503", "W504"]
|
||||
|
||||
[tool.uv.sources]
|
||||
aaredb = { index = "psi"}
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "psi"
|
||||
url = "https://gitea.psi.ch/api/packages/mx/pypi/simple"
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=75.6.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
@@ -1,29 +0,0 @@
|
||||
# Changelog
|
||||
## 0.2.70
|
||||
|
||||
### 0.2.69
|
||||
- check beamline state before running raster, rotation and automation
|
||||
- lowered polling rate of daq status to improve GUI performance
|
||||
- robot drys then parks after repeated mount failures
|
||||
### 0.2.68:
|
||||
- added check box to numberlineedits to switch between user input and spreadsheet
|
||||
- re-enabled MLprediction
|
||||
- fixed bug in gui where manual sample couldnt be created when run nubmer was negative.
|
||||
- Combined beamline view now split vertically not horizontally
|
||||
- New error handling in DAQ for robot mounting errors
|
||||
- Error box pop ups in GUI when sample gripper detection error occurs
|
||||
- Automation queue pauses if sample gripper detection error occurs multiple times to allow robot dry, if continues stops and wiats for user interverntion
|
||||
- added ingest_scan to send rotation and screening result to db
|
||||
|
||||
### 0.2.67:
|
||||
- added run number to sampleshortinfo
|
||||
- ingest raster result, including beam center to database
|
||||
- aerotech goes to mount postion for manual mount
|
||||
- logger config correctly expands file path for logger location.
|
||||
- smart_rotiation_panel fix - but will be mvoed to DAQ in future update
|
||||
- Fluoresence scan can be saved to CSV with right click
|
||||
- Wait for ready before sending Tell to dry
|
||||
|
||||
### 0.2.66:
|
||||
- moved rastergridingestmodel and centreofmassmodel from models to raster_grid.py
|
||||
- after raster load closest image to new centre
|
||||
@@ -1,149 +0,0 @@
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# For the full list of built-in configuration values, see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add project root to Python path so autodoc can import modules
|
||||
sys.path.insert(0, os.path.abspath('..'))
|
||||
sys.path.insert(0, os.path.abspath('../src')) # <-- ensure 'src' is on path
|
||||
sys.path.insert(0, os.path.abspath('../src/aaredaq'))
|
||||
sys.path.insert(0, os.path.abspath('../src/mxlibs3'))
|
||||
sys.path.insert(0, os.path.abspath('../../common'))
|
||||
sys.path.insert(0, os.path.abspath('../../common/src'))
|
||||
sys.path.insert(0, os.path.abspath('../../common/src/aaredaqlib'))
|
||||
sys.path.insert(0, os.path.abspath('../../gui'))
|
||||
sys.path.insert(0, os.path.abspath('../../gui/src'))
|
||||
sys.path.insert(0, os.path.abspath('../../gui/src/aaregui'))
|
||||
|
||||
project = 'AareDAQ'
|
||||
copyright = '2025, Paul Scherrer Institute'
|
||||
author = 'M. Vears Appleby'
|
||||
release = '0.2.70'
|
||||
master_doc = 'index'
|
||||
root_doc = 'index'
|
||||
# -- General configuration ---------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||
|
||||
autodoc_mock_imports = [
|
||||
'cv2',
|
||||
'redis',
|
||||
'jfjoch_client',
|
||||
'epics',
|
||||
'sseclient',
|
||||
'jwt',
|
||||
'aaredb',
|
||||
'aareDBclient',
|
||||
'numpy',
|
||||
'requests',
|
||||
'PyYAML',
|
||||
'loguru',
|
||||
'utility_tools',
|
||||
'extract_results',
|
||||
'testxds',
|
||||
'db_slurm_handler',
|
||||
'websocket',
|
||||
'dateutil',
|
||||
'PySide6',
|
||||
'scipy',
|
||||
'redis_lock',
|
||||
'fastapi',
|
||||
'uvicorn',
|
||||
'pydantic'
|
||||
]
|
||||
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
extensions = [
|
||||
'myst_parser',
|
||||
'sphinx_immaterial',
|
||||
'sphinx.ext.autodoc', # Add this
|
||||
'sphinx.ext.viewcode', # Optional: source code links
|
||||
'sphinx.ext.napoleon' # Optional: Google/NumPy docstring support
|
||||
]
|
||||
|
||||
|
||||
templates_path = ['_templates']
|
||||
exclude_patterns = []
|
||||
myst_enable_extensions = ['linkify', 'smartquotes']
|
||||
|
||||
|
||||
myst_heading_anchor = 3
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||
|
||||
html_theme = 'sphinx_immaterial'
|
||||
# html_static_path = ['../broker/redoc-static.html']
|
||||
#html_theme_options = {
|
||||
# 'sidebar_width': '300px',
|
||||
# 'page_width': 'auto'
|
||||
#}
|
||||
|
||||
# html_theme_options = {
|
||||
# 'repo_url': 'https://gitea.psi.ch/mx/aaredaq',
|
||||
# 'repo_name': 'AareDaq',
|
||||
# 'nav_title': 'PSI AareDaq',
|
||||
# # 'html_minify': True,
|
||||
# # 'css_minify': True,
|
||||
# 'globaltoc_depth': 2,
|
||||
# # 'repo_type': 'gitlab',
|
||||
# 'color_primary': 'indigo',
|
||||
# 'color_accent': 'lime',
|
||||
# # 'logo_icon': ''
|
||||
# }
|
||||
|
||||
# html_logo = 'jfjoch.png'
|
||||
# html_favicon = 'jfjoch.png'
|
||||
html_theme_options = {
|
||||
"icon": {
|
||||
"repo": "fontawesome/brands/github", # or gitlab/gitea icon
|
||||
},
|
||||
"site_url": "https://gitea.psi.ch/mx/aaredaq",
|
||||
"repo_url": "https://gitea.psi.ch/mx/aaredaq",
|
||||
"repo_name": "AareDAQ",
|
||||
"palette": [
|
||||
{
|
||||
"media": "(prefers-color-scheme: light)",
|
||||
"scheme": "default",
|
||||
"primary": "indigo",
|
||||
"accent": "lime",
|
||||
"toggle": {
|
||||
"icon": "material/lightbulb-outline",
|
||||
"name": "Switch to dark mode",
|
||||
},
|
||||
},
|
||||
{
|
||||
"media": "(prefers-color-scheme: dark)",
|
||||
"scheme": "slate",
|
||||
"primary": "indigo",
|
||||
"accent": "lime",
|
||||
"toggle": {
|
||||
"icon": "material/lightbulb",
|
||||
"name": "Switch to light mode",
|
||||
},
|
||||
},
|
||||
],
|
||||
"features": [
|
||||
"navigation.expand",
|
||||
"navigation.sections",
|
||||
"navigation.top",
|
||||
"search.share",
|
||||
"toc.follow",
|
||||
],
|
||||
"globaltoc_collapse": False,
|
||||
}
|
||||
html_theme_options["google_fonts"] = []
|
||||
html_sidebars = {
|
||||
"**": ["logo-text.html", "globaltoc.html", "searchbox.html"]
|
||||
}
|
||||
|
||||
|
||||
html_show_sourcelink = False
|
||||
|
||||
# language = "Python"
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
.. AareProc documentation master file, created by
|
||||
sphinx-quickstart on Sun Dec 21 14:22:05 2025.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to AareDAQ's documentation!
|
||||
====================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
:glob:
|
||||
|
||||
README
|
||||
CHANGELOG
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
:caption: OpenAPI Python client
|
||||
|
||||
modules/src.aaredaq
|
||||
modules/src.mxlibs3
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
alabaster==1.0.0
|
||||
babel==2.16.0
|
||||
beautifulsoup4==4.12.3
|
||||
certifi==2024.8.30
|
||||
charset-normalizer==3.4.0
|
||||
css-html-js-minify==2.5.5
|
||||
docutils==0.21.2
|
||||
idna==3.10
|
||||
imagesize==1.4.1
|
||||
Jinja2==3.1.4
|
||||
linkify-it-py==2.0.3
|
||||
lxml==5.3.0
|
||||
markdown-it-py==3.0.0
|
||||
MarkupSafe==3.0.2
|
||||
mdit-py-plugins==0.4.2
|
||||
mdurl==0.1.2
|
||||
myst-parser==4.0.0
|
||||
packaging==24.2
|
||||
Pygments==2.18.0
|
||||
python-slugify==8.0.4
|
||||
PyYAML==6.0.2
|
||||
requests==2.32.3
|
||||
snowballstemmer==2.2.0
|
||||
soupsieve==2.6
|
||||
Sphinx==8.1.3
|
||||
sphinx-material==0.0.36
|
||||
sphinxcontrib-applehelp==2.0.0
|
||||
sphinxcontrib-devhelp==2.0.0
|
||||
sphinxcontrib-htmlhelp==2.1.0
|
||||
sphinxcontrib-jsmath==1.0.1
|
||||
sphinxcontrib-qthelp==2.0.0
|
||||
sphinxcontrib-serializinghtml==2.0.0
|
||||
text-unidecode==1.3
|
||||
uc-micro-py==1.0.3
|
||||
Unidecode==1.3.8
|
||||
urllib3==2.2.3
|
||||
sphinx_immaterial==0.13.8
|
||||
Binary file not shown.
@@ -1,351 +0,0 @@
|
||||
import time
|
||||
|
||||
from epics import PV
|
||||
|
||||
from aaredaqlib.beamline import MXBeamline
|
||||
from aaredaqlib.coordinate import Coordinate
|
||||
from aaredaqlib.models import SampleCameraSettings, StagePositionEnum
|
||||
from mxlibs3 import aerotech, smargon
|
||||
from mxlibs3.area_detector import epicsAD
|
||||
from mxlibs3.enum_pv import EnumPv
|
||||
from mxlibs3.filter_transmission import FilterTransmission
|
||||
from mxlibs3.fluorimeter import Fluorimeter
|
||||
from mxlibs3.experimental_hutch_shutter import ExperimentalHutchShutter
|
||||
from mxlibs3.my_motor import MyMotor
|
||||
from mxlibs3.non_standard import NonStandard
|
||||
from mxlibs3.tell_client import TellClient
|
||||
from mxlibs3.workflow_tools import wait_position
|
||||
|
||||
class BeamlineDevices:
|
||||
def __init__(self, beamline: MXBeamline):
|
||||
#service_config = ServiceConfig(redis={"host": "x06da-bec-001.psi.ch", "port": 6379})
|
||||
#client = BECClient(service_config, name="Filips-Custom-Client")
|
||||
#client.start()
|
||||
|
||||
#self.__bec_dev = client.device_manager.devices
|
||||
#self.__bec_scans = client.scans
|
||||
|
||||
BEAMLINE = beamline.value.upper()
|
||||
|
||||
self.tell = TellClient(beamline)
|
||||
self.smargon = smargon.Smargon(beamline)
|
||||
|
||||
self.__energy_pv = PV(f"{BEAMLINE}-OP-DCCM:ENERGY1")
|
||||
self.__ring_current_pv = PV(f"ARS07-DPCT-0100:CURR")
|
||||
self.__cryojet_temp = PV(f"{BEAMLINE}-ES-CRSM:TEMP_RBV")
|
||||
|
||||
self.magnet_position_sensor = PV(f"{BEAMLINE}-ES-DF1:CBOX-CMP1")
|
||||
self.magnet_position_sensor_readout = PV(f"{BEAMLINE}-ES-DF1:CBOX-USER1")
|
||||
|
||||
self.dtz = MyMotor(f"{BEAMLINE}-ES-DET:TRZ1")
|
||||
|
||||
# self.dty = MyMotor(f"{BEAMLINE}-MOCK-DET:TRY1")
|
||||
|
||||
self.bsz = MyMotor(f"{BEAMLINE}-ES-BS:TRZ1")
|
||||
|
||||
self.bsx = MyMotor(f"{BEAMLINE}-ES-BS:TRX1")
|
||||
self.bsy = MyMotor(f"{BEAMLINE}-ES-BS:TRY1")
|
||||
|
||||
self.__bpm = PV(f"{BEAMLINE}-OP-XBPM1:SumAll:MeanValue_RBV")
|
||||
self.__anneal = PV(f"{BEAMLINE}-ES-ANN:SET_POS")
|
||||
|
||||
self.aerotech = aerotech.Abr(beamline)
|
||||
|
||||
self.aerotech.reload_programs()
|
||||
|
||||
self.gmx = NonStandard(
|
||||
name="GMX",
|
||||
setpv=f"{BEAMLINE}-ES-DF1:GMX-VAL",
|
||||
getpv=f"{BEAMLINE}-ES-DF1:GMX-RBV",
|
||||
speed=(f"{BEAMLINE}-ES-DF1:GMX-SETV", f"{BEAMLINE}-ES-DF1:GMX-SETV"),
|
||||
move_done_when=(f"{BEAMLINE}-ES-DF1:GMX-DONE", 1),
|
||||
)
|
||||
|
||||
self.transmission = FilterTransmission(beamline)
|
||||
|
||||
self.gmy = NonStandard(
|
||||
name="GMY",
|
||||
setpv=f"{BEAMLINE}-ES-DF1:GMY-VAL",
|
||||
getpv=f"{BEAMLINE}-ES-DF1:GMY-RBV",
|
||||
speed=(f"{BEAMLINE}-ES-DF1:GMY-SETV", f"{BEAMLINE}-ES-DF1:GMY-SETV"),
|
||||
move_done_when=(f"{BEAMLINE}-ES-DF1:GMY-DONE", 1),
|
||||
)
|
||||
|
||||
self.gmz = NonStandard(
|
||||
name="GMZ",
|
||||
setpv=f"{BEAMLINE}-ES-DF1:GMZ-VAL",
|
||||
getpv=f"{BEAMLINE}-ES-DF1:GMZ-RBV",
|
||||
speed=(f"{BEAMLINE}-ES-DF1:GMZ-SETV", f"{BEAMLINE}-ES-DF1:GMZ-SETV"),
|
||||
move_done_when=(f"{BEAMLINE}-ES-DF1:GMZ-DONE", 1),
|
||||
)
|
||||
|
||||
self.__lamp_light = NonStandard(
|
||||
name="FrontLight",
|
||||
setpv=f"{BEAMLINE}-ES-FL:SET-BRGHT",
|
||||
getpv=f"{BEAMLINE}-ES-FL:SET-BRGHT",
|
||||
tolerance=0.01,
|
||||
predefs={
|
||||
"on": 2.5,
|
||||
"half": 1.9,
|
||||
"off": 1.0,
|
||||
},
|
||||
)
|
||||
|
||||
self.cryo = NonStandard(
|
||||
name="CryoJet",
|
||||
setpv=f"{BEAMLINE}-ES-CJ:TRX1",
|
||||
getpv=f"{BEAMLINE}-ES-CJ:TRX1.RBV",
|
||||
move_done_when=(f"{BEAMLINE}-ES-CJ:TRX1.DMOV", 1),
|
||||
speed=(f"{BEAMLINE}-ES-CJ:TRX1.VELO", f"{BEAMLINE}-ES-CJ:TRX1.VELO"),
|
||||
)
|
||||
|
||||
self.__zoom = NonStandard(
|
||||
name="Zoom",
|
||||
setpv=f"{BEAMLINE}-ES-SAMCAM:ZOOM.VAL",
|
||||
getpv=f"{BEAMLINE}-ES-SAMCAM:ZOOM.RBV",
|
||||
move_done_when=(f"{BEAMLINE}-ES-SAMCAM:ZOOM.DMOV", 1),
|
||||
stoppv=(f"{BEAMLINE}-ES-SAMCAM:ZOOM.STOP", 1),
|
||||
)
|
||||
|
||||
self.__shutter_rbv = PV(f"{BEAMLINE}-ES-PH1:GET")
|
||||
self.__shutter = PV(f"{BEAMLINE}-ES-PH1:SET")
|
||||
|
||||
self.exp_shutter = ExperimentalHutchShutter(beamline)
|
||||
|
||||
# xrf_stage = EnumPv(
|
||||
# {
|
||||
# "name": "KetekStage",
|
||||
# "pv": f"{BEAMLINE}-MOCK-FD:SET-POS",
|
||||
# "readback": f"{BEAMLINE}-MOCK-FD:GET-POS",
|
||||
# "states": {"park": "park", "measure": "measure"},
|
||||
# }
|
||||
# )
|
||||
|
||||
self.__reflector = EnumPv(
|
||||
{
|
||||
"name": "BackReflector",
|
||||
"pv": f"{BEAMLINE}-ES-BL:SET-POS",
|
||||
"readback": f"{BEAMLINE}-ES-BL:GET-POS",
|
||||
"states": {
|
||||
"park": "park",
|
||||
"measure": "measure",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
self.detector_cover = PV(f"{BEAMLINE}-ES-DETCOV:SET")
|
||||
|
||||
self.__beamstop_stage = EnumPv(
|
||||
{
|
||||
"name": "Beamstop_Stage",
|
||||
"pv": f"{BEAMLINE}-ES-BS:SET-POS",
|
||||
"readback": f"{BEAMLINE}-ES-BS:GET-POS",
|
||||
"states": {
|
||||
"park": "park",
|
||||
"measure": "measure",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
self.__collimator = EnumPv(
|
||||
{
|
||||
"name": "Collimator",
|
||||
"pv": f"{BEAMLINE}-ES-COL:SET-POS",
|
||||
"readback": f"{BEAMLINE}-ES-COL:GET-POS",
|
||||
"states": {
|
||||
"parking": "parking",
|
||||
"measure": "measure",
|
||||
"down": "down",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
self.__scinti = EnumPv(
|
||||
{
|
||||
"name": "Scintillator",
|
||||
"pv": f"{BEAMLINE}-ES-SCL:SET-POS",
|
||||
"readback": f"{BEAMLINE}-ES-SCL:GET-POS",
|
||||
"states": {
|
||||
"parking": "parking",
|
||||
"measure": "measure",
|
||||
"down": "down",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
self.sample_cam = epicsAD(f"{BEAMLINE}-SAMCAM:")
|
||||
|
||||
self.fluorimeter = Fluorimeter(beamline)
|
||||
|
||||
|
||||
@property
|
||||
def lamp_light(self) -> float:
|
||||
return self.__lamp_light.value
|
||||
|
||||
@lamp_light.setter
|
||||
def lamp_light(self, v: float):
|
||||
self.__lamp_light.move(v, wait=False)
|
||||
|
||||
@property
|
||||
def zoom(self) -> float:
|
||||
return self.__zoom.value
|
||||
|
||||
@zoom.setter
|
||||
def zoom(self, value: float):
|
||||
self.__zoom.move(value, wait=False)
|
||||
# status = self.__bec_dev.samzoom.move(value)
|
||||
# status.wait()
|
||||
|
||||
def zoom_sync(self, value: float):
|
||||
self.__zoom.move(value, wait=True)
|
||||
|
||||
@property
|
||||
def collimator(self) -> StagePositionEnum:
|
||||
if self.__collimator.position_is("down"):
|
||||
return StagePositionEnum.DOWN
|
||||
elif self.__collimator.position_is("measure"):
|
||||
return StagePositionEnum.MEASURE
|
||||
elif self.__collimator.position_is("parking"):
|
||||
return StagePositionEnum.PARK
|
||||
else:
|
||||
return StagePositionEnum.UNKNOWN
|
||||
|
||||
@collimator.setter
|
||||
def collimator(self, value: StagePositionEnum):
|
||||
self.set_collimator(value, wait=True)
|
||||
|
||||
def set_collimator(self, value: StagePositionEnum, /, wait: bool = True):
|
||||
if value == StagePositionEnum.DOWN:
|
||||
self.__collimator.move("down", wait=wait)
|
||||
elif value == StagePositionEnum.MEASURE:
|
||||
self.__collimator.move("measure", wait=wait)
|
||||
elif value == StagePositionEnum.PARK:
|
||||
self.__collimator.move("parking", wait=wait)
|
||||
|
||||
@property
|
||||
def scintillator(self) -> StagePositionEnum:
|
||||
if self.__scinti.position_is("down"):
|
||||
return StagePositionEnum.DOWN
|
||||
elif self.__scinti.position_is("measure"):
|
||||
return StagePositionEnum.MEASURE
|
||||
elif self.__scinti.position_is("parking"):
|
||||
return StagePositionEnum.PARK
|
||||
else:
|
||||
return StagePositionEnum.UNKNOWN
|
||||
|
||||
@scintillator.setter
|
||||
def scintillator(self, value: StagePositionEnum):
|
||||
self.set_scintillator(value, wait=True)
|
||||
|
||||
def set_scintillator(self, value: StagePositionEnum, /, wait: bool = True):
|
||||
if value == StagePositionEnum.DOWN:
|
||||
self.__scinti.move("down", wait=wait)
|
||||
elif value == StagePositionEnum.MEASURE:
|
||||
self.__scinti.move("measure", wait=wait)
|
||||
elif value == StagePositionEnum.PARK:
|
||||
self.__scinti.move("parking", wait=wait)
|
||||
|
||||
@property
|
||||
def reflector_up(self) -> bool:
|
||||
return bool(self.__reflector.position_is("measure"))
|
||||
|
||||
@reflector_up.setter
|
||||
def reflector_up(self, value: bool):
|
||||
if value:
|
||||
self.__reflector.move("measure", wait=True)
|
||||
else:
|
||||
self.__reflector.move("park", wait=True)
|
||||
|
||||
@property
|
||||
def beamstop_stage_up(self) -> bool:
|
||||
return bool(self.__beamstop_stage.position_is("measure"))
|
||||
|
||||
@beamstop_stage_up.setter
|
||||
def beamstop_stage_up(self, value: bool):
|
||||
if value:
|
||||
self.__beamstop_stage.move("measure", wait=True)
|
||||
else:
|
||||
self.__beamstop_stage.move("park", wait=True)
|
||||
|
||||
@property
|
||||
def abr_pos(self) -> Coordinate:
|
||||
return Coordinate(x=self.gmx.readback, y=self.gmy.readback, z=self.gmz.readback)
|
||||
|
||||
@abr_pos.setter
|
||||
def abr_pos(self, pos: Coordinate):
|
||||
self.gmx.move(pos.x, wait=True)
|
||||
self.gmy.move(pos.y, wait=True)
|
||||
self.gmz.move(pos.z, wait=True)
|
||||
|
||||
wait_position(self.gmx, pos.x, 0.005)
|
||||
wait_position(self.gmy, pos.y, 0.005)
|
||||
wait_position(self.gmz, pos.z, 0.005)
|
||||
|
||||
@property
|
||||
def energy_kev(self) -> float:
|
||||
return self.__energy_pv.value
|
||||
|
||||
@property
|
||||
def ring_current(self) -> float:
|
||||
return max(0.0, self.__ring_current_pv.value)
|
||||
|
||||
@property
|
||||
def cryojet_temp(self) -> float:
|
||||
return self.__cryojet_temp.value
|
||||
|
||||
def enable_motors(self):
|
||||
self.aerotech.unlock()
|
||||
self.dtz.refresh() # Why ??
|
||||
self.bsz.refresh() # Why ??
|
||||
|
||||
def disable_motors(self):
|
||||
self.aerotech.lock()
|
||||
|
||||
@property
|
||||
def shutter(self) -> bool:
|
||||
return self.__shutter_rbv.value == 1
|
||||
|
||||
@shutter.setter
|
||||
def shutter(self, opened: bool):
|
||||
if opened:
|
||||
self.__shutter.put(1)
|
||||
else:
|
||||
self.__shutter.put(0)
|
||||
|
||||
@property
|
||||
def samcam_settings(self) -> SampleCameraSettings:
|
||||
return SampleCameraSettings(
|
||||
gain=self.sample_cam.gain.value,
|
||||
exposure=self.sample_cam.expo.value
|
||||
)
|
||||
|
||||
@samcam_settings.setter
|
||||
def samcam_settings(self, settings: SampleCameraSettings):
|
||||
self.sample_cam.setup(settings.gain, settings.exposure)
|
||||
|
||||
@property
|
||||
def flux(self) -> float:
|
||||
# Very approximate value - to be changed
|
||||
# 2 nA -> 4e11 ph/s
|
||||
if self.transmission.get() is None:
|
||||
return 0.0
|
||||
return self.transmission.get() * self.full_flux
|
||||
|
||||
@property
|
||||
def full_flux(self) -> float:
|
||||
# Very approximate value - to be changed
|
||||
# 2 nA -> 4e11 ph/s
|
||||
return self.__bpm.value / 2.0 * 4e11
|
||||
|
||||
@property
|
||||
def dtz_low(self) -> float:
|
||||
return self.dtz.get("LLM")
|
||||
|
||||
@property
|
||||
def dtz_high(self) -> float:
|
||||
return self.dtz.get("HLM")
|
||||
|
||||
def anneal(self, value: float):
|
||||
self.__anneal.put(1)
|
||||
time.sleep(value)
|
||||
self.__anneal.put(0)
|
||||
@@ -1,335 +0,0 @@
|
||||
import time
|
||||
|
||||
from epics import poll
|
||||
|
||||
from aaredaq.config import ABR_POS_MOUNT, ABR_OMEGA_MOUNT
|
||||
from aaredaq.devices import BeamlineDevices
|
||||
from aaredaq.config import BeamlineConfig
|
||||
from aaredaqlib.coordinate import Coordinate
|
||||
from aaredaqlib.logger_config import setup_logger
|
||||
from aaredaqlib.models import StagePositionEnum, SampleCameraSettings, ZoomModeEnum
|
||||
from mxlibs3.mx_lib import pv_wait
|
||||
|
||||
logger = setup_logger("aareDAQ")
|
||||
|
||||
def move_bsz(devs: BeamlineDevices, target: float):
|
||||
if abs(target - devs.bsz.position) > 0.1:
|
||||
beamstop_stage_measure = devs.beamstop_stage_up
|
||||
reflector_measure = devs.reflector_up
|
||||
devs.reflector_up = False
|
||||
devs.beamstop_stage_up = True
|
||||
|
||||
devs.bsz.move(target, wait=True)
|
||||
|
||||
if reflector_measure:
|
||||
devs.reflector_up = True
|
||||
|
||||
if not beamstop_stage_measure:
|
||||
devs.beamstop_stage_up = False
|
||||
|
||||
|
||||
def wait_for_dc_devices(devs: BeamlineDevices):
|
||||
timeout = time.time() + 60
|
||||
timeisup = False
|
||||
# devs.detector_cover.wait()
|
||||
while not timeisup and (
|
||||
not devs.dtz.done_moving
|
||||
):
|
||||
poll(0.1)
|
||||
timeisup = timeout < time.time()
|
||||
if timeisup:
|
||||
if not devs.dtz.done_moving:
|
||||
logger.info("DTZ still moving.")
|
||||
raise RuntimeError("Timeout moving devs. Dropping to maintenance.")
|
||||
|
||||
|
||||
def wait_for_se_devices(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
hub = cfg.settings
|
||||
hub_cryo = cfg.cryojet_settings
|
||||
|
||||
_cryo = hub_cryo.cryojet_in_use
|
||||
_cryopark = hub_cryo.cryojet_park_position
|
||||
|
||||
if _cryo and not devs.cryo.position_is(_cryopark):
|
||||
raise RuntimeError("Cryojet did not reach far position")
|
||||
|
||||
|
||||
def common_2rse(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
logger.info(" moving COLLIMATOR to PARK position")
|
||||
devs.collimator = StagePositionEnum.PARK
|
||||
logger.info(" moving SMARGON to HOME position")
|
||||
devs.smargon.move_home(wait=True)
|
||||
logger.info(" moving AEROTECH to MOUNT position")
|
||||
devs.aerotech.move(ABR_OMEGA_MOUNT, wait=True, direct=True)
|
||||
devs.abr_pos = ABR_POS_MOUNT
|
||||
logger.info(" moving REFLECTOR to DOWN position")
|
||||
devs.reflector_up = False
|
||||
logger.info(" LOCKING AEROTERCH")
|
||||
devs.aerotech.lock()
|
||||
logger.info(f" Smargon position in RSE: {devs.smargon.readback} ABS: {devs.abr_pos.x} {devs.abr_pos.y} {devs.abr_pos.z}")
|
||||
|
||||
|
||||
def m2se(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
print("executing MAINTENANCE -> sample exchange")
|
||||
devs.detector_cover.put(1) # Close detector cover
|
||||
|
||||
hub = cfg.settings
|
||||
hub_cryo = cfg.cryojet_settings
|
||||
|
||||
cfg.zoom_mode = ZoomModeEnum.User
|
||||
devs.samcam_settings = cfg.zoom_settings.get_camera_settings(devs.zoom)
|
||||
|
||||
devs.collimator = StagePositionEnum.PARK
|
||||
devs.scintillator = StagePositionEnum.DOWN
|
||||
|
||||
_dtzpark = hub.dtz_park
|
||||
_cryopark = hub_cryo.cryojet_park_position
|
||||
_cryo = hub_cryo.cryojet_in_use
|
||||
|
||||
# devs.detector_cover.move("closed")
|
||||
if not devs.aerotech.is_ready_and_willing():
|
||||
devs.aerotech.stop()
|
||||
time.sleep(5.0)
|
||||
raise RuntimeError("Aerotech device is not ready")
|
||||
|
||||
if devs.dtz.position < _dtzpark:
|
||||
devs.dtz.move(_dtzpark)
|
||||
|
||||
devs.gmx.set_speed(100.0)
|
||||
devs.abr_pos = ABR_POS_MOUNT
|
||||
devs.aerotech.move(ABR_OMEGA_MOUNT)
|
||||
|
||||
if _cryo:
|
||||
devs.cryo.move(_cryopark)
|
||||
|
||||
devs.reflector_up = False
|
||||
|
||||
devs.bsx.move(0.0)
|
||||
devs.bsy.move(0.0)
|
||||
move_bsz(devs, hub.bsz)
|
||||
|
||||
devs.beamstop_stage_up = False
|
||||
|
||||
|
||||
def sa2se(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
hub = cfg.settings
|
||||
hub_cryo = cfg.cryojet_settings
|
||||
logger.info(" moving cryojet to park position")
|
||||
_cryopark = hub_cryo.cryojet_park_position
|
||||
_dtzpark = hub.dtz_park
|
||||
|
||||
devs.aerotech.move(ABR_OMEGA_MOUNT)
|
||||
|
||||
if devs.cryo.position < _cryopark:
|
||||
devs.cryo.move(_cryopark)
|
||||
logger.info(" moving COLLIMATOR to DOWN position")
|
||||
devs.collimator = StagePositionEnum.DOWN
|
||||
logger.info(" moving REFLECTOR to DOWN position")
|
||||
devs.reflector_up = False
|
||||
devs.beamstop_stage_up = False
|
||||
logger.info(" moving SMARGON to HOME position")
|
||||
devs.smargon.move_home(wait=True)
|
||||
logger.info(" moving DETECTOR COVER to DOWN position")
|
||||
devs.detector_cover.put(1) # Close detector cover
|
||||
if devs.dtz.position < _dtzpark:
|
||||
logger.info(" moving DETECTOR to PARK position")
|
||||
devs.dtz.move(_dtzpark)
|
||||
|
||||
|
||||
def sa2rse(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
common_2rse(devs, cfg)
|
||||
|
||||
|
||||
def dc2rse(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
common_2rse(devs, cfg)
|
||||
|
||||
|
||||
def se2sa(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
hub = cfg.settings
|
||||
cfg.zoom_mode = ZoomModeEnum.User
|
||||
devs.detector_cover.put(1)
|
||||
devs.samcam_settings = cfg.zoom_settings.get_camera_settings(devs.zoom)
|
||||
hub_cryo = cfg.cryojet_settings
|
||||
_cryo = hub_cryo.cryojet_in_use
|
||||
cryo_meas = hub_cryo.cryojet_measurement_position
|
||||
_dtz_safety = hub.dtz_bsz_safety_margin
|
||||
devs.aerotech.unlock()
|
||||
logger.info("aerotech unlocked")
|
||||
devs.aerotech.set_direct_mode()
|
||||
logger.info("aerotech in direct mode")
|
||||
logger.info(" moving reflector to up position")
|
||||
devs.reflector_up = True
|
||||
logger.info("moving beamstop to up position")
|
||||
devs.beamstop_stage_up = True
|
||||
logger.info(" setting lamp to 2.5")
|
||||
devs.lamp_light = 2.5
|
||||
logger.info(" moving aerotech to measure position")
|
||||
devs.abr_pos = cfg.abr_meas_pos
|
||||
|
||||
if _cryo:
|
||||
logger.info(" moving cryojet to measure position")
|
||||
devs.cryo.move(cryo_meas)
|
||||
logger.info("moving detector to measure position")
|
||||
devs.dtz.move(cfg.dtz, wait=False)
|
||||
|
||||
def rse2sa(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
se2sa(devs, cfg)
|
||||
|
||||
def sa2dc(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
devs.detector_cover.put(2) # Open detector cover
|
||||
|
||||
devs.reflector_up = False
|
||||
|
||||
#devs.reflector.wait("park")
|
||||
devs.beamstop_stage_up = True
|
||||
devs.dtz.move(cfg.dtz, wait=True)
|
||||
devs.collimator = StagePositionEnum.MEASURE
|
||||
|
||||
pv_wait(devs.dtz, None, tolerance=1.0)
|
||||
wait_for_dc_devices(devs)
|
||||
|
||||
|
||||
def dc2sa(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
hub = cfg.settings
|
||||
devs.detector_cover.put(1) # Close detector cover
|
||||
devs.collimator = StagePositionEnum.PARK
|
||||
devs.reflector_up = True
|
||||
|
||||
# devs.gmz.move(cfg.abr_meas_pos.z, wait=True)
|
||||
|
||||
|
||||
def sa2xrf(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
"""sample alignment to XrfCollection"""
|
||||
pass
|
||||
|
||||
|
||||
def xrf2sa(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
"""XrfCollection to sample alignment"""
|
||||
pass
|
||||
|
||||
|
||||
def sa2ws(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
hub = cfg.settings
|
||||
_dtzwash = hub.dtz_wash_sample_distance
|
||||
devs.cryo.move(hub.cryojet_park_position)
|
||||
|
||||
if devs.dtz.position < _dtzwash:
|
||||
devs.dtz.move(_dtzwash)
|
||||
|
||||
devs.reflector_up = False
|
||||
devs.beamstop_stage_up = False
|
||||
|
||||
|
||||
def ws2sa(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
hub = cfg.settings
|
||||
hub_cryo = cfg.cryojet_settings
|
||||
|
||||
cryo_meas = hub_cryo.cryojet_measurement_position
|
||||
_cryo = hub_cryo.cryojet_in_use
|
||||
|
||||
devs.dtz.move(cfg.dtz)
|
||||
|
||||
if _cryo:
|
||||
devs.cryo.move(cryo_meas)
|
||||
|
||||
devs.reflector_up = True
|
||||
devs.beamstop_stage_up = True
|
||||
|
||||
|
||||
def sa2ba(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
hub = cfg.settings
|
||||
hub_cryo = cfg.cryojet_settings
|
||||
_cryopark = hub_cryo.cryojet_park_position
|
||||
_cryo = hub_cryo.cryojet_in_use
|
||||
|
||||
if _cryo:
|
||||
devs.cryo.move(_cryopark)
|
||||
|
||||
devs.gmx.move(-38.0)
|
||||
devs.lamp_light = 2.5
|
||||
devs.beamstop_stage_up = True
|
||||
move_bsz(devs, 0.0)
|
||||
|
||||
|
||||
def ba2sa(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
hub = cfg.settings
|
||||
devs.lamp_light = 2.5
|
||||
devs.beamstop_stage_up = True
|
||||
move_bsz(devs, hub.bsz)
|
||||
devs.reflector_up = True
|
||||
devs.abr_pos = cfg.abr_meas_pos
|
||||
|
||||
|
||||
def sa2bl(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
val=1000
|
||||
devs.zoom = val
|
||||
cfg.zoom_mode = ZoomModeEnum.BeamLocation
|
||||
devs.detector_cover.put(1) # Close detector cover
|
||||
devs.samcam_settings = cfg.zoom_settings.get_camera_settings(val)
|
||||
devs.lamp_light = 2.5
|
||||
devs.abr_pos = Coordinate(x=-26, y=0, z=0)
|
||||
devs.cryo.move(80)
|
||||
devs.set_scintillator(StagePositionEnum.MEASURE, wait=False)
|
||||
devs.set_collimator(StagePositionEnum.MEASURE, wait=False)
|
||||
devs.beamstop_stage_up = True
|
||||
devs.reflector_up = False
|
||||
devs.scintillator = StagePositionEnum.MEASURE
|
||||
devs.collimator = StagePositionEnum.MEASURE
|
||||
devs.shutter = True
|
||||
|
||||
def bl2sa(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
hub = cfg.settings
|
||||
hub_cryo = cfg.cryojet_settings
|
||||
cryo_meas = hub_cryo.cryojet_measurement_position
|
||||
_cryo = hub_cryo.cryojet_in_use
|
||||
|
||||
devs.shutter = False
|
||||
|
||||
devs.set_scintillator(StagePositionEnum.DOWN, wait=False)
|
||||
devs.set_collimator(StagePositionEnum.PARK, wait=False)
|
||||
|
||||
devs.reflector_up = True
|
||||
devs.scintillator = StagePositionEnum.DOWN
|
||||
devs.collimator = StagePositionEnum.PARK
|
||||
|
||||
val=1
|
||||
devs.zoom = val
|
||||
cfg.zoom_mode = ZoomModeEnum.User
|
||||
devs.samcam_settings = cfg.zoom_settings.get_camera_settings(val)
|
||||
|
||||
if _cryo:
|
||||
devs.cryo.move(cryo_meas)
|
||||
|
||||
devs.abr_pos = cfg.abr_meas_pos
|
||||
|
||||
|
||||
def bl2ba(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
"""beam location to beamstop alignment"""
|
||||
devs.gmx.move(-38)
|
||||
devs.reflector_up = True
|
||||
# devs.shutter.move("closed")
|
||||
time.sleep(0.5)
|
||||
move_bsz(devs, 0)
|
||||
|
||||
|
||||
def ba2bl(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
hub = cfg.settings
|
||||
move_bsz(devs, hub.bsz)
|
||||
devs.abr_pos = cfg.abr_meas_pos + Coordinate(x=1)
|
||||
devs.reflector_up = True
|
||||
|
||||
|
||||
def sa2dh(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
logger.info("moved to dewer transfer")
|
||||
if devs.tell.get_mounted_sample() is not None:
|
||||
try:
|
||||
# Best effort try to unmount
|
||||
devs.tell.unmount(wait=True)
|
||||
except Exception as e:
|
||||
print(f"Error for unmounting: {e}")
|
||||
devs.tell.dry(wait_cold=-1, wait=False)
|
||||
|
||||
|
||||
def dh2sa(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
#devs.tell.move_cold(wait=True)
|
||||
logger.info("return to sample alignment")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,126 +0,0 @@
|
||||
from typing import cast
|
||||
|
||||
from epics import PV
|
||||
|
||||
from .mx_lib import ValueWaitTimeout, pv_wait
|
||||
|
||||
|
||||
class EnumPv(object):
|
||||
def __init__(self, args):
|
||||
mandatory_args = {"pv", "readback", "states"}
|
||||
supplied_args = set(args.keys())
|
||||
if not mandatory_args.issubset(supplied_args):
|
||||
raise RuntimeError("Missing arguments: %s" % (", ".join(mandatory_args.difference(supplied_args))))
|
||||
|
||||
if type(args["states"]) is not dict:
|
||||
raise RuntimeError("states must be a dictionary mapping put/readback for waiting purposes")
|
||||
|
||||
self.val = PV(args["pv"])
|
||||
self.rbv = PV(args["readback"])
|
||||
|
||||
if "name" in args:
|
||||
self.device_name = args["name"]
|
||||
else:
|
||||
self.device_name = self.val.pvname
|
||||
|
||||
if not hasattr(self.val, "enum_strs"):
|
||||
raise RuntimeError("PV %s does not have enum_strs" % self.val.pvname)
|
||||
if not hasattr(self.rbv, "enum_strs"):
|
||||
raise RuntimeError("PV %s does not have enum_strs" % self.rbv.pvname)
|
||||
|
||||
self.__target = None
|
||||
|
||||
p = args.get("move_done_when")
|
||||
if p is not None:
|
||||
self.__wait_pv = PV(p[0])
|
||||
self.__wait_target = p[1]
|
||||
else:
|
||||
self.__wait_pv = None
|
||||
self.__wait_target = None
|
||||
|
||||
self.states = {}
|
||||
for k, v in list(args["states"].items()):
|
||||
self.states[k.upper()] = v.upper()
|
||||
|
||||
self.val_enums = [x.upper() for x in self.val.enum_strs] # type: ignore
|
||||
self.rbv_enums = [x.upper() for x in self.rbv.enum_strs] # type: ignore
|
||||
|
||||
if "timeout" not in args:
|
||||
self.__timeout = 60 # seconds
|
||||
else:
|
||||
self.__timeout = args["timeout"]
|
||||
|
||||
def __str__(self):
|
||||
return f"<{self.device_name} at {self.position}>"
|
||||
|
||||
def __repr__(self):
|
||||
s = (
|
||||
f"<{self.device_name} at {self.position} an {self.__class__.__name__} "
|
||||
f"instance at {hex(id(self))} positions = {self.positions}>"
|
||||
)
|
||||
return s
|
||||
|
||||
def has_position(self, position):
|
||||
return position.upper() in self.states
|
||||
|
||||
@property
|
||||
def positions(self):
|
||||
return self.rbv_enums
|
||||
|
||||
def __position(self) -> str:
|
||||
return self.rbv.enum_strs[self.rbv.get()] # type: ignore
|
||||
|
||||
position = property(__position)
|
||||
|
||||
def position_is(self, position) -> bool:
|
||||
if type(position) in (int, float):
|
||||
return int(position) == self.get()
|
||||
else:
|
||||
return str(position).upper() == str(self.__position()).upper()
|
||||
|
||||
def get(self, req_type=None) -> str | int:
|
||||
if req_type:
|
||||
res = self.position
|
||||
else:
|
||||
res = cast(int, self.rbv.get())
|
||||
return res
|
||||
|
||||
def put(self, value, wait=False) -> None:
|
||||
self.val.put(value)
|
||||
self.__target = self.rbv_enums.index(self.states[self.val_enums[value]])
|
||||
if wait:
|
||||
self.wait()
|
||||
|
||||
def move(self, position, wait=False) -> None:
|
||||
if type(position) is not int:
|
||||
if not self.has_position(position):
|
||||
raise ValueError(f"Invalid position {position} for {self.device_name}")
|
||||
target = self.val_enums.index(position.upper())
|
||||
else:
|
||||
target = position
|
||||
|
||||
self.put(target, wait=wait)
|
||||
|
||||
def equal_dbr_string(self, val) -> bool:
|
||||
value = self.rbv.get(as_string=True)
|
||||
return cast(str, value).lower() == val.lower()
|
||||
|
||||
def equal_dbr_int(self, val) -> bool:
|
||||
return val == self.rbv.get()
|
||||
|
||||
def wait(self, target=None):
|
||||
if target is None:
|
||||
target = self.__target
|
||||
|
||||
if self.__wait_pv:
|
||||
pv = self.__wait_pv
|
||||
else:
|
||||
pv = self.rbv
|
||||
|
||||
if target is None:
|
||||
target = self.__wait_target
|
||||
|
||||
try:
|
||||
pv_wait(pv, target, timeout=self.__timeout, verbose=True)
|
||||
except ValueWaitTimeout:
|
||||
raise ValueWaitTimeout(f"Timeout waiting for device to reach [{target}], currently at [{self.position}]")
|
||||
@@ -1,53 +0,0 @@
|
||||
import time
|
||||
|
||||
from epics import Motor
|
||||
|
||||
|
||||
class MyMotor(Motor):
|
||||
def __init__(self, name):
|
||||
super().__init__(name.upper())
|
||||
|
||||
@property
|
||||
def speed(self):
|
||||
return self.slew_speed
|
||||
|
||||
@speed.setter
|
||||
def speed(self, speed):
|
||||
self.slew_speed = speed
|
||||
|
||||
@property
|
||||
def pvname(self):
|
||||
return self._prefix[:-1]
|
||||
|
||||
def __readback(self):
|
||||
return self.readback
|
||||
|
||||
position = property(__readback)
|
||||
|
||||
def __value(self):
|
||||
return self.drive
|
||||
|
||||
value = property(__value)
|
||||
|
||||
|
||||
class BogusMotor:
|
||||
def __init__(self, name, **kwargs):
|
||||
self.name = name
|
||||
self.readback = kwargs.get("readback", 0.0)
|
||||
self.target = kwargs.get("target", 0.0)
|
||||
self.velo = kwargs.get("velo", 10.0)
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
return self.readback
|
||||
|
||||
def move(self, target, wait=False):
|
||||
if wait:
|
||||
time.sleep(abs(target - self.readback) / self.velo)
|
||||
self.readback = target
|
||||
|
||||
def refresh(self):
|
||||
pass
|
||||
|
||||
def get_position(self, *args, **kwargs):
|
||||
return self.readback
|
||||
@@ -1,285 +0,0 @@
|
||||
import time
|
||||
from typing import Callable, cast
|
||||
|
||||
from epics import PV, poll
|
||||
from epics.ca import pend_io
|
||||
|
||||
|
||||
def is_number(x):
|
||||
return isinstance(x, (int, float))
|
||||
|
||||
|
||||
class NonStandard(object):
|
||||
def __init__(self, **kwargs):
|
||||
mandatory = ["name", "setpv", "getpv"]
|
||||
if set(mandatory) != set(kwargs.keys()) & set(mandatory):
|
||||
raise RuntimeError(
|
||||
"missing mandatory argument(s): {}".format(str(list(set(mandatory) - set(kwargs.keys()))))
|
||||
)
|
||||
self.name = kwargs.get("name")
|
||||
self.__target_pos: float | int | str = 0
|
||||
self.device_name = self.name
|
||||
self.__setpv = PV(kwargs["setpv"])
|
||||
self.__getpv = PV(kwargs["getpv"])
|
||||
self.__timeout_margin = kwargs.get("timeout", 2.0) # add this safety margin to timeout calc from speed
|
||||
self.__timeout = kwargs.get("timeout", 60.0)
|
||||
self.__format = kwargs.get("format")
|
||||
|
||||
p = kwargs.get("speed")
|
||||
if p is not None:
|
||||
if is_number(p):
|
||||
self.__speed_set, self.__speed_get = p, p
|
||||
elif type(p) in [tuple, list] and len(p) == 2:
|
||||
self.__speed_set = PV(p[0])
|
||||
self.__speed_get = PV(p[1])
|
||||
else:
|
||||
raise Exception(f"{self.name} => don't know how to parse speed parameter")
|
||||
else:
|
||||
self.__speed_set, self.__speed_get = None, None
|
||||
#
|
||||
# move_done_when a tuple, first item the pv to read
|
||||
# second item, the int type indicating that it is DONE
|
||||
#
|
||||
p = kwargs.get("move_done_when")
|
||||
if p is not None:
|
||||
self.__move_flag = PV(p[0])
|
||||
self.__move_flag_done = p[1]
|
||||
else:
|
||||
self.__move_flag = None
|
||||
|
||||
#
|
||||
# stoppv a tuple, first item the pv to read
|
||||
# second item, the int type to put to stop the motor
|
||||
#
|
||||
p = kwargs.get("stoppv")
|
||||
if p is not None:
|
||||
self.__stoppv = PV(p[0])
|
||||
self.__stoppv_done = p[1]
|
||||
else:
|
||||
self.__stoppv = None
|
||||
|
||||
#
|
||||
# still_moving_when a tuple, first item the pv to read
|
||||
# second item, the int type indicating that it is still moving.
|
||||
#
|
||||
p = kwargs.get("still_moving_when")
|
||||
if p is not None:
|
||||
self.__still_moving_flag = PV(p[0])
|
||||
self.__still_moving_flag_value = p[1]
|
||||
else:
|
||||
self.__still_moving_flag = None
|
||||
|
||||
p = kwargs.get("predefs")
|
||||
if p:
|
||||
self.__predefs = p
|
||||
self.positions = list(p.keys())
|
||||
else:
|
||||
self.__predefs = {}
|
||||
self.positions = []
|
||||
|
||||
pend_io(5.0)
|
||||
|
||||
typ = self.__ret_type = self.__getpv.type
|
||||
|
||||
match str(typ):
|
||||
case "double" | "time_double":
|
||||
self.still_moving = self.__moving_float
|
||||
case "int" | "time_int":
|
||||
self.still_moving = self.__moving_int
|
||||
case "string" | "time_string":
|
||||
self.still_moving = self.__moving_str
|
||||
case _:
|
||||
self.still_moving = self.__moving_unknown
|
||||
|
||||
tolerance = kwargs.get("tolerance", None)
|
||||
if tolerance is not None and tolerance > 0:
|
||||
self.tolerance = tolerance
|
||||
elif tolerance is not None and tolerance < 0:
|
||||
self.still_moving = self.__moving_unknown
|
||||
|
||||
if self.__move_flag:
|
||||
self.still_moving = self.is_move_not_done
|
||||
elif self.__still_moving_flag:
|
||||
self.still_moving = self.still_moving_when
|
||||
|
||||
def __str__(self):
|
||||
if self.positions:
|
||||
return "<{} at {}({}) Pre-defined positions {}>".format(
|
||||
self.name, self.__position(predef=True), self.position, self.positions
|
||||
)
|
||||
else:
|
||||
return "<{} at {}>".format(self.name, self.__position())
|
||||
|
||||
def __repr__(self):
|
||||
if self.positions:
|
||||
return "<{} instance at {}: {} at {}({}) Pre-defined positions {}>".format(
|
||||
self.__class__.__name__,
|
||||
hex(id(self)),
|
||||
self.name,
|
||||
self.__position(predef=True),
|
||||
self.position,
|
||||
self.positions,
|
||||
)
|
||||
else:
|
||||
return "<{} instance at {}: {} at {}>".format(
|
||||
self.__class__.__name__, hex(id(self)), self.name, self.__position()
|
||||
)
|
||||
|
||||
def still_moving_when(self):
|
||||
return self.__still_moving_flag_value == self.__still_moving_flag.get() # type: ignore
|
||||
|
||||
def is_moving(self):
|
||||
return not self.is_move_done()
|
||||
|
||||
def is_move_done(self):
|
||||
return self.__move_flag_done == self.__move_flag.get() # type: ignore
|
||||
|
||||
def is_move_not_done(self):
|
||||
return not self.is_move_done()
|
||||
|
||||
def __moving_float(self):
|
||||
if type(self.__target_pos) is not float:
|
||||
raise RuntimeError("target position is not an float")
|
||||
current_value = cast(float, self.__getpv.get())
|
||||
return self.tolerance < abs(cast(float, self.__target_pos) - current_value)
|
||||
|
||||
def __moving_int(self):
|
||||
if type(self.__target_pos) is not int:
|
||||
raise RuntimeError("target position is not an integer")
|
||||
return self.__target_pos != self.__getpv.get()
|
||||
|
||||
def __moving_str(self):
|
||||
if type(self.__target_pos) is not str:
|
||||
raise RuntimeError("target position is not a string")
|
||||
current_value = cast(str, self.__getpv.get())
|
||||
return self.__target_pos.upper() != current_value.upper()
|
||||
|
||||
def __moving_unknown(self):
|
||||
return False
|
||||
|
||||
def __position(self, predef: bool = False):
|
||||
"""returns either a predefined position or a readback
|
||||
|
||||
:rtype: Union[str, float, int]
|
||||
"""
|
||||
cur = self.readback
|
||||
if not predef:
|
||||
return cur
|
||||
# loop over predefined positions, if one found and
|
||||
# readback matches it's value return "position"
|
||||
for pos_label, val_or_cbk in self.__predefs.items():
|
||||
if self.__ret_type is bytes:
|
||||
tst = val_or_cbk == cur
|
||||
elif type(val_or_cbk) is tuple:
|
||||
# a tuple (Callable, arg-to-Callable)
|
||||
val = val_or_cbk[0](*val_or_cbk[1])
|
||||
tst = 0.1 > abs(val - cur) # FIXME sooo bad
|
||||
else:
|
||||
tst = 0.1 > abs(val_or_cbk - cur)
|
||||
if tst:
|
||||
return pos_label
|
||||
return "unknown"
|
||||
|
||||
def __readback(self):
|
||||
return self.__getpv.get()
|
||||
|
||||
readback = property(__readback)
|
||||
position = property(__position)
|
||||
|
||||
def has_position(self, position: str) -> bool:
|
||||
return position.lower() in [p.lower() for p in self.positions]
|
||||
|
||||
def position_is(self, position, tolerance=0.1):
|
||||
pos = self.__predefs.get(position, position)
|
||||
cur = self.position
|
||||
|
||||
print(f"position_is: {pos} == {cur}")
|
||||
|
||||
if type(pos) is tuple:
|
||||
pos = pos[0](*pos[1])
|
||||
|
||||
print(f"position_is: {pos} == {cur}")
|
||||
|
||||
if self.__ret_type is bytes:
|
||||
print("checking bytes")
|
||||
return pos == cur
|
||||
else:
|
||||
print(f"checking numbers: abs({pos} - {cur}) < {tolerance}")
|
||||
return abs(pos - cur) < tolerance
|
||||
|
||||
def __value(self):
|
||||
val = self.__getpv.get()
|
||||
if self.__format is not None:
|
||||
val = self.__format % (val,)
|
||||
return val
|
||||
|
||||
value = property(__value)
|
||||
|
||||
def __raw_value(self):
|
||||
return self.__getpv.get()
|
||||
|
||||
raw_value = property(__raw_value)
|
||||
|
||||
def __char_value(self):
|
||||
return str(self.value)
|
||||
|
||||
char_value = property(__char_value)
|
||||
|
||||
def set_speed(self, speed):
|
||||
if isinstance(self.__speed_set, PV):
|
||||
self.__speed_set.put(speed)
|
||||
elif is_number(self.__speed_set):
|
||||
self.__speed_set = speed
|
||||
self.__speed_get = speed
|
||||
|
||||
def get_speed(self):
|
||||
if is_number(self.__speed_get):
|
||||
return self.__speed_get
|
||||
elif isinstance(self.__speed_get, PV):
|
||||
return self.__speed_get.value
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_timeout(self):
|
||||
if not self.__speed_get:
|
||||
timeout = self.__timeout
|
||||
else:
|
||||
speed = self.get_speed()
|
||||
curp = self.position
|
||||
if curp is None or self.__target_pos is None or speed is None:
|
||||
timeout = self.__timeout_margin
|
||||
raise RuntimeWarning("can't calculate timeout, ABR may be disconnected")
|
||||
else:
|
||||
timeout = self.__timeout_margin + (abs(curp - self.__target_pos) / speed)
|
||||
return timeout
|
||||
|
||||
def wait(self):
|
||||
pos = self.__target_pos
|
||||
timeout = time.time() + self.get_timeout()
|
||||
|
||||
while time.time() < timeout and self.still_moving():
|
||||
poll(0.1)
|
||||
if time.time() > timeout:
|
||||
msg = "timeout occurred when moving %s to %s" % (self.name, pos)
|
||||
print(msg)
|
||||
raise RuntimeWarning(msg)
|
||||
|
||||
def move(self, pos, relative=False, wait=False):
|
||||
pos = self.__predefs.get(pos, pos)
|
||||
if type(pos) is tuple:
|
||||
pos = pos[0](*pos[1])
|
||||
|
||||
if relative:
|
||||
pos += self.__getpv.get()
|
||||
|
||||
self.__target_pos = pos
|
||||
self.__setpv.put(pos)
|
||||
if wait:
|
||||
time.sleep(0.1)
|
||||
self.wait()
|
||||
|
||||
def stop(self):
|
||||
if self.__stoppv:
|
||||
self.__stoppv.put(self.__stoppv_done)
|
||||
else:
|
||||
raise RuntimeError("stop pv not configured for this motor")
|
||||
@@ -1,794 +0,0 @@
|
||||
import threading
|
||||
import time
|
||||
import sys
|
||||
import requests
|
||||
import json
|
||||
|
||||
|
||||
try:
|
||||
from urllib import quote # Python 2
|
||||
except ImportError:
|
||||
from urllib.parse import quote # Python 3
|
||||
|
||||
|
||||
class TimeoutException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
from sseclient import SSEClient
|
||||
except:
|
||||
SSEClient = None
|
||||
|
||||
class SSEReceiver:
|
||||
def __init__(self, url, subscribed_events):
|
||||
if SSEClient is None:
|
||||
raise Exception("sseclient library is not installed: server events are not available")
|
||||
self.url = url
|
||||
self.events = subscribed_events
|
||||
self._lock = threading.Lock()
|
||||
self._stop = threading.Event()
|
||||
self.session = None
|
||||
self.client = None
|
||||
self.debug = False
|
||||
self._subscribers = {}
|
||||
self.thread = threading.Thread(target=self.task, kwargs={})
|
||||
self.thread.daemon = True
|
||||
self.thread.start()
|
||||
|
||||
def task(self):
|
||||
try:
|
||||
while not self._stop.is_set():
|
||||
try:
|
||||
self.session = requests.Session()
|
||||
self.client = SSEClient(self.url, session=self.session)
|
||||
for msg in self.client:
|
||||
if self.is_closed():
|
||||
break
|
||||
event_name = msg.event or "message"
|
||||
|
||||
if (self.events is None) or (event_name in self.events):
|
||||
try:
|
||||
data = json.loads(msg.data)
|
||||
except:
|
||||
data = str(msg.data)
|
||||
#if self.debug:
|
||||
# print (event_name, data)
|
||||
with self._lock:
|
||||
subs = list(self._subscribers.values())
|
||||
|
||||
for events, callback in subs:
|
||||
if events is None or event_name in events:
|
||||
try:
|
||||
callback(event_name, data)
|
||||
except Exception as e:
|
||||
if self.debug:
|
||||
print(f"[SSEManager] Error in callback {callback}: {e}")
|
||||
|
||||
except IOError as e:
|
||||
# print(e)
|
||||
pass
|
||||
except:
|
||||
if self.debug:
|
||||
print("Error:", sys.exc_info()[1])
|
||||
finally:
|
||||
self._close_client()
|
||||
if self.is_closed():
|
||||
break
|
||||
else:
|
||||
time.sleep(1.0)
|
||||
finally:
|
||||
if self.debug:
|
||||
print("Exit SSE loop task")
|
||||
|
||||
def subscribe(self, callback, events=None):
|
||||
"""
|
||||
Subscribe to SSE events.
|
||||
|
||||
Args:
|
||||
callback: function(event_name, data)
|
||||
events: None (all events), str (one event), or list[str] (multiple events)
|
||||
"""
|
||||
if isinstance(events, str):
|
||||
events = [events]
|
||||
if events is not None:
|
||||
events = set(events)
|
||||
|
||||
with self._lock:
|
||||
self._subscribers[id(callback)] = (events, callback)
|
||||
|
||||
def unsubscribe(self, callback):
|
||||
"""Unsubscribe a previously subscribed callback."""
|
||||
with self._lock:
|
||||
self._subscribers.pop(id(callback), None)
|
||||
|
||||
def wait_events(self, events={}, timeout=-1):
|
||||
"""Wait any of the events matching the value (value None for any).
|
||||
|
||||
Args:
|
||||
events (dict event name->value)
|
||||
timeout:
|
||||
Returns:
|
||||
(event, value) or None if timeout
|
||||
"""
|
||||
rx = {}
|
||||
condition = threading.Condition()
|
||||
def callback(name, value):
|
||||
with condition:
|
||||
rx[name] = value
|
||||
condition.notify_all()
|
||||
|
||||
self.subscribe(callback, events.keys())
|
||||
try:
|
||||
start = time.time()
|
||||
with condition:
|
||||
while True:
|
||||
for name in events.keys():
|
||||
if name in rx.keys():
|
||||
values, rx_value = events[name], rx[name]
|
||||
if values is not None and type(values) is not list:
|
||||
values = [values]
|
||||
if values is None or rx_value in values:
|
||||
return name,rx_value
|
||||
|
||||
remaining = None
|
||||
if timeout >= 0:
|
||||
remaining = max(0, timeout - (time.time() - start))
|
||||
if remaining <= 0:
|
||||
return None
|
||||
condition.wait(timeout=remaining)
|
||||
finally:
|
||||
self.unsubscribe(callback)
|
||||
|
||||
|
||||
def _close_client(self):
|
||||
self.client = None
|
||||
"""
|
||||
if self.client is not None:
|
||||
try:
|
||||
if hasattr(self.client.resp, "raw"):
|
||||
conn = getattr(self.client.resp.raw, "_connection", None)
|
||||
if conn and hasattr(conn, "sock") and conn.sock:
|
||||
conn.sock.shutdown(2)
|
||||
conn.sock.close()
|
||||
self.client.resp.close()
|
||||
self.client = None
|
||||
except:
|
||||
pass
|
||||
"""
|
||||
if self.session is not None:
|
||||
try:
|
||||
self.session.close()
|
||||
self.session = None
|
||||
except:
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
self._stop.set()
|
||||
self._close_client()
|
||||
if self.debug:
|
||||
print("closed")
|
||||
|
||||
def is_closed(self):
|
||||
return self._stop.is_set()
|
||||
|
||||
class PShellClient:
|
||||
def __init__(self, url):
|
||||
if not url.endswith('/'):
|
||||
url = url + "/"
|
||||
self.url = url
|
||||
self.sse_event_loop_thread = None
|
||||
self.sse_client = None
|
||||
self.plot_defaults = {"format": "png", "width": 600, "height": 400}
|
||||
self.debug = False
|
||||
self.polling_interval = 0.1
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
|
||||
def _get(self, url, stream=False):
|
||||
url = self.url + url
|
||||
if self.debug:
|
||||
print("GET " + url)
|
||||
return requests.get(url=url, stream=stream)
|
||||
|
||||
def _put(self, url, json_data=None):
|
||||
url = self.url + url
|
||||
if self.debug:
|
||||
print("PUT " + url + " -> " + json.dumps(json_data))
|
||||
return requests.put(url=url, json=json_data)
|
||||
|
||||
def _del(self, url):
|
||||
url = self.url + url
|
||||
if self.debug:
|
||||
print("DEL " + url)
|
||||
return requests.delete(url=url)
|
||||
|
||||
def _get_response(self, response, is_json=True):
|
||||
if self.debug == True or self.debug == "rx":
|
||||
print(" -> " + str(response.status_code) + ((" - " + response.text) if self.debug == "rx" else ""))
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except:
|
||||
print(response.text)
|
||||
raise
|
||||
return json.loads(response.text) if is_json else response.text
|
||||
|
||||
def _get_binary_response(self, response):
|
||||
response.raise_for_status()
|
||||
return response.raw.read()
|
||||
|
||||
def get_plot_defaults(self):
|
||||
"""Return plot default properties.
|
||||
|
||||
Args:
|
||||
|
||||
Returns:
|
||||
Dictionary
|
||||
|
||||
"""
|
||||
return self.plot_defaults.copy()
|
||||
|
||||
def set_plot_defaults(self, defaults):
|
||||
"""Update plot default properties.
|
||||
|
||||
Args:
|
||||
Dictionary that will updated into the plot default properties.
|
||||
Returns:
|
||||
|
||||
"""
|
||||
self.plot_defaults.update(defaults)
|
||||
|
||||
def get_version(self):
|
||||
"""Return application version.
|
||||
|
||||
Args:
|
||||
|
||||
Returns:
|
||||
String with application version.
|
||||
|
||||
"""
|
||||
return self._get_response(self._get("version"), False)
|
||||
|
||||
def get_config(self):
|
||||
"""Return application configuration.
|
||||
|
||||
Args:
|
||||
|
||||
Returns:
|
||||
Dictionary.
|
||||
"""
|
||||
return self._get_response(self._get("config"))
|
||||
|
||||
def get_state(self):
|
||||
"""Return application state.
|
||||
|
||||
Args:
|
||||
|
||||
Returns:
|
||||
String: Invalid, Initializing,Ready, Paused, Busy, Disabled, Closing, Fault, Offline
|
||||
"""
|
||||
return self._get_response(self._get("state"))
|
||||
|
||||
def wait_state(self, state, timeout=-1):
|
||||
"""Wait application state equals.
|
||||
|
||||
Args:
|
||||
state (string or list of strings)
|
||||
timeout(number) wait timeout in seconds. If less or equal 0 then wait forever.
|
||||
Returns:
|
||||
"""
|
||||
if type(state) == str:
|
||||
state = [state]
|
||||
start = time.time()
|
||||
while self.get_state() not in state:
|
||||
if (timeout >= 0) and ((time.time() - start) > timeout):
|
||||
raise TimeoutException(f"Timeout waiting state {state}")
|
||||
time.sleep(self.polling_interval)
|
||||
|
||||
def wait_state_not(self, state, timeout=-1):
|
||||
"""Wait application state different than.
|
||||
|
||||
Args:
|
||||
state (string or list of strings)
|
||||
timeout(number) wait timeout in seconds. If less or equal 0 then wait forever.
|
||||
Returns:
|
||||
"""
|
||||
if type(state) == str:
|
||||
state = [state]
|
||||
start = time.time()
|
||||
while self.get_state() in state:
|
||||
if (timeout >= 0) and ((time.time() - start) > timeout):
|
||||
raise TimeoutException(f"Timeout waiting state not {state}")
|
||||
time.sleep(self.polling_interval)
|
||||
|
||||
def get_logs(self):
|
||||
"""Return application logs.
|
||||
|
||||
Args:
|
||||
|
||||
Returns:
|
||||
List of logs.
|
||||
Format of each log: [date, time, origin, level, description]
|
||||
|
||||
"""
|
||||
return self._get_response(self._get("logs"))
|
||||
|
||||
def get_history(self, index):
|
||||
"""Access console command history.
|
||||
|
||||
Args:
|
||||
index(int): Index of history entry (0 is the most recent)
|
||||
|
||||
Returns:
|
||||
History entry
|
||||
|
||||
"""
|
||||
return self._get_response(self._get("history/" + str(index)), False)
|
||||
|
||||
def get_script(self, path):
|
||||
"""Return script.
|
||||
|
||||
Args:
|
||||
path(str): Script path (absolute or relative to script folder)
|
||||
|
||||
Returns:
|
||||
String with file contents.
|
||||
|
||||
"""
|
||||
return self._get_response(self._get("script/" + str(path)), False)
|
||||
|
||||
def get_devices(self):
|
||||
"""Return global devices.
|
||||
|
||||
Args:
|
||||
|
||||
Returns:
|
||||
List of devices.
|
||||
Format of each device record: [name, type, state, value, age]
|
||||
|
||||
"""
|
||||
return self._get_response(self._get("devices"))
|
||||
|
||||
def abort(self, command_id=None):
|
||||
"""Abort execution of command
|
||||
|
||||
Args:
|
||||
command_id(optional, int): id of the command to be aborted.
|
||||
if None (default), aborts the foreground execution.
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
if command_id is None:
|
||||
self._get("abort")
|
||||
else:
|
||||
return self._get("abort/" + str(command_id))
|
||||
|
||||
def pause(self):
|
||||
"""Pause execution of command
|
||||
|
||||
Args:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
self._get("pause")
|
||||
|
||||
def resume(self):
|
||||
"""Resume execution of command
|
||||
|
||||
Args:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
self._get("resume")
|
||||
|
||||
def reinit(self):
|
||||
"""Reinitialize the software.
|
||||
|
||||
Args:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
self._get("reinit")
|
||||
|
||||
def stop(self):
|
||||
"""Stop all devices implementing the 'Stoppable' interface.
|
||||
|
||||
Args:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
self._get("stop")
|
||||
|
||||
def update(self):
|
||||
"""Update all global devices.
|
||||
|
||||
Args:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
self._get("update")
|
||||
|
||||
def eval(self, statement):
|
||||
"""Evaluates a statement in the interpreter.
|
||||
If the statement finishes by '&', it is executed in background.
|
||||
Otherwise statement is executed in foreground (exclusive).
|
||||
|
||||
Args:
|
||||
statement(str): input statement
|
||||
|
||||
Returns:
|
||||
String containing the console return.
|
||||
If an exception is produces in the interpretor, it is re-thrown here.
|
||||
"""
|
||||
statement = quote(statement)
|
||||
return self._get_response(self._get("eval/" + statement), False)
|
||||
|
||||
def run(self, script, pars=None, background=False):
|
||||
"""Executes script in the interpreter.
|
||||
|
||||
Args:
|
||||
script(str): name of the script (absolute or relative to the script base folder). Extension may be omitted.
|
||||
pars(optional, list or dict): if a list is given, it sets sys.argv for the script.
|
||||
If a dict is given, it sets global variable for the script.
|
||||
background(optional, bool): if True script is executed in background.
|
||||
|
||||
Returns:
|
||||
Return value of the script.
|
||||
If an exception is produces in the interpretor, it is re-thrown here.
|
||||
"""
|
||||
return self._get_response(
|
||||
self._put("run", {"script": script, "pars": pars, "background": background, "async": False}))
|
||||
|
||||
def start_eval(self, statement):
|
||||
"""Starts evaluation of a statement in the interpreter.
|
||||
If the statement finishes by '&', it is executed in background.
|
||||
Otherwise statement is executed in foreground (exclusive).
|
||||
|
||||
Args:
|
||||
statement(str): input statement
|
||||
|
||||
Returns:
|
||||
Command id (int), which is used to retrieve command execution status/result (get_result).
|
||||
"""
|
||||
statement = quote(statement)
|
||||
return int(self._get_response(self._get("evalAsync/" + statement), False))
|
||||
|
||||
def eval_json(self, statement):
|
||||
"""Evaluates a statement in the interpreter.
|
||||
Args:
|
||||
statement(str): input statement
|
||||
|
||||
Returns:
|
||||
Return object decoded from JSON string
|
||||
"""
|
||||
statement = quote(statement)
|
||||
return self._get_response(self._get("eval-json/" + statement), True)
|
||||
|
||||
def eval_then(self, statement, on_success=True, on_exception=True):
|
||||
"""Set a next execution stage for the interpreter - the statement is executed
|
||||
after the foreground task concludes, keeping application state busy.
|
||||
|
||||
Args:
|
||||
statement(str): statement for next execution stage
|
||||
on_successs(bool): statement is executed if foreground task completes successfully.
|
||||
on_exception(bool): statement is executed if foreground task throws exception.
|
||||
"""
|
||||
return self._get_response(
|
||||
self._put("then", {"statement": statement, "onSuccess": on_success, "onException": on_exception}))
|
||||
|
||||
def run_then(self, script, pars=None, on_success=True, on_exception=True):
|
||||
"""Set a next execution stage for the interpreter - the script is executed
|
||||
after the foreground task concludes, keeping application state busy.
|
||||
|
||||
Args:
|
||||
script(str): name of the script (absolute or relative to the script base folder). Extension may be omitted.
|
||||
pars(optional, list or dict): if a list is given, it sets sys.argv for the script.
|
||||
If a dict is given, it sets global variable for the script.
|
||||
on_successs(bool): statement is executed if foreground task completes successfully.
|
||||
on_exception(bool): statement is executed if foreground task throws exception.
|
||||
"""
|
||||
cmd = f"run('{script}', {pars})"
|
||||
return self.eval_then(cmd, on_success, on_exception)
|
||||
|
||||
def set_var(self, name, value):
|
||||
"""Sets interpreter variable.
|
||||
Args:
|
||||
name(str): variable name
|
||||
value(obj): value - must be JSON compatible
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
data = {}
|
||||
data["name"] = name
|
||||
data["value"] = value
|
||||
return self._get_response(self._put("set-var", data), False)
|
||||
|
||||
def start_run(self, script, pars=None, background=False):
|
||||
"""Starts execution of a script in the interpreter.
|
||||
|
||||
Args:
|
||||
script(str): name of the script (absolute or relative to the script base folder). Extension may be omitted.
|
||||
pars(optional, list or dict): if a list is given, it sets sys.argv for the script.
|
||||
If a dict is given, it sets global variable for the script.
|
||||
background(optional, bool): if True script is executed in background.
|
||||
|
||||
Returns:
|
||||
Command id (int), which is used to retrieve command execution status/result (get_result).
|
||||
"""
|
||||
return int(self._get_response(
|
||||
self._put("run", {"script": script, "pars": pars, "background": background, "async": True})))
|
||||
|
||||
def get_result(self, command_id=-1):
|
||||
"""Gets status/result of a command executed asynchronously (start_eval and start_run).
|
||||
|
||||
Args:
|
||||
command_id(optional, int): command id. If equals to -1 (default) return status/result of the foreground task.
|
||||
|
||||
Returns:
|
||||
Dictionary with the fields: 'id' (int): command id
|
||||
'status' (str): unlaunched, invalid, removed, running, aborted, failed or completed.
|
||||
'exception' (str): if status equals 'failed', holds exception string.
|
||||
'return' (obj): if status equals 'completed', holds return value of script (start_run)
|
||||
or console return (start_eval)
|
||||
"""
|
||||
return self._get_response(self._get("result/" + str(command_id)))
|
||||
|
||||
def help(self, input="<builtins>"):
|
||||
"""Returns help or auto-completion strings.
|
||||
|
||||
Args:
|
||||
input(optional, str): - ":" for control commands
|
||||
- "<builtins>" for builtin functions
|
||||
- "devices" for device names
|
||||
- builtin function name for function help
|
||||
- else contains entry for auto-completion
|
||||
|
||||
Returns:
|
||||
List
|
||||
|
||||
"""
|
||||
return self._get_response(self._get("autocompletion/" + input))
|
||||
|
||||
def get_contents(self, path=None):
|
||||
"""Returns contents of data path.
|
||||
|
||||
Args:
|
||||
path(optional, str): Path to data relative to data home path.
|
||||
- Folder
|
||||
- File
|
||||
- File (data root) | internal path
|
||||
- internal path (on currently open data root)
|
||||
|
||||
Returns:
|
||||
List of contents
|
||||
|
||||
"""
|
||||
return self._get_response(self._get("contents" + ("" if path is None else ("/" + path))), False)
|
||||
|
||||
def get_data(self, path, type="txt"):
|
||||
"""Returns data on a given path.
|
||||
|
||||
Args:
|
||||
path(str): Path to data relative to data home path.
|
||||
- File (data root) | internal path
|
||||
- internal path (on currently open data root)
|
||||
type(optional, str): txt, "json", "bin", "bs"
|
||||
|
||||
Returns:
|
||||
Data accordind to selected format/.
|
||||
|
||||
"""
|
||||
if type == "json":
|
||||
return self._get_response(self._get("data-json/" + path), True)
|
||||
elif type == "bin":
|
||||
return self._get_binary_response(self._get("data-bin/" + path, stream=True))
|
||||
elif type == "bs":
|
||||
from collections import OrderedDict
|
||||
bs = self._get_binary_response(self._get("data-bs/" + path, stream=True))
|
||||
index = 0
|
||||
msg = []
|
||||
for i in range(4):
|
||||
size = int.from_bytes(bs[index:index + 4], byteorder='big', signed=False)
|
||||
index = index + 4
|
||||
msg.append(bs[index:index + size])
|
||||
index = index + size
|
||||
[main_header, data_header, data, timestamp] = msg
|
||||
main_header = json.loads(main_header, object_pairs_hook=OrderedDict)
|
||||
data_header = json.loads(data_header, object_pairs_hook=OrderedDict)
|
||||
channel = data_header["channels"][0]
|
||||
channel["encoding"] = "<" if channel.get("encoding", "little") else ">"
|
||||
from bsread.data.helpers import get_channel_reader
|
||||
channel_value_reader = get_channel_reader(channel)
|
||||
return channel_value_reader(data)
|
||||
|
||||
return self._get_response(self._get("data" + ("" if path is None else ("/" + path))), False)
|
||||
|
||||
def get_data_attrs(self, path):
|
||||
return self._get_response(self._get("data-attr/" + path), True)
|
||||
|
||||
def get_data_info(self, path):
|
||||
return self._get_response(self._get("data-info/" + path), True)
|
||||
|
||||
def get_scan_data(self, layout, path, group, device, type="txt"):
|
||||
"""Returns scan data of a device.
|
||||
|
||||
Args:
|
||||
layout(str): data layout
|
||||
path(str): scan path
|
||||
group(str): scan group
|
||||
device(str): device name
|
||||
type(optional, str): txt, "json", "bin"
|
||||
|
||||
Returns:
|
||||
Data accordind to selected format.
|
||||
|
||||
"""
|
||||
if layout is None or layout.strip() == "" or path is None:
|
||||
raise Exception("Invalid scan persistence path or layout")
|
||||
path = path.replace("/", "<br>")
|
||||
path = path.replace("|", "<p>")
|
||||
group = group.replace("/", "<br>")
|
||||
layout = layout.replace(".", "<br>")
|
||||
url = layout + "/" + path + "/" + group + "/" + device
|
||||
if type == "json":
|
||||
url = "scandata-json/" + url
|
||||
return self._get_response(self._get(url), True)
|
||||
elif type == "bin":
|
||||
url = "scandata-bin/" + url
|
||||
return self._get_binary_response(self._get(url, stream=True))
|
||||
url = "scandata/" + url
|
||||
return self._get_response(self._get(url), False)
|
||||
|
||||
def get_plot_contexts(self):
|
||||
"""Return list of plot contexts
|
||||
|
||||
Args:
|
||||
|
||||
Returns:
|
||||
List of names
|
||||
|
||||
"""
|
||||
return self._get_response(self._get("plots"))
|
||||
|
||||
def delete_plot_context(self, title):
|
||||
"""
|
||||
Delete a plotting context.
|
||||
|
||||
Args:
|
||||
title(str): name of the plotting context
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
return self._get_response(self._del("plots/" + title), False)
|
||||
|
||||
def get_num_plots(self, title=None):
|
||||
"""Return number of plots in a given plotting context.
|
||||
|
||||
Args:
|
||||
title(str): name of the plotting context
|
||||
|
||||
Returns:
|
||||
Number of plots
|
||||
|
||||
"""
|
||||
if title is None:
|
||||
title = "null"
|
||||
return int(self._get_response(self._get("plots/" + title)))
|
||||
|
||||
def get_plot(self, title=None, index=0, format="png", width=None, height=None):
|
||||
"""Return a plot as a given image type.
|
||||
|
||||
Args:
|
||||
title(str): name of the plotting context
|
||||
index(int): plot index (0-based)
|
||||
format(str): plot format ("jpg", "png", "gif", "tif")
|
||||
width(int): plot width (if 0 gets plot staddard size)
|
||||
height(int): plot height (if 0 gets plot staddard size)
|
||||
Returns:
|
||||
Image file byte array
|
||||
|
||||
"""
|
||||
if title is None:
|
||||
title = "null"
|
||||
if format is None:
|
||||
format = self.plot_defaults["format"]
|
||||
if width is None:
|
||||
width = self.plot_defaults["width"]
|
||||
if height is None:
|
||||
height = self.plot_defaults["height"]
|
||||
|
||||
url = "plot/" + title + "/" + str(index) + "/" + format + "/" + str(width) + "/" + str(height)
|
||||
return self._get_binary_response(self._get(url, stream=True))
|
||||
|
||||
def print_logs(self):
|
||||
for l in self.get_logs():
|
||||
print("%s %s %-20s %-8s %s" % tuple(l))
|
||||
|
||||
def print_devices(self):
|
||||
for l in self.get_devices():
|
||||
print("%-16s %-32s %-10s %-32s %s" % tuple(l))
|
||||
|
||||
def print_help(self, input="<builtins>"):
|
||||
for l in self.help(input):
|
||||
print(l)
|
||||
|
||||
def _get_sse(self):
|
||||
if self.sse_client is None:
|
||||
self.sse_client = SSEReceiver(self.url + "events", None)
|
||||
self.sse_client.debug = self.debug
|
||||
return self.sse_client
|
||||
|
||||
def subscribe(self, callback=None, events=None):
|
||||
"""
|
||||
Subscribe to SSE events.
|
||||
|
||||
Args:
|
||||
callback: function(event_name, data), If None, calls self.on_event
|
||||
events: None (all events), str (one event), or list[str] (multiple events)
|
||||
|
||||
Usage example:
|
||||
def on_event(name, value):
|
||||
if name == "state":
|
||||
print ("State changed: ", value)
|
||||
elif name == "record":
|
||||
print ("Received scan record: ", value)
|
||||
|
||||
pc.subscribe(["state", "record"], on_event)
|
||||
"""
|
||||
if callback == None:
|
||||
callback = self.on_event
|
||||
self._get_sse().subscribe(callback, events)
|
||||
|
||||
def unsubscribe(self, callback):
|
||||
"""Unsubscribe a previously subscribed callback."""
|
||||
self._get_sse().unsubscribe(callback)
|
||||
|
||||
def wait_events(self, events={}, timeout=-1):
|
||||
"""Wait any of the events matching the value (value None for any).
|
||||
|
||||
Args:
|
||||
events (dict event name->value)
|
||||
timeout(number) wait timeout in seconds. If less or equal 0 then wait forever.
|
||||
Returns:
|
||||
(event, value) or raises TimeoutException
|
||||
|
||||
Usage example:
|
||||
def on_event(name, value):
|
||||
if name == "state":
|
||||
print ("State changed: ", value)
|
||||
elif name == "record":
|
||||
print ("Received scan record: ", value)
|
||||
|
||||
pc.subscribe(["state", "record"], on_event)
|
||||
|
||||
"""
|
||||
ret = self._get_sse().wait_events(events, timeout)
|
||||
if ret is None:
|
||||
raise TimeoutException(f"Timeout waiting for events {events}")
|
||||
return ret
|
||||
|
||||
def on_event(self, name, value):
|
||||
"""
|
||||
Default event callback
|
||||
Args:
|
||||
name: event name.
|
||||
value: event value.
|
||||
"""
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
if self.sse_client is not None:
|
||||
self.sse_client.close()
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
from enum import Enum
|
||||
from time import sleep, time
|
||||
|
||||
import requests
|
||||
|
||||
from aaredaqlib.beamline import MXBeamline
|
||||
from aaredaqlib.coordinate import SmargonCoordinate, Coordinate
|
||||
|
||||
|
||||
class SmargonMode(Enum):
|
||||
UNINITIALIZED = 0
|
||||
INITIALIZING = 1
|
||||
READY = 2
|
||||
ERROR = 99
|
||||
|
||||
|
||||
class Smargon(object):
|
||||
SMARGON_HOME = SmargonCoordinate(
|
||||
sh_mm=Coordinate(x=0, y=0, z=18), phi_deg=0, chi_deg=0
|
||||
)
|
||||
|
||||
def __init__(self, bl: MXBeamline):
|
||||
if bl == MXBeamline.X06DA:
|
||||
self.__simulated = False
|
||||
self.__base = "http://x06da-smargopolo.psi.ch:3000"
|
||||
elif bl == MXBeamline.SIMULATED:
|
||||
self.__simulated = True
|
||||
self.__pos = self.SMARGON_HOME
|
||||
else:
|
||||
raise Exception("unknown beamline")
|
||||
|
||||
def gonget(self, thing: str) -> dict:
|
||||
"""issue a GET for some API component on the smargopolo server"""
|
||||
cmd = f"{self.__base}/{thing}"
|
||||
r = requests.get(cmd)
|
||||
if not r.ok:
|
||||
raise Exception(
|
||||
f"error getting {thing}; server returned {r.status_code} => {r.reason}"
|
||||
)
|
||||
return r.json()
|
||||
|
||||
def gonput(self, thing: str):
|
||||
cmd = f"{self.__base}/{thing}"
|
||||
r = requests.put(cmd)
|
||||
if not r.ok:
|
||||
raise Exception(
|
||||
f"error putting {thing}; server returned {r.status_code} => {r.reason}"
|
||||
)
|
||||
|
||||
def move_home(self, wait=False) -> None:
|
||||
self.target = self.SMARGON_HOME
|
||||
if wait:
|
||||
self.wait()
|
||||
|
||||
@property
|
||||
def mode(self) -> SmargonMode:
|
||||
return SmargonMode.INITIALIZING
|
||||
|
||||
@mode.setter
|
||||
def mode(self, mode: SmargonMode):
|
||||
if self.__simulated:
|
||||
return
|
||||
self.gonput(f"mode?mode={mode}")
|
||||
|
||||
def initialize(self):
|
||||
self.mode = SmargonMode.UNINITIALIZED
|
||||
sleep(0.1)
|
||||
self.mode = SmargonMode.INITIALIZING
|
||||
|
||||
def enable_correction(self):
|
||||
if self.__simulated:
|
||||
return
|
||||
|
||||
self.gonput("corr_type?corr_type=1")
|
||||
|
||||
def disable_correction(self):
|
||||
if self.__simulated:
|
||||
return
|
||||
|
||||
self.gonput("corr_type?corr_type=0")
|
||||
|
||||
@property
|
||||
def readback(self) -> SmargonCoordinate:
|
||||
if self.__simulated:
|
||||
return self.__pos
|
||||
|
||||
scs = self.gonget("readbackSCS")
|
||||
return SmargonCoordinate(
|
||||
sh_mm=Coordinate(x=scs["SHX"], y=scs["SHY"], z=scs["SHZ"]),
|
||||
phi_deg=scs["PHI"],
|
||||
chi_deg=scs["CHI"],
|
||||
)
|
||||
|
||||
@property
|
||||
def target(self) -> SmargonCoordinate:
|
||||
if self.__simulated:
|
||||
return self.__pos
|
||||
|
||||
scs = self.gonget("targetSCS")
|
||||
return SmargonCoordinate(
|
||||
sh_mm=Coordinate(x=scs["SHX"], y=scs["SHY"], z=scs["SHZ"]),
|
||||
phi_deg=scs["PHI"],
|
||||
chi_deg=scs["CHI"],
|
||||
)
|
||||
|
||||
@target.setter
|
||||
def target(self, coord: SmargonCoordinate):
|
||||
if self.__simulated:
|
||||
self.__pos = coord
|
||||
return
|
||||
|
||||
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
|
||||
)
|
||||
if coord.chi_deg is not None:
|
||||
target_string += "&CHI={:.5f}".format(coord.chi_deg)
|
||||
if coord.phi_deg is not None:
|
||||
target_string += "&PHI={:.5f}".format(coord.phi_deg)
|
||||
if target_string:
|
||||
self.gonput(f"targetSCS?{target_string}")
|
||||
|
||||
def wait(self, timeout=60.0, tol=0.01, poll_time=0.01):
|
||||
target = self.target
|
||||
timeout = timeout + time()
|
||||
while time() < timeout:
|
||||
if target.eq(self.readback, tol):
|
||||
break
|
||||
if time() > timeout:
|
||||
raise TimeoutError("Timed out waiting for Smargon to reach target")
|
||||
sleep(poll_time)
|
||||
@@ -1,688 +0,0 @@
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
from typing import List
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
|
||||
from aaredaqlib.logger_config import setup_logger
|
||||
from aaredaqlib.models import (
|
||||
PuckLoadedInfo,
|
||||
SampleShortInfo,
|
||||
DewarAddress,
|
||||
SampleDewarAddress,
|
||||
#PuckInfo,
|
||||
)
|
||||
from aareDBclient import (
|
||||
PuckWithTellPosition,
|
||||
)
|
||||
from aaredaqlib.beamline import MXBeamline # noqa: F401
|
||||
from mxlibs3.pshell_client import PShellClient
|
||||
|
||||
logger = setup_logger("aareDAQ")
|
||||
|
||||
class ManualMountException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SmartMagnetFaultException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class TellMountFailedException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class TellCommandWhileBusyException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class TellConnectionException(Exception):
|
||||
pass
|
||||
|
||||
VALID_DEWAR_POSITIONS = [f"{p}{n}" for n in "12345" for p in "ABCDEFX"]
|
||||
|
||||
POSITION_PARK = "pPark"
|
||||
POSITION_COLD = "pCold"
|
||||
POSITION_AUX = "pAux"
|
||||
POSITION_DEWAR = "pDewar"
|
||||
POSITION_HOME = "pHome"
|
||||
POSITION_HEATER = "pHeatB"
|
||||
|
||||
#Nov 26 13:36:00 mx-x06da-queue-01.psi.ch AareDAQ[2944444]: 2025-11-26 13:36:00,388 - aareDAQ - ERROR - Error getting status: ('Connection aborted.', ConnectionResetError(104, 'Connection reset by peer'))
|
||||
|
||||
class TellClient:
|
||||
def __init__(self, bl: MXBeamline):
|
||||
self.__url = None
|
||||
if bl == MXBeamline.SIMULATED:
|
||||
self.__simulation = True
|
||||
elif bl == MXBeamline.X06DA:
|
||||
self.__simulation = False
|
||||
self.__url = "http://x06da-tell.psi.ch:22222"
|
||||
|
||||
if self.__simulation:
|
||||
print("TELL P-Shell in SIMULATION mode")
|
||||
else:
|
||||
print(f"Connecting TELL p-shell service at {self.__url} ...", end="")
|
||||
hostname = urlparse(self.__url).hostname
|
||||
try:
|
||||
requests.get(f"{self.__url}/history/0", timeout=1.0)
|
||||
except ConnectionError:
|
||||
print(f"...connection to {hostname} failed")
|
||||
raise
|
||||
except requests.ReadTimeout:
|
||||
print(f"...PShell service {hostname} is down")
|
||||
raise
|
||||
self.pshell = PShellClient(self.__url)
|
||||
self._simulated_samples_info = {}
|
||||
self._simulated_detected_pucks = []
|
||||
self._simulated_mounted_sample = ""
|
||||
self._simulated_current = 30.0
|
||||
self._simulated_suppress = True
|
||||
self._simulated_state = "Ready"
|
||||
self._simulated_offset = 0.0
|
||||
self._aborted = False
|
||||
self.state = self.get_state()
|
||||
self.debug = False
|
||||
self._last_cmd_id = -1
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self.__url
|
||||
|
||||
def smc_get_current(self) -> float:
|
||||
if self.__simulation:
|
||||
return 30.0
|
||||
return json.loads(self.pshell.eval("smart_magnet.get_current()&"))
|
||||
|
||||
def smc_set_current(self, current: float):
|
||||
if self.__simulation:
|
||||
return
|
||||
self.pshell.eval(f"smart_magnet.set_current({current:.1f})&")
|
||||
|
||||
def smc_get_suppress(self) -> bool:
|
||||
if self.__simulation:
|
||||
return self._simulated_suppress
|
||||
return json.loads(self.pshell.eval("smart_magnet.get_supress()&"))
|
||||
|
||||
def smc_set_suppress(self, state: bool):
|
||||
if self.__simulation:
|
||||
self._simulated_suppress = state
|
||||
return
|
||||
self.pshell.eval(f"smart_magnet.set_supress({state})&")
|
||||
|
||||
def check_smc(self):
|
||||
self.pshell.eval("smart_magnet.set_supress(False)&")
|
||||
self.pshell.eval("smart_magnet.set_resting_current()&")
|
||||
self.pshell.eval("smart_magnet.check_mounted(idle_time=1.0, timeout=1.0")
|
||||
|
||||
def magnet_blower(self, state):
|
||||
"""turn on/off blower to the magnet for de-icying purposes
|
||||
|
||||
state = false (blower is off)
|
||||
state = true (blower is on)
|
||||
"""
|
||||
if X06DA:
|
||||
self.pshell.eval(f"set_pin_cleaner({state})&")
|
||||
else:
|
||||
cmd = "true" if state else "false"
|
||||
self.pshell.eval(f'robot.evaluate("doFOut1={cmd}")&')
|
||||
|
||||
def get_state(self):
|
||||
if self.__simulation:
|
||||
return self._simulated_state
|
||||
self.state = self.pshell.get_state()
|
||||
return self.state
|
||||
|
||||
def get_result(self, command_id=-1):
|
||||
if self.__simulation:
|
||||
return {
|
||||
"id": self._last_cmd_id,
|
||||
"status": "completed",
|
||||
"exception": "",
|
||||
"return": (True, "PINCODE6546"),
|
||||
}
|
||||
return self.pshell.get_result(command_id)
|
||||
|
||||
def wait_ready(self):
|
||||
if self.__simulation:
|
||||
logger.info("simulated wait_ready")
|
||||
time.sleep(3.0)
|
||||
self._simulated_state = "Ready"
|
||||
return
|
||||
# Monitors event but polls every second just n case an event is missed
|
||||
logger.debug(f"Waiting for robot to be ready. Current state: {self.state} Get state: {self.get_state()}")
|
||||
while True:
|
||||
if self.state != "Busy":
|
||||
logger.debug(f"Robot is now ready...? {self.state} Get:{self.get_state()}")
|
||||
break
|
||||
time.sleep(0.2)
|
||||
self.get_state()
|
||||
if self.state != "Ready":
|
||||
if self.state == "Initializing":
|
||||
raise Exception("Tell reconnecting")
|
||||
elif self.state == "Closing":
|
||||
raise Exception("Tell is disconnecting")
|
||||
raise Exception("Invalid state: " + str(self.state))
|
||||
|
||||
def set_in_mount_position(self, value):
|
||||
if self.__simulation:
|
||||
return
|
||||
self.pshell.eval("in_mount_position = " + str(value) + "&")
|
||||
|
||||
def is_in_mount_position(self):
|
||||
if self.__simulation:
|
||||
return True
|
||||
return self.pshell.eval("in_mount_position&").lower() == "true"
|
||||
|
||||
def set_simulated_mounted_sample(self, info):
|
||||
self._simulated_mounted_sample = info
|
||||
|
||||
def set_simulated_samples_info(self, samples):
|
||||
self._simulated_samples_info = samples
|
||||
|
||||
def set_simulated_detected_pucks(self, pucks):
|
||||
self._simulated_detected_pucks = pucks
|
||||
|
||||
def get_samples_info(self) -> List[SampleShortInfo]:
|
||||
if self.__simulation:
|
||||
j = self._simulated_samples_info # FIXME
|
||||
else:
|
||||
j = json.loads(self.pshell.eval("get_samples_info()&"))
|
||||
|
||||
output: List[SampleShortInfo] = []
|
||||
for i in j:
|
||||
if len(i["puckAddress"]) == 2:
|
||||
dewar_location = DewarAddress(
|
||||
segment=i["puckAddress"][0], pos=i["puckAddress"][1]
|
||||
)
|
||||
else:
|
||||
dewar_location = None
|
||||
|
||||
output.append(
|
||||
SampleShortInfo(
|
||||
puck_name=i["puckBarcode"],
|
||||
dewar_name=i["dewarName"],
|
||||
sample_name=i["sampleName"],
|
||||
pin=i["samplePosition"],
|
||||
user=i["userName"],
|
||||
location=dewar_location,
|
||||
)
|
||||
)
|
||||
return output
|
||||
|
||||
def set_samples_info(self, info: List[PuckWithTellPosition]):
|
||||
if self.__simulation:
|
||||
return
|
||||
|
||||
j = []
|
||||
for x in info:
|
||||
j.append(
|
||||
{
|
||||
"userName": x.pgroup,
|
||||
"dewarName": x.dewar_name or "",
|
||||
"puckName": x.puck_name,
|
||||
"puckType": "Unipuck", # could use x.puck_type
|
||||
"puckAddress": x.tell_position or "",
|
||||
"puckBarcode": x.puck_name,
|
||||
"sampleBarcode": "",
|
||||
"sampleMountCount": 0,
|
||||
"sampleName": "",
|
||||
"samplePosition": 1,
|
||||
"sampleStatus": "",
|
||||
}
|
||||
)
|
||||
|
||||
self.pshell.run("data/set_samples_info", pars=[json.dumps(j)], background=True)
|
||||
# self.pshell.eval("set_samples_info(" + json.dumps(info) + ")&")
|
||||
|
||||
def start_cmd(self, cmd, *argv):
|
||||
if self.__simulation:
|
||||
return 666 # FIXME should be different?
|
||||
cmd = cmd + "("
|
||||
for a in argv:
|
||||
cmd = cmd + (("'" + a + "'") if type(a) is str else str(a)) + ", "
|
||||
cmd = cmd + ")"
|
||||
ret = self.pshell.start_eval(cmd)
|
||||
self.get_state()
|
||||
return ret
|
||||
|
||||
def wait_cmd(self, cmd):
|
||||
if self.__simulation:
|
||||
return {True, "BARCODE_BIGUS"}
|
||||
self.wait_ready()
|
||||
result = self.get_result(cmd)
|
||||
# print (result)
|
||||
if result["exception"] is not None:
|
||||
raise Exception(result["exception"])
|
||||
return result["return"]
|
||||
|
||||
def is_cmd_completed(self, cmd):
|
||||
if self.__simulation:
|
||||
return True
|
||||
return self.get_result(cmd)["status"] != "running"
|
||||
|
||||
def wait_mount_complete(self, timeout: float = 360):
|
||||
if self.__simulation:
|
||||
time.sleep(1.0)
|
||||
return
|
||||
logger.debug(f"Waiting for mount to complete. Is busy? {self.is_busy()}")
|
||||
timeisup = timeout + time.time()
|
||||
while time.time() < timeisup:
|
||||
if not self.is_busy():
|
||||
logger.debug(f"Finished waiting for mount to complete. Is busy? {self.is_busy()}")
|
||||
break
|
||||
time.sleep(0.2)
|
||||
|
||||
def check_command_ok(self, timeout: float = 360.0, msg: str = ""):
|
||||
self.wait_mount_complete(timeout)
|
||||
result = self.get_result(self._last_cmd_id)
|
||||
if "completed" != result["status"]:
|
||||
raise TellMountFailedException(f"{msg} {result}")
|
||||
|
||||
def estimate_mounting_time(self, segment) -> int:
|
||||
try:
|
||||
current_mounted = self.get_mounted_sample()
|
||||
gripper_in_cold = self.is_in_cold()
|
||||
|
||||
if current_mounted is None:
|
||||
unmount_needs_drying = 0 # might not have anything
|
||||
unmount_needs_cooling = 0
|
||||
else:
|
||||
segment_in_cold = current_mounted.puck.segment in "ABCDEF"
|
||||
unmount_needs_drying = int(gripper_in_cold and not segment_in_cold)
|
||||
unmount_needs_cooling = int(not gripper_in_cold and segment_in_cold)
|
||||
|
||||
mount_needs_cooling = int(segment in "ABCDEF" and not gripper_in_cold)
|
||||
mount_needs_drying = int(segment not in "ABCDEF" and gripper_in_cold)
|
||||
|
||||
needs_cooling = mount_needs_cooling + unmount_needs_cooling
|
||||
needs_drying = mount_needs_drying + unmount_needs_drying
|
||||
return needs_cooling * 30 + needs_drying * 120
|
||||
except:
|
||||
return 0
|
||||
|
||||
def mount(
|
||||
self,
|
||||
address: SampleDewarAddress,
|
||||
force: bool = False, # kept for future
|
||||
read_dm: bool = False, # read data matrix
|
||||
auto_unmount: bool = False, # single command, if False it will raise exception
|
||||
wait: bool = False, # blocking operation
|
||||
timeout: float = 600.0,
|
||||
):
|
||||
SampleDewarAddress.model_validate(address)
|
||||
|
||||
segment = address.puck.segment
|
||||
puck = address.puck.pos
|
||||
sample = address.pin
|
||||
|
||||
if self.__simulation:
|
||||
self._last_cmd_id = random.randint(1000, 9999)
|
||||
print(
|
||||
f"simulated mount({segment}, {puck}, {sample}) -> {self._last_cmd_id}"
|
||||
)
|
||||
self._simulated_mounted_sample = f"{segment}{puck}{sample}"
|
||||
if random.random() < 0.1:
|
||||
print("simulated failed mount")
|
||||
raise TellMountFailedException(f"mount failed for {segment}{puck}")
|
||||
|
||||
return self._last_cmd_id
|
||||
|
||||
if self.is_busy():
|
||||
raise TellCommandWhileBusyException("mount received while robot is busy")
|
||||
|
||||
logger.info(f"loading sample {sample} from segment {segment} - {puck}")
|
||||
|
||||
self._last_cmd_id = self.start_cmd(
|
||||
"mount", segment, puck, sample, force, read_dm, auto_unmount
|
||||
)
|
||||
|
||||
wait_timeout = timeout + self.estimate_mounting_time(segment)
|
||||
logger.info("waiting for mount to complete")
|
||||
if wait and segment in "ABCDEF":
|
||||
event, value = self.pshell.wait_events({"state": None, "motion_task": "dry", "gripper_detection" : "No Pin in Gripper"}, timeout=wait_timeout)
|
||||
if event is None or event == "state":
|
||||
logger.info(f"event: {event} occurred with value: {value}, checking command completed okay")
|
||||
self.check_command_ok(
|
||||
timeout=wait_timeout, msg=f"Mount failed for {segment}{puck}-{sample}: "
|
||||
)
|
||||
return value
|
||||
elif event == "gripper_detection":
|
||||
logger.info(f"gripper detection: {event} occurred with value: {value}")
|
||||
return value
|
||||
elif event == "motion_task" and value == "dry":
|
||||
logger.info(f"event: {event} occurred with value: {value}")
|
||||
logger.info(" Drying occurring, releasing interface to user")
|
||||
return value
|
||||
else:
|
||||
logger.info(f"Unexpected event: {event} occurred with value: {value}")
|
||||
logger.info("Checking command completed okay anyway")
|
||||
self.check_command_ok(
|
||||
timeout=wait_timeout, msg=f"Mount failed for {segment}{puck}-{sample}: "
|
||||
)
|
||||
elif wait and segment == "X":
|
||||
logger.info("Loading an auxiliary puck")
|
||||
self.check_command_ok(
|
||||
timeout=wait_timeout, msg=f"Mount failed for {segment}{puck}-{sample}: "
|
||||
)
|
||||
logger.info("post waiting")
|
||||
return None
|
||||
|
||||
def unmount(self, force=False, wait=False, timeout=360.0):
|
||||
# Force has a meaning, will unmount even if smart magnet is not detecting sample
|
||||
if self.__simulation:
|
||||
print("simulated unmount")
|
||||
return
|
||||
|
||||
if self.is_busy():
|
||||
raise TellCommandWhileBusyException("mount received while robot is busy")
|
||||
|
||||
self._last_cmd_id = self.start_cmd("unmount", None, None, None, force)
|
||||
|
||||
if wait:
|
||||
self.check_command_ok(timeout=timeout, msg="Unmount failed: ")
|
||||
|
||||
return self._last_cmd_id
|
||||
|
||||
def scan_pin(self, segment, puck, sample, force=False):
|
||||
return self.start_cmd("scan_pin", segment, puck, sample, force)
|
||||
|
||||
def scan_puck(self, segment, puck, force=False):
|
||||
return self.start_cmd("scan_puck", segment, puck, force)
|
||||
|
||||
def dry(self, heat_time=None, speed=None, wait_cold=None, wait=False):
|
||||
if self.__simulation:
|
||||
time.sleep(5.0)
|
||||
self.pshell.wait_state("Ready", timeout=30.0)
|
||||
self._last_cmd_id = self.start_cmd("dry", heat_time, speed, wait_cold)
|
||||
if wait:
|
||||
self.check_command_ok(timeout=360.0, msg=f"Dry failed")
|
||||
|
||||
def move_park(self, wait=False):
|
||||
if self.__simulation:
|
||||
return
|
||||
self._last_cmd_id = self.start_cmd("move_park")
|
||||
|
||||
if wait:
|
||||
self.check_command_ok(timeout=360.0, msg=f"Move to park failed")
|
||||
|
||||
def move_cold(self, reset_timestamp=False, wait=False):
|
||||
if self.__simulation:
|
||||
return
|
||||
self._last_cmd_id = self.start_cmd("move_cold", reset_timestamp)
|
||||
|
||||
if wait:
|
||||
self.check_command_ok(timeout=360.0, msg=f"Move to cold failed")
|
||||
|
||||
def trash(self):
|
||||
if self.__simulation:
|
||||
return
|
||||
return self.start_cmd("trash_sample")
|
||||
|
||||
def abort_cmd(self):
|
||||
if self.__simulation:
|
||||
print("simulated abort")
|
||||
return
|
||||
self.pshell.abort()
|
||||
self.pshell.eval("robot.stop_task()&")
|
||||
|
||||
def set_gonio_mount_position(self, homing=False):
|
||||
if self.__simulation:
|
||||
return
|
||||
if homing:
|
||||
self.pshell.eval("home_fast_table()")
|
||||
self.pshell.eval("set_mount_position()")
|
||||
|
||||
def set_setting(self, key: str, value: str):
|
||||
self.pshell.eval(f"set_setting('{key}', '{value}')&")
|
||||
|
||||
def get_setting(self, key: str) -> str:
|
||||
return self.pshell.eval(f"get_setting('{key}')&")
|
||||
|
||||
def enable_room_temperature(self):
|
||||
self.set_setting("room_temperature_enabled", "true")
|
||||
|
||||
def disable_room_temperature(self):
|
||||
self.set_setting("room_temperature_enabled", "false")
|
||||
|
||||
def get_mounted_sample(self) -> SampleDewarAddress | None:
|
||||
if self.__simulation:
|
||||
ret = self._simulated_mounted_sample
|
||||
else:
|
||||
ret = self.pshell.eval("get_setting('mounted_sample_position')&").strip()
|
||||
if not ret or len(ret) == 0:
|
||||
return None
|
||||
|
||||
match = re.match(r"([A-Z])(\d)(\d{1,2})", ret)
|
||||
|
||||
if match:
|
||||
segment, puck, sample = match.groups()
|
||||
dewar_location = DewarAddress(segment=segment, pos=int(puck))
|
||||
return SampleDewarAddress(puck=dewar_location, pin=int(sample))
|
||||
else:
|
||||
logger.warning(f"Failed to decode mounted sample position: {ret}")
|
||||
return None
|
||||
|
||||
def get_system_check(self):
|
||||
if self.__simulation:
|
||||
if random.random() < 0.1:
|
||||
raise RuntimeError("get_system_check_failed")
|
||||
return "Ok"
|
||||
return self.pshell.eval("system_check_msg()&")
|
||||
|
||||
def get_robot_state(self):
|
||||
if self.__simulation:
|
||||
return "Ready"
|
||||
return self.pshell.eval("robot.state&")
|
||||
|
||||
def get_robot_status(self):
|
||||
if self.__simulation:
|
||||
return {
|
||||
"powered": True,
|
||||
"settled": True,
|
||||
"speed": 100,
|
||||
"empty": True,
|
||||
"mode": "remote",
|
||||
"task": None,
|
||||
"pos": "pCold",
|
||||
"open": True,
|
||||
"status": "move",
|
||||
}
|
||||
|
||||
status = self.pshell.eval("robot.take()&")
|
||||
return eval(status) # FIXME ALL functions must return a valid JSON object
|
||||
|
||||
def get_speed(self) -> float:
|
||||
if self.__simulation:
|
||||
if random.random() < 0.1:
|
||||
return random.choice([1, 5, 25, 50, 75, 90])
|
||||
return 100.0
|
||||
speed = self.get_robot_status()["speed"]
|
||||
return float(speed)
|
||||
|
||||
def get_detected_pucks(self) -> List[PuckLoadedInfo]:
|
||||
if self.__simulation:
|
||||
j = self._simulated_detected_pucks
|
||||
else:
|
||||
j = json.loads(self.pshell.eval("get_pucks_info()&"))
|
||||
|
||||
output = []
|
||||
|
||||
for i in j:
|
||||
if i["puckState"] == "Present":
|
||||
puck_address = i["puckAddress"]
|
||||
if len(puck_address) == 2:
|
||||
output.append(
|
||||
PuckLoadedInfo(
|
||||
puck_name=i["puckBarcode"],
|
||||
location=DewarAddress(
|
||||
segment=puck_address[0], pos=int(puck_address[1])
|
||||
),
|
||||
),
|
||||
)
|
||||
return output
|
||||
|
||||
def set_pin_offset(self, value):
|
||||
if self.__simulation:
|
||||
print(f"simulated set_pin_offset {value}")
|
||||
self._simulated_offset = value
|
||||
return
|
||||
self.pshell.eval("set_pin_offset(" + str(value) + ")&")
|
||||
|
||||
def get_pin_offset(self):
|
||||
if self.__simulation:
|
||||
print(f"simulated get_pin_offset -> {self._simulated_offset}")
|
||||
return self._simulated_offset
|
||||
try:
|
||||
offset = float(self.pshell.eval("get_pin_offset()&"))
|
||||
except Exception:
|
||||
offset = 0.0
|
||||
return offset
|
||||
|
||||
def get_current(self):
|
||||
if self.__simulation:
|
||||
return self._simulated_current
|
||||
current = self.pshell.eval("smart_magnet.get_current_rb()&")
|
||||
return float(current)
|
||||
|
||||
def set_current(self, current):
|
||||
if self.__simulation:
|
||||
self._simulated_current = current
|
||||
return
|
||||
self.pshell.eval("smart_magnet.set_current({:.1f})&".format(current))
|
||||
current = self.pshell.eval("smart_magnet.get_current_rb()&")
|
||||
return float(current)
|
||||
|
||||
def print_info(self):
|
||||
print("State: " + str(self.get_state()))
|
||||
print("Mounted sample: " + str(self.get_mounted_sample()))
|
||||
print("System check: " + str(self.get_system_check()))
|
||||
print("Robot state: " + str(self.get_robot_state()))
|
||||
print("Robot status: ")
|
||||
status = self.get_robot_status()
|
||||
status = status
|
||||
for k, v in status.items():
|
||||
print(f"{k:>10s}: {v}")
|
||||
print("Pin offset: " + str(self.get_pin_offset()))
|
||||
print("Mount position: " + str(self.is_in_mount_position()))
|
||||
print("")
|
||||
|
||||
def is_powered(self):
|
||||
if self.__simulation:
|
||||
return True
|
||||
return self.get_robot_status()["powered"]
|
||||
|
||||
def check_enable_motion(self):
|
||||
if self.__simulation:
|
||||
if random.random() < 0.1:
|
||||
raise RuntimeError("check_enable_motion failed")
|
||||
if not self.is_powered():
|
||||
self.pshell.eval("enable_motion()&")
|
||||
|
||||
def is_in_park(self):
|
||||
if self.__simulation:
|
||||
return True
|
||||
return self.is_position(POSITION_PARK)
|
||||
|
||||
def is_in_home(self):
|
||||
if self.__simulation:
|
||||
return False
|
||||
return self.is_position(POSITION_HOME)
|
||||
|
||||
def is_in_cold(self):
|
||||
if self.__simulation:
|
||||
return False
|
||||
return self.is_position(POSITION_COLD)
|
||||
|
||||
def is_position(self, position: str) -> bool:
|
||||
return position == self.get_robot_status()["pos"]
|
||||
|
||||
def get_task(self):
|
||||
"""
|
||||
robot_status = {
|
||||
'powered': False,
|
||||
'settled': True,
|
||||
'speed': 10,
|
||||
'empty': True,
|
||||
'mode': 'remote',
|
||||
'task': None,
|
||||
'pos': 'pPark',
|
||||
'open': True,
|
||||
'status': 'hold'
|
||||
}
|
||||
:return:
|
||||
"""
|
||||
status = self.get_robot_status()
|
||||
return status["task"]
|
||||
|
||||
def is_ready(self):
|
||||
return "ready" == self.get_state().lower()
|
||||
|
||||
def is_busy(self):
|
||||
return "busy" == self.get_state().lower()
|
||||
|
||||
def check_smart_magnet_mounted(self, timeout: float = 10.0, idle_time: float = 1.0, interval: float = 0.1):
|
||||
initial_state = self.pshell.eval("smart_magnet.state&")
|
||||
logger.debug(f"checking smart magnet_initial state: {initial_state}")
|
||||
if initial_state == "Paused":
|
||||
self.pshell.eval("smart_magnet.set_supress(False)&")
|
||||
self.pshell.eval("smart_magnet.set_resting_current()&")
|
||||
elif initial_state == "Fault":
|
||||
logger.error(f"tell smart magnet is in unknown state {initial_state}")
|
||||
raise SmartMagnetFaultException
|
||||
#time.sleep(1.0)
|
||||
state = self.pshell.eval("smart_magnet.state&")
|
||||
|
||||
try:
|
||||
# sample_present = bool(self.pshell.eval(
|
||||
# f"smart_magnet.check_mounted(idle_time={str(idle_time)}, timeout={str(timeout)}, interval={str(interval)})"))
|
||||
#logger.debug(f"sample present: {sample_present} of type {type(sample_present)}")
|
||||
#time.sleep(1.0)
|
||||
if state == "Busy":
|
||||
logger.debug('state busy')
|
||||
self.pshell.eval("smart_magnet.set_supress(True)&")
|
||||
self.pshell.eval("smart_magnet.state&")
|
||||
sample_present = True
|
||||
elif state == "Ready":
|
||||
logger.debug('No sample detected, ready to mount')
|
||||
sample_present = False
|
||||
elif state == "Paused":
|
||||
logger.debug("Smart magnet detection is paused")
|
||||
return None
|
||||
else:
|
||||
self.pshell.eval("smart_magnet.set_supress(True)&")
|
||||
logger.error(f"Tell smart magnet is in unknown state {state}")
|
||||
raise SmartMagnetFaultException
|
||||
if sample_present:
|
||||
print(self.get_mounted_sample())
|
||||
if self.get_mounted_sample() is None:
|
||||
logger.warning("Check mount: A manually mounted sample is detected.")
|
||||
logger.warning("Remove before mounting with the robot.")
|
||||
raise ManualMountException
|
||||
return True
|
||||
elif self.get_mounted_sample():
|
||||
logger.error("Check mount: No sample detected, but robot thinks is mounted")
|
||||
raise SmartMagnetFaultException
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"check_smart_magnet_mounted failed: {e}")
|
||||
raise e
|
||||
#sample_present = False
|
||||
|
||||
|
||||
def is_true(value):
|
||||
"""check if argument is semantically true"""
|
||||
value = str(value).lower()
|
||||
return value != "0" or value in ("true", "yes", "on", "enabled")
|
||||
|
||||
|
||||
def is_false(value):
|
||||
return not is_true(value)
|
||||
|
||||
|
||||
def is_valid_dewar_position(position):
|
||||
return position in VALID_DEWAR_POSITIONS
|
||||
@@ -1,28 +0,0 @@
|
||||
[project]
|
||||
name = "aaregui"
|
||||
version = "0.2.72"
|
||||
description = "Beamline control GUI"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"pyjwt==2.10.1",
|
||||
"pyzmq==26.4.0",
|
||||
"opencv-python-headless==4.11.0.86",
|
||||
"PySide6==6.9.0",
|
||||
"aaredaqlib==0.2.72"
|
||||
]
|
||||
|
||||
[lint]
|
||||
ignore = ["F401", "F541", "W503", "W504"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=75.6.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.uv.sources]
|
||||
aaredb = { index = "psi"}
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "psi"
|
||||
url = "https://gitea.psi.ch/api/packages/mx/pypi/simple"
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import os
|
||||
import jwt
|
||||
import requests
|
||||
|
||||
from aaredaqlib.models import TokenData
|
||||
|
||||
from aaredaqlib.logger_config import setup_logger
|
||||
|
||||
logger = setup_logger('aareGUI')
|
||||
|
||||
def auth(base_url: str | None) -> str:
|
||||
curr_user = os.getlogin()
|
||||
if base_url is None:
|
||||
token_data = TokenData(sub=curr_user,
|
||||
staff=True,
|
||||
session=15,
|
||||
pgroups=["p16371", "p22233"])
|
||||
return jwt.encode(token_data.model_dump(), "ABC123")
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{base_url}/token",
|
||||
data={
|
||||
"username": curr_user,
|
||||
"password": ""
|
||||
},
|
||||
headers={
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
response_json = response.json()
|
||||
if "access_token" in response_json:
|
||||
return response_json["access_token"]
|
||||
|
||||
except requests.RequestException as e:
|
||||
logger.error(f"Authentication request failed: {e}")
|
||||
|
||||
return ""
|
||||
@@ -1,38 +0,0 @@
|
||||
from PySide6.QtCore import Qt, Signal, Slot
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QSlider
|
||||
from aaredaqlib.models import DAQStatusModel
|
||||
|
||||
from aaregui.widgets.title_label import TitleLabel
|
||||
|
||||
|
||||
class IlluminationPanel(QWidget):
|
||||
light = Signal(int)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
grid_layout = QGridLayout(self)
|
||||
|
||||
grid_layout.addWidget(TitleLabel("Front light", self), 0, 0, 1, 2)
|
||||
|
||||
self.is_sliding = False
|
||||
|
||||
self.slider = QSlider(orientation=Qt.Orientation.Horizontal, parent=self)
|
||||
self.slider.setRange(0, 100)
|
||||
self.slider.sliderPressed.connect(self.on_slider_pressed)
|
||||
self.slider.sliderReleased.connect(self.on_slider_released)
|
||||
|
||||
grid_layout.addWidget(self.slider, 1, 0, 1, 2)
|
||||
|
||||
@Slot()
|
||||
def on_slider_pressed(self):
|
||||
self.is_sliding = True
|
||||
|
||||
@Slot()
|
||||
def on_slider_released(self):
|
||||
self.is_sliding = False
|
||||
self.light.emit(self.slider.value())
|
||||
|
||||
@Slot(DAQStatusModel)
|
||||
def update_daq_status(self, s: DAQStatusModel):
|
||||
if not self.is_sliding: # Update only if not sliding
|
||||
self.slider.setValue(round(s.bl.light))
|
||||
@@ -1,98 +0,0 @@
|
||||
import json
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import zmq
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
from PySide6.QtGui import QImage, QPixmap
|
||||
|
||||
|
||||
class SampleCameraThread(QThread):
|
||||
# Define a signal to communicate messages from the thread to the main GUI
|
||||
camera_image = Signal(QPixmap)
|
||||
|
||||
def __init__(self, zmq_url: str, parent=None):
|
||||
super().__init__(parent)
|
||||
context = zmq.Context()
|
||||
self.__socket = context.socket(zmq.SUB)
|
||||
self.__socket.setsockopt(zmq.SUBSCRIBE, b"")
|
||||
self.__socket.setsockopt(zmq.RCVTIMEO, 500)
|
||||
self.__socket.connect(zmq_url)
|
||||
|
||||
self.running = True
|
||||
|
||||
def run(self):
|
||||
while self.running:
|
||||
try:
|
||||
r = self.__socket.recv_multipart()
|
||||
if len(r) != 2:
|
||||
continue
|
||||
meta, data = r
|
||||
header = json.loads(meta)
|
||||
header_shape = header["shape"]
|
||||
if header["type"] == "uint8" and len(header_shape) == 2:
|
||||
bayer_image = np.frombuffer(data, dtype=np.uint8)
|
||||
bayer_image = bayer_image.reshape(header_shape)
|
||||
rgb_image = cv2.cvtColor(bayer_image, cv2.COLOR_BAYER_RG2RGB)
|
||||
#cv2.COLOR_BAYER_GB2RGB for ethernet connection
|
||||
rgb_image = rgb_image[:, ::-1, :].copy()
|
||||
qimage = QImage(rgb_image.data, header_shape[1], header_shape[0], #For ethernet need header_shape[0], header_shape[1], header_shape[0]*3,
|
||||
QImage.Format.Format_RGB888)
|
||||
self.camera_image.emit(QPixmap.fromImage(qimage))
|
||||
else:
|
||||
print("Sample camera image has wrong dimensions")
|
||||
except zmq.Again: # Timeout occurred
|
||||
continue # Check self.running again
|
||||
except Exception as e:
|
||||
print(f"Error in sample camera thread {e}")
|
||||
|
||||
def stop(self):
|
||||
# Signal the thread to stop
|
||||
self.running = False
|
||||
|
||||
if self.__socket:
|
||||
self.__socket.close()
|
||||
self.quit()
|
||||
self.wait()
|
||||
|
||||
class PredictionSubscriber(QThread):
|
||||
# emits parsed JSON payload (dict with keys: time, frame_id, shape, boxes)
|
||||
prediction = Signal(dict)
|
||||
|
||||
def __init__(self, pred_zmq_url: str, topic: bytes = b"", parent=None):
|
||||
super().__init__(parent)
|
||||
self._ctx = zmq.Context()
|
||||
self._sock = self._ctx.socket(zmq.SUB)
|
||||
if topic:
|
||||
self._sock.setsockopt(zmq.SUBSCRIBE, topic)
|
||||
else:
|
||||
self._sock.setsockopt(zmq.SUBSCRIBE, b"")
|
||||
self._sock.connect(pred_zmq_url)
|
||||
self.running = True
|
||||
|
||||
def run(self):
|
||||
while self.running:
|
||||
try:
|
||||
parts = self._sock.recv_multipart()
|
||||
if not parts:
|
||||
continue
|
||||
# publisher sends either raw JSON or [topic, json]
|
||||
payload_bytes = parts[-1]
|
||||
try:
|
||||
payload = json.loads(payload_bytes.decode("utf-8"))
|
||||
except Exception:
|
||||
continue
|
||||
self.prediction.emit(payload)
|
||||
except Exception as e:
|
||||
print("PredictionSubscriber error:", e)
|
||||
break
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
try:
|
||||
self._sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
self.quit()
|
||||
print("Prediction thread stopped. Exiting...")
|
||||
self.wait()
|
||||
@@ -1,24 +1,28 @@
|
||||
[project]
|
||||
name = "aaredaq"
|
||||
version = "0.2.72"
|
||||
description = "AareDAQ data acquisition server"
|
||||
version = "0.3.0"
|
||||
description = "AareDAQ (with GUI)"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"pydantic==2.11.4",
|
||||
"numpy==2.2.5",
|
||||
"jfjoch_client==1.0.0rc126",
|
||||
"pyJWT==2.10.1",
|
||||
"pyzmq==26.4.0",
|
||||
"opencv-python-headless==4.11.0.86",
|
||||
"PySide6==6.9.0",
|
||||
"requests==2.32.4",
|
||||
"pyepics==3.5.8",
|
||||
"redis==6.2.0",
|
||||
"python-redis-lock==4.0.0",
|
||||
"PyJWT==2.10.1",
|
||||
"fastapi==0.115.13",
|
||||
"uvicorn==0.34.2",
|
||||
"ultralytics==8.3.133",
|
||||
"aaredb==0.1.1a42",
|
||||
"opencv-python-headless==4.11.0.86",
|
||||
"python_multipart==0.0.20",
|
||||
"websocket-client==1.8.0",
|
||||
"sseclient-py==1.8.0",
|
||||
"aaredaqlib==0.2.72"
|
||||
"psi-pshell==2.1.0",
|
||||
]
|
||||
|
||||
[lint]
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,59 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
def focus_measure_edges(gray: np.ndarray, mask: np.ndarray | None = None, verbose: bool = False) -> float:
|
||||
# mild denoise (optional but usually stabilizes the curve)
|
||||
gray = cv2.GaussianBlur(gray, (3, 3), 0)
|
||||
|
||||
gx = cv2.Scharr(gray, cv2.CV_64F, 1, 0)
|
||||
gy = cv2.Scharr(gray, cv2.CV_64F, 0, 1)
|
||||
g2 = gx * gx + gy * gy
|
||||
|
||||
roi = g2[mask] if mask is not None else g2.reshape(-1)
|
||||
if roi.size == 0:
|
||||
return 0.0
|
||||
|
||||
# threshold relative to median -> knocks out noise floor
|
||||
t = float(np.median(roi) * 3.0)
|
||||
strong = roi[roi > t]
|
||||
|
||||
if strong.size == 0:
|
||||
return 0.0
|
||||
if verbose:
|
||||
print(f"mask pixels: {mask.sum()}, "
|
||||
f"focus={strong.mean():.2f}"
|
||||
f"strong_size={strong.size}")
|
||||
|
||||
return float(strong.mean()) # higher = sharper
|
||||
|
||||
def focus_measure_blob_size(gray: np.ndarray, mask: np.ndarray | None = None) -> float:
|
||||
"""
|
||||
Measures sharpness for a single bright blob.
|
||||
Higher = sharper (smaller blob).
|
||||
"""
|
||||
g = gray.astype(np.float64)
|
||||
|
||||
if mask is not None:
|
||||
g = np.where(mask, g, 0.0)
|
||||
|
||||
# Background subtraction is crucial for blob metrics
|
||||
# Use a large-ish blur as background estimate (tune ksize to your scale)
|
||||
bg = cv2.GaussianBlur(g, (0, 0), sigmaX=10.0, sigmaY=10.0)
|
||||
s = g - bg
|
||||
s[s < 0] = 0.0
|
||||
|
||||
total = float(s.sum())
|
||||
if total <= 0:
|
||||
return 0.0
|
||||
|
||||
h, w = s.shape
|
||||
y, x = np.mgrid[0:h, 0:w]
|
||||
|
||||
cx = float((s * x).sum() / total)
|
||||
cy = float((s * y).sum() / total)
|
||||
|
||||
# intensity-weighted second central moment (variance)
|
||||
var = float((s * ((x - cx) ** 2 + (y - cy) ** 2)).sum() / total)
|
||||
|
||||
# smaller var => sharper, so invert
|
||||
return float(1.0 / (var + 1e-9))
|
||||
@@ -10,8 +10,6 @@ class MXBeamline(Enum):
|
||||
|
||||
|
||||
def mx_beamline() -> MXBeamline:
|
||||
name = os.getenv("BEAMLINE_XNAME")
|
||||
if name is None:
|
||||
name = os.getenv("BEAMLINE", "SIMULATED")
|
||||
name = os.getenv("BEAMLINE")
|
||||
name=name.strip().upper()
|
||||
return MXBeamline[name] if name in MXBeamline.__members__ else MXBeamline.SIMULATED
|
||||
@@ -102,3 +102,21 @@ def positive_coords(value: Coordinate) -> Coordinate:
|
||||
if value.x <= 0 or value.y <= 0:
|
||||
raise ValueError("Coordinates must be positive")
|
||||
return value
|
||||
|
||||
|
||||
class AerotechCoordinate(BaseModel):
|
||||
at_mm: Optional[Coordinate] = None
|
||||
omega_deg: Optional[float] = None
|
||||
|
||||
def eq(self, other: "AerotechCoordinate", tol: float) -> bool:
|
||||
return (
|
||||
abs(self.at_mm.x - other.at_mm.x) < tol
|
||||
and abs(self.at_mm.y - other.at_mm.y) < tol
|
||||
and abs(self.at_mm.z - other.at_mm.z) < tol
|
||||
and abs(self.omega_deg - other.omega_deg) < tol
|
||||
)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if isinstance(other, AerotechCoordinate):
|
||||
return self.eq(other, tol=0.01)
|
||||
return NotImplemented
|
||||
+1
-1
@@ -36,7 +36,7 @@ class DiffractionGeometry(BaseModel):
|
||||
|
||||
def resolution_angstrom(self, exp_dtz_mm: float) -> float:
|
||||
if exp_dtz_mm <= 0:
|
||||
raise ValueError("Detector distance must be positive")
|
||||
raise ValueError(f"Detector distance must be positive {exp_dtz_mm}")
|
||||
|
||||
theta = math.atan(self.detector_radius_mm / exp_dtz_mm)*0.5
|
||||
return self.wavelength_angstrom / (2 * math.sin(theta))
|
||||
@@ -0,0 +1,113 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import StrEnum
|
||||
|
||||
class AuthErrorCode(StrEnum):
|
||||
"""
|
||||
Stable, machine-readable error codes used across the API.
|
||||
|
||||
Rules:
|
||||
- never rename an existing value (treat as public API)
|
||||
- only add new values
|
||||
"""
|
||||
|
||||
# Generic / defaults
|
||||
AUTHENTICATION_ERROR = "AUTHENTICATION_ERROR"
|
||||
AUTHENTICATION_FAILED = "AUTHENTICATION_FAILED"
|
||||
FORBIDDEN = "FORBIDDEN"
|
||||
HTTP_ERROR = "HTTP_ERROR"
|
||||
INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR"
|
||||
|
||||
# Auth/JWT
|
||||
INVALID_TOKEN = "INVALID_TOKEN"
|
||||
SESSION_ALREADY_ACTIVE = "SESSION_ALREADY_ACTIVE"
|
||||
|
||||
# Authorization
|
||||
NOT_STAFF = "NOT_STAFF"
|
||||
NOT_IN_ACTIVE_PGROUP = "NOT_IN_ACTIVE_PGROUP"
|
||||
|
||||
|
||||
class DAQErrorCode(StrEnum):
|
||||
BEAMLINE_BUSY = "BEAMLINE_BUSY"
|
||||
|
||||
_ERROR_CODE_HELP: dict[str, str] = {
|
||||
# Auth/JWT
|
||||
AuthErrorCode.AUTHENTICATION_ERROR: (
|
||||
"Generic authentication problem. Usually means the request lacked valid credentials "
|
||||
"(expired/invalid token, missing Authorization header, etc.)."
|
||||
),
|
||||
AuthErrorCode.AUTHENTICATION_FAILED: (
|
||||
"Authentication failed during login/token creation. Typically incorrect credentials "
|
||||
"or an inability to validate the user."
|
||||
),
|
||||
AuthErrorCode.INVALID_TOKEN: (
|
||||
"The provided token could not be decoded/validated (bad signature, expired, malformed). "
|
||||
"Re-authenticate to obtain a new token."
|
||||
),
|
||||
AuthErrorCode.SESSION_ALREADY_ACTIVE: (
|
||||
"A different session currently owns control. Use “force current session” (if allowed) "
|
||||
"or wait for the active session to expire/end."
|
||||
),
|
||||
# Authorization
|
||||
AuthErrorCode.FORBIDDEN: (
|
||||
"Generic permissions failure. The user is authenticated but not allowed to perform this action."
|
||||
),
|
||||
AuthErrorCode.NOT_STAFF: (
|
||||
"This action requires staff privileges. Log in with a staff account or ask staff to perform it."
|
||||
),
|
||||
AuthErrorCode.NOT_IN_ACTIVE_PGROUP: (
|
||||
"You are not a member of the currently active p-group. Change p-group or use an account "
|
||||
"that belongs to the active group."
|
||||
),
|
||||
# Generic
|
||||
AuthErrorCode.HTTP_ERROR: (
|
||||
"Generic HTTP error wrapper. The server returned an HTTPException that wasn’t mapped to a more specific code."
|
||||
),
|
||||
AuthErrorCode.INTERNAL_SERVER_ERROR: (
|
||||
"Unhandled server error. Check server logs for a stack trace and context."
|
||||
),
|
||||
DAQErrorCode.BEAMLINE_BUSY: (
|
||||
"Beamline state is set to Busy by prior action. If this state persists an additional error may have occurred, "
|
||||
"preventing the state from being released, this should timeout within 10 minutes."
|
||||
"If this occurs please seek assistance from your local contact."
|
||||
)
|
||||
}
|
||||
|
||||
def error_code_help(code: str) -> str | None:
|
||||
"""
|
||||
Return a human help message for a code string, if known.
|
||||
Accepts either enum value strings or raw strings.
|
||||
"""
|
||||
if not code:
|
||||
return None
|
||||
return _ERROR_CODE_HELP.get(str(code))
|
||||
|
||||
|
||||
def export_error_code_help() -> dict[str, str]:
|
||||
"""
|
||||
Export help text as {"CODE": "help text", ...}
|
||||
"""
|
||||
return {str(k): str(v) for k, v in _ERROR_CODE_HELP.items()}
|
||||
|
||||
def export_error_codes_grouped() -> dict[str, dict[str, str]]:
|
||||
"""
|
||||
Export codes grouped by enum class name:
|
||||
|
||||
{
|
||||
"AuthErrorCode": {"INVALID_TOKEN": "INVALID_TOKEN", ...},
|
||||
"DAQErrorCode": {"BEAMLINE_BUSY": "BEAMLINE_BUSY", ...}
|
||||
}
|
||||
"""
|
||||
enums: tuple[type[StrEnum], ...] = (AuthErrorCode, DAQErrorCode)
|
||||
return {e.__name__: {c.name: str(c.value) for c in e} for e in enums}
|
||||
|
||||
def export_error_codes() -> dict[str, str]:
|
||||
"""
|
||||
Backwards-compatible, flat export used by older clients/tests/docs:
|
||||
|
||||
{"INVALID_TOKEN": "INVALID_TOKEN", ...}
|
||||
|
||||
NOTE: This intentionally exports only AuthErrorCode to avoid breaking
|
||||
existing consumers that assume a flat map and/or specific keys.
|
||||
"""
|
||||
return {c.name: str(c.value) for c in AuthErrorCode}
|
||||
@@ -0,0 +1,190 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from aare.common.logger_config import setup_logger
|
||||
from aare.common.error_codes import AuthErrorCode, DAQErrorCode
|
||||
|
||||
logger = setup_logger("aareDAQ")
|
||||
|
||||
class TransformationInvalidException(Exception):
|
||||
def __init__(self, message: str = "Transformation is not implemented"):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
logger.error(message, extra={"exception:": Exception})
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
|
||||
|
||||
class LoopCenteringFailed(Exception):
|
||||
def __init__(self, message: str = "Loop Centering did not detect a sample"):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
logger.error(message, extra={"exception:": Exception})
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
|
||||
|
||||
class MountingFailed(Exception):
|
||||
def __init__(self, message: str = "A sample was not mounted"):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
logger.error(message, extra={"exception:": Exception})
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
|
||||
|
||||
class WarningTellException(Exception):
|
||||
def __init__(self, message: str = "Warning error in TELL"):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
logger.error(message, extra={"exception:": Exception})
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
|
||||
|
||||
class CriticalTellException(Exception):
|
||||
def __init__(self, message: str = "Critical error in TELL"):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
logger.error(message, extra={"exception:": Exception})
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
|
||||
|
||||
class AXCFailed(Exception):
|
||||
def __init__(self, message: str = "Auto X-ray centering failed"):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
logger.error(message, extra={"exception:": Exception})
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
|
||||
|
||||
class BeamlineBusyException(Exception):
|
||||
def __init__(self, message: str = "Beamline is in busy state"):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
logger.error(message, extra={"exception:": Exception})
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
|
||||
|
||||
class SampleException(Exception):
|
||||
def __init__(self, message: str = "Sample not found"):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
logger.error(message, extra={"exception:": Exception})
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
|
||||
|
||||
class AuthenticationException(Exception):
|
||||
def __init__(self,
|
||||
message: str = "Authentication failed.",
|
||||
*,
|
||||
status_code: int = 401,
|
||||
headers: dict[str, str] | None = None,
|
||||
code: AuthErrorCode = AuthErrorCode.AUTHENTICATION_FAILED):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
self.status_code = status_code
|
||||
self.headers = headers
|
||||
self.code = code
|
||||
logger.error(message, extra={"exception:": Exception})
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
|
||||
|
||||
class UserRightsException(Exception):
|
||||
def __init__(self,
|
||||
message: str = "User does not have rights to perform this action.",
|
||||
*,
|
||||
status_code: int = 403,
|
||||
headers: dict[str, str] | None = None,
|
||||
code: AuthErrorCode = AuthErrorCode.FORBIDDEN):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
self.status_code = status_code
|
||||
self.headers = headers
|
||||
self.code = code
|
||||
logger.error(message, extra={"exception:": Exception})
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
|
||||
|
||||
class SmargonCommunicationError(Exception):
|
||||
"""
|
||||
Raised when Smargon HTTP communication fails (connection refused, timeout, bad HTTP status, etc).
|
||||
Keep the original exception in `__cause__` by using `raise ... from e`.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "Smargon communication error",
|
||||
*,
|
||||
endpoint: str | None = None,
|
||||
base_url: str | None = None,
|
||||
operation: str | None = None, # e.g. "GET" / "PUT"
|
||||
status_code: int | None = None,
|
||||
):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
self.endpoint = endpoint
|
||||
self.base_url = base_url
|
||||
self.operation = operation
|
||||
self.status_code = status_code
|
||||
logger.error(
|
||||
message,
|
||||
extra={
|
||||
"device": "smargon",
|
||||
"operation": operation,
|
||||
"endpoint": endpoint,
|
||||
"base_url": base_url,
|
||||
"status_code": status_code,
|
||||
},
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
|
||||
|
||||
class TellCommunicationError(Exception):
|
||||
"""
|
||||
Raised when TELL HTTP/PShell communication fails (timeouts, connection refused, etc).
|
||||
Intended to be caught centrally by FastAPI exception handlers.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "TELL communication error",
|
||||
*,
|
||||
endpoint: str | None = None,
|
||||
base_url: str | None = None,
|
||||
operation: str | None = None, # e.g. "GET"
|
||||
):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
self.endpoint = endpoint
|
||||
self.base_url = base_url
|
||||
self.operation = operation
|
||||
logger.error(
|
||||
message,
|
||||
extra={
|
||||
"device": "tell",
|
||||
"operation": operation,
|
||||
"endpoint": endpoint,
|
||||
"base_url": base_url,
|
||||
},
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
@@ -1,12 +1,11 @@
|
||||
import json
|
||||
import time
|
||||
from typing import Tuple, Dict, List, Iterable, Optional
|
||||
from typing import Tuple, Dict, List, Optional
|
||||
import numpy as np
|
||||
from scipy.optimize import curve_fit
|
||||
import statistics
|
||||
import math
|
||||
from aaredaqlib.logger_config import setup_logger
|
||||
from aaredaqlib.models import MLBoxModel, MLOutputModel
|
||||
from aare.common.logger_config import setup_logger
|
||||
|
||||
logger = setup_logger("aareDAQ")
|
||||
|
||||
@@ -3,10 +3,9 @@ from typing import List, Optional, Callable
|
||||
import numpy as np
|
||||
from scipy import ndimage
|
||||
|
||||
from aaredaqlib.coordinate import Coordinate
|
||||
from aaredaqlib.models import CrystalSize
|
||||
from aaredaqlib.raster_grid import RasterGridRequest, CenterOfMassModel
|
||||
from aaredaqlib.logger_config import setup_logger
|
||||
from aare.common.models import CrystalSize
|
||||
from aare.common.raster_grid import RasterGridRequest, CenterOfMassModel
|
||||
from aare.common.logger_config import setup_logger
|
||||
|
||||
logger = setup_logger('aareDAQ')
|
||||
|
||||
@@ -63,7 +63,7 @@ def setup_logger(name="aareDAQ", base_dir: str | None = '~/tmp/mxlogs', config_p
|
||||
h["filename"] = abs_filename
|
||||
log_dir = os.path.dirname(abs_filename)
|
||||
if log_dir and not os.path.exists(log_dir):
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
|
||||
logging.config.dictConfig(config)
|
||||
logging.getLogger("redis_lock").setLevel(logging.WARNING)
|
||||
@@ -3,16 +3,14 @@ import re
|
||||
from enum import Enum
|
||||
from typing import Annotated, Literal, Tuple, List, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator, AfterValidator
|
||||
|
||||
from aaredaqlib.coordinate import Coordinate, positive_coords
|
||||
from aaredaqlib.diffraction_geometry import DiffractionGeometry
|
||||
from aaredaqlib.sample_geometry import SampleGeometryModel
|
||||
from aare.common.coordinate import Coordinate, positive_coords
|
||||
from aare.common.diffraction_geometry import DiffractionGeometry
|
||||
from aare.common.sample_geometry import SampleGeometryModel
|
||||
from jfjoch_client.models.scan_result import ScanResult
|
||||
|
||||
from aaredaqlib.beamline import MXBeamline
|
||||
from aare.common.beamline import MXBeamline
|
||||
|
||||
class StagePositionEnum(Enum):
|
||||
MEASURE = 0
|
||||
@@ -651,7 +649,8 @@ def def_loop_centering_zoom(beamline) -> ZoomModel:
|
||||
if beamline == MXBeamline.X06DA:
|
||||
return ZoomModel(z={1: SampleCameraSettings(gain=0, exposure=0.05)})
|
||||
elif beamline == MXBeamline.X10SA:
|
||||
return ZoomModel(z={1: SampleCameraSettings(gain=0, exposure=0.05)})
|
||||
return ZoomModel(z={1: SampleCameraSettings(gain=0, exposure=0.002),
|
||||
280: SampleCameraSettings(gain=0, exposure=0.002)})
|
||||
elif beamline == MXBeamline.X06SA:
|
||||
return ZoomModel(z={1: SampleCameraSettings(gain=0, exposure=0.05)})
|
||||
elif beamline == MXBeamline.SIMULATED:
|
||||
@@ -675,8 +674,8 @@ def zoom_manager(mode: ZoomModeEnum = ZoomModeEnum.User, beamline: MXBeamline =
|
||||
|
||||
|
||||
class AutofocusSettings(BaseModel):
|
||||
center_x_pxl: float
|
||||
center_y_pxl: float
|
||||
center_x_pxl: float | None # Use beam center
|
||||
center_y_pxl: float | None # Use beam center
|
||||
radius_pxl: float
|
||||
z_range_um: float
|
||||
z_steps: int
|
||||
@@ -684,7 +683,8 @@ class AutofocusSettings(BaseModel):
|
||||
class BeamlineStatus(BaseModel):
|
||||
name: str
|
||||
ring_current_mA: float
|
||||
light: Annotated[float, Field(ge=0.0, le=100.0)]
|
||||
front_light: Annotated[float, Field(ge=0.0, le=100.0)]
|
||||
back_light: Annotated[float, Field(ge=0.0, le=100.0)]
|
||||
cryojet_K: float
|
||||
shutter_open: bool
|
||||
exp_shutter_open: bool | None
|
||||
@@ -719,6 +719,12 @@ class DAQStatusModel(BaseModel):
|
||||
last_best_b_factor: float | None = None
|
||||
crystal_size: CrystalSize = CrystalSize(x=0,y=0,z=0)
|
||||
|
||||
tell_connected: bool = True
|
||||
tell_error: str | None = None
|
||||
|
||||
smargon_connected: bool = True
|
||||
smargon_error: str | None = None
|
||||
|
||||
class BeamlineSettingsModel(BaseModel):
|
||||
dtz_max: float | None = 1600.0
|
||||
dtz_min: float | None = 120.0
|
||||
@@ -731,7 +737,7 @@ class BeamlineSettingsModel(BaseModel):
|
||||
camera_max_magnification: float | None = 1.0
|
||||
camera_min_magnification: float | None = 500.0
|
||||
camera_translation_factor_a: float | None = 0.00253
|
||||
camera_translation_factor_b: float | None = 255.0
|
||||
camera_translation_factor_b: float | None = 512.0
|
||||
|
||||
class CryojetSettingsModel(BaseModel):
|
||||
cryojet_park_position: float | None = 12.0
|
||||
@@ -777,4 +783,7 @@ class ScanResultPayloadModel(BaseModel):
|
||||
sample_id: int
|
||||
attach_image: bool = True
|
||||
beam_mark_pxl: tuple[float, float]
|
||||
beam_size_mm: Annotated[Coordinate, AfterValidator(positive_coords)]
|
||||
beam_size_mm: Annotated[Coordinate, AfterValidator(positive_coords)]
|
||||
|
||||
class RecoveryActionRequest(BaseModel):
|
||||
confirmation_code: str
|
||||
@@ -5,8 +5,8 @@ import numpy as np
|
||||
from jfjoch_client.models.scan_result import ScanResult
|
||||
from pydantic import Field, AfterValidator, BaseModel
|
||||
|
||||
from aaredaqlib.coordinate import Coordinate, positive_coords, SmargonCoordinate
|
||||
from aaredaqlib.sample_geometry import SampleGeometryModel
|
||||
from aare.common.coordinate import Coordinate, positive_coords, SmargonCoordinate
|
||||
from aare.common.sample_geometry import SampleGeometryModel
|
||||
|
||||
|
||||
class RasterGridRequest(BaseModel):
|
||||
@@ -1,7 +1,7 @@
|
||||
from jfjoch_client.models.scan_result import ScanResult
|
||||
from pydantic import BaseModel
|
||||
|
||||
from aaredaqlib.coordinate import SmargonCoordinate
|
||||
from aare.common.coordinate import SmargonCoordinate
|
||||
|
||||
|
||||
class RotationScanRequest(BaseModel):
|
||||
@@ -3,7 +3,7 @@ from typing import Annotated
|
||||
import numpy as np
|
||||
from pydantic import BaseModel, Field, AfterValidator
|
||||
|
||||
from aaredaqlib.coordinate import Coordinate, SmargonCoordinate, positive_coords
|
||||
from aare.common.coordinate import Coordinate, SmargonCoordinate, positive_coords
|
||||
|
||||
|
||||
class SampleGeometryModel(BaseModel):
|
||||
@@ -4,11 +4,11 @@ import json
|
||||
import os
|
||||
from typing import List, Optional
|
||||
|
||||
import aareDBclient
|
||||
import aareDB
|
||||
import cv2
|
||||
import numpy as np
|
||||
import requests
|
||||
from aareDBclient import (
|
||||
from aareDB import (
|
||||
SetTellPosition,
|
||||
SampleEventCreate,
|
||||
SampleEventType,
|
||||
@@ -21,21 +21,18 @@ from aareDBclient import (
|
||||
BeamlineParametersInput,
|
||||
ExperimentParametersCreate)
|
||||
|
||||
from aaredaqlib.coordinate import Coordinate
|
||||
from aaredaqlib.diffraction_geometry import DiffractionGeometry
|
||||
from aaredaqlib.logger_config import setup_logger
|
||||
from aaredaqlib.models import (
|
||||
from aare.common.coordinate import Coordinate
|
||||
from aare.common.logger_config import setup_logger
|
||||
from aare.common.models import (
|
||||
SampleShortInfo,
|
||||
PuckLoadedInfo,
|
||||
DewarAddress,
|
||||
SampleShortInfoList,
|
||||
DAQStatusModel, SessionStatus, ScanResultPayloadModel,
|
||||
DAQStatusModel, ScanResultPayloadModel,
|
||||
)
|
||||
|
||||
from aaredaqlib.beamline import MXBeamline
|
||||
from aaredaqlib.raster_grid import RasterGridRequest, RasterPayloadModel, CenterOfMassModel
|
||||
from aaredaqlib.rotation_scan import RotationScanRequest
|
||||
from aaredaqlib.sample_geometry import SampleGeometryModel
|
||||
from aare.common.beamline import MXBeamline
|
||||
from aare.common.raster_grid import RasterGridRequest, RasterPayloadModel, CenterOfMassModel
|
||||
from aare.common.rotation_scan import RotationScanRequest
|
||||
from aare.common.sample_geometry import SampleGeometryModel
|
||||
|
||||
from jfjoch_client.models import ScanResult
|
||||
|
||||
@@ -47,16 +44,16 @@ class AareWrapper:
|
||||
bl: MXBeamline,
|
||||
host: str = "https://mx-db-01.psi.ch/dispatcher",
|
||||
):
|
||||
configuration = aareDBclient.Configuration(host=host)
|
||||
configuration = aareDB.Configuration(host=host)
|
||||
configuration.verify_ssl = False # Disable SSL verification
|
||||
|
||||
self.client = aareDBclient.ApiClient(configuration)
|
||||
self.client = aareDB.ApiClient(configuration)
|
||||
self.client.default_headers["X-Shared-Password"] = os.getenv("AAREDB_SHARED_PASSWORD")
|
||||
self.__host = host
|
||||
self.__tell_api = aareDBclient.TellsRunnerApi(self.client)
|
||||
self.__sample_api = aareDBclient.SamplesRunnerApi(self.client)
|
||||
self.__proc_api = aareDBclient.ProcessingsRunnerApi(self.client)
|
||||
self.__raster_api = aareDBclient.GridscanRunnerApi(self.client)
|
||||
self.__tell_api = aareDB.TellsRunnerApi(self.client)
|
||||
self.__sample_api = aareDB.SamplesRunnerApi(self.client)
|
||||
self.__proc_api = aareDB.ProcessingsRunnerApi(self.client)
|
||||
self.__raster_api = aareDB.GridscanRunnerApi(self.client)
|
||||
self.__bl = bl
|
||||
|
||||
def set_pucks_beamline(self, input_list: List[PuckLoadedInfo]):
|
||||
@@ -76,7 +73,7 @@ class AareWrapper:
|
||||
print(ret)
|
||||
|
||||
def create_manual_sample(self, s: SampleShortInfo):
|
||||
from aareDBclient.models import ManualSampleCreate
|
||||
from aareDB.models import ManualSampleCreate
|
||||
|
||||
manual_sample = ManualSampleCreate(
|
||||
pgroup=s.user,
|
||||
@@ -188,7 +185,7 @@ class AareWrapper:
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending message to db: {e}")
|
||||
|
||||
def upload_image(self, sample_id: int, filename: str, bgr_image: np.ndarray):
|
||||
def upload_image(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)
|
||||
url = f"{self.__host}/protected_router/sample_runner/{sample_id}/upload-images"
|
||||
@@ -196,9 +193,16 @@ class AareWrapper:
|
||||
"accept": "application/json",
|
||||
"X-Shared-Password": os.getenv("AAREDB_SHARED_PASSWORD")
|
||||
}
|
||||
response = requests.post(url,
|
||||
files={'uploaded_file': (filename + ".jpg", jpeg_bytes, "image/jpeg")},
|
||||
verify=False, headers=headers)
|
||||
|
||||
request_kwargs = {
|
||||
"files": {'uploaded_file': (filename + ".jpg", jpeg_bytes, "image/jpeg")},
|
||||
"verify": False,
|
||||
"headers": headers,
|
||||
}
|
||||
if message is not None:
|
||||
request_kwargs["data"] = {"comment": message}
|
||||
|
||||
response = requests.post(url, **request_kwargs)
|
||||
logger.debug(f"Response status code: {response.status_code}")
|
||||
|
||||
def upload_jpg(self, sample_id: int, filename: str, jpg_image):
|
||||
@@ -9,7 +9,9 @@ from fastapi import HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from pydantic import BaseModel
|
||||
|
||||
from aaredaq.config import BeamlineConfig
|
||||
from aare.daq.config import BeamlineConfig
|
||||
|
||||
from aare.common.exception_handler import AuthenticationException, UserRightsException, AuthErrorCode
|
||||
|
||||
if os.environ.get("JWT_AAREDAQ_KEY") is None:
|
||||
raise Exception("JWT_AAREDAQ_KEY environment variable not set, cannot guarantee safe authentication.")
|
||||
@@ -58,20 +60,21 @@ def parse_token(token: str) -> TokenData:
|
||||
token = TokenData(**payload)
|
||||
return token
|
||||
except jwt.PyJWTError as e:
|
||||
print(f"JWT error {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token",
|
||||
)
|
||||
raise AuthenticationException(
|
||||
message="Invalid token",
|
||||
status_code=401,
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
code=AuthErrorCode.INVALID_TOKEN,
|
||||
) from e
|
||||
|
||||
|
||||
def check_jwt_ro(cfg: BeamlineConfig, data: TokenData) -> None:
|
||||
active_pgroup = cfg.pgroup
|
||||
if not data.staff and (active_pgroup is None or active_pgroup not in data.pgroups):
|
||||
print("Not member of a currently active p-group.")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Not member of a currently active p-group.",
|
||||
raise UserRightsException(
|
||||
message="Not member of a currently active p-group.",
|
||||
status_code=403,
|
||||
code=AuthErrorCode.NOT_IN_ACTIVE_PGROUP,
|
||||
)
|
||||
|
||||
|
||||
@@ -81,30 +84,32 @@ def check_jwt_rw(cfg: BeamlineConfig, data: TokenData) -> None:
|
||||
try:
|
||||
cfg.try_set_active_session(data.session, SESSION_EXPIRE_SECONDS)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Another session is active.",
|
||||
raise AuthenticationException(
|
||||
message="Another session is active.",
|
||||
status_code=401,
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
code=AuthErrorCode.SESSION_ALREADY_ACTIVE,
|
||||
) from e
|
||||
|
||||
def check_jwt_staff_only(data: TokenData) -> None:
|
||||
if not data.staff:
|
||||
raise UserRightsException(
|
||||
message="Not member of the MX staff.",
|
||||
status_code=403,
|
||||
code=AuthErrorCode.NOT_STAFF,
|
||||
)
|
||||
|
||||
def check_jwt_staff(cfg: BeamlineConfig, data: TokenData) -> None:
|
||||
if not data.staff:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Not member of the MX staff.",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
check_jwt_staff_only(data)
|
||||
try:
|
||||
cfg.try_set_active_session(data.session, SESSION_EXPIRE_SECONDS)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Another session is active.",
|
||||
raise AuthenticationException(
|
||||
message="Another session is active.",
|
||||
status_code=401,
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
code=AuthErrorCode.SESSION_ALREADY_ACTIVE,
|
||||
) from e
|
||||
|
||||
def force_current_sesion(cfg: BeamlineConfig, data: TokenData) -> None:
|
||||
cfg.force_set_active_session(data.session, SESSION_EXPIRE_SECONDS)
|
||||
@@ -6,8 +6,8 @@ from typing import Tuple, List
|
||||
import numpy as np
|
||||
import redis
|
||||
import redis_lock
|
||||
from aaredaqlib.coordinate import Coordinate
|
||||
from aaredaqlib.models import (
|
||||
from aare.common.coordinate import Coordinate
|
||||
from aare.common.models import (
|
||||
BeamlineSettingsModel,
|
||||
BeamMarkCoeffModel,
|
||||
ZoomModeEnum,
|
||||
@@ -18,11 +18,13 @@ from aaredaqlib.models import (
|
||||
FluorescenceSpectrumOutputModel, CrystalSize, SimpleStrategyInputModel, SimpleScanParameters
|
||||
)
|
||||
|
||||
from aaredaqlib.beamline import MXBeamline
|
||||
from aaredaqlib.logger_config import setup_logger
|
||||
from aare.common.beamline import MXBeamline
|
||||
from aare.common.logger_config import setup_logger
|
||||
|
||||
from aare.common.exception_handler import BeamlineBusyException
|
||||
|
||||
ABR_POS_ALIGN_DEF = Coordinate(x=-18, y=-0.266, z=0)
|
||||
ABR_POS_MOUNT = Coordinate(x=-18, y=0, z=0)
|
||||
ABR_POS_MOUNT = Coordinate(x=0, y=0, z=0)#Coordinate(x=-18, y=0, z=0)
|
||||
ABR_OMEGA_MOUNT = 0.0
|
||||
|
||||
logger = setup_logger("aareDAQ")
|
||||
@@ -47,10 +49,6 @@ def base64_to_numpy(encoded_str: str | None) -> np.ndarray | None:
|
||||
return np.load(buffer) # Load buffer as a NumPy array
|
||||
|
||||
|
||||
class BeamlineBusyException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BeamlineConfig:
|
||||
"""
|
||||
Manages the configuration and state of a beamline system by interacting with a Redis
|
||||
@@ -84,10 +82,23 @@ class BeamlineConfig:
|
||||
|
||||
@property
|
||||
def active_session(self) -> int | None:
|
||||
tmp = self.__client.get(f"{self.__bl}:active_session")
|
||||
if tmp is None:
|
||||
"""
|
||||
Read active_session from Redis and convert to int.
|
||||
|
||||
Returns:
|
||||
int if present and valid, otherwise None.
|
||||
"""
|
||||
raw = self.__client.get(f"{self.__bl}:active_session")
|
||||
if raw is None:
|
||||
return None
|
||||
try:
|
||||
return int(raw)
|
||||
except (TypeError, ValueError):
|
||||
logger.warning(
|
||||
"Invalid active_session value in redis; treating as missing",
|
||||
extra={"beamline": self.__bl, "raw": raw},
|
||||
)
|
||||
return None
|
||||
return int(tmp)
|
||||
|
||||
def session_status(self, session: int) -> SessionStatus:
|
||||
return SessionStatus(session=self.session_state(session),
|
||||
@@ -104,12 +115,12 @@ class BeamlineConfig:
|
||||
|
||||
def try_set_active_session(self, session: int, expiry_sec: int) -> None:
|
||||
with redis_lock.Lock(
|
||||
self.__client, f"{self.__bl}:active_session_lock", expire=10
|
||||
self.__client, f"{self.__bl}:active_session_lock", expire=10
|
||||
):
|
||||
tmp = self.__client.get(f"{self.__bl}:active_session")
|
||||
if tmp is None:
|
||||
active = self.active_session
|
||||
if active is None:
|
||||
self.__client.set(f"{self.__bl}:active_session", session)
|
||||
elif int(tmp) != session:
|
||||
elif active != session:
|
||||
raise Exception(
|
||||
"There is already active session with different id. Try again later."
|
||||
)
|
||||
@@ -118,26 +129,28 @@ class BeamlineConfig:
|
||||
#TODO finish setting this up!
|
||||
def try_extend_active_session(self, session: int, expiry_sec: int) -> None:
|
||||
with redis_lock.Lock(
|
||||
self.__client, f"{self.__bl}:active_session_lock", expire=10
|
||||
self.__client, f"{self.__bl}:active_session_lock", expire=10
|
||||
):
|
||||
tmp = self.__client.get(f"{self.__bl}:active_session")
|
||||
if tmp == session:
|
||||
self.__client.expire(f"{self.__bl}:active_session", expiry_sec, gt=True)
|
||||
elif int(tmp) != session:
|
||||
raise Exception(
|
||||
"There is already active session with different id. Try again later."
|
||||
)
|
||||
else:
|
||||
active = self.active_session
|
||||
if active is None:
|
||||
raise Exception(
|
||||
"There is no active session with given id. Try again later."
|
||||
)
|
||||
|
||||
if active == session:
|
||||
self.__client.expire(f"{self.__bl}:active_session", expiry_sec, gt=True)
|
||||
else:
|
||||
raise Exception(
|
||||
"There is already active session with different id. Try again later."
|
||||
)
|
||||
|
||||
def end_active_session(self, session: int) -> None:
|
||||
with redis_lock.Lock(
|
||||
self.__client, f"{self.__bl}:active_session_lock", expire=10
|
||||
):
|
||||
if int(self.__client.get(f"{self.__bl}:active_session")) == session:
|
||||
active = self.active_session
|
||||
if active is None:
|
||||
return
|
||||
if active == session:
|
||||
self.__client.delete(f"{self.__bl}:active_session")
|
||||
|
||||
def force_set_active_session(self, session: int, expiry_sec: int) -> None:
|
||||
@@ -554,12 +567,6 @@ class BeamlineConfig:
|
||||
else:
|
||||
self.__client.set(f"{self.__bl}:last_best_b_factor", last_best_b_factor)
|
||||
|
||||
|
||||
|
||||
@crystal_size.setter
|
||||
def crystal_size(self, xtal_size: CrystalSize):
|
||||
self.__client.set(f"{self.__bl}:crystal_size", xtal_size.model_dump_json())
|
||||
|
||||
@property
|
||||
def simple_input_parameters(self) -> SimpleStrategyInputModel | None:
|
||||
tmp = self.__client.get(f"{self.__bl}:simple_input_params")
|
||||
@@ -600,7 +607,14 @@ class BeamlineConfig:
|
||||
tmp = self.__client.get(f"{self.__bl}:failed_mount_count")
|
||||
if tmp is None:
|
||||
return 0
|
||||
return tmp
|
||||
try:
|
||||
return int(tmp)
|
||||
except (TypeError, ValueError):
|
||||
logger.warning(
|
||||
"Failed Mount Count is not an integer, resetting to 0.",
|
||||
extra={"beamline": self.__bl, "tmp": tmp},
|
||||
)
|
||||
return 0
|
||||
|
||||
@failed_mount_count.setter
|
||||
def failed_mount_count(self, count:int):
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,355 @@
|
||||
# Abstractions of devices for beamline
|
||||
|
||||
# Each "standard" device needs three elements:
|
||||
# - property to read device value
|
||||
# - setter with option to do sync/async move
|
||||
# - property setter, which assumes that sync move is done (excl. zoom, which is async by default)
|
||||
|
||||
import time
|
||||
from enum import Enum
|
||||
|
||||
import numpy as np
|
||||
from epics import PV
|
||||
from fontTools.feaLib.ast import deviceToString
|
||||
|
||||
from aare.common.beamline import MXBeamline
|
||||
from aare.common.coordinate import Coordinate, SmargonCoordinate
|
||||
from aare.common.models import SampleCameraSettings, StagePositionEnum
|
||||
from aare.devices import smargon, aerotech
|
||||
from aare.devices.area_detector import epicsAD, AutoEnum
|
||||
from aare.devices.enum_pv import EnumPV
|
||||
from aare.devices.my_motor import MyMotor
|
||||
|
||||
from aare.devices.set_get_pv import SetGetPV, PredefinedPV
|
||||
from aare.devices.tell_client import make_tell_client
|
||||
|
||||
class BeamlineDevices:
|
||||
def __init__(self, beamline: MXBeamline):
|
||||
BEAMLINE = beamline.value.upper()
|
||||
self.tell = make_tell_client(beamline)
|
||||
self.__aerotech = aerotech.AerotechControllerEpics(beamline)
|
||||
self.aerotech = aerotech.AerotechController(controller_ip="129.129.118.96")
|
||||
self.__smargon = smargon.Smargon(beamline)
|
||||
|
||||
self.__ring_current_pv = PV(f"ARS07-DPCT-0100:CURR")
|
||||
|
||||
self.__dtz = MyMotor(f"{BEAMLINE}-ES-DET:TRZ")
|
||||
self.__dty = MyMotor(f"{BEAMLINE}-ES-DET:TRY")
|
||||
|
||||
self.__det_cov = EnumPV(name="det_cov",
|
||||
setpv=f"{BEAMLINE}-ES-DETCOV:SET",
|
||||
getpv=f"{BEAMLINE}-ES-DETCOV:GET")
|
||||
|
||||
self.__sample_cam = epicsAD(f"{BEAMLINE}-ES-MS:")
|
||||
|
||||
self.__front_light = PredefinedPV(name='front_light',
|
||||
setpv=f"{BEAMLINE}-ES-FL:SET",
|
||||
getpv=f"{BEAMLINE}-ES-FL:SET",
|
||||
predefs={"off":1.49,
|
||||
'half':2.0,
|
||||
'max':3.0},
|
||||
timeout=10.0
|
||||
)
|
||||
self.__back_light = PredefinedPV(name='back_light',
|
||||
setpv =f"{BEAMLINE}-ES-BL:SET",
|
||||
getpv=f"{BEAMLINE}-ES-BL:SET",
|
||||
predefs={"off": 0,
|
||||
'half': 0.98,
|
||||
'max': 1.2},
|
||||
timeout=10.0
|
||||
)
|
||||
|
||||
self.__back_light_pos = EnumPV(name = "back_light_pos",
|
||||
setpv = f"{BEAMLINE}-ES-BL:POS-SET",
|
||||
getpv = f"{BEAMLINE}-ES-BL:POS-GET",
|
||||
timeout = 10.0)
|
||||
|
||||
self.__collimator_pos = MyMotor(f"{BEAMLINE}-ES-COL:TRY")
|
||||
self.__collimator_X = MyMotor(f"{BEAMLINE}-ES-COL:TRX") # HOW TO HANDLE!!!
|
||||
|
||||
self.__scintillator_pos = PV(f"{BEAMLINE}-ES-SCL:TRY") # how to handle!!!
|
||||
self.__scintillator_z = PV(f"{BEAMLINE}-ES-SCL:TRZ")
|
||||
|
||||
self.__beamstop_pos = EnumPV(name = "beamstop_pos",
|
||||
setpv = f"{BEAMLINE}-ES-BS:POS-SET",
|
||||
getpv = f"{BEAMLINE}-ES-BS:POS-GET",
|
||||
timeout = 10.0)
|
||||
|
||||
self.__beamstop_x = MyMotor(f"{BEAMLINE}-ES-BS:TRX")
|
||||
self.__beamstop_y = MyMotor(f"{BEAMLINE}-ES-BS:TRY")
|
||||
self.__beamstop_z = MyMotor(f"{BEAMLINE}-ES-BS:TRZ")
|
||||
|
||||
|
||||
self.__zoom = SetGetPV(name = f"zoom",
|
||||
setpv = f"{BEAMLINE}-ES-MS:ZOOM.VAL",
|
||||
getpv = f"{BEAMLINE}-ES-MS:ZOOM.RBV")
|
||||
|
||||
self.__cryojet_pos = EnumPV(name='cryojet_pos',
|
||||
setpv = f"{BEAMLINE}-ES-CS:POS-SET",
|
||||
getpv = f"{BEAMLINE}-ES-CS:POS-GET",
|
||||
timeout = 10.0)
|
||||
|
||||
self.__cryojet_x = MyMotor(f"{BEAMLINE}-ES-CS:TRX") #currently in is 5 out is 15?
|
||||
|
||||
|
||||
|
||||
# Transmission
|
||||
@property
|
||||
def transmission(self) -> float:
|
||||
return 1.0
|
||||
def set_transmission(self, value: float, /, wait: bool = True):
|
||||
pass
|
||||
|
||||
@transmission.setter
|
||||
def transmission(self, value: float):
|
||||
self.set_transmission(value, wait=False)
|
||||
|
||||
# Lamp light
|
||||
@property
|
||||
def lamp_light(self) -> float:
|
||||
return self.__front_light.value
|
||||
|
||||
@lamp_light.setter
|
||||
def lamp_light(self, v: float):
|
||||
self.set_front_light(v, wait=False)
|
||||
|
||||
def set_front_light(self, v: float, /, wait: bool = True):
|
||||
self.__front_light.move(v, wait=wait)
|
||||
|
||||
# Back light
|
||||
@property
|
||||
def back_light(self) -> float:
|
||||
return self.__back_light.value
|
||||
|
||||
@back_light.setter
|
||||
def back_light(self, v: float):
|
||||
self.set_back_light(v, wait=False)
|
||||
|
||||
def set_back_light(self, v: float, /, wait: bool = True):
|
||||
self.__back_light.move(v, wait=wait)
|
||||
|
||||
# Zoom
|
||||
@property
|
||||
def zoom(self) -> float:
|
||||
return self.__zoom.value
|
||||
|
||||
@zoom.setter
|
||||
def zoom(self, value: float):
|
||||
self.set_zoom(value, wait=True)
|
||||
|
||||
def set_zoom(self, value: float, /, wait: bool = True):
|
||||
self.__zoom.move(value, wait=wait)
|
||||
|
||||
# Collimator
|
||||
@property
|
||||
def collimator(self) -> float:
|
||||
return self.__collimator_pos.get()
|
||||
|
||||
@collimator.setter
|
||||
def collimator(self, value: float):
|
||||
self.set_collimator(value, wait=True)
|
||||
|
||||
def set_collimator(self, value: float, /, wait: bool = True):
|
||||
self.__collimator_pos.move(value, wait=wait)
|
||||
|
||||
# Scintillator
|
||||
@property
|
||||
def scintillator(self) -> float:
|
||||
return self.__scintillator_pos.get()
|
||||
|
||||
@scintillator.setter
|
||||
def scintillator(self, value: float):
|
||||
self.set_scintillator(value, wait=True)
|
||||
|
||||
def set_scintillator(self, value: float, /, wait: bool = True):
|
||||
self.__scintillator_pos.put(value, wait=wait)
|
||||
|
||||
# Reflector (backlight?)
|
||||
@property
|
||||
def reflector_up(self) -> bool:
|
||||
return self.__back_light_pos.position.upper() == StagePositionEnum.MEASURE.name
|
||||
|
||||
@reflector_up.setter
|
||||
def reflector_up(self, value: StagePositionEnum):
|
||||
self.set_reflector_up(value, wait=True)
|
||||
|
||||
def set_reflector_up(self, value: StagePositionEnum, /, wait: bool = True):
|
||||
self.__back_light_pos.move(value, wait=wait)
|
||||
|
||||
# Beamstop
|
||||
@property
|
||||
def beamstop_stage_up(self) -> bool:
|
||||
return False
|
||||
|
||||
@beamstop_stage_up.setter
|
||||
def beamstop_stage_up(self, value: bool):
|
||||
pass
|
||||
|
||||
@property
|
||||
def beamstop_z(self) -> float:
|
||||
return 35.0
|
||||
|
||||
@beamstop_z.setter
|
||||
def beamstop_z(self, value: float):
|
||||
pass
|
||||
|
||||
# Optics
|
||||
@property
|
||||
def energy_kev(self) -> float:
|
||||
return 12.4
|
||||
|
||||
@property
|
||||
def ring_current(self) -> float:
|
||||
return max(0.0, 0.0)
|
||||
|
||||
@property
|
||||
def flux(self) -> float:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def full_flux(self) -> float:
|
||||
return 0
|
||||
|
||||
# Cryojet
|
||||
@property
|
||||
def cryojet_temp(self) -> float:
|
||||
return 100
|
||||
|
||||
def anneal(self, time: float):
|
||||
pass
|
||||
|
||||
@property
|
||||
def cryojet_pos(self) -> StagePositionEnum:
|
||||
return StagePositionEnum(self.__cryojet_pos.position.upper())
|
||||
|
||||
@cryojet_pos.setter
|
||||
def cryojet_pos(self, value: StagePositionEnum):
|
||||
self.cryojet_pos_setter(value, wait=True)
|
||||
|
||||
def cryojet_pos_setter(self, value: StagePositionEnum, wait:bool=False):
|
||||
self.__cryojet_pos.move(value, wait=wait)
|
||||
|
||||
# Shutter
|
||||
@property
|
||||
def shutter(self) -> bool:
|
||||
return False
|
||||
|
||||
@shutter.setter
|
||||
def shutter(self, opened: bool):
|
||||
pass
|
||||
|
||||
# Sample camera
|
||||
@property
|
||||
def samcam_settings(self) -> SampleCameraSettings:
|
||||
return SampleCameraSettings(
|
||||
gain=self.__sample_cam.gain_rbv.value,
|
||||
exposure=self.__sample_cam.expo_rbv.value
|
||||
)
|
||||
|
||||
@samcam_settings.setter
|
||||
def samcam_settings(self, settings: SampleCameraSettings):
|
||||
self.__sample_cam.setup(settings.gain, settings.exposure)
|
||||
|
||||
def samcam_get_image(self, /, gray: bool = False) -> np.ndarray:
|
||||
return self.__sample_cam.get_image(gray=gray)
|
||||
|
||||
def samcam_auto(self, state: AutoEnum):
|
||||
self.__sample_cam.set_auto(state)
|
||||
|
||||
def samcam_frame_id(self) -> int:
|
||||
"""
|
||||
Camera UniqueId for the last produced frame (monotonic counter from AreaDetector).
|
||||
"""
|
||||
return int(self.__sample_cam.uid.get())
|
||||
|
||||
|
||||
|
||||
# Detector Z
|
||||
@property
|
||||
def dtz(self) -> float:
|
||||
return self.__dtz.readback
|
||||
|
||||
@dtz.setter
|
||||
def dtz(self, value: float):
|
||||
self.set_dtz(value, wait=True)
|
||||
|
||||
def set_dtz(self, value: float, /, wait: bool = True):
|
||||
self.__dtz.move(value, wait=wait)
|
||||
|
||||
@property
|
||||
def dtz_low(self) -> float:
|
||||
return self.__dtz.get("LLM")
|
||||
|
||||
@property
|
||||
def dtz_high(self) -> float:
|
||||
return self.__dtz.get("HLM")
|
||||
|
||||
# Aertoech Automation1
|
||||
@property
|
||||
def aerotech_pos(self) -> Coordinate:
|
||||
return Coordinate(x=self.__aerotech.gmx.readback,
|
||||
y=self.__aerotech.gmy.readback, z=self.__aerotech.gmz.readback)
|
||||
|
||||
@aerotech_pos.setter
|
||||
def aerotech_pos(self, pos: Coordinate):
|
||||
# self.__aerotech.gmx.move(pos.x, wait=False)
|
||||
# self.__aerotech.gmy.move(pos.y, wait=False)
|
||||
# self.__aerotech.gmz.move(pos.z, wait=False)
|
||||
self.__aerotech.gmx.move(pos.x, wait=True)
|
||||
self.__aerotech.gmy.move(pos.y, wait=True)
|
||||
self.__aerotech.gmz.move(pos.z, wait=True)
|
||||
#TODO add wait pos?
|
||||
|
||||
@property
|
||||
def aerotech_omega(self) -> float:
|
||||
return self.__aerotech.omega.readback
|
||||
|
||||
@aerotech_omega.setter
|
||||
def aerotech_omega(self, val: float):
|
||||
self.set_aerotech_omega(val, wait=True)
|
||||
|
||||
def set_aerotech_omega(self, val: float, /, wait: bool = True):
|
||||
self.__aerotech.omega.move(val, wait=wait)
|
||||
|
||||
@property
|
||||
def aerotech_lock(self) -> bool:
|
||||
return False
|
||||
|
||||
@aerotech_lock.setter
|
||||
def aerotech_lock(self, val: bool):
|
||||
pass
|
||||
|
||||
def aerotech_stop(self):
|
||||
pass
|
||||
|
||||
# Smargon goniometer
|
||||
@property
|
||||
def smargon_pos(self) -> SmargonCoordinate:
|
||||
return self.__smargon.readback
|
||||
|
||||
def set_smargon_pos(self, pos: SmargonCoordinate, /, wait: bool = True):
|
||||
self.__smargon.target = pos
|
||||
|
||||
@smargon_pos.setter
|
||||
def smargon_pos(self, pos: SmargonCoordinate):
|
||||
self.set_smargon_pos(pos, wait=True)
|
||||
|
||||
def smargon_wait(self, timeout: float = 10.0):
|
||||
self.__smargon.wait(timeout=timeout)
|
||||
|
||||
def smargon_move_home(self):
|
||||
self.__smargon.move_home(wait=True)
|
||||
|
||||
def smargon_aerotech_wait(self):
|
||||
self.__smargon.wait_aerotech(timeout=10.0)
|
||||
|
||||
if __name__ == "__main__":
|
||||
from aare.common.beamline import mx_beamline
|
||||
beamline = mx_beamline()
|
||||
devs = BeamlineDevices(beamline)
|
||||
print(devs.aerotech_pos)
|
||||
# devs.aerotech_pos = Coordinate(x=124.0, y=1.0, z=1.0)
|
||||
# print(devs.aerotech_pos)
|
||||
# devs.aerotech_omega = 0.0
|
||||
print(devs.reflector_up)
|
||||
devs.reflector_up = StagePositionEnum.MEASURE
|
||||
@@ -1,24 +1,41 @@
|
||||
|
||||
from typing import Tuple, Optional, Iterable
|
||||
from enum import Enum
|
||||
from typing import Optional, Iterable
|
||||
|
||||
import cv2
|
||||
import requests
|
||||
|
||||
from aaredaqlib.models import MLBoxModel, MLOutputModel, MLBoxType, BoundingBoxModel
|
||||
from aaredaqlib.logger_config import setup_logger
|
||||
from aare.common.beamline import MXBeamline
|
||||
from aare.common.models import MLBoxModel, MLOutputModel, MLBoxType, BoundingBoxModel
|
||||
from aare.common.logger_config import setup_logger
|
||||
|
||||
logger=setup_logger("aareDAQ")
|
||||
|
||||
class BoxClassEnum(Enum):
|
||||
"""Enum for MLBoxType values, should be updated if model changes
|
||||
Loop_all = 0. Green box on camera
|
||||
Pin = 1. Red box on camera
|
||||
Crystal = 2. Blue box on camera
|
||||
Loop_face = 3. Yellow box on camera
|
||||
"""
|
||||
Loop_all = 0
|
||||
Pin = 1
|
||||
Crystal = 2
|
||||
Loop_face = 3
|
||||
|
||||
class MlBox:
|
||||
|
||||
def __init__(self, url="http://mx-aare-test.psi.ch:8002/predict/?model=best_v8_20102025.pt"): #mx-aare-test.psi.ch, mx-ml.psi.ch
|
||||
self.__url = url
|
||||
# self.class_info = [
|
||||
# ["loop_all", (255, 0, 0)], # class 0: Blue for loop_all
|
||||
# ["pin", (0, 255, 0)], # class 1: Green for pin
|
||||
# ["crystal", (0, 0, 255)], # class 2: Red for crystal
|
||||
# ["loop_face", (255, 255, 0)] # class 3: Yellow for loop_face
|
||||
# ]
|
||||
def __init__(self, bl:MXBeamline, url="http://mx-aare-test.psi.ch:8002/predict/?model=best_v8_20102025.pt"): #mx-aare-test.psi.ch, mx-ml.psi.ch
|
||||
if bl == MXBeamline.SIMULATED:
|
||||
self.__url = None
|
||||
elif bl == MXBeamline.X06DA:
|
||||
self.__url = "http://mx-aare-test.psi.ch:8002/predict/?model=best_v8_20102025.pt"
|
||||
elif bl == MXBeamline.X10SA:
|
||||
self.__url = "http://x10sa-spark-01.psi.ch:8002/predict/?model=best_v12_22092025.engine"
|
||||
elif bl == MXBeamline.X06SA:
|
||||
self.__url = ""
|
||||
raise NotImplemented(f"MLBox not implemente for {bl}")
|
||||
else:
|
||||
raise Exception(f"unknown beamline {bl}")
|
||||
|
||||
def get_response(self, image):
|
||||
ok, buf = cv2.imencode(".jpg", image)
|
||||
@@ -1,34 +1,42 @@
|
||||
import asyncio
|
||||
import hmac
|
||||
import io
|
||||
import os, time
|
||||
from typing import Tuple, AsyncGenerator
|
||||
from typing import AsyncGenerator
|
||||
import json
|
||||
import cv2
|
||||
import urllib3
|
||||
import uvicorn
|
||||
from aaredaqlib.coordinate import SmargonCoordinate, Coordinate
|
||||
from aaredaqlib.logger_config import setup_logger
|
||||
from aaredaqlib.models import SampleShortInfo, DAQStatusModel, BeamlineStateEnum, BeamlineSettingsModel, \
|
||||
from aare.common.coordinate import SmargonCoordinate, Coordinate
|
||||
from aare.common.error_codes import export_error_codes, export_error_codes_grouped
|
||||
from aare.common.logger_config import setup_logger
|
||||
from aare.common.models import SampleShortInfo, DAQStatusModel, BeamlineStateEnum, BeamlineSettingsModel, \
|
||||
SampleShortInfoList, SessionStatus, SampleCameraSettings, AutofocusSettings, TokenData, \
|
||||
CryojetSettingsModel, SimpleScanParameters, CrystalSize, FluorescenceSpectrumParameterModel, \
|
||||
FluorescenceSpectrumOutputModel
|
||||
from aaredaqlib.raster_grid import RasterGridRequest, CompletedRasterGrid
|
||||
from aaredaqlib.rotation_scan import RotationScanRequest, CompletedRotationScan
|
||||
from aaredaqlib.sample_geometry import SampleGeometryModel
|
||||
FluorescenceSpectrumOutputModel, RecoveryActionRequest
|
||||
from aare.common.raster_grid import RasterGridRequest, CompletedRasterGrid
|
||||
from aare.common.rotation_scan import RotationScanRequest, CompletedRotationScan
|
||||
from aare.common.sample_geometry import SampleGeometryModel
|
||||
from fastapi import FastAPI, Depends
|
||||
from fastapi import HTTPException
|
||||
from fastapi import status as api_status
|
||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||
from starlette.responses import StreamingResponse
|
||||
from urllib3.exceptions import InsecureRequestWarning
|
||||
|
||||
from aaredaq import auth
|
||||
from aaredaqlib.beamline import mx_beamline
|
||||
from aaredaq.config import BeamlineConfig
|
||||
from aaredaq.daq import (AareDAQ, LoopCenteringFailed, TransformationInvalidException,
|
||||
MountingFailed, WarningTellException, CriticalTellException)
|
||||
from aare.daq import auth
|
||||
from aare.common.beamline import mx_beamline
|
||||
from aare.daq.config import BeamlineConfig
|
||||
from aare.daq.daq import AareDAQ
|
||||
|
||||
from aare.daq.server_exception_handler import register_exception_handlers
|
||||
|
||||
from aare.common.exception_handler import (
|
||||
SampleException,
|
||||
UserRightsException,
|
||||
)
|
||||
logger = setup_logger("aareDAQ")
|
||||
app = FastAPI()
|
||||
register_exception_handlers(app)
|
||||
|
||||
# OAuth2 setup
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
||||
@@ -37,56 +45,119 @@ bl = mx_beamline()
|
||||
cfg = BeamlineConfig(bl)
|
||||
daq = AareDAQ(cfg, bl)
|
||||
|
||||
try:
|
||||
daq.sync_current_sample_from_tell(force=True)
|
||||
except Exception as e:
|
||||
logger.warning(f"Initial sample sync from TELL failed: {e}")
|
||||
|
||||
_all_pgroups_cache: dict[str, tuple[list[str], float]] = {}
|
||||
_ALL_PGROUPS_TTL_S = 60.0 # adjust TTL as needed
|
||||
|
||||
logger = setup_logger("aareDAQ")
|
||||
_face_detection_state: dict = {
|
||||
"seq": 0,
|
||||
"running": False,
|
||||
"samples": [],
|
||||
"height_fit": {},
|
||||
"area_fit": {},
|
||||
}
|
||||
_face_detection_state_lock = asyncio.Lock()
|
||||
|
||||
def _required_recovery_code() -> str:
|
||||
code = os.getenv("AARE_RECOVERY_CODE", "").strip()
|
||||
if not code:
|
||||
logger.error("AARE_RECOVERY_CODE is not configured.")
|
||||
raise HTTPException(
|
||||
status_code=api_status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Recovery confirmation code is not configured on the server.",
|
||||
)
|
||||
return code
|
||||
|
||||
def _validate_recovery_code(confirmation_code: str) -> None:
|
||||
expected = _required_recovery_code()
|
||||
provided = str(confirmation_code or "").strip()
|
||||
if not hmac.compare_digest(provided, expected):
|
||||
logger.warning("Invalid recovery confirmation code.")
|
||||
raise HTTPException(
|
||||
status_code=api_status.HTTP_403_FORBIDDEN,
|
||||
detail="Invalid confirmation code.",
|
||||
)
|
||||
|
||||
def _sample_is_mounted() -> bool:
|
||||
try:
|
||||
return daq.sample is not None
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _push_face_detection_progress(payload: dict) -> None:
|
||||
global _face_detection_state
|
||||
try:
|
||||
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}")
|
||||
|
||||
|
||||
async def face_detection_event_stream() -> AsyncGenerator[str, None]:
|
||||
last_seq = -1
|
||||
try:
|
||||
while True:
|
||||
state = dict(_face_detection_state)
|
||||
seq = int(state.get("seq", 0))
|
||||
if seq != last_seq:
|
||||
last_seq = seq
|
||||
yield f"data: {json.dumps(state, separators=(',', ':'))}\n\n"
|
||||
await asyncio.sleep(0.15)
|
||||
except asyncio.CancelledError:
|
||||
return
|
||||
|
||||
daq.set_face_detection_progress_callback(_push_face_detection_progress)
|
||||
|
||||
@app.post("/token")
|
||||
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
data = auth.authenticate_user(cfg, form_data)
|
||||
return {"access_token": data, "token_type": "bearer"}
|
||||
|
||||
@app.get("/meta/error-codes")
|
||||
async def meta_error_codes() -> dict[str, dict[str, str]]:
|
||||
"""
|
||||
Public, stable registry of machine-readable error codes.
|
||||
Useful for GUIs, tests, and diagnostics.
|
||||
"""
|
||||
return export_error_codes_grouped()
|
||||
|
||||
@app.get("/status")
|
||||
async def status(token: str = Depends(oauth2_scheme)) -> DAQStatusModel:
|
||||
data = auth.parse_token(token)
|
||||
|
||||
try:
|
||||
full = daq.status
|
||||
active_pgroup = cfg.pgroup
|
||||
is_staff = data.staff
|
||||
in_allowed_groups = (active_pgroup is not None and active_pgroup in data.pgroups)
|
||||
in_ro = is_staff or in_allowed_groups
|
||||
sample_pgroup = full.sample.user if full.sample is not None else None
|
||||
sample_view_allowed = sample_pgroup in data.pgroups or is_staff
|
||||
|
||||
full.sample = full.sample if in_ro and sample_view_allowed else None
|
||||
full.box = full.box if in_ro else None
|
||||
full.last_best_res = full.last_best_res if in_ro else None
|
||||
full.last_best_b_factor = full.last_best_b_factor if in_ro else None
|
||||
full.crystal_size = full.crystal_size if in_ro else CrystalSize(x=0,y=0,z=0)
|
||||
full.session = SessionStatus(
|
||||
current_pgroup=cfg.pgroup,
|
||||
session=cfg.session_state(data.session),
|
||||
staff=data.staff
|
||||
)
|
||||
return full
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting status: {e}")
|
||||
raise HTTPException(
|
||||
status_code=api_status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error getting status: {e}"
|
||||
)
|
||||
full = daq.status
|
||||
active_pgroup = cfg.pgroup
|
||||
is_staff = data.staff
|
||||
in_allowed_groups = (active_pgroup is not None and active_pgroup in data.pgroups)
|
||||
in_ro = is_staff or in_allowed_groups
|
||||
sample_pgroup = full.sample.user if full.sample is not None else None
|
||||
sample_view_allowed = sample_pgroup in data.pgroups or is_staff
|
||||
|
||||
full.sample = full.sample if in_ro and sample_view_allowed else None
|
||||
full.box = full.box if in_ro else None
|
||||
full.last_best_res = full.last_best_res if in_ro else None
|
||||
full.last_best_b_factor = full.last_best_b_factor if in_ro else None
|
||||
full.crystal_size = full.crystal_size if in_ro else CrystalSize(x=0,y=0,z=0)
|
||||
full.session = SessionStatus(
|
||||
current_pgroup=cfg.pgroup,
|
||||
session=cfg.session_state(data.session),
|
||||
staff=data.staff
|
||||
)
|
||||
return full
|
||||
|
||||
@app.get("/beamline/geometry")
|
||||
async def sample_geometry(
|
||||
token: str = Depends(oauth2_scheme),
|
||||
) -> SampleGeometryModel:
|
||||
auth.check_jwt_ro(cfg, auth.parse_token(token))
|
||||
return daq.sample_geometry
|
||||
return daq.status.geom
|
||||
|
||||
|
||||
@app.put("/beamline/omega")
|
||||
@@ -96,14 +167,26 @@ async def omega(val: float, token: str = Depends(oauth2_scheme)):
|
||||
daq.omega = val
|
||||
return "OK"
|
||||
|
||||
|
||||
@app.put("/beamline/light")
|
||||
async def light(val: float, token: str = Depends(oauth2_scheme)):
|
||||
logger.debug(f"Setting light to {val}")
|
||||
@app.put("/beamline/omega_rel")
|
||||
async def omega(val: float, token: str = Depends(oauth2_scheme)):
|
||||
logger.debug(f"Moving omega by {val}")
|
||||
auth.check_jwt_rw(cfg, auth.parse_token(token))
|
||||
daq.light = val
|
||||
daq.omega_rel(val)
|
||||
return "OK"
|
||||
|
||||
@app.put("/beamline/front_light")
|
||||
async def front_light(val: float, token: str = Depends(oauth2_scheme)):
|
||||
logger.debug(f"Setting light to {val}")
|
||||
auth.check_jwt_rw(cfg, auth.parse_token(token))
|
||||
daq.front_light = val
|
||||
return "OK"
|
||||
|
||||
@app.put("/beamline/back_light")
|
||||
async def back_light(val: float, token: str = Depends(oauth2_scheme)):
|
||||
logger.debug(f"Setting back light to {val}")
|
||||
auth.check_jwt_rw(cfg, auth.parse_token(token))
|
||||
daq.back_light = val
|
||||
return "OK"
|
||||
|
||||
@app.put("/beamline/zoom")
|
||||
async def zoom(val: float, token: str = Depends(oauth2_scheme)):
|
||||
@@ -190,7 +273,7 @@ async def samcam_settings(s: SampleCameraSettings, token: str = Depends(oauth2_s
|
||||
async def samcam_autofocus(s: AutofocusSettings, token: str = Depends(oauth2_scheme)):
|
||||
logger.debug(f"SamCam AutoFocus")
|
||||
auth.check_jwt_rw(cfg, auth.parse_token(token))
|
||||
daq.autofocus(s)
|
||||
daq.auto_focus(s)
|
||||
return "OK"
|
||||
|
||||
@app.post("/beamline/shutter")
|
||||
@@ -251,41 +334,14 @@ async def mount(dbid: int, token: str = Depends(oauth2_scheme), reference: bool
|
||||
index = i
|
||||
|
||||
if index == -1:
|
||||
raise HTTPException(
|
||||
status_code=api_status.HTTP_404_NOT_FOUND,
|
||||
detail="Sample not found",
|
||||
)
|
||||
if token_data.staff or st.s[index].user in token_data.pgroups:
|
||||
try:
|
||||
daq.sample = st.s[index]
|
||||
except MountingFailed as e:
|
||||
raise HTTPException(
|
||||
status_code=api_status.HTTP_404_NOT_FOUND,
|
||||
detail=f"{e}",
|
||||
)
|
||||
except WarningTellException as e:
|
||||
raise HTTPException(
|
||||
status_code=api_status.HTTP_410_GONE,
|
||||
detail=f"{e}",
|
||||
)
|
||||
except CriticalTellException as e:
|
||||
raise HTTPException(
|
||||
status_code=api_status.HTTP_417_EXPECTATION_FAILED,
|
||||
detail=f"{e}"
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=api_status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"{e}"
|
||||
)
|
||||
return "OK"
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=api_status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Sample belongs to a different user.",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
raise SampleException(message="Sample not found")
|
||||
|
||||
if not (token_data.staff or st.s[index].user in token_data.pgroups):
|
||||
raise UserRightsException(message="Sample belongs to a different user.")
|
||||
|
||||
daq.sample = st.s[index]
|
||||
|
||||
return "OK"
|
||||
|
||||
@app.post("/sample/unmount")
|
||||
async def unmount(token: str = Depends(oauth2_scheme)):
|
||||
@@ -303,6 +359,16 @@ async def manual(s: SampleShortInfo, token: str = Depends(oauth2_scheme)):
|
||||
daq.create_sample(s)
|
||||
print(f"DB ID after creating {s.db_id}")
|
||||
|
||||
@app.post("/sample/resync")
|
||||
async def sample_resync(token: str = Depends(oauth2_scheme)) -> dict:
|
||||
auth.check_jwt_rw(cfg, auth.parse_token(token))
|
||||
daq.sync_current_sample_from_tell(force=True)
|
||||
logger.info("TELL sample cache resynced via API request.")
|
||||
return {
|
||||
"ok": True,
|
||||
"message": "TELL sample cache resynced.",
|
||||
}
|
||||
|
||||
|
||||
def get_spreadsheet(data: TokenData) -> SampleShortInfoList:
|
||||
if data.staff:
|
||||
@@ -392,6 +458,116 @@ async def beam_location(token: str = Depends(oauth2_scheme)):
|
||||
auth.check_jwt_staff(cfg, auth.parse_token(token))
|
||||
daq.state = BeamlineStateEnum.BeamLocation
|
||||
|
||||
@app.post("/state/maintenance")
|
||||
async def maintenance(token: str = Depends(oauth2_scheme)) -> str:
|
||||
auth.check_jwt_staff(cfg, auth.parse_token(token))
|
||||
cfg.state = BeamlineStateEnum.Maintenance
|
||||
logger.warning("Beamline state set to Maintenance via protected endpoint.")
|
||||
return "OK"
|
||||
|
||||
@app.post("/access/take_over_beamline")
|
||||
async def take_over_beamline(payload: RecoveryActionRequest, token: str = Depends(oauth2_scheme)) -> str:
|
||||
data = auth.parse_token(token)
|
||||
auth.check_jwt_staff_only(data)
|
||||
_validate_recovery_code(payload.confirmation_code)
|
||||
auth.force_current_sesion(cfg, data)
|
||||
logger.warning(
|
||||
"Beamline session forcefully taken over.",
|
||||
extra={"session": getattr(data, "session", None)},
|
||||
)
|
||||
return "OK"
|
||||
|
||||
@app.post("/state/free_beamline")
|
||||
async def free_beamline(payload: RecoveryActionRequest, token: str = Depends(oauth2_scheme)) -> str:
|
||||
data = auth.parse_token(token)
|
||||
auth.check_jwt_staff_only(data)
|
||||
_validate_recovery_code(payload.confirmation_code)
|
||||
cfg.state_busy = False
|
||||
logger.warning(
|
||||
"Beamline busy flag cleared via protected endpoint.",
|
||||
extra={"session": getattr(data, "session", None)},
|
||||
)
|
||||
return "OK"
|
||||
|
||||
@app.post("/recovery/recover_beamline")
|
||||
async def recover_beamline(payload: RecoveryActionRequest, token: str = Depends(oauth2_scheme)) -> dict:
|
||||
data = auth.parse_token(token)
|
||||
auth.check_jwt_staff_only(data)
|
||||
_validate_recovery_code(payload.confirmation_code)
|
||||
|
||||
sample_mounted = _sample_is_mounted()
|
||||
prev_state = cfg.state
|
||||
prev_busy = cfg.state_busy
|
||||
|
||||
auth.force_current_sesion(cfg, data)
|
||||
cfg.state_busy = False
|
||||
cfg.state = BeamlineStateEnum.Maintenance
|
||||
|
||||
logger.warning(
|
||||
"Beamline recovery action executed.",
|
||||
extra={
|
||||
"session": getattr(data, "session", None),
|
||||
"previous_state": getattr(prev_state, "name", str(prev_state)),
|
||||
"previous_busy": prev_busy,
|
||||
"sample_mounted": sample_mounted,
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"sample_mounted": sample_mounted,
|
||||
"previous_state": getattr(prev_state, "name", str(prev_state)),
|
||||
"previous_busy": prev_busy,
|
||||
"new_state": BeamlineStateEnum.Maintenance.name,
|
||||
}
|
||||
@app.post("/recovery/unmount_sample")
|
||||
async def recovery_unmount_sample(payload: RecoveryActionRequest, token: str = Depends(oauth2_scheme)) -> dict:
|
||||
data = auth.parse_token(token)
|
||||
auth.check_jwt_staff_only(data)
|
||||
_validate_recovery_code(payload.confirmation_code)
|
||||
|
||||
auth.force_current_sesion(cfg, data)
|
||||
|
||||
if cfg.state_busy:
|
||||
raise HTTPException(
|
||||
status_code=api_status.HTTP_409_CONFLICT,
|
||||
detail="Beamline is busy. Clear or recover the beamline before attempting recovery unmount.",
|
||||
)
|
||||
|
||||
status = daq.status
|
||||
if not getattr(status, "tell_connected", False):
|
||||
raise HTTPException(
|
||||
status_code=api_status.HTTP_409_CONFLICT,
|
||||
detail=f"TELL is not connected: {getattr(status, 'tell_error', 'unknown error')}",
|
||||
)
|
||||
|
||||
sample_mounted = _sample_is_mounted()
|
||||
if not sample_mounted:
|
||||
return {
|
||||
"ok": True,
|
||||
"sample_mounted": False,
|
||||
"message": "No sample appears to be mounted.",
|
||||
}
|
||||
|
||||
prev_state = cfg.state
|
||||
daq.recovery_unmount_sample()
|
||||
|
||||
logger.warning(
|
||||
"Recovery sample unmount executed.",
|
||||
extra={
|
||||
"session": getattr(data, "session", None),
|
||||
"previous_state": getattr(prev_state, "name", str(prev_state)),
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"sample_mounted": True,
|
||||
"previous_state": getattr(prev_state, "name", str(prev_state)),
|
||||
"new_state": getattr(cfg.state, "name", str(cfg.state)),
|
||||
"message": "Recovery unmount completed.",
|
||||
}
|
||||
|
||||
# Scans
|
||||
@app.post("/scan/raster")
|
||||
async def raster(val: RasterGridRequest, auto: bool = False, token: str = Depends(oauth2_scheme)) -> CompletedRasterGrid:
|
||||
@@ -407,40 +583,7 @@ async def rotation(val: RotationScanRequest, token: str = Depends(oauth2_scheme)
|
||||
@app.post("/scan/auto")
|
||||
async def auto(s: SampleShortInfo, token: str = Depends(oauth2_scheme)):
|
||||
auth.check_jwt_rw(cfg, auth.parse_token(token))
|
||||
try:
|
||||
runtime = daq.measure(s)
|
||||
return f"{runtime:0.3f}"
|
||||
except LoopCenteringFailed as e:
|
||||
raise HTTPException(
|
||||
status_code=api_status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Loop centering failed: {e}",
|
||||
)
|
||||
except TransformationInvalidException as e:
|
||||
raise HTTPException(
|
||||
status_code=api_status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Transformation invalid: {e}",
|
||||
)
|
||||
except MountingFailed as e:
|
||||
raise HTTPException(
|
||||
status_code=api_status.HTTP_404_NOT_FOUND,
|
||||
detail=f"{e}",
|
||||
)
|
||||
except WarningTellException as e:
|
||||
raise HTTPException(
|
||||
status_code=api_status.HTTP_410_GONE,
|
||||
detail=f"{e}",
|
||||
)
|
||||
except CriticalTellException as e:
|
||||
raise HTTPException(
|
||||
status_code=api_status.HTTP_417_EXPECTATION_FAILED,
|
||||
detail=f"{e}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Exception in auto: {e}")
|
||||
raise HTTPException(
|
||||
status_code=api_status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"error {e}"
|
||||
)
|
||||
runtime = daq.measure(s)
|
||||
return f"{runtime:0.3f}"
|
||||
|
||||
|
||||
@@ -479,14 +622,34 @@ async def alc_ml_bounding_box(token: str = Depends(oauth2_scheme)) -> RasterGrid
|
||||
auth.check_jwt_rw(cfg, auth.parse_token(token))
|
||||
return daq.ml_bounding_box()
|
||||
|
||||
|
||||
@app.post("/face_detection/run")
|
||||
async def face_detection_run(steps: int, step_size: int, token: str = Depends(oauth2_scheme)) -> dict:
|
||||
logger.debug(f"Face detection run: {steps} steps, {step_size} step size")
|
||||
auth.check_jwt_rw(cfg, auth.parse_token(token))
|
||||
_push_face_detection_progress({
|
||||
"running": True,
|
||||
"status": "starting",
|
||||
"samples": [],
|
||||
"height_fit": {},
|
||||
"area_fit": {},
|
||||
})
|
||||
result = daq.face_detection(steps=steps, step_size=step_size)
|
||||
return result
|
||||
|
||||
@app.get("/sse/face_detection")
|
||||
async def sse_face_detection(token: str = Depends(oauth2_scheme)):
|
||||
auth.check_jwt_ro(cfg, auth.parse_token(token))
|
||||
return StreamingResponse(
|
||||
face_detection_event_stream(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Headers": "Cache-Control"
|
||||
}
|
||||
)
|
||||
|
||||
# Access management
|
||||
@app.get("/access/pgroup")
|
||||
async def pgroup(token: str = Depends(oauth2_scheme)) -> str:
|
||||
@@ -670,6 +833,17 @@ async def sse_fluorimeter(token: str = Depends(oauth2_scheme)):
|
||||
}
|
||||
)
|
||||
|
||||
@app.post("/samcam/send_screenshot_db")
|
||||
async def send_screenshot_db(
|
||||
filename: str | None = None,
|
||||
message: str | None = None,
|
||||
token: str = Depends(oauth2_scheme),
|
||||
) -> str:
|
||||
data = auth.parse_token(token)
|
||||
auth.check_jwt_rw(cfg, data)
|
||||
daq.send_screenshot_db(filename=filename, message=message)
|
||||
return "OK"
|
||||
|
||||
|
||||
LOGGING_CONFIG = {
|
||||
"version": 1,
|
||||
@@ -708,7 +882,7 @@ def main():
|
||||
urllib3.disable_warnings()
|
||||
|
||||
# Run the application using uvicorn
|
||||
uvicorn.run("aaredaq.server:app", host="0.0.0.0", port=5210, workers=4, log_config=LOGGING_CONFIG)
|
||||
uvicorn.run("aare.daq.server:app", host="0.0.0.0", port=5210, workers=4, log_config=LOGGING_CONFIG)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -0,0 +1,154 @@
|
||||
# aare/common/server_error_handler.py
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import HTTPException
|
||||
from fastapi import status as api_status
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from aare.common.logger_config import setup_logger
|
||||
from aare.common.exception_handler import (
|
||||
MountingFailed,
|
||||
WarningTellException,
|
||||
CriticalTellException,
|
||||
LoopCenteringFailed,
|
||||
TransformationInvalidException,
|
||||
BeamlineBusyException,
|
||||
AuthenticationException,
|
||||
SampleException,
|
||||
UserRightsException,
|
||||
SmargonCommunicationError, TellCommunicationError
|
||||
)
|
||||
|
||||
logger = setup_logger("aareDAQ")
|
||||
|
||||
|
||||
def _error_payload(*, code: str, message: str, extra: dict | None = None) -> dict:
|
||||
payload = {"code": code, "message": message}
|
||||
if extra:
|
||||
payload["extra"] = extra
|
||||
return payload
|
||||
|
||||
|
||||
def register_exception_handlers(app) -> None:
|
||||
"""
|
||||
Register server-wide exception handlers on the given FastAPI app.
|
||||
Call once right after `app = FastAPI()`.
|
||||
"""
|
||||
|
||||
@app.exception_handler(HTTPException)
|
||||
async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
|
||||
# Keep explicit HTTP errors, but normalize response shape
|
||||
detail = exc.detail
|
||||
if isinstance(detail, dict) and "code" in detail and "message" in detail:
|
||||
body = detail
|
||||
else:
|
||||
body = _error_payload(code="HTTP_ERROR", message=str(detail))
|
||||
return JSONResponse(status_code=exc.status_code, content=body, headers=exc.headers)
|
||||
|
||||
@app.exception_handler(MountingFailed)
|
||||
async def mounting_failed_handler(request: Request, exc: MountingFailed) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
status_code=api_status.HTTP_404_NOT_FOUND,
|
||||
content=_error_payload(code="MOUNTING_FAILED", message=str(exc)),
|
||||
)
|
||||
|
||||
@app.exception_handler(WarningTellException)
|
||||
async def warning_tell_handler(request: Request, exc: WarningTellException) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
status_code=api_status.HTTP_410_GONE,
|
||||
content=_error_payload(code="TELL_WARNING", message=str(exc)),
|
||||
)
|
||||
|
||||
@app.exception_handler(CriticalTellException)
|
||||
async def critical_tell_handler(request: Request, exc: CriticalTellException) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
status_code=api_status.HTTP_417_EXPECTATION_FAILED,
|
||||
content=_error_payload(code="TELL_CRITICAL", message=str(exc)),
|
||||
)
|
||||
|
||||
@app.exception_handler(LoopCenteringFailed)
|
||||
async def loop_centering_failed_handler(request: Request, exc: LoopCenteringFailed) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
status_code=api_status.HTTP_404_NOT_FOUND,
|
||||
content=_error_payload(code="LOOP_CENTERING_FAILED", message=str(exc)),
|
||||
)
|
||||
|
||||
@app.exception_handler(TransformationInvalidException)
|
||||
async def transformation_invalid_handler(request: Request, exc: TransformationInvalidException) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
status_code=api_status.HTTP_400_BAD_REQUEST,
|
||||
content=_error_payload(code="TRANSFORMATION_INVALID", message=str(exc)),
|
||||
)
|
||||
|
||||
@app.exception_handler(BeamlineBusyException)
|
||||
async def beamline_busy_handler(request: Request, exc: BeamlineBusyException) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
status_code=api_status.HTTP_423_LOCKED,
|
||||
content=_error_payload(code="BEAMLINE_BUSY", message=str(exc) or "Beamline is busy"),
|
||||
)
|
||||
|
||||
@app.exception_handler(AuthenticationException)
|
||||
async def authentication_exception_handler(request: Request, exc: AuthenticationException) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
status_code=getattr(exc, "status_code", api_status.HTTP_401_UNAUTHORIZED),
|
||||
content=_error_payload(
|
||||
code=str(getattr(exc, "code", "AUTHENTICATION_ERROR")),
|
||||
message=str(exc) or "Invalid authentication",
|
||||
),
|
||||
headers=getattr(exc, "headers", None),
|
||||
)
|
||||
|
||||
@app.exception_handler(UserRightsException)
|
||||
async def user_rights_exception_handler(request: Request, exc: UserRightsException) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
status_code=getattr(exc, "status_code", api_status.HTTP_403_FORBIDDEN),
|
||||
content=_error_payload(
|
||||
code=str(getattr(exc, "code", "FORBIDDEN")),
|
||||
message=str(exc) or "Forbidden",
|
||||
),
|
||||
headers=getattr(exc, "headers", None),
|
||||
)
|
||||
|
||||
@app.exception_handler(SampleException)
|
||||
async def sample_exception_handler(request: Request, exc: SampleException) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
status_code=api_status.HTTP_404_NOT_FOUND,
|
||||
content=_error_payload(code="SAMPLE_NOT_FOUND", message=str(exc) or "Sample not found"),
|
||||
)
|
||||
|
||||
@app.exception_handler(SmargonCommunicationError)
|
||||
async def smargon_comm_handler(request: Request, exc: SmargonCommunicationError) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
status_code=api_status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
content=_error_payload(
|
||||
code="SMARGON_UNAVAILABLE",
|
||||
message=str(exc) or "Smargon is unavailable",
|
||||
extra={
|
||||
"operation": getattr(exc, "operation", None),
|
||||
"endpoint": getattr(exc, "endpoint", None),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@app.exception_handler(TellCommunicationError)
|
||||
async def tell_comm_handler(request: Request, exc: TellCommunicationError) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
status_code=api_status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
content=_error_payload(
|
||||
code="TELL_UNAVAILABLE",
|
||||
message=str(exc) or "TELL is unavailable",
|
||||
extra={
|
||||
"operation": getattr(exc, "operation", None),
|
||||
"endpoint": getattr(exc, "endpoint", None),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse:
|
||||
logger.exception("Unhandled server exception")
|
||||
return JSONResponse(
|
||||
status_code=api_status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
content=_error_payload(code="INTERNAL_SERVER_ERROR", message=str(exc) or "Internal server error"),
|
||||
)
|
||||
@@ -2,12 +2,13 @@ import os
|
||||
import json
|
||||
import websocket
|
||||
import time
|
||||
from aareDBclient.models import PuckWithTellPosition
|
||||
from aaredaqlib.models import SampleShortInfoList, SampleShortInfo, DewarAddress
|
||||
from aareDB.models import PuckWithTellPosition
|
||||
from aare.common.models import SampleShortInfoList, SampleShortInfo, DewarAddress
|
||||
from config import BeamlineConfig
|
||||
from aaredaqlib.beamline import MXBeamline
|
||||
from aare.common.beamline import MXBeamline, mx_beamline
|
||||
|
||||
SLOT_IDENTIFIER = "X10SA"
|
||||
beamline = mx_beamline()
|
||||
SLOT_IDENTIFIER = beamline.value.upper()
|
||||
WS_URL = f"wss://mx-db-01.psi.ch/dispatcher/protected_router/tell_runner/ws/samples-spreadsheet/{SLOT_IDENTIFIER}"
|
||||
|
||||
# Ensure the environment variable for the shared password is set
|
||||
@@ -16,9 +17,8 @@ if not password:
|
||||
raise ValueError("The AAREDB_SHARED_PASSWORD environment variable is not set.")
|
||||
WS_HEADERS = [f"X-Shared-Password: {password}"]
|
||||
|
||||
beamline = MXBeamline.X10SA
|
||||
config = BeamlineConfig(bl=beamline)
|
||||
|
||||
config = BeamlineConfig(bl=beamline)
|
||||
# Cache the current spreadsheet for change detection
|
||||
current_spreadsheet = None
|
||||
|
||||
@@ -1,25 +1,33 @@
|
||||
import os
|
||||
import json
|
||||
import threading
|
||||
|
||||
import websocket
|
||||
import sseclient
|
||||
import requests
|
||||
import time
|
||||
from aareDBclient.models import PuckWithTellPosition
|
||||
#from mxlibs3.tell_client import TellClient
|
||||
#from aaredb import AareWrapper # Make sure the import path fits your project
|
||||
#from aaredaqlib.beamline import MXBeamline
|
||||
from aareDB.models import PuckWithTellPosition
|
||||
|
||||
from aare.devices.tell_client import TellClient
|
||||
|
||||
from aare.common.logger_config import setup_logger
|
||||
from aaredb import AareWrapper # Make sure the import path fits your project
|
||||
from aare.common.beamline import MXBeamline, mx_beamline
|
||||
|
||||
logger = setup_logger("aareDAQ")
|
||||
|
||||
# Configuration
|
||||
SLOT_IDENTIFIER = "X10SA"
|
||||
#WS_URL = f"wss://mx-db-01.psi.ch/dispatcher/protected_router/wstell/ws/slot/{SLOT_IDENTIFIER}"
|
||||
WS_URL = f"wss://localhost:8001/protected_router/wstell/ws/slot/{SLOT_IDENTIFIER}"
|
||||
beamline = mx_beamline()
|
||||
SLOT_IDENTIFIER = beamline.value.upper()
|
||||
WS_URL = f"wss://mx-db-01.psi.ch/dispatcher/protected_router/wstell/ws/slot/{SLOT_IDENTIFIER}"
|
||||
#WS_URL = f"wss://localhost:8001/protected_router/wstell/ws/slot/{SLOT_IDENTIFIER}"
|
||||
WS_HEADERS = [f"X-Shared-Password: {os.getenv('AAREDB_SHARED_PASSWORD')}"]
|
||||
print(WS_HEADERS)
|
||||
|
||||
# Initialize TELL client and DB wrapper
|
||||
beamline = None #MXBeamline.X06DA # Use your beamline enum/value
|
||||
tell_client = None #TellClient(bl=beamline)
|
||||
aare_db = None #AareWrapper(bl=beamline)
|
||||
# Use your beamline enum/value
|
||||
tell_client = TellClient(bl=beamline)
|
||||
aare_db = AareWrapper(bl=beamline)
|
||||
|
||||
# Track current state
|
||||
current_pucks = []
|
||||
@@ -32,22 +40,17 @@ def listen_to_sse():
|
||||
return
|
||||
sse_url = tell_client.url + "/events"
|
||||
try:
|
||||
response = requests.get(sse_url, stream=True)
|
||||
client = sseclient.SSEClient(response)
|
||||
#response = requests.get(sse_url, stream=True)
|
||||
client = sseclient.SSEClient(sse_url)
|
||||
|
||||
# Immediately fetch current detected pucks once on connect so the system starts with
|
||||
# up-to-date state (instead of waiting for the first DewarContentUpdate event).
|
||||
try:
|
||||
print("[SSE][listen_to_sse] Initial detected pucks fetch on connect")
|
||||
handle_tell_change_event()
|
||||
except Exception as exc:
|
||||
print(f"[SSE][listen_to_sse][WARN] Initial fetch failed: {exc}")
|
||||
print("[SSE][listen_to_sse] Initial detected pucks fetch on connect")
|
||||
handle_tell_change_event()
|
||||
|
||||
for event in client.events():
|
||||
print(f"event = {event.event} with data: {event.data}")
|
||||
if event.event == "DewarContentUpdate":
|
||||
print(f"[SSE][listen_to_sse] event: {event.event}, data: {event.data}")
|
||||
on_sse_event(event)
|
||||
except requests.exceptions.RequestException as exc:
|
||||
except Exception as exc:
|
||||
print(f"[SSE][listen_to_sse][ERROR] Failed to connect to {sse_url}: {exc}")
|
||||
|
||||
def compare_and_report_change(old, new, key_func):
|
||||
@@ -69,8 +72,8 @@ def compare_and_report_change(old, new, key_func):
|
||||
|
||||
def ws_update_samples_info(pucks):
|
||||
"""Send sample info to TELL robot."""
|
||||
#tell_client.set_samples_info(pucks)
|
||||
print(pucks)
|
||||
tell_client.set_samples_info(pucks)
|
||||
logger.info(f"Payload sent to TELL: {pucks}")
|
||||
|
||||
def handle_tell_change_event():
|
||||
"""Fetch the latest detected pucks from TELL and update the database."""
|
||||
@@ -139,8 +142,8 @@ def on_open(ws):
|
||||
|
||||
def main():
|
||||
# Start SSE listener in a separate background thread
|
||||
#sse_thread = threading.Thread(target=listen_to_sse, daemon=True)
|
||||
#sse_thread.start()
|
||||
sse_thread = threading.Thread(target=listen_to_sse, daemon=True)
|
||||
sse_thread.start()
|
||||
|
||||
# Main thread runs websocket client loop
|
||||
while True:
|
||||
@@ -0,0 +1,144 @@
|
||||
from aare.common.coordinate import Coordinate
|
||||
from aare.common.models import StagePositionEnum
|
||||
from aare.daq.devices import BeamlineDevices
|
||||
from aare.daq.config import BeamlineConfig, ABR_POS_MOUNT
|
||||
|
||||
|
||||
def move_bsz(devs: BeamlineDevices, target: float):
|
||||
if abs(target - devs.bsz.position) > 0.1:
|
||||
beamstop_stage_measure = devs.beamstop_stage_up
|
||||
reflector_measure = devs.reflector_up
|
||||
devs.reflector_up = False
|
||||
devs.beamstop_stage_up = True
|
||||
devs.beamstop_z = target
|
||||
|
||||
if reflector_measure:
|
||||
devs.reflector_up = True
|
||||
|
||||
if not beamstop_stage_measure:
|
||||
devs.beamstop_stage_up = False
|
||||
|
||||
|
||||
def wait_for_dc_devices(devs: BeamlineDevices):
|
||||
# Should wait for DTZ
|
||||
pass
|
||||
|
||||
def wait_for_se_devices(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
# Should wait for Cryojet go far
|
||||
pass
|
||||
|
||||
def common_2rse(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
print('move collimator and scintillator to 20')
|
||||
devs.collimator = 20.0
|
||||
devs.scintillator = 20.0
|
||||
print('move smargon home')
|
||||
devs.smargon_move_home()
|
||||
print('try to move aerotech')
|
||||
devs.aerotech_pos = Coordinate(x=0.0, y=0.0, z=0.0)
|
||||
devs.aerotech_omega = 0.0
|
||||
#print('move bl to park')
|
||||
#devs.reflector_up = StagePositionEnum.PARK
|
||||
print('move bs to park')
|
||||
devs.beamstop_stage_up = StagePositionEnum.PARK
|
||||
#print('move cryo to park')
|
||||
#devs.__cryojet_pos = StagePositionEnum.PARK
|
||||
|
||||
def m2se(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
pass
|
||||
|
||||
def sa2se(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
print('move collimator and scintillator to 20')
|
||||
devs.collimator = 20.0
|
||||
devs.scintillator = 20.0
|
||||
print('move smargon home')
|
||||
devs.smargon_move_home()
|
||||
print('try to move aerotech')
|
||||
devs.aerotech_pos = Coordinate(x=0.0, y=0.0, z=0.0)
|
||||
devs.aerotech_omega = 0.0
|
||||
print('move bl to park')
|
||||
devs.reflector_up = StagePositionEnum.PARK
|
||||
print('move bs to park')
|
||||
devs.beamstop_stage_up = StagePositionEnum.PARK
|
||||
#print('move cryo to park')
|
||||
#devs.__cryojet_pos = StagePositionEnum.PARK
|
||||
|
||||
def sa2rse(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
common_2rse(devs, cfg)
|
||||
|
||||
def dc2rse(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
common_2rse(devs, cfg)
|
||||
|
||||
def se2sa(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
devs.reflector_up = StagePositionEnum.MEASURE
|
||||
devs.aerotech_pos = cfg.abr_meas_pos
|
||||
|
||||
def rse2sa(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
se2sa(devs, cfg)
|
||||
|
||||
def sa2dc(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
pass
|
||||
|
||||
def dc2sa(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
pass
|
||||
|
||||
def sa2xrf(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
"""sample alignment to XrfCollection"""
|
||||
pass
|
||||
|
||||
|
||||
def xrf2sa(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
"""XrfCollection to sample alignment"""
|
||||
pass
|
||||
|
||||
|
||||
def sa2ws(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
pass
|
||||
|
||||
|
||||
def ws2sa(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
pass
|
||||
|
||||
|
||||
def sa2ba(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
pass
|
||||
|
||||
|
||||
def ba2sa(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
pass
|
||||
|
||||
def sa2bl(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
devs.scintillator = 20.0
|
||||
|
||||
def bl2sa(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
devs.scintillator = 20.0
|
||||
|
||||
|
||||
def bl2ba(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
devs.scintillator = 20.0
|
||||
|
||||
def ba2bl(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
devs.scintillator = 20.0
|
||||
|
||||
def sa2dh(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
devs.scintillator = 20.0
|
||||
if devs.tell.get_mounted_sample() is not None:
|
||||
try:
|
||||
# Best effort try to unmount
|
||||
devs.tell.unmount(wait=True)
|
||||
except Exception as e:
|
||||
print(f"Error for unmounting: {e}")
|
||||
devs.tell.dry(wait_cold=-1, wait=False)
|
||||
|
||||
def dh2sa(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
devs.scintillator = 20.0
|
||||
devs.reflector_up = StagePositionEnum.MEASURE
|
||||
|
||||
if __name__ == "__main__":
|
||||
from aare.common.beamline import mx_beamline
|
||||
import time
|
||||
bl = mx_beamline()
|
||||
cfg = BeamlineConfig(bl)
|
||||
devs = BeamlineDevices(bl)
|
||||
common_2rse(devs, cfg)
|
||||
time.sleep(10.0)
|
||||
rse2sa(devs, cfg)
|
||||
@@ -0,0 +1,386 @@
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from enum import Enum
|
||||
from typing import Union
|
||||
|
||||
import automation1 as a1
|
||||
from epics import PV, Motor, caput, caget
|
||||
|
||||
from aare.common.beamline import MXBeamline, mx_beamline
|
||||
from aare.devices.my_motor import MyMotor
|
||||
|
||||
class TaskEnum(Enum):
|
||||
TASK_0 = 0
|
||||
TASK_1 = 1
|
||||
TASK_2 = 2
|
||||
TASK_3 = 3
|
||||
TASK_4 = 4
|
||||
|
||||
class AxisEnum(Enum):
|
||||
X = "gmx"
|
||||
Y = "gmy"
|
||||
Z = "gmz"
|
||||
OMEGA = "Omega"
|
||||
|
||||
class AerotechRunEnum(Enum):
|
||||
STOP = 0
|
||||
START = 1
|
||||
RUN = 2
|
||||
LOAD = 3
|
||||
PAUSE = 4
|
||||
RESET = 5
|
||||
|
||||
class VariableTypeEnum(Enum):
|
||||
INT = 0
|
||||
REAL = 1
|
||||
STRING = 2
|
||||
|
||||
class AerotechControllerEpics:
|
||||
def __init__(self, beamline: MXBeamline):
|
||||
BEAMLINE = beamline.value.upper()
|
||||
self.aerotech_pv_prefix = f"{BEAMLINE}-ES-DF1"
|
||||
trx_prefix = f"{self.aerotech_pv_prefix}:TRX1"
|
||||
try_prefix = f"{self.aerotech_pv_prefix}:TRY1"
|
||||
trz_prefix = f"{self.aerotech_pv_prefix}:TRZ1"
|
||||
rotu_prefix = f"{self.aerotech_pv_prefix}:ROTU"
|
||||
self.gmx = MyMotor(trx_prefix)
|
||||
self.gmy = MyMotor(try_prefix)
|
||||
self.gmz = MyMotor(trz_prefix)
|
||||
self.omega = MyMotor(rotu_prefix)
|
||||
self.__enable_all = PV(f"{self.aerotech_pv_prefix}:EnableAll")
|
||||
self.__disable_all = PV(f"{self.aerotech_pv_prefix}:DisableAll")
|
||||
self.__acknowledge_all = PV(f"{self.aerotech_pv_prefix}:AckAll")
|
||||
self.__stop_all = PV(f"{self.aerotech_pv_prefix}:StopAll")
|
||||
self.__task_filename = PV(f"{self.aerotech_pv_prefix}:TASK:FILENAME")
|
||||
self.__task_id = PV(f"{self.aerotech_pv_prefix}:TASK:TASKIDX") # values can be 1 to 4 DO NOT USE 0!!!!
|
||||
self.__task_run_enum = PV(f"{self.aerotech_pv_prefix}:TASK:SWITCH") # 0 Stop, 1 Start, 2 Run,
|
||||
# 3 Load, 4 Pause, 5 Reset
|
||||
self.enable_all()
|
||||
|
||||
def enable_all(self):
|
||||
self.__enable_all.put(1)
|
||||
self.__enable_all.put(0)
|
||||
|
||||
def acknowledge_all(self):
|
||||
self.__acknowledge_all.put(1)
|
||||
self.__acknowledge_all.put(0)
|
||||
|
||||
def _disable_all(self):
|
||||
self.__disable_all.put(1)
|
||||
self.__disable_all.put(0)
|
||||
|
||||
def stop_all(self):
|
||||
self.__stop_all.put(1)
|
||||
self.__stop_all.put(0)
|
||||
|
||||
def __task_stop(self):
|
||||
self.__task_run_enum.put(0)
|
||||
|
||||
def __task_start(self):
|
||||
self.__task_run_enum.put(1)
|
||||
|
||||
def __task_run(self):
|
||||
self.__task_run_enum.put(2)
|
||||
|
||||
def __task_pause(self):
|
||||
self.__task_run_enum.put(4)
|
||||
|
||||
def __task_load(self):
|
||||
self.__task_run_enum.put(3)
|
||||
|
||||
def __task_reset(self):
|
||||
self.__task_run_enum.put(5)
|
||||
|
||||
def __set_task_id(self, task_id: int):
|
||||
if task_id not in range(1, 5):
|
||||
raise ValueError(f"Invalid task id {task_id}")
|
||||
self.__task_id.put(task_id)
|
||||
|
||||
def __put(self, axis: MyMotor, attr: str, value: float):
|
||||
try:
|
||||
axis.put(attr=attr, value=value)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Error setting {axis.name} {attr} to {value}: {e}")
|
||||
|
||||
def set_offset(self, axis: MyMotor, offset: float):
|
||||
self.__put(axis=axis, attr="OFF", value=offset)
|
||||
|
||||
def home_all(self, task_id: int = 3):
|
||||
#self.__task_id.put(2)
|
||||
self.__task_reset()
|
||||
time.sleep(0.2)
|
||||
self.__task_id.put(task_id)
|
||||
print(self.__task_id.get())
|
||||
time.sleep(0.2)
|
||||
self.__task_filename.put("home_all.a1exe")
|
||||
print(self.__task_filename.get())
|
||||
# self.__task_load()
|
||||
#time.sleep(0.2)
|
||||
#print(self.__task_run_enum.get())
|
||||
time.sleep(1.0)
|
||||
self.__task_run()
|
||||
print(self.__task_run_enum.get())
|
||||
|
||||
def get_tast_status(self, task_id: int = 3):
|
||||
task_status = PV(f"{self.aerotech_pv_prefix}:TASK:T{task_id}:STATUS")
|
||||
return task_status.get(as_string=True)
|
||||
|
||||
|
||||
def __set_global(self, index:int, value: Union[int, float, str],
|
||||
timeout:float=10.0):
|
||||
"""Set a global variable in aerotech, it takes ~200 ms for the value to be set
|
||||
:param index: select a variable to change. an integer between 0..256 for int and real variable for 0..31 for strings
|
||||
:param value: the value to set can be int, float (REAL) or string
|
||||
:param timeout: timeout for vairable change, default 10.0 seconds """
|
||||
var_type = self.__get_var_type(value)
|
||||
caput(f"{self.aerotech_pv_prefix}:VAR:{var_type.upper()}-ADDR", index)
|
||||
if var_type == 'STRING':
|
||||
var_type = 'STRING-SHORT'
|
||||
caput(f"{self.aerotech_pv_prefix}:VAR:{var_type.upper()}", value)
|
||||
start = time.perf_counter()
|
||||
while time.perf_counter() - start < timeout:
|
||||
rbv = self.__read_global_from_index(index, var_type)
|
||||
if rbv == str(value):
|
||||
return
|
||||
time.sleep(0.1)
|
||||
|
||||
raise TimeoutError(f"Timeout setting global variable {index} to {value}")
|
||||
|
||||
|
||||
def __read_global_feedback(self, index, var_type: str):
|
||||
caput(f"{self.aerotech_pv_prefix}:VAR:{var_type.upper()}-RBV.PROC", 1)
|
||||
time.sleep(0.5)
|
||||
return caget(f"{self.aerotech_pv_prefix}:VAR:{var_type.upper()}-RBV", as_string=True)
|
||||
|
||||
def __read_global_from_index(self, index, var_type: str):
|
||||
return caget(f"{self.aerotech_pv_prefix}:VAR:{var_type.upper()}{index}_RBV", as_string=True)
|
||||
|
||||
def __get_var_type(self, value):
|
||||
if type(value) is int:
|
||||
return VariableTypeEnum.INT.name
|
||||
elif type(value) is float:
|
||||
return VariableTypeEnum.REAL.name
|
||||
elif type(value) is str:
|
||||
return VariableTypeEnum.STRING.name
|
||||
else:
|
||||
raise ValueError(f"Invalid type {type(value)} for global variable")
|
||||
|
||||
def get_global_variable(self, index: int, var_type:VariableTypeEnum):
|
||||
return self.__read_global_from_index(index, var_type.name)
|
||||
|
||||
def set_global_variable(self, index: int, value: Union[int, float, str]):
|
||||
self.__set_global(index, value)
|
||||
|
||||
|
||||
class AerotechController:
|
||||
def __init__(self, controller_ip: str):
|
||||
if controller_ip is None:
|
||||
self.controller = None
|
||||
else:
|
||||
self.controller = a1.Controller.connect(controller_ip)
|
||||
|
||||
self.status_item_configuration = a1.StatusItemConfiguration()
|
||||
self.start_controller()
|
||||
|
||||
def start_controller(self):
|
||||
self.controller.start()
|
||||
|
||||
def disconnect(self):
|
||||
self.controller.disconnect()
|
||||
|
||||
def enable_motion(self, axis: str):
|
||||
self.controller.runtime.commands.motion.enable(axis.upper())
|
||||
|
||||
def home_motor(self, axis: str):
|
||||
self.__configure_axis_status(axis, a1.AxisStatusItem.AxisEnabled)
|
||||
result = self.get_status_via_status_items(name=axis, status_item=a1.AxisStatusItem.AxisEnabled)
|
||||
if not result == a1.AxisStatusItem.AxisEnabled:
|
||||
self.enable_motion(axis)
|
||||
self.controller.runtime.commands.motion.home(axis.upper())
|
||||
|
||||
def __configure_axis_status(self, axis_name: str, axis_status_item: a1.AxisStatusItem):
|
||||
self.status_item_configuration.axis.add(axis_status_item=axis_status_item, axis=axis_name)
|
||||
|
||||
def __configure_task_status(self, task_id:int, task_status_item: a1.TaskStatusItem):
|
||||
self.status_item_configuration.task.add(task_status_item=task_status_item, task=f"Task {task_id}")
|
||||
|
||||
def get_status_via_status_items(self, name: str | int, status_item: a1.TaskStatusItem | a1.AxisStatusItem):
|
||||
result = self.controller.runtime.status.get_status_items(self.status_item_configuration)
|
||||
if int(name):
|
||||
return result.task.get(status_item, f"Task {name}").value
|
||||
elif str(name):
|
||||
return result.axis.get(status_item, name).value
|
||||
else:
|
||||
raise ValueError(f"Invalid status item {status_item} for task {name}")
|
||||
|
||||
def set_global_variable(self, index:int, value: Union[int, float, str]):
|
||||
if type(value) is int:
|
||||
self.controller.runtime.variables.global_.set_integer(index, value)
|
||||
elif type(value) is float:
|
||||
self.controller.runtime.variables.global_.set_real(index, value)
|
||||
elif type(value) is str:
|
||||
self.controller.runtime.variables.global_.set_string(index, value)
|
||||
else:
|
||||
raise ValueError(f"Invalid type {type(value)} for global variable")
|
||||
|
||||
def get_axis_status_via_status_items(self, axis_name:str, status_item: a1.AxisStatusItem):
|
||||
result = self.controller.runtime.status.get_status_items(self.status_item_configuration)
|
||||
return result.task.get(status_item, axis_name).value
|
||||
|
||||
def wait_for_status_to_change(self, name: str | int, status_item:a1.TaskStatusItem | a1.AxisStatusItem, enum, timeout: float = 60.0):
|
||||
start = time.perf_counter()
|
||||
while self.get_status_via_status_items(name, status_item) == enum:
|
||||
time.sleep(0.1)
|
||||
if time.perf_counter() - start > timeout:
|
||||
print(self.get_status_via_status_items(name, status_item))
|
||||
raise TimeoutError(f"Timeout waiting for task {name} to finish")
|
||||
return
|
||||
|
||||
def wait_program_finish(self, task_id:int=3, timeout:float=60.0):
|
||||
start = time.perf_counter()
|
||||
status_item = a1.TaskStatusItem.TaskState
|
||||
while True:
|
||||
status = self.get_status_via_status_items(task_id, status_item)
|
||||
if status != a1.TaskState.ProgramRunning:
|
||||
if status == a1.TaskState.Idle:
|
||||
return
|
||||
elif status == a1.TaskState.ProgramComplete:
|
||||
print(f"Program completed on Task {task_id}")
|
||||
return
|
||||
elif status == a1.TaskState.Error:
|
||||
raise RuntimeError(f"Task {task_id} failed to start")
|
||||
elif status == a1.TaskState.ProgramPaused:
|
||||
print(f"Program paused on Task {task_id}, not sure how")
|
||||
else:
|
||||
raise RuntimeError(f"Unknown status {status} for task {task_id}")
|
||||
if time.perf_counter() - start > timeout:
|
||||
print(self.get_status_via_status_items(task_id, status_item))
|
||||
raise TimeoutError(f"Timeout waiting for task {task_id} to finish")
|
||||
time.sleep(0.05)
|
||||
print(f"Task {task_id} finished with status {status}")
|
||||
|
||||
def __run_program(self, script_name:str, task_id:int=3, timeout:float=60.0):
|
||||
self.__configure_task_status(task_id, a1.TaskStatusItem.TaskState)
|
||||
state = self.get_status_via_status_items(task_id, a1.TaskStatusItem.TaskState)
|
||||
print(f"Task {task_id} is in state {state}: {a1.TaskState(state).name}")
|
||||
if state != a1.TaskState.ProgramComplete and state != a1.TaskState.Idle:
|
||||
if a1.TaskState.ProgramRunning == state:
|
||||
self.wait_for_status_to_change(task_id, a1.TaskStatusItem.TaskState, state, timeout)
|
||||
elif a1.TaskState.ProgramComplete == state:
|
||||
print('can continue')
|
||||
else:
|
||||
print(f"Task {task_id} is not Idle: {a1.TaskState(state).name} , aborting")
|
||||
return
|
||||
try:
|
||||
print(f"Running script {script_name} on task {task_id}")
|
||||
self.controller.runtime.tasks[task_id].program.run(script_name)
|
||||
self.wait_program_finish(task_id, timeout)
|
||||
except Exception as e:
|
||||
print(f"Error executing script {script_name}: {e}")
|
||||
|
||||
def run_grid_scan(self, cell_height_mm:float, num_rows:int,
|
||||
row_width_mm:float, time_per_row_s: float, task_id:int =3):
|
||||
self.set_global_variable(0, cell_height_mm)
|
||||
self.set_global_variable(1, row_width_mm)
|
||||
self.set_global_variable(2, time_per_row_s)
|
||||
self.set_global_variable(1, num_rows)
|
||||
self.set_global_variable(0, 1)
|
||||
#self.__run_program(script_name="grid_scan.a1exe", task_id=task_id, timeout=120.0)
|
||||
def home_all(self, task_id:int = 3):
|
||||
self.__run_program(script_name="home_all.a1exe", task_id=task_id, timeout=120.0)
|
||||
|
||||
def rotation_scan(self, task_id = 3, start_angle:float = 0.0, end_angle:float = 360.0, step_size:float = 1.0, num_steps:int = 10):
|
||||
self.__run_program(script_name="rotation_scan.a1exe", task_id=task_id, timeout=90.0)
|
||||
|
||||
def move_motor_absolute(self, axis:str, position:float, speed:float=1.0):
|
||||
self.controller.runtime.commands.motion.moveabsolute(axis.upper(), [position], [speed])
|
||||
|
||||
def move_motor_linear(self, axis: str, position: float, speed: float = 1.0):
|
||||
self.controller.runtime.commands.motion.movelinear(axis.upper(), [position], speed)
|
||||
|
||||
if __name__ == "__main__":
|
||||
beamline = mx_beamline()
|
||||
print(beamline)
|
||||
### test on 10S
|
||||
aerotech = AerotechController(controller_ip="129.129.118.96")
|
||||
#aerotech.enable_motion("X")
|
||||
#aerotech.home_all()
|
||||
rw = 0.320
|
||||
ch = 0.010
|
||||
nr = 10
|
||||
tpr_s = rw / ch * 0.02
|
||||
#aerotech.run_grid_scan(cell_height_mm=ch, num_rows=nr,
|
||||
# row_width_mm=rw, time_per_row_s=tpr_s)
|
||||
st = time.perf_counter()
|
||||
aerotech.move_motor_absolute("Z", 0, 10000)
|
||||
print(f"time to move: {time.perf_counter() - st}")
|
||||
aerotech.disconnect()
|
||||
|
||||
#aerotech_epics = AerotechControllerEpics(beamline)
|
||||
#aerotech_epics.omega.speed = 80.0
|
||||
#print(aerotech_epics.get_global_variable(0, VariableTypeEnum.REAL))
|
||||
#aerotech_epics.set_global_variable(0, 100.0)
|
||||
#print(aerotech_epics.get_global_variable(0, VariableTypeEnum.REAL))
|
||||
# print('enable all motors')
|
||||
# aerotech_epics.enable_all()
|
||||
# print('home_all')
|
||||
# aerotech_epics.home_all()
|
||||
# print('wait for home to finish')
|
||||
# start = time.perf_counter()
|
||||
# status = aerotech_epics.get_tast_status(2)
|
||||
# print(f'current status: {status}')
|
||||
# if status == 'Idle':
|
||||
# time.sleep(0.2)
|
||||
# test_counter = 0
|
||||
# while status != 'Ready':
|
||||
# time.sleep(0.1)
|
||||
# if time.perf_counter() - start > 360.0:
|
||||
# raise TimeoutError("Timeout waiting for home all task to finish")
|
||||
# elif status == 'Idle':
|
||||
# time.sleep(0.5)
|
||||
# if test_counter == 1:
|
||||
# raise RuntimeError("Home all task failed to start")
|
||||
# time.sleep(1.0)
|
||||
# print('restarting home all')
|
||||
# aerotech_epics.home_all()
|
||||
# time.sleep(1.0)
|
||||
# test_counter = 1
|
||||
#
|
||||
# status = arotech_epics.get_tast_status(2)
|
||||
# for i in range(10):
|
||||
# print(f'moving to {(i+1)*90}')
|
||||
# aerotech_epics.omega.move(90.0, relative=True, wait=True)
|
||||
# time.sleep(1.0)
|
||||
# if i == 5:
|
||||
# print('simulating disable')
|
||||
# aerotech_epics._disable_all()
|
||||
# time.sleep(10.0)
|
||||
# print('re-enabling')
|
||||
# aerotech_epics.enable_all()
|
||||
# time.sleep(1.0)
|
||||
# print('home all')
|
||||
# aerotech_epics.home_all()
|
||||
# print('wait for home to finish')
|
||||
# start = time.perf_counter()
|
||||
# status = aerotech_epics.get_tast_status(2)
|
||||
# print(f'current status: {status}')
|
||||
# test_counter = 0
|
||||
# while status != 'Ready':
|
||||
# time.sleep(0.1)
|
||||
# if time.perf_counter() - start > 360.0:
|
||||
# raise TimeoutError("Timeout waiting for home all task to finish")
|
||||
# status = aerotech_epics.get_tast_status(2)
|
||||
# time.sleep(0.5)
|
||||
# if test_counter == 1:
|
||||
# raise RuntimeError("Home all task failed to start")
|
||||
# time.sleep(1.0)
|
||||
# print('restarting home all')
|
||||
# aerotech_epics.home_all()
|
||||
# time.sleep(1.0)
|
||||
# test_counter = 1
|
||||
|
||||
|
||||
#aerotech_epics.home_all()
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
from enum import Enum
|
||||
|
||||
import epics
|
||||
import numpy as np
|
||||
|
||||
class AutoEnum(Enum):
|
||||
MANUAL = 0
|
||||
ONCE = 1
|
||||
AUTO = 2
|
||||
|
||||
class epicsAD(object):
|
||||
def __init__(self, prefix, cam="cam1:", image="image1:"):
|
||||
@@ -12,10 +18,15 @@ class epicsAD(object):
|
||||
self.acquire = epics.PV(prefix + cam + "Acquire")
|
||||
self.color = epics.PV(prefix + cam + "ColorMode")
|
||||
self.gain_mode = epics.PV(prefix + cam + "GainAuto")
|
||||
self.gain_mode_rbv = epics.PV(prefix + cam + "GainAuto_RBV")
|
||||
# 0 - manual, GainMode, 2 - auto GainAuto
|
||||
self.expo_mode = epics.PV(prefix + cam + "ExposureAuto")
|
||||
# 0 - manual, ExposureMode, 2 - auto ExposureAutoself.gain = epics.PV(prefix + cam + "Gain")
|
||||
self.expo_mode_rbv = epics.PV(prefix + cam + "ExposureAuto_RBV")
|
||||
# 0 - manual, ExposureMode, 1 - once 2 - auto ExposureAuto
|
||||
self.gain = epics.PV(prefix + cam + "Gain")
|
||||
self.gain_rbv = epics.PV(prefix + cam + "Gain_RBV")
|
||||
self.expo = epics.PV(prefix + cam + "AcquireTime")
|
||||
self.expo_rbv = epics.PV(prefix + cam + "AcquireTime_RBV")
|
||||
|
||||
self.ndim = epics.PV(prefix + image + "NDimensions_RBV")
|
||||
self.dim0 = epics.PV(prefix + image + "ArraySize0_RBV")
|
||||
@@ -128,10 +139,20 @@ class epicsAD(object):
|
||||
self.expo.put(expo) # 20 hz acquisition
|
||||
self.acquire.put(1)
|
||||
|
||||
def set_auto(self):
|
||||
def set_auto(self, state: AutoEnum):
|
||||
self.acquire.put(0, wait=True)
|
||||
self.gain_mode.put(2) # automatic gain control
|
||||
self.expo_mode.put(2) # automatic exposure control
|
||||
self.gain_mode.put(state.value) # automatic gain control
|
||||
self.expo_mode.put(state.value) # automatic exposure control
|
||||
self.acquire.put(1)
|
||||
|
||||
def set_auto_once(self):
|
||||
self.expo_mode.put(1)
|
||||
self.gain_mode.put(1)
|
||||
|
||||
def set_manual(self):
|
||||
self.acquire.put(0, wait=True)
|
||||
self.gain_mode.put(0) # automatic gain control
|
||||
self.expo_mode.put(0) # automatic exposure control
|
||||
self.acquire.put(1)
|
||||
|
||||
def restore(self, gain: float = 5, expo: float = 0.025):
|
||||
@@ -0,0 +1,259 @@
|
||||
from bec_lib.client import BECClient
|
||||
from bec_ipython_client import BECIPythonClient
|
||||
from bec_lib.service_config import ServiceConfig
|
||||
from bec_lib.procedures.helper import FrontendProcedureHelper, BackendProcedureHelper
|
||||
|
||||
from aare.common.beamline import MXBeamline, mx_beamline
|
||||
|
||||
|
||||
#from pxii_bec.macros.pxii_guards import GuardViolation
|
||||
|
||||
|
||||
#specify up to 10 queue to runs in parallel, request more if needed!
|
||||
# st = client.proc.request_new("sleep", ((), {"time_s":5}), queue="test")
|
||||
|
||||
#to see all deevices
|
||||
#devs.show_all
|
||||
|
||||
#helper fucntions
|
||||
# helper.get.active_and_pending_queue_names()
|
||||
# helper.get.running_procedures()
|
||||
# helper.request.abort_queue()
|
||||
|
||||
def bec_exception_handler(exception: Exception):
|
||||
print(f"Exception: {exception}")
|
||||
|
||||
|
||||
class MultiPositionDevice:
|
||||
"""inherits from the MultiPositionDevice BEC class
|
||||
can move in and out
|
||||
:param device is inisitialised with initialise_PD_devices() and can be called with PD.device_name
|
||||
:states is a dictionary of str, float from bec.dev.device_name.user_parameter
|
||||
"""
|
||||
|
||||
def __init__(self, device, states: dict[str, float] | None = None):
|
||||
self.device = device
|
||||
self.states = states
|
||||
|
||||
def move_to(self, position: str):
|
||||
self.device.move_to(position)
|
||||
|
||||
@property
|
||||
def actual(self) -> float:
|
||||
return self.device.actual
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
"""returns the current state of the device as a string or unknown if out of a valid state position"""
|
||||
return self.device.state
|
||||
|
||||
@property
|
||||
def is_at(self, state:str) -> bool:
|
||||
return self.device.is_at("state")
|
||||
|
||||
@property
|
||||
def is_clear(self) -> bool:
|
||||
return self.device.is_clear()
|
||||
|
||||
|
||||
class PositionedDevice:
|
||||
"""inherits from the PositionedDevice BEC class
|
||||
can move in and out
|
||||
:param device is inisitialised with initialise_PD_devices() and can be called with PD.device_name
|
||||
"""
|
||||
|
||||
def __init__(self, device):
|
||||
self.device = device
|
||||
self.states = ["in", "out"]
|
||||
|
||||
def move_in(self):
|
||||
self.device.mvin()
|
||||
|
||||
|
||||
def move_out(self):
|
||||
self.device.mvout()
|
||||
|
||||
@property
|
||||
def is_in(self) -> bool:
|
||||
return self.device.is_in()
|
||||
|
||||
@property
|
||||
def is_out(self) -> bool:
|
||||
return self.device.is_out()
|
||||
|
||||
|
||||
class GuardedPositionedDevice:
|
||||
"""inherits from the GuardedAxis BEC class
|
||||
:param device is inisitialised with initialise_PD_devices() and can be called with PD.device_name
|
||||
allowed functions are move(float) and actual which returns the feedback"""
|
||||
|
||||
def __init__(self, device):
|
||||
self.device = device
|
||||
|
||||
def move(self, position: float):
|
||||
self.device.move(position)
|
||||
|
||||
@property
|
||||
def actual(self) -> float:
|
||||
return self.device.actual
|
||||
|
||||
|
||||
|
||||
class OurBECDevices:
|
||||
def __init__(self, BECDevices):
|
||||
print("initialising devices")
|
||||
init_positioned_devices()
|
||||
dev = BECDevices
|
||||
print("devices initialised")
|
||||
self.coll_y = MultiPositionDevice(device = PD.coll_y,
|
||||
states = dev.coll_y.user_parameter)
|
||||
self.scintillator_diode_y = MultiPositionDevice(device = PD.diag_y,
|
||||
states = dev.diag_y.user_parameter)
|
||||
self.backlight_pos = PositionedDevice(device = PD.bl_pos)
|
||||
#self.beamstop_pos = PositionedDevice(device = PD.bs_pos)
|
||||
self.beamstop_z = GuardedPositionedDevice(device = PD.bs_z)
|
||||
|
||||
self.goniometer_x = GuardedPositionedDevice(device = PD.gon_x) # may get deprecated with the IOC
|
||||
|
||||
class BECClientWorker:
|
||||
def __init__(self, beamline:MXBeamline, name:str = "default"):
|
||||
BEAMLINE = beamline.value.lower()
|
||||
service_config = ServiceConfig(redis={"host": f"{BEAMLINE}-bec-001.psi.ch", "port": 6379})
|
||||
print(service_config.config)
|
||||
service_config.config["log_writer"]["base_path"]='./logs'
|
||||
print(service_config.config)
|
||||
self.client = BECIPythonClient(config=service_config)
|
||||
#self.client.config.update_session_with_file("/sls/x10sa/config/bec/production/bec/bec_lib/bec_lib/config_helper.py")
|
||||
self.client.start()
|
||||
self.dev = self.client.device_manager.devices
|
||||
self.scans = self.client.scans
|
||||
self.macros = self.client.macros
|
||||
self.load_user_macros()
|
||||
self.helper = FrontendProcedureHelper(self.client.connector)
|
||||
self.ProtectedDevices = OurBECDevices(BECDevices=self.dev)
|
||||
|
||||
|
||||
def run_macro(self, macro_name:str, *args, queue:str = "default", **kwargs):
|
||||
return self.client.proc.run_macro(macro_name, *args, queue=queue)
|
||||
|
||||
def run_macro_blocked(self, macro_name:str, *args, queue:str = "default", **kwargs):
|
||||
try:
|
||||
status = self.run_macro(macro_name, *args, queue=queue)
|
||||
print(status)
|
||||
status.wait()
|
||||
print(status)
|
||||
return status
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return None
|
||||
|
||||
def show_all_devices(self):
|
||||
return self.dev.show_all
|
||||
|
||||
def list_all_macros(self):
|
||||
return self.macros.list_user_macros()
|
||||
|
||||
def load_user_macros(self):
|
||||
self.macros.load_all_user_macros()
|
||||
|
||||
def shutdown_client(self):
|
||||
self.client.shutdown()
|
||||
|
||||
def common2rse(self):
|
||||
status = self.scans.umv(self.dev.blight_pos, 0, relative=False)
|
||||
try:
|
||||
SE.blpos.checkpos()
|
||||
except NotImplemented as e:
|
||||
print(f"Backlight position not checked: {e}")
|
||||
print(f"Moved backlight to position {self.dev.blight_pos.read()} with status {status.status}")
|
||||
self.scans.umv(self.dev.bs_z, 72.0, self.dev.coll_y, 20.0, self.dev.scin_y, 20.0, self.dev.cryo_pos, 1, relative=False)
|
||||
self.scans.umv(self.dev.bs_pos, 0, relative=False)
|
||||
print(f"Moved everything else to park position with status {status.status}")
|
||||
|
||||
if self.dev.bs_z.read().value != 72.0:# or self.dev.coll_y.read() != 20.0 or self.dev.scin_y.read() != 20.0:
|
||||
print(f"is bs_z correct: {self.dev.bs_z.read().value != 72.0}")# is coll_y correct {self.dev.coll_y.read() != 20.0}, is scin_y correct {self.dev.scin_y.read() != 20.0}")
|
||||
raise Exception("Error: something went wrong")
|
||||
|
||||
def rse2sa(self):
|
||||
self.scans.umv(self.dev.blight_pos, 1, relative=False)
|
||||
self.scans.umv(self.dev.bs_pos, 1, relative = False)
|
||||
|
||||
def mono_pitch_scan_runner(self):
|
||||
print('this is a print statement from inside DAQ not BEC')
|
||||
try:
|
||||
self.macros.mono_pitch_scan(False)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
# not implemented yet
|
||||
# self.cryo_pos = PD.cryo_pos
|
||||
# self.xrf_pos = PD.xrf_po
|
||||
|
||||
if __name__ == "__main__":
|
||||
import time
|
||||
print('starting BEC Client')
|
||||
beamline = mx_beamline()
|
||||
try:
|
||||
|
||||
client = BECClientWorker(beamline)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
import sys
|
||||
sys.exit(1)
|
||||
|
||||
client.show_all_devices()
|
||||
client.list_all_macros()
|
||||
try:
|
||||
# a=client.a2e_runner(160, "iln")
|
||||
# print(a)
|
||||
# print(convert_from_energy(12))
|
||||
# energy = get_current_energy()
|
||||
# pos = get_dcm_motors_positions(energy)
|
||||
# print(energy, pos)
|
||||
print(bs_z_policy(15.0))
|
||||
#client.scans.umv(client.dev.xeye_x, 0, relative=False)
|
||||
#client.mono_pitch_scan_runner()
|
||||
#client.mono_pitch_scan_runner()
|
||||
#b=client.run_macro_blocked("a2e", 160, "iln", queue="test")
|
||||
#b = client.run_macro_blocked("mono_pitch_scan", False, queue="default")
|
||||
|
||||
#status = client.mono_pitch_scan_runner
|
||||
#print(status)
|
||||
|
||||
#client.rse2sa()
|
||||
#time.sleep(10.0)
|
||||
#time.sleep(10.0)
|
||||
#print(status)
|
||||
#client.common2rse()
|
||||
#print(status)
|
||||
except GuardViolation as e:
|
||||
print(f"GuardViolation: {e}")
|
||||
except RuntimeError as e:
|
||||
print(f"RuntimeError: {e}")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
client.shutdown_client()
|
||||
|
||||
#
|
||||
# try:
|
||||
# st = client.proc.run_macro("a2e", 160, "iln", queue="test")
|
||||
# print(st)
|
||||
# st.wait()
|
||||
# print(st)
|
||||
# status_1 = scans.umv(bec_dev.bs_x, -1.0, bec_dev.bs_y, -1.0, relative = True) # blocking
|
||||
# print(
|
||||
# f"Moved to position {bec_dev.bs_x.position} with status {status.status}"
|
||||
# )
|
||||
# status = scans.mv(bec_dev.bs_x, 1.0, bec_dev.bs_y, 1.0, relative=True) # none blocking
|
||||
# status.wait()
|
||||
# print(
|
||||
# f"Moved to position {bec_dev.bs_x.position} with status {status.status}"
|
||||
# )
|
||||
# except Exception as e:
|
||||
# print(f"Error: {e}")
|
||||
#
|
||||
# client.shutdown()
|
||||
|
||||
|
||||
#backend wont work unless bec server will work, frontend anywehre with user access
|
||||
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from aare.devices.set_get_pv import SetGetPV, MoveResult
|
||||
|
||||
class EnumPV(SetGetPV):
|
||||
def __init__(self, name: str, setpv: str, getpv: str, **kwargs):
|
||||
super().__init__(name, setpv, getpv, **kwargs)
|
||||
if not self.setpoint_pv.enum_strs:
|
||||
raise RuntimeError(f"{setpv} is not an ENUM PV")
|
||||
|
||||
@property
|
||||
def position(self) -> str:
|
||||
return self.readback_pv.get(as_string=True)
|
||||
|
||||
def _resolve(self, x: Any) -> MoveResult:
|
||||
# accept Enum member
|
||||
if isinstance(x, Enum):
|
||||
x = x.name
|
||||
|
||||
# accept index
|
||||
if isinstance(x, int):
|
||||
try:
|
||||
return MoveResult(target=self.setpoint_pv.enum_strs[x], name=None)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Bad enum index {x}") from e
|
||||
|
||||
# accept name -> match against enum strings (case-insensitive)
|
||||
if isinstance(x, str):
|
||||
for s in self.setpoint_pv.enum_strs:
|
||||
if s.strip().lower() == x.strip().lower():
|
||||
return MoveResult(target=s, name=s)
|
||||
raise ValueError(f"'{x}' not in {list(self.setpoint_pv.enum_strs)}")
|
||||
|
||||
raise TypeError(f"Unsupported enum command type: {type(x).__name__}")
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
from aaredaqlib.beamline import MXBeamline
|
||||
from aare.common.beamline import MXBeamline
|
||||
from epics import PV
|
||||
|
||||
class ExperimentalHutchShutter:
|
||||
@@ -2,7 +2,7 @@ import time
|
||||
|
||||
from epics import PV, poll
|
||||
|
||||
from aaredaqlib.beamline import MXBeamline
|
||||
from aare.common.beamline import MXBeamline
|
||||
|
||||
|
||||
class FilterTransmission:
|
||||
@@ -1,12 +1,10 @@
|
||||
import time
|
||||
from typing import Callable, cast
|
||||
|
||||
from epics import PV, poll
|
||||
from epics.ca import pend_io
|
||||
|
||||
from aaredaqlib.beamline import MXBeamline
|
||||
from aare.common.beamline import MXBeamline
|
||||
|
||||
from aaredaqlib.logger_config import setup_logger
|
||||
from aare.common.logger_config import setup_logger
|
||||
|
||||
logger = setup_logger("aareaDAQ")
|
||||
|
||||
@@ -2,11 +2,10 @@ import math
|
||||
|
||||
import jfjoch_client
|
||||
|
||||
from aaredaqlib.beamline import MXBeamline
|
||||
from aaredaqlib.diffraction_geometry import DiffractionGeometry
|
||||
from aaredaqlib.models import SampleShortInfo, DAQStatusModel, FluorescenceSpectrumOutputModel
|
||||
from aaredaqlib.raster_grid import RasterGridRequest
|
||||
from aaredaqlib.rotation_scan import RotationScanRequest
|
||||
from aare.common.beamline import MXBeamline
|
||||
from aare.common.models import DAQStatusModel, FluorescenceSpectrumOutputModel
|
||||
from aare.common.raster_grid import RasterGridRequest
|
||||
from aare.common.rotation_scan import RotationScanRequest
|
||||
|
||||
|
||||
class JFJochWrapper:
|
||||
@@ -14,6 +13,8 @@ class JFJochWrapper:
|
||||
match bl:
|
||||
case MXBeamline.X06DA:
|
||||
self.__url = "http://sls-gpu-001:8080"
|
||||
case MXBeamline.X10SA:
|
||||
self.__url = "http://sls-gpu-002:8080"
|
||||
case MXBeamline.SIMULATED:
|
||||
self.__url = "http://localhost:8080"
|
||||
case _:
|
||||
@@ -1,44 +1,9 @@
|
||||
import datetime
|
||||
import re
|
||||
import time
|
||||
from typing import Callable, Union
|
||||
from typing import Callable, Union, Any
|
||||
|
||||
from epics import PV, Motor, poll
|
||||
|
||||
|
||||
def timestamp():
|
||||
"""
|
||||
Returns a fixed width string (15 characters) with a timestamp
|
||||
in the format (24Hour:Minute:Second.Microsecond).
|
||||
|
||||
Example: 13:09:43.009508
|
||||
"""
|
||||
x = datetime.datetime(1, 1, 1).now()
|
||||
return "%4d-%02d-%02d %02d:%02d:%02d,%03d" % (
|
||||
x.year,
|
||||
x.month,
|
||||
x.day,
|
||||
x.hour,
|
||||
x.minute,
|
||||
x.second,
|
||||
x.microsecond / 1000,
|
||||
)
|
||||
|
||||
|
||||
def itoa(x, base=10):
|
||||
is_negative = x < 0
|
||||
if is_negative:
|
||||
x = -x
|
||||
digits = []
|
||||
while x > 0:
|
||||
x, last_digit = divmod(x, base)
|
||||
digits.append("0123456789abcdefghijklmnopqrstuvwxyz"[last_digit])
|
||||
if is_negative:
|
||||
digits.append("-")
|
||||
digits.reverse()
|
||||
return "".join(digits)
|
||||
|
||||
|
||||
def wait_for_movement_to_finish(*motors):
|
||||
"""
|
||||
Wait for all {motors} passed in argument to finish movement.
|
||||
@@ -51,10 +16,6 @@ def wait_for_movement_to_finish(*motors):
|
||||
|
||||
Returns: nothing
|
||||
"""
|
||||
# timeout = 1.5 * max([abs(motor.get_position(readback=True) -
|
||||
# motor.get_position()) / motor.slew_speed
|
||||
# for motor in motors])
|
||||
#
|
||||
poll(0.3)
|
||||
longest = 0.0
|
||||
for motor in motors:
|
||||
@@ -77,15 +38,31 @@ class ValueWaitTimeout(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def pv_wait(pv, value, *, timeout=30.0, polling=0.2, tolerance=None, verbose=False):
|
||||
if not (isinstance(pv, PV) or isinstance(pv, Motor)):
|
||||
raise ValueError("wait what!? what pv?!")
|
||||
|
||||
def pv_wait(pv: PV | Motor, value: Any, *, timeout: float = 60.0,
|
||||
polling: float = 0.1, tolerance: float | None=None, verbose: bool =False):
|
||||
"""wait until an epics.PV reaches a value
|
||||
pv: epics.PV | epics.Motor
|
||||
the PV on which you want to wait on
|
||||
value/target: any
|
||||
this value depends on the PV type: str, enum, double, ...
|
||||
:timeout: float default = 60.0
|
||||
timeout in seconds
|
||||
:polling: float default = 0.1
|
||||
polling interval in seconds
|
||||
tolerance: float or None
|
||||
provide a tolerance to accept when comparing values, currently None by default, however certain PVs and motors
|
||||
have inbuilt tolerances that can be used instead
|
||||
verbose: bool default = False
|
||||
NotImplemented
|
||||
"""
|
||||
if isinstance(pv, Motor):
|
||||
|
||||
def checker(m, target=None, tolerance=None):
|
||||
if tolerance is None:
|
||||
tolerance = m.get("RDBD")
|
||||
if tolerance is None:
|
||||
print(f"WARNING: motor {m._prefix[:-1]} has no RDBD, using 0.001")
|
||||
tolerance = 0.001 #FIXME
|
||||
if target is None:
|
||||
target = m.drive
|
||||
|
||||
@@ -107,14 +84,25 @@ def pv_wait(pv, value, *, timeout=30.0, polling=0.2, tolerance=None, verbose=Fal
|
||||
|
||||
|
||||
def is_epics_type(pv: PV, pv_type: str) -> bool:
|
||||
"""Check to see if a PV is of a certain type such as double, enum, string, ..."""
|
||||
if isinstance(pv_type, type):
|
||||
pv_type = pv_type.__name__
|
||||
|
||||
return pv_type == pv.type
|
||||
|
||||
|
||||
def wait_string_condition(pv, target: Union[str, re.Pattern], *, timeout=60.0, polling=0.1):
|
||||
if not (isinstance(pv, PV) or "string" not in pv.type):
|
||||
def wait_string_condition(pv: PV, target: Union[str, re.Pattern], *, timeout: float = 60.0, polling: float = 0.1):
|
||||
"""wait until an epics.PV of type string reaches target
|
||||
:pv: epics.PV
|
||||
PV should be of type string
|
||||
:target: str or re.Pattern
|
||||
:timeout: float default = 60.0
|
||||
timeout in seconds
|
||||
:polling: float default = 0.1
|
||||
polling interval in seconds
|
||||
"""
|
||||
|
||||
if not (isinstance(pv, PV) and "string" not in pv.type):
|
||||
raise AttributeError("argument 'pv' must be an epics.PV of type string")
|
||||
|
||||
if not isinstance(target, re.Pattern):
|
||||
@@ -131,7 +119,8 @@ def wait_string_condition(pv, target: Union[str, re.Pattern], *, timeout=60.0, p
|
||||
raise TimeoutError(f"timeout waiting for string {pv.pvname} == {target}; actual value == {pv.char_value}")
|
||||
|
||||
|
||||
def wait_float_condition(pv: PV, value, *, timeout: float = 60.0, **kwargs):
|
||||
def wait_float_condition(pv: PV, value:float, *, timeout: float = 60.0,
|
||||
polling: float = 0.1, tolerance: float| None = None):
|
||||
"""wait until an epics.PV of type double reaches value
|
||||
pv: epics.PV
|
||||
the PV enum on which you want to wait on
|
||||
@@ -139,30 +128,37 @@ def wait_float_condition(pv: PV, value, *, timeout: float = 60.0, **kwargs):
|
||||
value: float
|
||||
the target value
|
||||
|
||||
polling: float
|
||||
how often pv is checked during wait loop
|
||||
|
||||
tolerance: float or None
|
||||
the tolerance to accept when comparing values, if None (default) we
|
||||
try to figure an appropriate value
|
||||
|
||||
|
||||
timeout: double
|
||||
timeout: float default = 60.0
|
||||
a timeout in seconds
|
||||
|
||||
return: nothing
|
||||
|
||||
raises: TimeoutError if a timeout occurs
|
||||
"""
|
||||
if not (isinstance(pv, PV) or "double" not in pv.type):
|
||||
raise AttributeError("argument 'pv' must be an epics.PV of type double")
|
||||
|
||||
try:
|
||||
value = float(value)
|
||||
except ValueError:
|
||||
raise AttributeError("argument 'value' must be a number")
|
||||
|
||||
tolerance = kwargs.get("tolerance", pow(10, -(pv.precision - 1))) # type: ignore
|
||||
polling = kwargs.get("polling", 0.1)
|
||||
if tolerance is None:
|
||||
# If pv.precision is missing/None, fall back to a default
|
||||
precision = getattr(pv, "precision", None)
|
||||
if precision is None:
|
||||
tolerance = 1e-6
|
||||
else:
|
||||
tolerance = pow(10, -(precision - 1))
|
||||
|
||||
tout = time.time() + timeout
|
||||
|
||||
while time.time() < tout:
|
||||
if abs(pv.value - value) < tolerance:
|
||||
return
|
||||
@@ -190,13 +186,13 @@ def wait_motor_position(motor: Motor, tester: Callable, *, timeout: float = 50.0
|
||||
|
||||
raises: TimeoutError if a timeout occurs
|
||||
"""
|
||||
if not isinstance(tester, Callable):
|
||||
if not callable(tester):
|
||||
raise RuntimeError("argument 'tester' must be a function")
|
||||
|
||||
try:
|
||||
move_time = abs(motor.drive - motor.readback) / motor.speed
|
||||
except Exception:
|
||||
move_time = 1.0 # in case we're dealing with weird motor record
|
||||
move_time = 1.0 # in case of unusual motor record
|
||||
|
||||
tout = move_time + time.time() + timeout
|
||||
|
||||
@@ -209,7 +205,7 @@ def wait_motor_position(motor: Motor, tester: Callable, *, timeout: float = 50.0
|
||||
raise TimeoutError(f"timeout waiting for a condition on {motor} {motor.drive} != {motor.readback}")
|
||||
|
||||
|
||||
def wait_enum_condition(pv: PV, value: Union[str, int, re.Pattern], *, timeout: float = 60.0, polling=0.1, **kwargs):
|
||||
def wait_enum_condition(pv: PV, value: Union[str, int, re.Pattern], *, timeout: float = 60.0, polling=0.1):
|
||||
"""wait until an epics.PV enum reaches value
|
||||
pv: epics.PV
|
||||
the PV enum on which you want to wait on
|
||||
@@ -224,7 +220,7 @@ def wait_enum_condition(pv: PV, value: Union[str, int, re.Pattern], *, timeout:
|
||||
|
||||
raises: TimeoutError if a timeout occurs
|
||||
"""
|
||||
if not (isinstance(pv, PV) or "enum" != pv.type[-4:].lower()):
|
||||
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)):
|
||||
@@ -248,4 +244,12 @@ def wait_enum_condition(pv: PV, value: Union[str, int, re.Pattern], *, timeout:
|
||||
poll(polling)
|
||||
|
||||
if time.time() > tout:
|
||||
raise TimeoutError(f"timeout waiting for enum {pv.pvname} == {value}")
|
||||
raise TimeoutError(f"timeout waiting for enum {pv.pvname} == {value},"
|
||||
f"current value is {pv.get(as_string=True)}")
|
||||
|
||||
def clean_filename(self, filename: str) -> str:
|
||||
cleaned = re.sub(r"[^A-Za-z0-9._-]", "_", filename.strip())
|
||||
cleaned = cleaned.strip("._-")
|
||||
if not cleaned:
|
||||
raise ValueError("Filename is empty after sanitization.")
|
||||
return cleaned
|
||||
@@ -0,0 +1,102 @@
|
||||
import time
|
||||
|
||||
from epics import Motor
|
||||
|
||||
|
||||
class MyMotor(Motor):
|
||||
"""Wrapper for the EPICS motor PV."""
|
||||
def __init__(self, name, timeout=5.0):
|
||||
super().__init__(name.upper(), timeout=timeout)
|
||||
|
||||
@property
|
||||
def speed(self):
|
||||
"""Gets the current motor.slew_speed value"""
|
||||
return self.get('VELO')
|
||||
|
||||
@speed.setter
|
||||
def speed(self, v):
|
||||
"""Sets the motor slew speed"""
|
||||
self.put('VELO', v)
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
"""Gets the current motor readback value"""
|
||||
return self.readback
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
"""Gets the current motor.drive value"""
|
||||
return self.drive
|
||||
|
||||
@value.setter
|
||||
def value(self, v):
|
||||
"""Sets motor.drive to a set value"""
|
||||
self.drive = v
|
||||
|
||||
def stop(self):
|
||||
"""Stops the motor"""
|
||||
self.stop_motor()
|
||||
|
||||
@property
|
||||
def moving(self):
|
||||
"""Returns True if the motor is moving"""
|
||||
return bool(self.moving_flag)
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
"""Returns the units of the motor as a string"""
|
||||
return self.get("EGU", as_string=True)
|
||||
|
||||
@property
|
||||
def limits(self):
|
||||
"""Returns (low_limit, high_limit)"""
|
||||
return self.get('HLM'), self.get('LLM')
|
||||
|
||||
@limits.setter
|
||||
def limits(self, limits):
|
||||
"""Sets (low_limit, high_limit)"""
|
||||
low, high = limits
|
||||
self.put('LLM', low)
|
||||
self.put('HLM', high)
|
||||
|
||||
def move_motor(self, val, relative=False, wait=False, timeout=300.0):
|
||||
"""
|
||||
Moves the motor to an absolute or relative position.
|
||||
:param val: Position to move to
|
||||
:param relative: If True, moves relative to current position
|
||||
:param wait: If True, waits for completion (synchronous)
|
||||
:param timeout: Maximum time to wait for completion
|
||||
"""
|
||||
|
||||
return self.move(val, relative=relative, wait=wait, timeout=timeout)
|
||||
|
||||
def home(self, direction='forward', wait=False):
|
||||
"""
|
||||
Homes the motor.
|
||||
:param direction: 'forward' or 'reverse'
|
||||
"""
|
||||
field = 'HOMF' if direction == 'forward' else 'HOMR'
|
||||
self.put(field, 1)
|
||||
if wait:
|
||||
self.wait_for_stop()
|
||||
|
||||
def wait_for_stop(self, timeout=300.0, poll_rate=0.01):
|
||||
"""
|
||||
Synchronous wait until the motor stops moving.
|
||||
"""
|
||||
start_time = time.time()
|
||||
while self.moving:
|
||||
time.sleep(poll_rate)
|
||||
if time.time() - start_time > timeout:
|
||||
raise RuntimeError(f"Timeout waiting for motor {self.name} to stop")
|
||||
|
||||
async def wait_for_stop_async(self, timeout=300.0, poll_rate=0.01):
|
||||
"""
|
||||
Asynchronous wait until the motor stops moving.
|
||||
"""
|
||||
import asyncio
|
||||
start_time = time.time()
|
||||
while self.moving:
|
||||
await asyncio.sleep(poll_rate)
|
||||
if time.time() - start_time > timeout:
|
||||
raise RuntimeError(f"Timeout waiting for motor {self.name} to stop")
|
||||
@@ -0,0 +1,66 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Mapping, Optional, Union, Callable
|
||||
|
||||
from epics import PV, poll
|
||||
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
|
||||
]
|
||||
|
||||
@dataclass
|
||||
class MoveResult:
|
||||
target: RawValue
|
||||
name: Optional[str] = None
|
||||
|
||||
|
||||
class SetGetPV:
|
||||
def __init__(self, name: str, setpv: str, getpv: str, *, timeout: float = 60.0, tolerance: float | None = None):
|
||||
self.name = name
|
||||
self.setpoint_pv = PV(setpv)
|
||||
self.readback_pv = PV(getpv)
|
||||
self.default_timeout = timeout
|
||||
self.tolerance = tolerance
|
||||
self._last_target: RawValue | None = None
|
||||
|
||||
@property
|
||||
def value(self) -> Any:
|
||||
return self.readback_pv.get()
|
||||
|
||||
def _resolve(self, x: Any) -> MoveResult:
|
||||
# Default: treat input as raw value
|
||||
return MoveResult(target=x, name=None)
|
||||
|
||||
def move(self, x: Any, *, wait: bool = False, timeout: float | None = None) -> MoveResult:
|
||||
res = self._resolve(x)
|
||||
self._last_target = res.target
|
||||
self.setpoint_pv.put(res.target)
|
||||
if wait:
|
||||
self.wait(timeout=timeout)
|
||||
return res
|
||||
|
||||
def wait(self, *, timeout: float | None = None):
|
||||
if self._last_target is None:
|
||||
return
|
||||
pv_wait(self.readback_pv, self._last_target, timeout=timeout or self.default_timeout, tolerance=self.tolerance)
|
||||
|
||||
class PredefinedPV(SetGetPV):
|
||||
def __init__(self, name: str, setpv: str, getpv: str, predefs: Mapping[str, ResolverValue], **kwargs):
|
||||
super().__init__(name, setpv, getpv, **kwargs)
|
||||
self._predefs = dict(predefs)
|
||||
|
||||
@property
|
||||
def positions(self) -> list[str]:
|
||||
return list(self._predefs.keys())
|
||||
|
||||
def _resolve(self, x: Any) -> MoveResult:
|
||||
if isinstance(x, str) and x in self._predefs:
|
||||
v = self._predefs[x]
|
||||
if isinstance(v, tuple) and callable(v[0]):
|
||||
v = v[0](*v[1])
|
||||
return MoveResult(target=v, name=x)
|
||||
return MoveResult(target=x, name=None)
|
||||
@@ -0,0 +1,234 @@
|
||||
from enum import Enum
|
||||
from time import sleep, time
|
||||
|
||||
import requests
|
||||
|
||||
from aare.common.beamline import MXBeamline
|
||||
from aare.common.coordinate import SmargonCoordinate, Coordinate, AerotechCoordinate
|
||||
from aare.common.exception_handler import SmargonCommunicationError
|
||||
|
||||
|
||||
class SmargonMode(Enum):
|
||||
UNINITIALIZED = 0
|
||||
INITIALIZING = 1
|
||||
READY = 2
|
||||
ERROR = 99
|
||||
|
||||
|
||||
class Smargon(object):
|
||||
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)
|
||||
|
||||
def __init__(self, bl: MXBeamline):
|
||||
if bl == MXBeamline.X06DA:
|
||||
self.__simulated = False
|
||||
self.__base = "http://x06da-smargopolo.psi.ch:3000"
|
||||
elif bl == MXBeamline.X10SA:
|
||||
self.__simulated = False
|
||||
self.__base = "http://x10sa-smargopolo.psi.ch:3000"
|
||||
elif bl == MXBeamline.SIMULATED:
|
||||
self.__simulated = True
|
||||
self.__pos = self.SMARGON_HOME
|
||||
self.__pos_aero = self.AERO_HOME
|
||||
else:
|
||||
raise Exception("unknown beamline")
|
||||
|
||||
def gonget(self, thing: str) -> dict:
|
||||
"""issue a GET for some API component on the smargopolo server
|
||||
short hand for goniometer get"""
|
||||
cmd = f"{self.__base}/{thing}"
|
||||
try:
|
||||
r = requests.get(cmd, timeout=2.0)
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise SmargonCommunicationError(
|
||||
f"Smargon GET failed for '{thing}'",
|
||||
endpoint=thing,
|
||||
base_url=self.__base,
|
||||
operation="GET",
|
||||
) from e
|
||||
|
||||
if not r.ok:
|
||||
raise SmargonCommunicationError(
|
||||
f"Smargon GET returned HTTP {r.status_code} for '{thing}': {r.reason}",
|
||||
endpoint=thing,
|
||||
base_url=self.__base,
|
||||
operation="GET",
|
||||
status_code=r.status_code,
|
||||
)
|
||||
|
||||
try:
|
||||
return r.json()
|
||||
except ValueError as e:
|
||||
raise SmargonCommunicationError(
|
||||
f"Smargon GET returned invalid JSON for '{thing}'",
|
||||
endpoint=thing,
|
||||
base_url=self.__base,
|
||||
operation="GET",
|
||||
status_code=r.status_code,
|
||||
) from e
|
||||
|
||||
def gonput(self, thing: str):
|
||||
"""issue a PUT command for some API component on the smargopolo server
|
||||
short hand for goniometer put"""
|
||||
cmd = f"{self.__base}/{thing}"
|
||||
try:
|
||||
r = requests.put(cmd, timeout=2.0)
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise SmargonCommunicationError(
|
||||
f"Smargon PUT failed for '{thing}'",
|
||||
endpoint=thing,
|
||||
base_url=self.__base,
|
||||
operation="PUT",
|
||||
) from e
|
||||
|
||||
if not r.ok:
|
||||
raise SmargonCommunicationError(
|
||||
f"Smargon PUT returned HTTP {r.status_code} for '{thing}': {r.reason}",
|
||||
endpoint=thing,
|
||||
base_url=self.__base,
|
||||
operation="PUT",
|
||||
status_code=r.status_code,
|
||||
)
|
||||
|
||||
def move_home(self, wait=False) -> None:
|
||||
self.target = self.SMARGON_HOME
|
||||
if wait:
|
||||
self.wait()
|
||||
|
||||
@property
|
||||
def mode(self) -> SmargonMode:
|
||||
return SmargonMode.INITIALIZING
|
||||
|
||||
@mode.setter
|
||||
def mode(self, mode: SmargonMode):
|
||||
if self.__simulated:
|
||||
return
|
||||
self.gonput(f"mode?mode={mode}")
|
||||
|
||||
def initialize(self):
|
||||
self.mode = SmargonMode.UNINITIALIZED
|
||||
sleep(0.1)
|
||||
self.mode = SmargonMode.INITIALIZING
|
||||
|
||||
def enable_correction(self):
|
||||
if self.__simulated:
|
||||
return
|
||||
|
||||
self.gonput("corr_type?corr_type=1")
|
||||
|
||||
def disable_correction(self):
|
||||
if self.__simulated:
|
||||
return
|
||||
|
||||
self.gonput("corr_type?corr_type=0")
|
||||
|
||||
@property
|
||||
def readback(self) -> SmargonCoordinate:
|
||||
if self.__simulated:
|
||||
return self.__pos
|
||||
|
||||
scs = self.gonget("readbackSCS")
|
||||
return SmargonCoordinate(
|
||||
sh_mm=Coordinate(x=scs["SHX"], y=scs["SHY"], z=scs["SHZ"]),
|
||||
phi_deg=scs["PHI"],
|
||||
chi_deg=scs["CHI"],
|
||||
)
|
||||
|
||||
@property
|
||||
def readback_aerotech(self) -> AerotechCoordinate:
|
||||
if self.__simulated:
|
||||
return self.__pos_aero
|
||||
|
||||
acs = self.gonget("readbackAEROTECH")
|
||||
return AerotechCoordinate(at_mm = Coordinate(x=acs["GMX"], y = acs["GMY"], z = acs["GMZ"]),
|
||||
omega_deg = acs["GMU"])
|
||||
|
||||
@property
|
||||
def target(self) -> SmargonCoordinate:
|
||||
if self.__simulated:
|
||||
return self.__pos
|
||||
|
||||
scs = self.gonget("targetSCS") #targetAEROTECH, #targetOMEGA
|
||||
return SmargonCoordinate(
|
||||
sh_mm=Coordinate(x=scs["SHX"], y=scs["SHY"], z=scs["SHZ"]),
|
||||
phi_deg=scs["PHI"],
|
||||
chi_deg=scs["CHI"],
|
||||
)
|
||||
|
||||
@target.setter
|
||||
def target(self, coord: SmargonCoordinate):
|
||||
if self.__simulated:
|
||||
self.__pos = coord
|
||||
return
|
||||
|
||||
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
|
||||
)
|
||||
if coord.chi_deg is not None:
|
||||
target_string += "&CHI={:.5f}".format(coord.chi_deg)
|
||||
if coord.phi_deg is not None:
|
||||
target_string += "&PHI={:.5f}".format(coord.phi_deg)
|
||||
if target_string:
|
||||
self.gonput(f"targetSCS?{target_string}")
|
||||
|
||||
@property
|
||||
def target_aerotech(self) -> AerotechCoordinate:
|
||||
if self.__simulated:
|
||||
return self.__pos_aero
|
||||
|
||||
acs = self.gonget("targetAEROTECH") #targetAEROTECH, #targetOMEGA
|
||||
return AerotechCoordinate(at_mm = Coordinate(x=acs["GMX"], y = acs["GMY"], z = acs["GMZ"]),
|
||||
omega_deg = acs["GMU"])
|
||||
|
||||
@target_aerotech.setter
|
||||
def target_aerotech(self, coord: AerotechCoordinate):
|
||||
if self.__simulated:
|
||||
self.__pos_aero = coord
|
||||
return
|
||||
|
||||
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
|
||||
)
|
||||
if coord.omega_deg is not None:
|
||||
target_string += "&GMU={:.5f}".format(coord.omega_deg)
|
||||
if target_string:
|
||||
self.gonput(f"targetAEROTECH?{target_string}")
|
||||
|
||||
|
||||
def wait(self, timeout=60.0, tol=0.01, poll_time=0.01):
|
||||
target = self.target
|
||||
timeout = timeout + time()
|
||||
while time() < timeout:
|
||||
if target.eq(self.readback, tol):
|
||||
break
|
||||
if time() > timeout:
|
||||
raise TimeoutError("Timed out waiting for Smargon to reach target")
|
||||
sleep(poll_time)
|
||||
|
||||
def wait_aerotech(self, timeout=60.0, tol=0.01, poll_time=0.01):
|
||||
target = self.target
|
||||
timeout = timeout + time()
|
||||
while time() < timeout:
|
||||
if self.target_aerotech.eq(self.readback_aerotech, tol):
|
||||
break
|
||||
if time() > timeout:
|
||||
raise TimeoutError("Timed out waiting for Aerotech to reach target")
|
||||
sleep(poll_time)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
smargon = Smargon(MXBeamline.X10SA)
|
||||
x = smargon.readback_aerotech
|
||||
print(x)
|
||||
y = smargon.target_aerotech
|
||||
smargon.target_aerotech = AerotechCoordinate(at_mm=Coordinate(x=0, y=0, z=0))
|
||||
print(y)
|
||||
smargon.wait_aerotech()
|
||||
print(f"aerotech reached {smargon.readback_aerotech}")
|
||||
|
||||
Executable
+664
@@ -0,0 +1,664 @@
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
from typing import List
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
|
||||
from aare.common.exception_handler import TellCommunicationError
|
||||
from aare.common.logger_config import setup_logger
|
||||
from aare.common.models import (
|
||||
PuckLoadedInfo,
|
||||
DewarAddress,
|
||||
SampleDewarAddress,
|
||||
)
|
||||
from aareDB import PuckWithTellPosition
|
||||
|
||||
from aare.common.beamline import MXBeamline # noqa: F401
|
||||
from pshell import PShellClient
|
||||
|
||||
logger = setup_logger("aareDAQ")
|
||||
|
||||
class ManualMountException(Exception):
|
||||
"""Custom exception for manual mounting"""
|
||||
pass
|
||||
|
||||
|
||||
class SmartMagnetFaultException(Exception):
|
||||
"""Custom exception for smart magnet fault"""
|
||||
pass
|
||||
|
||||
|
||||
class TellMountFailedException(Exception):
|
||||
"""Custom exception for mount failure"""
|
||||
pass
|
||||
|
||||
|
||||
class TellCommandWhileBusyException(Exception):
|
||||
"""Custom exception for trying to move Tell when it is busy"""
|
||||
pass
|
||||
|
||||
|
||||
class TellConnectionException(Exception):
|
||||
"""Custom exception for connection problems"""
|
||||
pass
|
||||
|
||||
VALID_DEWAR_POSITIONS = [f"{p}{n}" for n in "12345" for p in "ABCDEFX"]
|
||||
|
||||
def is_valid_dewar_position(position):
|
||||
"""check if argument is a valid dewar position"""
|
||||
return position in VALID_DEWAR_POSITIONS
|
||||
|
||||
POSITION_PARK = "pPark"
|
||||
POSITION_COLD = "pCold"
|
||||
POSITION_AUX = "pAux"
|
||||
POSITION_DEWAR = "pDewar"
|
||||
POSITION_HOME = "pHome"
|
||||
POSITION_HEATER = "pHeatB"
|
||||
|
||||
#Nov 26 13:36:00 mx-x06da-queue-01.psi.ch AareDAQ[2944444]: 2025-11-26 13:36:00,388 - aareDAQ - ERROR - Error getting status: ('Connection aborted.', ConnectionResetError(104, 'Connection reset by peer'))
|
||||
|
||||
class TellClient:
|
||||
"""High-level Tell robot API using PShellClient"""
|
||||
def __init__(self, bl: MXBeamline):
|
||||
self.__url = None
|
||||
beamline = bl.value.lower()
|
||||
self.__beamline = bl
|
||||
if bl == MXBeamline.X06DA:
|
||||
self.__url = f"http://{beamline}-tell.psi.ch:22222"
|
||||
|
||||
elif bl == MXBeamline.X10SA:
|
||||
self.__url = f"http://PC17488:22222"
|
||||
|
||||
elif bl == MXBeamline.X06SA:
|
||||
self.__url = f""
|
||||
raise NotImplemented(f"TellClient not implemente for {beamline}")
|
||||
elif bl == MXBeamline.SIMULATED:
|
||||
raise NotImplemented(f"Use SimClient, generate tell client using"
|
||||
f"make_tell_client(beamline)")
|
||||
else:
|
||||
raise ValueError(f"Unknown beamline {beamline}")
|
||||
|
||||
print(f"Connecting TELL p-shell service at {self.__url} ...", end="")
|
||||
hostname = urlparse(self.__url).hostname
|
||||
try:
|
||||
requests.get(f"{self.__url}/history/0", timeout=1.0)
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"...connection to {hostname} failed")
|
||||
raise TellCommunicationError(
|
||||
f"TELL connection failed ({hostname})",
|
||||
base_url=self.__url,
|
||||
endpoint="history/0",
|
||||
operation="GET",
|
||||
) from e
|
||||
except requests.ReadTimeout as e:
|
||||
print(f"...PShell service {hostname} is down")
|
||||
raise TellCommunicationError(
|
||||
f"TELL connection timedout ({hostname})",
|
||||
base_url=self.__url,
|
||||
endpoint="history/0",
|
||||
operation="GET",
|
||||
) from e
|
||||
|
||||
self.pshell = PShellClient(self.__url)
|
||||
|
||||
self._aborted = False
|
||||
self.state = self.get_state()
|
||||
self.debug = False
|
||||
self._last_cmd_id = -1
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
"""returns the configured base url for the Tell robot"""
|
||||
return self.__url
|
||||
|
||||
def get_state(self):
|
||||
"""returns the current state of the robot"""
|
||||
self.state = self.pshell.get_state()
|
||||
return self.state
|
||||
|
||||
def get_result(self, command_id=-1):
|
||||
"""returns the result of the last command issued to the robot"""
|
||||
return self.pshell.get_result(command_id)
|
||||
|
||||
def wait_ready(self, timeout: float = 360.0):
|
||||
"""waits until the robot is ready to accept commands returns None if simulation
|
||||
and raises an exception if the robot is not ready"""
|
||||
self.pshell.wait_state("Ready", timeout=timeout)
|
||||
|
||||
def wait_not_busy(self, timeout: float = 360.0):
|
||||
"""waits until the robot is not busy and returns None if simulation
|
||||
and raises an exception if the robot is busy"""
|
||||
self.pshell.wait_state_not("Busy", timeout=timeout)
|
||||
state = self.get_state()
|
||||
if state != "Ready":
|
||||
if state == "Initializing":
|
||||
raise TellConnectionException("Tell reconnecting")
|
||||
elif state == "Closing":
|
||||
raise TellConnectionException("Tell is disconnecting")
|
||||
raise Exception("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
|
||||
:param value """
|
||||
self.pshell.eval("in_mount_position = " + str(value) + "&")
|
||||
|
||||
def is_in_mount_position(self) -> bool:
|
||||
"""checks to see if the robot is in the mount position and returns a boolean"""
|
||||
return self.pshell.eval("in_mount_position&").lower() == "true"
|
||||
|
||||
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"""
|
||||
|
||||
j = []
|
||||
for x in info:
|
||||
j.append(
|
||||
{
|
||||
"userName": x.pgroup,
|
||||
"dewarName": x.dewar_name or "",
|
||||
"puckName": x.puck_name,
|
||||
"puckType": "Unipuck", # could use x.puck_type
|
||||
"puckAddress": x.tell_position or "",
|
||||
"puckBarcode": x.puck_name,
|
||||
"sampleBarcode": "",
|
||||
"sampleMountCount": 0,
|
||||
"sampleName": "",
|
||||
"samplePosition": 1,
|
||||
"sampleStatus": "",
|
||||
}
|
||||
)
|
||||
|
||||
self.pshell.run("data/set_samples_info", pars=[json.dumps(j)], background=True)
|
||||
# self.pshell.eval("set_samples_info(" + json.dumps(info) + ")&")
|
||||
|
||||
def start_cmd(self, cmd, *argv):
|
||||
"""starts a command on the robot and returns the command id"""
|
||||
cmd = cmd + "("
|
||||
for a in argv:
|
||||
cmd = cmd + (("'" + a + "'") if type(a) is str else str(a)) + ", "
|
||||
cmd = cmd + ")"
|
||||
ret = self.pshell.start_eval(cmd)
|
||||
self.get_state()
|
||||
return ret
|
||||
|
||||
def check_command_ok(self, timeout: float = 360.0, msg: str = ""):
|
||||
"""checks to see if the last command issued to the robot was completed and returns the result
|
||||
Returns an exception if the command result doesnt return completed or removed"""
|
||||
self.wait_not_busy(timeout)
|
||||
result = self.get_result(self._last_cmd_id)
|
||||
logger.debug(f"{msg} {result}")
|
||||
status = result["status"]
|
||||
if "completed" != status: #FIXME this is very limiting and depends on tell reporting statuses
|
||||
if "removed" != status:
|
||||
raise TellMountFailedException(f"{msg} {result}")
|
||||
else:
|
||||
return f"{msg} {result}"
|
||||
|
||||
def estimate_mounting_time(self, segment) -> int:
|
||||
"""Adds additional time if cooling/drying is expected based on requested segment,
|
||||
current sample segment and gripper position.
|
||||
:param segment: any - however valid segment ABCDEFX """
|
||||
try:
|
||||
current_mounted = self.get_mounted_sample()
|
||||
gripper_in_cold = self.is_in_cold()
|
||||
|
||||
if current_mounted is None:
|
||||
unmount_needs_drying = 0 # might not have anything
|
||||
unmount_needs_cooling = 0
|
||||
else:
|
||||
segment_in_cold = current_mounted.puck.segment in "ABCDEF"
|
||||
unmount_needs_drying = int(gripper_in_cold and not segment_in_cold)
|
||||
unmount_needs_cooling = int(not gripper_in_cold and segment_in_cold)
|
||||
|
||||
mount_needs_cooling = int(segment in "ABCDEF" and not gripper_in_cold)
|
||||
mount_needs_drying = int(segment not in "ABCDEF" and gripper_in_cold)
|
||||
|
||||
needs_cooling = mount_needs_cooling + unmount_needs_cooling
|
||||
needs_drying = mount_needs_drying + unmount_needs_drying
|
||||
return needs_cooling * 30 + needs_drying * 120
|
||||
except:
|
||||
return 0
|
||||
|
||||
def mount(
|
||||
self,
|
||||
address: SampleDewarAddress,
|
||||
force: bool = False, # kept for future
|
||||
read_dm: bool = False, # read data matrix
|
||||
auto_unmount: bool = False, # single command, if False it will raise exception
|
||||
wait: bool = False, # blocking operation
|
||||
timeout: float = 600.0,
|
||||
):
|
||||
"""send api request to mount sample from dewer after validating dewer address returns None or repsonse.
|
||||
If the robot is busy, mount will raise an exception.
|
||||
:param address: SampleDewarAddress
|
||||
:param force: bool
|
||||
:param read_dm: bool
|
||||
:param auto_unmount: bool
|
||||
:param wait: bool
|
||||
:param timeout: float
|
||||
"""
|
||||
SampleDewarAddress.model_validate(address)
|
||||
|
||||
segment = address.puck.segment
|
||||
puck = address.puck.pos
|
||||
sample = address.pin
|
||||
|
||||
if self.is_busy():
|
||||
raise TellCommandWhileBusyException("mount received while robot is busy")
|
||||
|
||||
logger.info(f"loading sample {sample} from segment {segment} - {puck}")
|
||||
|
||||
self._last_cmd_id = self.start_cmd(
|
||||
"mount", segment, puck, sample, force, read_dm, auto_unmount
|
||||
)
|
||||
|
||||
wait_timeout = timeout + self.estimate_mounting_time(segment)
|
||||
logger.info("waiting for mount to complete")
|
||||
if wait and segment in "ABCDEF":
|
||||
event, value = self.pshell.wait_events({"state": None, "Motion Task": "dry",
|
||||
"Gripper detection" : None,
|
||||
"Motion Sync": "Robot Clear after mount"}, timeout=wait_timeout)
|
||||
if event is None or event == "state":
|
||||
logger.info(f"event: {event} occurred with value: {value}, checking command completed okay")
|
||||
self.check_command_ok(
|
||||
timeout=wait_timeout, msg=f"Mount {segment}{puck}-{sample}: "
|
||||
)
|
||||
return value
|
||||
elif event == "Gripper detection" and value == "No Pin in Gripper":
|
||||
logger.info(f"gripper detection: {event} occurred with value: {value}")
|
||||
return value
|
||||
elif event == "Gripper detection" and value == "Pin still in Gripper":
|
||||
logger.info(f"gripper detection: {event} occurred with value: {value}")
|
||||
return value
|
||||
elif event == "Gripper detection" and value == "Pin is lost":
|
||||
logger.info(f"gripper detection: {event} occurred with value: {value}")
|
||||
return value
|
||||
elif event == "Motion Task" and value == "dry":
|
||||
logger.info(f"event: {event} occurred with value: {value}")
|
||||
logger.info(" Drying occurring, releasing interface to user")
|
||||
return value
|
||||
elif event == "Motion Sync" and value == "Robot Clear after mount":
|
||||
logger.info(f"event: {event} occurred with value: {value}")
|
||||
logger.info(" Mounting complete, releasing interface to user")
|
||||
return value
|
||||
else:
|
||||
logger.info(f"Unexpected event: {event} occurred with value: {value}")
|
||||
logger.info("Checking command completed okay anyway")
|
||||
self.check_command_ok(
|
||||
timeout=wait_timeout, msg=f"Mount {segment}{puck}-{sample}: "
|
||||
)
|
||||
elif wait and segment == "X":
|
||||
logger.info("Loading an auxiliary puck")
|
||||
self.check_command_ok(
|
||||
timeout=wait_timeout, msg=f"Mount {segment}{puck}-{sample}: "
|
||||
)
|
||||
logger.info("post waiting")
|
||||
return None
|
||||
|
||||
def unmount(self, force=False, wait=False, timeout=360.0):
|
||||
"""send api request to unmount sample from dewer returns None or repsonse.
|
||||
:param force: bool Force has a meaning, will unmount even if smart magnet is not detecting sample
|
||||
:param wait: bool If true will wait until unmount is completed
|
||||
:timeout: float"""
|
||||
|
||||
if self.is_busy():
|
||||
raise TellCommandWhileBusyException("mount received while robot is busy")
|
||||
|
||||
self._last_cmd_id = self.start_cmd("unmount", None, None, None, force)
|
||||
|
||||
if wait:
|
||||
self.check_command_ok(timeout=timeout, msg="Unmount message: ")
|
||||
|
||||
return self._last_cmd_id
|
||||
|
||||
def dry(self, heat_time=None, speed=None, wait_cold=None, wait=False):
|
||||
"""send api request to dry tell gripper.
|
||||
:param: heat_time float if None Tell will use default for drying time
|
||||
:param: speed float if None Tell will use default for drying speed
|
||||
:param: wait_cold bool if -1 to go to park after dry. if None Tell will use default time to wait_cold.
|
||||
:param wait: bool If true will wait until drying is completed
|
||||
"""
|
||||
self.pshell.wait_state("Ready", timeout=30.0)
|
||||
self._last_cmd_id = self.start_cmd("dry", heat_time, speed, wait_cold)
|
||||
if wait:
|
||||
self.check_command_ok(timeout=360.0, msg=f"Dry failed")
|
||||
|
||||
def move_park(self, wait=False):
|
||||
"""send api request to move robot to park position"""
|
||||
self._last_cmd_id = self.start_cmd("move_park")
|
||||
|
||||
if wait:
|
||||
self.check_command_ok(timeout=360.0, msg=f"Move to park failed")
|
||||
|
||||
def move_cold(self, reset_timestamp=False, wait=False):
|
||||
"""send api request to move robot to cold position"""
|
||||
self._last_cmd_id = self.start_cmd("move_cold", reset_timestamp)
|
||||
|
||||
if wait:
|
||||
self.check_command_ok(timeout=360.0, msg=f"Move to cold failed")
|
||||
|
||||
def abort_cmd(self):
|
||||
"""sends an abort pshell requesst and a robot stop task command"""
|
||||
self.pshell.abort()
|
||||
self.pshell.eval("robot.stop_task()&")
|
||||
|
||||
def set_setting(self, key: str, value: str):
|
||||
"""wrapper for pshell eval set_setting command
|
||||
:param key str, name of a setting in tell
|
||||
:param value str, the new value of the setting as a string"""
|
||||
self.pshell.eval(f"set_setting('{key}', '{value}')&")
|
||||
|
||||
def get_setting(self, key: str) -> str:
|
||||
"""wrapper for pshell eval get_setting command, returns the current value for key as a string
|
||||
:param key str, name of a setting in tell"""
|
||||
return self.pshell.eval(f"get_setting('{key}')&")
|
||||
|
||||
def get_mounted_sample(self) -> SampleDewarAddress | None:
|
||||
"""get the current mounted sample and return a SampleDewarAddress object or None if no sample is mounted"""
|
||||
ret = self.get_setting('mounted_sample_position').strip()
|
||||
if not ret or len(ret) == 0:
|
||||
return None
|
||||
|
||||
match = re.match(r"([A-Z])(\d)(\d{1,2})", ret)
|
||||
|
||||
if match:
|
||||
segment, puck, sample = match.groups()
|
||||
dewar_location = DewarAddress(segment=segment, pos=int(puck))
|
||||
return SampleDewarAddress(puck=dewar_location, pin=int(sample))
|
||||
else:
|
||||
logger.warning(f"Failed to decode mounted sample position: {ret}")
|
||||
return None
|
||||
|
||||
def get_system_check(self):
|
||||
"""returns the current system check status"""
|
||||
return self.pshell.eval("system_check_msg()&")
|
||||
|
||||
def get_robot_state(self):
|
||||
"""returns the current robot state"""
|
||||
return self.pshell.eval("robot.state&")
|
||||
|
||||
def get_robot_status(self):
|
||||
"""returns the current robot status"""
|
||||
status = self.pshell.eval("robot.take()&")
|
||||
return eval(status) # FIXME ALL functions must return a valid JSON object
|
||||
|
||||
def get_detected_pucks(self) -> List[PuckLoadedInfo]:
|
||||
"""returns a list of detected pucks as PuckLoadedInfo objects"""
|
||||
j = json.loads(self.pshell.eval("get_pucks_info()&"))
|
||||
|
||||
output = []
|
||||
|
||||
for i in j:
|
||||
if i["puckState"] == "Present":
|
||||
puck_address = i["puckAddress"]
|
||||
if len(puck_address) == 2:
|
||||
output.append(
|
||||
PuckLoadedInfo(
|
||||
puck_name=i["puckBarcode"],
|
||||
location=DewarAddress(
|
||||
segment=puck_address[0], pos=int(puck_address[1])
|
||||
),
|
||||
),
|
||||
)
|
||||
return output
|
||||
|
||||
def get_pin_offset(self):
|
||||
"""get the pin offset for the smart magnet, returns offset as a float"""
|
||||
try:
|
||||
offset = float(self.pshell.eval("get_pin_offset()&"))
|
||||
except Exception:
|
||||
offset = 0.0
|
||||
return offset
|
||||
|
||||
def get_current(self):
|
||||
"""get the current drawn by the smart magnet, returns current as a float in mA"""
|
||||
current = self.pshell.eval("smart_magnet.get_current_rb()&")
|
||||
return float(current)
|
||||
|
||||
def set_current(self, current: float) -> float:
|
||||
"""set the current drawn by the smart magnet, returns current as a float in mA"""
|
||||
self.pshell.eval("smart_magnet.set_current({:.1f})&".format(current))
|
||||
current = self.pshell.eval("smart_magnet.get_current_rb()&")
|
||||
return float(current)
|
||||
|
||||
def is_powered(self):
|
||||
"""returns True if the robot is powered on"""
|
||||
return self.get_robot_status()["powered"]
|
||||
|
||||
def check_enable_motion(self):
|
||||
"""check if the robot is powered on and enable motion if not"""
|
||||
if not self.is_powered():
|
||||
self.pshell.eval("enable_motion()&")
|
||||
|
||||
def is_in_cold(self):
|
||||
"""Compare current robot position to the set cold position. Returns True if in cold position, False otherwise."""
|
||||
return self.is_position(POSITION_COLD)
|
||||
|
||||
def is_position(self, position: str) -> bool:
|
||||
"""Compare current robot position to a given position. Returns True if in position, False otherwise."""
|
||||
return position == self.get_robot_status()["pos"]
|
||||
|
||||
def is_ready(self):
|
||||
"""returns True if the robot is ready to receive commands"""
|
||||
return "ready" == self.get_state().lower()
|
||||
|
||||
def is_busy(self):
|
||||
"""returns True if the robot is busy"""
|
||||
return "busy" == self.get_state().lower()
|
||||
|
||||
def check_smart_magnet_mounted(self, timeout: float = 10.0, idle_time: float = 1.0, interval: float = 0.1):
|
||||
"""Reads smart_magent state and tries to infer if a sample is present
|
||||
Handles: PAUSED, Fault, Busy and Ready states.
|
||||
Raises a ManualMountException is the amgnet indicates a sample is present but get_mounted_sample is None.
|
||||
Raises a SmartMagnetFaultException if the magnet detects no sample but the robot thinks a sample is mounted"""
|
||||
#TODO tidy up
|
||||
initial_state = self.pshell.eval("smart_magnet.state&")
|
||||
|
||||
logger.debug(f"checking smart magnet_initial state: {initial_state}")
|
||||
|
||||
if initial_state == "Paused":
|
||||
self.pshell.eval("smart_magnet.set_supress(False)&")
|
||||
self.pshell.eval("smart_magnet.set_resting_current()&")
|
||||
|
||||
elif initial_state == "Fault":
|
||||
logger.error(f"tell smart magnet is in unknown state {initial_state}")
|
||||
raise SmartMagnetFaultException
|
||||
|
||||
state = self.pshell.eval("smart_magnet.state&")
|
||||
|
||||
try:
|
||||
if state == "Busy":
|
||||
logger.debug('state busy')
|
||||
self.pshell.eval("smart_magnet.set_supress(True)&")
|
||||
self.pshell.eval("smart_magnet.state&")
|
||||
sample_present = True
|
||||
if self.get_mounted_sample() is None:
|
||||
logger.warning("Check mount: A manually mounted sample is detected.")
|
||||
logger.warning("Remove before mounting with the robot.")
|
||||
raise ManualMountException
|
||||
return True
|
||||
elif state == "Ready":
|
||||
logger.debug('No sample detected, ready to mount')
|
||||
sample_present = False
|
||||
if self.get_mounted_sample():
|
||||
logger.error("Check mount: No sample detected, but robot thinks is mounted")
|
||||
raise SmartMagnetFaultException
|
||||
return False
|
||||
elif state == "Paused":
|
||||
logger.debug("Smart magnet detection is paused")
|
||||
return None
|
||||
else:
|
||||
self.pshell.eval("smart_magnet.set_supress(True)&")
|
||||
logger.error(f"Tell smart magnet is in unknown state {state}")
|
||||
raise SmartMagnetFaultException
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"check_smart_magnet_mounted failed: {e}")
|
||||
raise e
|
||||
|
||||
class SimTellClient:
|
||||
"""
|
||||
Simulation-only Tell client.
|
||||
|
||||
Keeps behavior deterministic-ish and stateful without needing PShellClient.
|
||||
Implement more methods as your callers need them.
|
||||
"""
|
||||
def __init__(self):
|
||||
self._state = "Ready"
|
||||
self._last_cmd_id = 1000
|
||||
self._mounted_sample: str = ""
|
||||
self._simulated_samples_info = {}
|
||||
self._simulated_detected_pucks = []
|
||||
self._simulated_current = 30.0
|
||||
self._simulated_suppress = True
|
||||
self._simulated_offset = 0.0
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return None
|
||||
|
||||
def _next_cmd_id(self) -> int:
|
||||
self._last_cmd_id += 1
|
||||
return self._last_cmd_id
|
||||
|
||||
def get_state(self) -> str:
|
||||
return self._state
|
||||
|
||||
def is_ready(self) -> bool:
|
||||
return self._state.lower() == "ready"
|
||||
|
||||
def is_busy(self) -> bool:
|
||||
return self._state.lower() == "busy"
|
||||
|
||||
def wait_ready(self, timeout: float = 360.0):
|
||||
# Keep it simple: flip to Ready quickly.
|
||||
time.sleep(0.05)
|
||||
self._state = "Ready"
|
||||
|
||||
def mount(
|
||||
self,
|
||||
address: SampleDewarAddress,
|
||||
force: bool = False,
|
||||
read_dm: bool = False,
|
||||
auto_unmount: bool = False,
|
||||
wait: bool = False,
|
||||
timeout: float = 600.0,
|
||||
):
|
||||
SampleDewarAddress.model_validate(address)
|
||||
if self.is_busy():
|
||||
raise TellCommandWhileBusyException("mount received while robot is busy")
|
||||
|
||||
cmd_id = self._next_cmd_id()
|
||||
self._state = "Busy"
|
||||
|
||||
segment = address.puck.segment
|
||||
puck = address.puck.pos
|
||||
sample = address.pin
|
||||
self._mounted_sample = f"{segment}{puck}{sample}"
|
||||
|
||||
if wait:
|
||||
self.wait_ready(timeout=timeout)
|
||||
else:
|
||||
# quickly become ready anyway, but asynchronously-ish
|
||||
time.sleep(0.01)
|
||||
self._state = "Ready"
|
||||
|
||||
return cmd_id
|
||||
|
||||
def unmount(self, force: bool = False, wait: bool = False, timeout: float = 360.0):
|
||||
if self.is_busy():
|
||||
raise TellCommandWhileBusyException("unmount received while robot is busy")
|
||||
|
||||
cmd_id = self._next_cmd_id()
|
||||
self._state = "Busy"
|
||||
self._mounted_sample = ""
|
||||
if wait:
|
||||
self.wait_ready(timeout=timeout)
|
||||
else:
|
||||
time.sleep(0.01)
|
||||
self._state = "Ready"
|
||||
return cmd_id
|
||||
|
||||
def get_mounted_sample(self) -> SampleDewarAddress | None:
|
||||
ret = self._mounted_sample
|
||||
if not ret:
|
||||
return None
|
||||
match = re.match(r"([A-Z])(\d)(\d{1,2})", ret)
|
||||
if not match:
|
||||
return None
|
||||
segment, puck, sample = match.groups()
|
||||
return SampleDewarAddress(puck=DewarAddress(segment=segment, pos=int(puck)), pin=int(sample))
|
||||
|
||||
class TellClientProxy:
|
||||
"""
|
||||
Lazy-connecting Tell client proxy that retries periodically.
|
||||
- Server can start even if TELL is down.
|
||||
- First use triggers connect; failures raise TellCommunicationError.
|
||||
"""
|
||||
def __init__(self, bl: MXBeamline, *, retry_interval_s: float = 2.0):
|
||||
self._bl = bl
|
||||
self._client: TellClient | None = None
|
||||
self._retry_interval_s = float(retry_interval_s)
|
||||
self._last_attempt_ts = 0.0
|
||||
self._last_error: Exception | None = None
|
||||
|
||||
def _get_client(self) -> TellClient:
|
||||
if self._client is not None:
|
||||
return self._client
|
||||
|
||||
now = time.monotonic()
|
||||
if now - self._last_attempt_ts < self._retry_interval_s and self._last_error is not None:
|
||||
raise self._last_error
|
||||
|
||||
self._last_attempt_ts = now
|
||||
try:
|
||||
self._client = TellClient(self._bl)
|
||||
self._last_error = None
|
||||
return self._client
|
||||
except TellCommunicationError as e:
|
||||
self._last_error = e
|
||||
raise
|
||||
except Exception as e:
|
||||
wrapped = TellCommunicationError(
|
||||
"TELL connection failed",
|
||||
operation="CONNECT",
|
||||
)
|
||||
self._last_error = wrapped
|
||||
raise wrapped from e
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self._get_client().url
|
||||
|
||||
# Delegate methods used by DAQ; add more as needed
|
||||
def get_mounted_sample(self) -> SampleDewarAddress | None:
|
||||
return self._get_client().get_mounted_sample()
|
||||
|
||||
def get_state(self):
|
||||
return self._get_client().get_state()
|
||||
|
||||
def wait_not_busy(self, timeout: float = 360.0):
|
||||
return self._get_client().wait_not_busy(timeout=timeout)
|
||||
|
||||
def check_enable_motion(self):
|
||||
return self._get_client().check_enable_motion()
|
||||
|
||||
def set_in_mount_position(self, value):
|
||||
return self._get_client().set_in_mount_position(value)
|
||||
|
||||
def mount(self, *args, **kwargs):
|
||||
return self._get_client().mount(*args, **kwargs)
|
||||
|
||||
def unmount(self, *args, **kwargs):
|
||||
return self._get_client().unmount(*args, **kwargs)
|
||||
|
||||
def abort_cmd(self):
|
||||
return self._get_client().abort_cmd()
|
||||
|
||||
def make_tell_client(bl: MXBeamline) -> TellClient | SimTellClient | TellClientProxy:
|
||||
if bl == MXBeamline.SIMULATED:
|
||||
return SimTellClient()
|
||||
return TellClientProxy(bl, retry_interval_s=2.0)
|
||||
@@ -0,0 +1,65 @@
|
||||
import os
|
||||
import jwt
|
||||
import requests
|
||||
|
||||
from aare.common.models import TokenData
|
||||
|
||||
from aare.common.logger_config import setup_logger
|
||||
|
||||
logger = setup_logger('aareGUI')
|
||||
|
||||
def auth(base_url: str | None) -> str:
|
||||
curr_user = os.getlogin()
|
||||
if base_url is None:
|
||||
token_data = TokenData(sub=curr_user,
|
||||
staff=True,
|
||||
session=15,
|
||||
pgroups=["p16371", "p22233"])
|
||||
return jwt.encode(token_data.model_dump(), "ABC123")
|
||||
|
||||
url = f"{base_url}/token"
|
||||
try:
|
||||
response = requests.post(
|
||||
url,
|
||||
data={
|
||||
"username": curr_user,
|
||||
"password": ""
|
||||
},
|
||||
headers={
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
},
|
||||
timeout=(2.0, 5.0), # (connect timeout, read timeout)
|
||||
)
|
||||
except requests.RequestException as e:
|
||||
logger.error(f"Authentication request failed (network): {e}")
|
||||
raise RuntimeError(
|
||||
"Cannot reach AareDAQ server (network error). "
|
||||
"Please check the server is running and your connection."
|
||||
) from e
|
||||
|
||||
if response.status_code != 200:
|
||||
# Avoid dumping full HTML/tracebacks into the GUI; keep it short and actionable
|
||||
logger.error(f"Authentication request failed: HTTP {response.status_code}. Body: {response.text[:500]}")
|
||||
raise RuntimeError(
|
||||
f"Authentication failed (HTTP {response.status_code}). "
|
||||
"The server may be starting up or unavailable."
|
||||
)
|
||||
|
||||
try:
|
||||
response_json = response.json()
|
||||
except ValueError as e:
|
||||
logger.error(f"Authentication response was not JSON. Body: {response.text[:500]}")
|
||||
raise RuntimeError(
|
||||
"Authentication failed (invalid server response). "
|
||||
"The server may be starting up or misconfigured."
|
||||
) from e
|
||||
|
||||
token = response_json.get("access_token")
|
||||
if not token or not isinstance(token, str):
|
||||
logger.error(f"Authentication response missing access_token. Keys: {list(response_json.keys())}")
|
||||
raise RuntimeError(
|
||||
"Authentication failed (missing token in server response). "
|
||||
"The server may be starting up."
|
||||
)
|
||||
|
||||
return token
|
||||
@@ -0,0 +1,670 @@
|
||||
import time
|
||||
from typing import Callable, Iterable
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from aare.common.autofocus_tools import focus_measure_edges
|
||||
from aare.common.beamline import mx_beamline
|
||||
from aare.common.coordinate import Coordinate, SmargonCoordinate
|
||||
from aare.common.models import AutofocusSettings
|
||||
from aare.daq.config import BeamlineConfig
|
||||
from aare.daq.daq import AareDAQ
|
||||
from aare.daq.devices import BeamlineDevices
|
||||
|
||||
def make_circular_mask(shape_hw: tuple[int, int], center_x: float, center_y: float, radius: float) -> np.ndarray:
|
||||
h, w = int(shape_hw[0]), int(shape_hw[1])
|
||||
y, x = np.ogrid[:h, :w]
|
||||
return (x - float(center_x)) ** 2 + (y - float(center_y)) ** 2 <= float(radius) ** 2
|
||||
|
||||
def _parabola_vertex(x1, y1, x2, y2, x3, y3) -> float | None:
|
||||
# Fit parabola through 3 points; return vertex x if it's a maximum.
|
||||
denom = (x1 - x2) * (x1 - x3) * (x2 - x3)
|
||||
if abs(denom) < 1e-15:
|
||||
return None
|
||||
a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / denom
|
||||
b = (x3**2 * (y1 - y2) + x2**2 * (y3 - y1) + x1**2 * (y2 - y3)) / denom
|
||||
if a >= 0:
|
||||
return None
|
||||
return float(-b / (2 * a))
|
||||
|
||||
class AutofocusController:
|
||||
"""
|
||||
Fast autofocus: bracket -> ternary -> optional parabola.
|
||||
|
||||
You inject:
|
||||
- get_gray_image(): np.ndarray (2D)
|
||||
- get_frame_id(): int (UniqueId) OR None
|
||||
- move_to(z): move stage to requested z (units are up to you)
|
||||
- wait_for_stop(): block until motion ends
|
||||
|
||||
The key speed/robustness trick is waiting for a *new frame id* after motion.
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
get_gray_image,
|
||||
focus_measure,
|
||||
move_to,
|
||||
wait_for_stop,
|
||||
get_frame_id=None,
|
||||
fps: float = 25.0,
|
||||
):
|
||||
self.get_gray_image = get_gray_image
|
||||
self.focus_measure = focus_measure
|
||||
self.move_to = move_to
|
||||
self.wait_for_stop = wait_for_stop
|
||||
self.get_frame_id = get_frame_id
|
||||
self.fps = float(fps)
|
||||
|
||||
self._uid_stuck_count = 0
|
||||
self._uid_stuck_disable_after = 3
|
||||
|
||||
def _wait_new_frames(self, frames: int = 1, timeout_s: float = 0.12) -> bool:
|
||||
"""
|
||||
Wait for new frames by UniqueId.
|
||||
If uid appears stuck (common in standalone tests if acquisition isn't running),
|
||||
quickly fall back to a short sleep so autofocus stays fast.
|
||||
"""
|
||||
if self.get_frame_id is None or self._uid_stuck_count >= self._uid_stuck_disable_after:
|
||||
time.sleep(max(0.0, float(frames)) / max(1e-6, self.fps))
|
||||
return True
|
||||
start = int(self.get_frame_id())
|
||||
print(f"Waiting for {frames} frames (uid={start})...")
|
||||
target = start + int(frames)
|
||||
deadline = time.perf_counter() + float(timeout_s)
|
||||
|
||||
while time.perf_counter() < deadline:
|
||||
if int(self.get_frame_id()) >= target:
|
||||
self._uid_stuck_count = 0
|
||||
print(f"Acquired {frames} frames (uid={target}, start={start})")
|
||||
return True
|
||||
time.sleep(0.001)
|
||||
|
||||
# uid didn't advance in time -> count as "stuck" and fall back
|
||||
print(f"UID stuck for {timeout_s} s, falling back to sleep...")
|
||||
self._uid_stuck_count += 1
|
||||
time.sleep(1.0 / max(1e-6, self.fps))
|
||||
return False
|
||||
|
||||
def _score_at(self, z, mask: np.ndarray | None, robust_frames: int) -> float:
|
||||
st = time.perf_counter()
|
||||
self.move_to(z)
|
||||
print(f"move_to command to z={z:.2f} (t={time.perf_counter() - st:.5f} s)")
|
||||
st = time.perf_counter()
|
||||
self.wait_for_stop()
|
||||
print(f"wait_for_stop command (t={time.perf_counter() - st:.5f} s)")
|
||||
|
||||
# Ensure next image is not a stale buffer
|
||||
print("Waiting for new frame...")
|
||||
st = time.perf_counter()
|
||||
self._wait_new_frames(frames=1, timeout_s=0.4)
|
||||
print(f"Acquired new frame (t={time.perf_counter() - st:.5f} s)")
|
||||
st = time.perf_counter()
|
||||
if robust_frames <= 1:
|
||||
gray = self.get_gray_image()
|
||||
print(f"got grey image (t={time.perf_counter() - st:.5f} s)")
|
||||
return float(self.focus_measure(gray, mask))
|
||||
|
||||
vals: list[float] = []
|
||||
for _ in range(int(robust_frames)):
|
||||
gray = self.get_gray_image()
|
||||
vals.append(float(self.focus_measure(gray, mask)))
|
||||
self._wait_new_frames(frames=1, timeout_s=0.4)
|
||||
print(f'Got values after {time.perf_counter() - st:.5f} s: {vals}')
|
||||
return float(np.median(np.asarray(vals, dtype=np.float64)))
|
||||
|
||||
def run_once(
|
||||
self,
|
||||
*,
|
||||
z0: float,
|
||||
z_range: float,
|
||||
mask: np.ndarray | None = None,
|
||||
robust_frames: int = 1,
|
||||
ternary_iters: int = 4,
|
||||
do_parabola: bool = True,
|
||||
edge_stop: bool = True,
|
||||
flat_rel_tol: float = 0.03,
|
||||
) -> tuple[float, float]:
|
||||
"""
|
||||
Returns (best_z, best_focus).
|
||||
|
||||
edge_stop:
|
||||
If True and the best bracket point is at ±0.5*z_range, stop early.
|
||||
(Means the peak is likely outside the search window.)
|
||||
|
||||
flat_rel_tol:
|
||||
If (max-min)/max is below this, treat focus curve as flat and stop early.
|
||||
"""
|
||||
R = float(z_range)
|
||||
|
||||
# 1) 5-point bracket
|
||||
zs = np.array(
|
||||
[z0 - 0.5 * R, z0 - 0.25 * R, z0, z0 + 0.25 * R, z0 + 0.5 * R],
|
||||
dtype=np.float64,
|
||||
)
|
||||
fs = np.array([self._score_at(float(z), mask, robust_frames) for z in zs], dtype=np.float64)
|
||||
f0 = float(fs[2]) # z0
|
||||
f_max = float(fs.max())
|
||||
if f0 > 0 and (f_max / f0) < 1.05: # <5% improvement available
|
||||
return float(zs[2]), f0
|
||||
|
||||
best_i = int(np.argmax(fs))
|
||||
z_best = float(zs[best_i])
|
||||
f_best = float(fs[best_i])
|
||||
|
||||
# Early exit if the curve is basically flat (no meaningful improvement)
|
||||
f_min = float(fs.min())
|
||||
if f_max > 0 and (f_max - f_min) / f_max < float(flat_rel_tol):
|
||||
return z_best, f_best
|
||||
|
||||
# Early exit if best is at range edge: bracket does not contain a maximum
|
||||
if edge_stop and (best_i == 0 or best_i == len(zs) - 1):
|
||||
return z_best, f_best
|
||||
|
||||
# Local bracket for ternary search
|
||||
iL = max(0, best_i - 1)
|
||||
iR = min(len(zs) - 1, best_i + 1)
|
||||
zL, zR = float(zs[iL]), float(zs[iR])
|
||||
|
||||
sampled: dict[float, float] = {float(zs[i]): float(fs[i]) for i in range(len(zs))}
|
||||
|
||||
if zL == zR:
|
||||
return z_best, f_best
|
||||
|
||||
# 2) ternary search in local bracket (assumes unimodal-ish)
|
||||
for _ in range(int(ternary_iters)):
|
||||
a, b = (zL, zR) if zL < zR else (zR, zL)
|
||||
z1 = a + (b - a) / 3.0
|
||||
z2 = b - (b - a) / 3.0
|
||||
|
||||
if z1 not in sampled:
|
||||
sampled[z1] = self._score_at(float(z1), mask, robust_frames)
|
||||
if z2 not in sampled:
|
||||
sampled[z2] = self._score_at(float(z2), mask, robust_frames)
|
||||
|
||||
if sampled[z1] < sampled[z2]:
|
||||
zL = z1
|
||||
else:
|
||||
zR = z2
|
||||
|
||||
# 3) optional 3-point parabola around current best sample
|
||||
if do_parabola and len(sampled) >= 3:
|
||||
items = sorted(sampled.items(), key=lambda t: t[0])
|
||||
zz = np.array([p[0] for p in items], dtype=np.float64)
|
||||
ff = np.array([p[1] for p in items], dtype=np.float64)
|
||||
k = int(np.argmax(ff))
|
||||
|
||||
if 0 < k < len(zz) - 1:
|
||||
zv = _parabola_vertex(
|
||||
float(zz[k - 1]), float(ff[k - 1]),
|
||||
float(zz[k]), float(ff[k]),
|
||||
float(zz[k + 1]), float(ff[k + 1]),
|
||||
)
|
||||
if zv is not None and float(zz[k - 1]) <= zv <= float(zz[k + 1]):
|
||||
if zv not in sampled:
|
||||
sampled[zv] = self._score_at(float(zv), mask, robust_frames)
|
||||
|
||||
z_best, f_best = max(sampled.items(), key=lambda t: t[1])
|
||||
return float(z_best), float(f_best)
|
||||
|
||||
def __auto_focus(settings: AutofocusSettings) -> float:
|
||||
"""
|
||||
Fast autofocus on Smargon Z:
|
||||
- bracket (5 points)
|
||||
- ternary search (few iters)
|
||||
- optional parabola refine
|
||||
|
||||
Returns:
|
||||
Best Z offset in mm (beamline Z delta) relative to the starting position.
|
||||
"""
|
||||
geom = daq.sample_geometry
|
||||
start_smargon = devs.smargon_pos
|
||||
|
||||
# ROI center: use provided, else use beam location (beam mark)
|
||||
center_x = float(settings.center_x_pxl) if settings.center_x_pxl is not None else float(geom.beam_location_pxl.x)
|
||||
center_y = float(settings.center_y_pxl) if settings.center_y_pxl is not None else float(geom.beam_location_pxl.y)
|
||||
radius_pxl = float(settings.radius_pxl)
|
||||
|
||||
z_range_mm = float(settings.z_range_um) / 1000.0
|
||||
z_steps = int(settings.z_steps)
|
||||
|
||||
# Build mask once (needs image shape)
|
||||
first = daq.camera_image_gray
|
||||
if first is None:
|
||||
raise RuntimeError("Autofocus: no camera image available.")
|
||||
if first.ndim != 2:
|
||||
raise RuntimeError("Autofocus: expected grayscale image (2D).")
|
||||
|
||||
#mask = make_circular_mask(first.shape[:2], center_x=center_x, center_y=center_y, radius=radius_pxl)
|
||||
|
||||
height, width = first.shape
|
||||
y, x = np.ogrid[:height, :width]
|
||||
mask = (x - center_x) ** 2 + (y - center_y) ** 2 <= radius_pxl ** 2
|
||||
|
||||
def move_to_delta_z_mm(dz_mm: float) -> None:
|
||||
# Apply relative motion in *beamline Z* via the geometry transform
|
||||
sh_new = start_smargon.sh_mm + geom.smargon_nudge(Coordinate(z=float(dz_mm)))
|
||||
target = SmargonCoordinate(
|
||||
sh_mm=sh_new,
|
||||
phi_deg=start_smargon.phi_deg,
|
||||
chi_deg=start_smargon.chi_deg,
|
||||
)
|
||||
devs.smargon_pos = target
|
||||
|
||||
def wait_for_stop() -> None:
|
||||
devs.smargon_wait(timeout=30)
|
||||
|
||||
def get_gray() -> np.ndarray:
|
||||
img = daq.camera_image_gray
|
||||
if img is None:
|
||||
raise RuntimeError("Autofocus: failed to acquire image.")
|
||||
return img
|
||||
|
||||
ctrl = StepwiseAutofocus(
|
||||
get_gray_image=get_gray,
|
||||
focus_measure=focus_measure_edges,
|
||||
move_to=move_to_delta_z_mm,
|
||||
wait_for_stop=wait_for_stop,
|
||||
get_frame_id=devs.samcam_frame_id,
|
||||
fps=25.0,
|
||||
)
|
||||
|
||||
# Robustness vs speed:
|
||||
# - 1 is fastest
|
||||
# - 2 is more stable (median of 2 frames) and often still < 1 s total
|
||||
robust_frames = 1
|
||||
|
||||
best_dz, best_f, zs, fs = ctrl.run(z0=0.0, z_range=z_range_mm, z_steps=z_steps, mask=mask,
|
||||
refine=False, include_baseline=False)
|
||||
|
||||
# Move to the best position (controller ends at last probed z; ensure final is best)
|
||||
move_to_delta_z_mm(best_dz)
|
||||
wait_for_stop()
|
||||
|
||||
print(
|
||||
f"Autofocus complete: best_dz={best_dz * 1000.0:.1f} um, focus={best_f:.2f}, "
|
||||
f"roi_center=({center_x:.1f},{center_y:.1f}), r={radius_pxl:.1f}px"
|
||||
)
|
||||
|
||||
return float(best_dz)
|
||||
|
||||
# def auto_focus(self, settings: AutofocusSettings) -> float:
|
||||
# """
|
||||
# Public autofocus method. Only allowed in SampleAlignment state.
|
||||
# Returns best Z offset in mm (beamline Z delta) relative to start.
|
||||
# """
|
||||
# self.__cfg.set_busy(BeamlineStateEnum.SampleAlignment)
|
||||
# try:
|
||||
# best_dz_mm = self.__auto_focus(settings)
|
||||
# self.__cfg.state_busy = False
|
||||
# return best_dz_mm
|
||||
# except Exception as e:
|
||||
# logger.error(f"Autofocus failed: {e}")
|
||||
# self.__cfg.state_busy = False
|
||||
# raise
|
||||
class StepwiseAutofocus:
|
||||
def __init__(self, *, get_gray_image, focus_measure, move_to, wait_for_stop, get_frame_id=None, fps=25.0):
|
||||
self.get_gray_image = get_gray_image
|
||||
self.focus_measure = focus_measure
|
||||
self.move_to = move_to
|
||||
self.wait_for_stop = wait_for_stop
|
||||
self.get_frame_id = get_frame_id
|
||||
self.fps = float(fps)
|
||||
|
||||
def _wait_new_frame(self, timeout_s: float = 0.25) -> None:
|
||||
if self.get_frame_id is None:
|
||||
time.sleep(1.0 / max(1e-6, self.fps))
|
||||
return
|
||||
start = int(self.get_frame_id())
|
||||
deadline = time.perf_counter() + float(timeout_s)
|
||||
while time.perf_counter() < deadline:
|
||||
if int(self.get_frame_id()) > start:
|
||||
return
|
||||
time.sleep(0.001)
|
||||
# fallback: don't hang
|
||||
time.sleep(1.0 / max(1e-6, self.fps))
|
||||
|
||||
def score_at(self, z: float, mask=None) -> float:
|
||||
self.move_to(float(z))
|
||||
self.wait_for_stop()
|
||||
self._wait_new_frame(timeout_s=0.25)
|
||||
gray = self.get_gray_image()
|
||||
return float(self.focus_measure(gray, mask))
|
||||
|
||||
def run(
|
||||
self,
|
||||
*,
|
||||
z0: float,
|
||||
z_range: float,
|
||||
z_steps: int,
|
||||
mask=None,
|
||||
refine: bool = False,
|
||||
include_baseline: bool = False,
|
||||
) -> tuple[float, float, np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Returns:
|
||||
(best_z, best_focus, z_positions, focus_values)
|
||||
|
||||
If include_baseline=False and refine=False, this will evaluate focus exactly `z_steps` times.
|
||||
"""
|
||||
z_steps = int(z_steps)
|
||||
if z_steps < 3:
|
||||
raise ValueError("z_steps must be >= 3 for a meaningful scan.")
|
||||
|
||||
if include_baseline:
|
||||
_ = self.score_at(float(z0), mask=mask)
|
||||
|
||||
zs = np.linspace(z0 - 0.5 * float(z_range), z0 + 0.5 * float(z_range), z_steps, dtype=np.float64)
|
||||
fs = np.empty_like(zs)
|
||||
|
||||
for i, z in enumerate(zs):
|
||||
fs[i] = self.score_at(float(z), mask=mask)
|
||||
|
||||
best_i = int(np.argmax(fs))
|
||||
best_z = float(zs[best_i])
|
||||
best_f = float(fs[best_i])
|
||||
|
||||
if refine and 0 < best_i < (len(zs) - 1):
|
||||
dz = float(zs[best_i + 1] - zs[best_i])
|
||||
z_candidates = np.array([best_z - dz, best_z, best_z + dz], dtype=np.float64)
|
||||
f_candidates = np.array([self.score_at(float(zc), mask=mask) for zc in z_candidates], dtype=np.float64)
|
||||
j = int(np.argmax(f_candidates))
|
||||
best_z = float(z_candidates[j])
|
||||
best_f = float(f_candidates[j])
|
||||
|
||||
return best_z, best_f, zs, fs
|
||||
|
||||
def __auto_focus_with_aerotech(settings: AutofocusSettings) -> float:
|
||||
"""
|
||||
1) Fast focus scan on Aerotech GMZ (true focus axis)
|
||||
2) Return GMZ to home position
|
||||
3) Apply one Smargon move to preserve the focus (using a local Jacobian estimate)
|
||||
|
||||
Returns:
|
||||
Smargon delta (in the same "beamline z command" units you use in geom.smargon_nudge(Coordinate(z=...))).
|
||||
"""
|
||||
geom = daq.sample_geometry
|
||||
start_smargon = devs.smargon_pos
|
||||
|
||||
center_x = float(geom.beam_location_pxl.x)
|
||||
center_y = float(geom.beam_location_pxl.y)
|
||||
radius_pxl = float(settings.radius_pxl)
|
||||
|
||||
z_range_mm = float(settings.z_range_um) / 1000.0
|
||||
z_steps = int(settings.z_steps)
|
||||
|
||||
def get_gray() -> np.ndarray:
|
||||
"""
|
||||
Match GUI pipeline:
|
||||
- if Bayer: debayer -> RGB
|
||||
- flip horizontally
|
||||
- convert to gray (uint8)
|
||||
"""
|
||||
img = daq.camera_image # <-- NOTE: use raw, not camera_image_gray
|
||||
if img is None:
|
||||
raise RuntimeError("Autofocus: failed to acquire image.")
|
||||
|
||||
# If already grayscale
|
||||
if img.ndim == 2:
|
||||
bayer = img.astype(np.uint8, copy=False)
|
||||
rgb = cv2.cvtColor(bayer, cv2.COLOR_BAYER_GB2RGB)
|
||||
rgb = rgb[:, ::-1, :].copy()
|
||||
gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
|
||||
return gray
|
||||
|
||||
# If RGB-like
|
||||
if img.ndim == 3 and img.shape[2] >= 3:
|
||||
rgb = img[:, :, :3]
|
||||
rgb = rgb[:, ::-1, :].copy()
|
||||
if rgb.dtype != np.uint8:
|
||||
rgb = np.clip(rgb, 0, 255).astype(np.uint8)
|
||||
gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
|
||||
return gray
|
||||
|
||||
raise RuntimeError(f"Autofocus: unexpected image shape {img.shape}")
|
||||
|
||||
|
||||
first = get_gray()
|
||||
print(first.shape[:2])
|
||||
print(center_x, center_y, radius_pxl)
|
||||
print((first.shape[1]-1) - center_x)
|
||||
if first is None or first.ndim != 2:
|
||||
raise RuntimeError("Autofocus: no grayscale image available.")
|
||||
mask = make_circular_mask(first.shape[:2], center_x=center_x, center_y=center_y, radius=radius_pxl)
|
||||
|
||||
def score_focus() -> float:
|
||||
gray = get_gray()
|
||||
|
||||
# Ensure we're comparing apples-to-apples in logs
|
||||
g = gray
|
||||
if g.dtype != np.uint8:
|
||||
g_u8 = np.clip(g, 0, 255).astype(np.uint8)
|
||||
else:
|
||||
g_u8 = g
|
||||
|
||||
roi = g_u8[mask]
|
||||
mean_dn = float(roi.mean()) if roi.size else 0.0
|
||||
std_dn = float(roi.std()) if roi.size else 0.0
|
||||
|
||||
raw = float(focus_measure_edges(g_u8, mask))
|
||||
|
||||
# Normalize to reduce exposure/gain dependence (gradient energy scales ~ intensity^2)
|
||||
norm = raw / ((mean_dn + 1e-6) ** 2)
|
||||
|
||||
print(f"AF: mean={mean_dn:.1f} std={std_dn:.1f} raw_focus={raw:.2f} norm_focus={norm:.6f}")
|
||||
return norm
|
||||
|
||||
# ---------
|
||||
# A) Aerotech GMZ scan (relative to current GMZ = "home" for this autofocus call)
|
||||
# ---------
|
||||
aero0 = devs.aerotech_pos
|
||||
gmz0 = float(aero0.z)
|
||||
|
||||
gmz_offsets = np.linspace(-0.5 * z_range_mm, 0.5 * z_range_mm, z_steps, dtype=np.float64)
|
||||
gmz_scores = []
|
||||
|
||||
for dz in gmz_offsets:
|
||||
devs.aerotech.move_motor_linear("Z", gmz0 + float(dz), 10)
|
||||
# wait 1 new frame after motion so we don't score an old buffer
|
||||
start_uid = devs.samcam_frame_id()
|
||||
t_deadline = time.perf_counter() + 0.25
|
||||
while time.perf_counter() < t_deadline and devs.samcam_frame_id() == start_uid:
|
||||
time.sleep(0.001)
|
||||
gmz_scores.append(score_focus())
|
||||
|
||||
gmz_scores = np.asarray(gmz_scores, dtype=np.float64)
|
||||
best_i = int(np.argmax(gmz_scores))
|
||||
best_gmz_offset = float(gmz_offsets[best_i])
|
||||
best_focus = float(gmz_scores[best_i])
|
||||
|
||||
# Move GMZ back to "home" (gmz0)
|
||||
devs.aerotech.move_motor_absolute("Z", gmz0, 10000)
|
||||
|
||||
# If best was ~0 anyway, nothing to bake in
|
||||
if abs(best_gmz_offset) < 1e-6:
|
||||
print(f"Aerotech prefocus: best_gmz_offset≈0, focus={best_focus:.2f}")
|
||||
return 0.0
|
||||
|
||||
# ---------
|
||||
# B) Estimate local Jacobian: how Aerotech GMZ changes per unit Smargon beamline-z command
|
||||
# We do two probe moves in the Smargon command space and measure GMZ readback.
|
||||
# ---------
|
||||
def move_smargon_beamline_dz(dz_mm: float) -> None:
|
||||
sh_new = start_smargon.sh_mm + geom.smargon_nudge(Coordinate(z=float(dz_mm)))
|
||||
target = SmargonCoordinate(
|
||||
sh_mm=sh_new,
|
||||
phi_deg=start_smargon.phi_deg,
|
||||
chi_deg=start_smargon.chi_deg,
|
||||
)
|
||||
devs.smargon_pos = target
|
||||
devs.smargon_wait(timeout=30)
|
||||
|
||||
move_smargon_beamline_dz(best_gmz_offset)
|
||||
|
||||
print(
|
||||
f"Aerotech prefocus: best_gmz_offset={best_gmz_offset*1000} um, focus={best_focus:.2f} "
|
||||
f"Smargon_start: {start_smargon.sh_mm} um, Smargon_end: {devs.smargon_pos.sh_mm} um"
|
||||
)
|
||||
return float(best_gmz_offset)
|
||||
|
||||
def focus_measure_laplacian(gray: np.ndarray, mask: np.ndarray | None = None) -> float:
|
||||
"""
|
||||
Fast focus metric: variance of Laplacian.
|
||||
|
||||
Notes:
|
||||
- Works best on uint8 images.
|
||||
- Use a mask/ROI to avoid scoring irrelevant background.
|
||||
"""
|
||||
if gray is None:
|
||||
return 0.0
|
||||
if gray.ndim != 2:
|
||||
raise ValueError(f"Expected 2D grayscale image, got shape={gray.shape}")
|
||||
|
||||
g = gray
|
||||
if g.dtype != np.uint8:
|
||||
g = np.clip(g, 0, 255).astype(np.uint8)
|
||||
|
||||
if mask is not None:
|
||||
roi = g[mask]
|
||||
if roi.size < 64: # too few pixels -> unstable variance
|
||||
return 0.0
|
||||
# Laplacian needs 2D input; reshape ROI to a thin image is awkward.
|
||||
# Better: compute Laplacian on full image and then mask the result.
|
||||
lap = cv2.Laplacian(g, cv2.CV_64F, ksize=3)
|
||||
v = float(lap[mask].var())
|
||||
return v
|
||||
|
||||
lap = cv2.Laplacian(g, cv2.CV_64F, ksize=3)
|
||||
return float(lap.var())
|
||||
|
||||
|
||||
def _wait_for_new_uid(
|
||||
get_frame_id: Callable[[], int] | None,
|
||||
last_uid: int | None,
|
||||
*,
|
||||
frames: int = 1,
|
||||
timeout_s: float = 0.30,
|
||||
poll_s: float = 0.002,
|
||||
fallback_sleep_s: float = 0.04,
|
||||
) -> int | None:
|
||||
"""
|
||||
Wait until UniqueId advances by `frames`.
|
||||
Returns the new uid (or last_uid if we couldn't observe advancement).
|
||||
"""
|
||||
if get_frame_id is None:
|
||||
time.sleep(fallback_sleep_s)
|
||||
return last_uid
|
||||
|
||||
try:
|
||||
uid0 = int(get_frame_id()) if last_uid is None else int(last_uid)
|
||||
except Exception:
|
||||
time.sleep(fallback_sleep_s)
|
||||
return last_uid
|
||||
|
||||
target = uid0 + int(frames)
|
||||
deadline = time.perf_counter() + float(timeout_s)
|
||||
|
||||
while time.perf_counter() < deadline:
|
||||
try:
|
||||
uid = int(get_frame_id())
|
||||
except Exception:
|
||||
uid = uid0
|
||||
|
||||
if uid >= target:
|
||||
return uid
|
||||
|
||||
time.sleep(poll_s)
|
||||
|
||||
# Timeout: don't hang autofocus; just do a small sleep to reduce stale-buffer chance.
|
||||
time.sleep(fallback_sleep_s)
|
||||
return uid0
|
||||
|
||||
|
||||
def autofocus_gpt(
|
||||
z_positions: Iterable[float],
|
||||
move_stage_fn: Callable[[float], None],
|
||||
*,
|
||||
get_frame_id: Callable[[], int] | None = None,
|
||||
wait_for_stop: Callable[[], None] | None = None,
|
||||
mask: np.ndarray | None = None,
|
||||
robust_frames: int = 1,
|
||||
) -> tuple[float, list[tuple[float, float]]]:
|
||||
"""
|
||||
Simple autofocus scan with reliability improvements:
|
||||
- waits for a new UniqueId after motion (avoids scoring stale frames)
|
||||
- optional median-of-N scoring per z
|
||||
"""
|
||||
measures: list[tuple[float, float]] = []
|
||||
last_uid: int | None = None
|
||||
|
||||
# Prime last_uid so the first point also waits for a "fresh" frame
|
||||
if get_frame_id is not None:
|
||||
try:
|
||||
last_uid = int(get_frame_id())
|
||||
except Exception:
|
||||
last_uid = None
|
||||
|
||||
for z in z_positions:
|
||||
move_stage_fn(float(z))
|
||||
if wait_for_stop is not None:
|
||||
wait_for_stop()
|
||||
|
||||
# Wait for camera to deliver a frame AFTER the move
|
||||
last_uid = _wait_for_new_uid(get_frame_id, last_uid, frames=1, timeout_s=0.35)
|
||||
|
||||
if robust_frames <= 1:
|
||||
img = daq.camera_image_gray
|
||||
score = focus_measure_laplacian(img, mask=mask)
|
||||
else:
|
||||
vals: list[float] = []
|
||||
for _ in range(int(robust_frames)):
|
||||
img = daq.camera_image_gray
|
||||
vals.append(focus_measure_laplacian(img, mask=mask))
|
||||
last_uid = _wait_for_new_uid(get_frame_id, last_uid, frames=1, timeout_s=0.35)
|
||||
score = float(np.median(np.asarray(vals, dtype=np.float64)))
|
||||
|
||||
measures.append((float(z), float(score)))
|
||||
print(f"Z={z:.6f}, sharpness={score:.3f}")
|
||||
|
||||
best_z = max(measures, key=lambda x: x[1])[0]
|
||||
return best_z, measures
|
||||
|
||||
# ---- Example z positions ----
|
||||
coarse = np.linspace(-0.1, 0.1, 10) # 0 to 200 microns in 10µm steps
|
||||
|
||||
def move_stage(z):
|
||||
# Insert your hardware code here:
|
||||
print(z)
|
||||
devs.aerotech.move_motor_absolute("Z", z, 1000)
|
||||
#devs.aerotech.controller.read_status()
|
||||
# e.g. serial.write(f"MOVE Z {z}")
|
||||
|
||||
|
||||
def get_frame_id():
|
||||
return int(devs.samcam_frame_id())
|
||||
|
||||
if __name__ == "__main__":
|
||||
devs = BeamlineDevices(mx_beamline())
|
||||
cfg = BeamlineConfig(mx_beamline())
|
||||
daq = AareDAQ(cfg, bl=mx_beamline())
|
||||
zoom = devs.zoom
|
||||
beam_center = cfg.get_beam_mark(zoom)
|
||||
settings = AutofocusSettings(center_x_pxl=beam_center[0], center_y_pxl=beam_center[1],
|
||||
radius_pxl=30, z_range_um=400, z_steps=40)
|
||||
st = time.perf_counter()
|
||||
best_z, curve = autofocus_gpt(coarse, move_stage, get_frame_id=get_frame_id)
|
||||
move_stage(0)
|
||||
#move_stage(best_z)
|
||||
geom = daq.sample_geometry
|
||||
start_smargon = devs.smargon_pos
|
||||
|
||||
sh_new = start_smargon.sh_mm + geom.smargon_nudge(Coordinate(z=float(best_z)))
|
||||
target = SmargonCoordinate(
|
||||
sh_mm=sh_new,
|
||||
phi_deg=start_smargon.phi_deg,
|
||||
chi_deg=start_smargon.chi_deg,
|
||||
)
|
||||
devs.smargon_pos = target
|
||||
devs.smargon_wait(timeout=30)
|
||||
print("Best focus at:", best_z)
|
||||
print(f"Total time: {time.perf_counter() - st:.5f} s")
|
||||
@@ -5,11 +5,11 @@ from PySide6.QtCore import QCommandLineParser, QCommandLineOption
|
||||
from PySide6.QtWidgets import QApplication, QMessageBox
|
||||
|
||||
|
||||
from aaredaqlib.logger_config import setup_logger
|
||||
from aaregui.main_window import MainWindow
|
||||
from aare.common.logger_config import setup_logger
|
||||
from aare.gui.main_window import MainWindow
|
||||
#from aaregui.widgets.login import LoginDialog
|
||||
from aaredaqlib.beamline import MXBeamline, mx_beamline
|
||||
from aaregui.auth import auth
|
||||
from aare.common.beamline import MXBeamline, mx_beamline
|
||||
from aare.gui.auth import auth
|
||||
|
||||
logger = setup_logger("aareGUI")
|
||||
|
||||
@@ -32,18 +32,30 @@ if __name__ == "__main__":
|
||||
default_url = "http://mx-x06da-queue-01.psi.ch:5210"
|
||||
default_zmq_addr = "tcp://x06da-pserv-01:9089" #129.129.110.12:9089
|
||||
default_pred_zmq_addr = "tcp://mx-ml:9091"
|
||||
default_beamline_cam_addr = "x06da-axis-1.psi.ch"
|
||||
default_gonio_cam_addr = "axis-accc8ed2972e.psi.ch"
|
||||
default_gonio_camera_id = 3
|
||||
case MXBeamline.X10SA:
|
||||
default_url = "http://mx-x10sa-queue-01.psi.ch:5210"
|
||||
default_zmq_addr = ""
|
||||
default_pred_zmq_addr = ""
|
||||
default_url = "http://127.0.0.1:5210"
|
||||
default_zmq_addr = "tcp://x10sa-spark-01:9091" #"tcp://x10sa-pserv-01:9089" #
|
||||
default_pred_zmq_addr = "tcp://x10sa-spark-01:9091" #"tcp://sls-gpu-003:9089"#""
|
||||
default_beamline_cam_addr = "axis-accc8eb02488.psi.ch"
|
||||
default_gonio_cam_addr = "axis-accc8ea5e463.psi.ch"
|
||||
default_gonio_camera_id = 1
|
||||
case MXBeamline.X06SA:
|
||||
default_url = "http://mx-x06sa-queue-01.psi.ch:5210"
|
||||
default_zmq_addr = ""
|
||||
default_pred_zmq_addr = ""
|
||||
default_beamline_cam_addr = ""
|
||||
default_gonio_cam_addr = ""
|
||||
default_gonio_camera_id = 1
|
||||
case _:
|
||||
default_url = ""
|
||||
default_zmq_addr = ""
|
||||
default_pred_zmq_addr = ""
|
||||
default_beamline_cam_addr = ""
|
||||
default_gonio_cam_addr = ""
|
||||
default_gonio_camera_id = 1
|
||||
|
||||
# Add custom options as needed
|
||||
urlOption = QCommandLineOption(["u", "aaredaq-url"],
|
||||
@@ -91,6 +103,11 @@ if __name__ == "__main__":
|
||||
|
||||
try:
|
||||
token = auth(base_url)
|
||||
if not token or token.count(".") != 2:
|
||||
raise RuntimeError(
|
||||
"Authentication did not return a valid token. "
|
||||
"Please check the server is running (it may still be initialising)."
|
||||
)
|
||||
logger.info("Authentication successful")
|
||||
except Exception as e:
|
||||
logger.error(f"Cannot connect to AareDAQ server. Exiting. {e}")
|
||||
@@ -107,7 +124,10 @@ if __name__ == "__main__":
|
||||
token=token,
|
||||
default_image=default_image,
|
||||
zmq_addr=zmq_addr,
|
||||
pred_zmq_addr=pred_zmq_addr)
|
||||
pred_zmq_addr=pred_zmq_addr,
|
||||
beamline_cam_addr = default_beamline_cam_addr,
|
||||
gonio_cam_addr = default_gonio_cam_addr,
|
||||
gonio_cam_id = default_gonio_camera_id)
|
||||
win.show()
|
||||
sys.exit(app.exec())
|
||||
except Exception as e:
|
||||
@@ -118,8 +138,9 @@ if __name__ == "__main__":
|
||||
QMessageBox.critical(
|
||||
None,
|
||||
"Fatal Error",
|
||||
f"An error occurred during startup:\n\n{str(e)}\n\nSee console for details."
|
||||
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"
|
||||
)
|
||||
except:
|
||||
pass
|
||||
@@ -1,42 +1,52 @@
|
||||
import time
|
||||
|
||||
import jwt
|
||||
from PySide6.QtCore import Qt, Slot, Signal
|
||||
from PySide6.QtGui import QAction
|
||||
from PySide6.QtCore import Qt, Slot, Signal, QTimer, QSettings
|
||||
from PySide6.QtGui import QAction, QPixmap
|
||||
from PySide6.QtWidgets import (
|
||||
QMainWindow,
|
||||
QWidget,
|
||||
QSplitter,
|
||||
QHBoxLayout,
|
||||
QVBoxLayout,
|
||||
QMessageBox,
|
||||
QApplication,
|
||||
QDockWidget, QTabWidget)
|
||||
QDockWidget,
|
||||
QTabWidget, QFrame, QSizePolicy, QLabel)
|
||||
|
||||
from aaredaqlib.coordinate import Coordinate, SmargonCoordinate
|
||||
from aaredaqlib.diffraction_geometry import DiffractionGeometry
|
||||
from aaredaqlib.logger_config import setup_logger
|
||||
from aaredaqlib.models import SampleShortInfoList, TokenData, DAQStatusModel, BeamlineStateEnum
|
||||
from aaredaqlib.sample_geometry import SampleGeometryModel
|
||||
from aaregui.panels.LogPanel import LogDock
|
||||
from aare.common.coordinate import Coordinate, SmargonCoordinate
|
||||
from aare.common.diffraction_geometry import DiffractionGeometry
|
||||
from aare.common.logger_config import setup_logger
|
||||
from aare.common.models import SampleShortInfoList, TokenData, DAQStatusModel, BeamlineStateEnum
|
||||
from aare.common.sample_geometry import SampleGeometryModel
|
||||
from aare.gui.panels.LogPanel import LogDock
|
||||
|
||||
from aaregui.panels.beamline_controls import BeamlineControls
|
||||
from aaregui.panels.data_collection_settings import DataCollectionSettings
|
||||
from aaregui.panels.manual_sample_panel import ManualSamplePanel
|
||||
from aaregui.panels.reference_tools_panel import ReferenceToolsPanel
|
||||
from aaregui.panels.sample_queue_panel import SampleQueuePanel
|
||||
from aaregui.panels.tell_sample_panel import TellSamplePanel
|
||||
from aaregui.panels.face_detection_panel import FaceDetectionPanel
|
||||
from aaregui.scan_logic.raster_grid_manager import RasterGridManager
|
||||
from aaregui.scan_logic.rotation_scan_manager import RotationScanManager
|
||||
from aaregui.scan_logic.sample_mount_logic import SampleMountLogic
|
||||
from aaregui.threads.axis_video_thread import VideoThread
|
||||
from aaregui.threads.camera_thread import SampleCameraThread, PredictionSubscriber
|
||||
from aaregui.threads.daq_worker import DAQWorker
|
||||
from aaregui.threads.jfjoch_viewer import JFJochDBusClient
|
||||
from aaregui.widgets.camera_image import SampleCameraImageLabel
|
||||
from aaregui.widgets.no_wheel_scroll_area import NoWheelScrollArea
|
||||
from aaregui.widgets.status_bar import StatusBar
|
||||
from aaregui.widgets.video_image import VideoGraphicsView
|
||||
from aaregui.panels.fluorescence_panel import FluorescencePanel
|
||||
from aare.gui.panels.beamline_controls import BeamlineControls
|
||||
from aare.gui.panels.data_collection_settings import DataCollectionSettings
|
||||
from aare.gui.panels.developer_help_dialog import DeveloperHelpDialog
|
||||
from aare.gui.panels.beamline_recovery_panel import BeamlineRecoveryDialog
|
||||
from aare.gui.panels.manual_sample_panel import ManualSamplePanel
|
||||
from aare.gui.panels.reference_tools_panel import ReferenceToolsPanel
|
||||
from aare.gui.panels.sample_queue_panel import SampleQueuePanel
|
||||
from aare.gui.panels.tell_sample_panel import TellSamplePanel
|
||||
from aare.gui.panels.face_detection_panel import FaceDetectionPanel
|
||||
from aare.gui.panels.smargon_trace_panel import SmargonTracePanel
|
||||
from aare.gui.scan_logic.raster_grid_manager import RasterGridManager
|
||||
from aare.gui.scan_logic.rotation_scan_manager import RotationScanManager
|
||||
from aare.gui.scan_logic.sample_mount_logic import SampleMountLogic
|
||||
from aare.gui.threads.axis_video_thread import VideoThread
|
||||
from aare.gui.tutorials.tutorial_manager import TutorialManager, TutorialStep
|
||||
from aare.gui.tutorials.controls_help_dialog import ControlsHelpDialog
|
||||
|
||||
from aare.gui.threads.camera_thread import SampleCameraThread
|
||||
from aare.gui.threads.prediction_subscriber import PredictionSubscriber
|
||||
from aare.gui.threads.daq_worker import DAQWorker
|
||||
from aare.gui.threads.jfjoch_viewer import JFJochDBusClient
|
||||
from aare.gui.tutorials.tutorial_registration import register_tutorials
|
||||
from aare.gui.widgets.alert_banner import AlertBanner
|
||||
from aare.gui.widgets.camera_image import SampleCameraImageLabel
|
||||
from aare.gui.widgets.no_wheel_scroll_area import NoWheelScrollArea
|
||||
from aare.gui.widgets.status_bar import StatusBar
|
||||
from aare.gui.widgets.video_image import VideoGraphicsView
|
||||
from aare.gui.panels.fluorescence_panel import FluorescencePanel
|
||||
|
||||
logger = setup_logger("aareGUI")
|
||||
|
||||
@@ -47,29 +57,65 @@ class MainWindow(QMainWindow):
|
||||
token: str,
|
||||
default_image: str | None,
|
||||
zmq_addr: str | None,
|
||||
pred_zmq_addr: str | None):
|
||||
pred_zmq_addr: str | None,
|
||||
beamline_cam_addr: str | None,
|
||||
gonio_cam_addr: str | None,
|
||||
gonio_cam_id: int | None
|
||||
):
|
||||
super().__init__()
|
||||
self.__base_url = base_url
|
||||
self.__token = token
|
||||
self.__mounting = False
|
||||
self._dev_help_dialog = None
|
||||
self._beamline_recovery_dialog = None
|
||||
self._controls_help_dialog = None
|
||||
self._cleanup_done = False
|
||||
|
||||
# Tutorial manager (define tutorials after widgets exist)
|
||||
self.tutorial_manager = TutorialManager(self)
|
||||
|
||||
self.viewer = JFJochDBusClient()
|
||||
|
||||
# Decode the JWT without signature verification
|
||||
self.__decoded_token = TokenData(**jwt.decode(token, options={"verify_signature": False}))
|
||||
logger.debug(self.__decoded_token)
|
||||
try:
|
||||
token_str = (token or "").strip()
|
||||
if token_str.count(".") != 2:
|
||||
raise ValueError(
|
||||
"Invalid authentication token received (not a JWT). "
|
||||
"This usually happens when the server is not running or still starting."
|
||||
)
|
||||
|
||||
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)
|
||||
QMessageBox.critical(
|
||||
None,
|
||||
"Authentication Error",
|
||||
"Could not start the GUI because authentication data was invalid.\n\n"
|
||||
"Most commonly the server is not running yet (or is still initialising).\n"
|
||||
"Please start/restart the server and try again."
|
||||
)
|
||||
raise
|
||||
|
||||
self.setStyleSheet("background-color: rgb(216, 228, 253);")
|
||||
|
||||
top_widget = QWidget(parent=self)
|
||||
root_widget = QWidget(parent=self)
|
||||
root_layout = QVBoxLayout(root_widget)
|
||||
root_layout.setContentsMargins(0, 0, 0, 0)
|
||||
root_layout.setSpacing(0)
|
||||
|
||||
self.alert_banner = AlertBanner(parent=root_widget)
|
||||
root_layout.addWidget(self.alert_banner)
|
||||
|
||||
top_widget = QWidget(parent=root_widget)
|
||||
top_widget_layout = QHBoxLayout(top_widget)
|
||||
top_widget.setLayout(top_widget_layout)
|
||||
|
||||
diffraction = DiffractionGeometry(
|
||||
energy_keV=12.4,
|
||||
dtz_mm=100,
|
||||
detector_size_pxl=(1553,1630),
|
||||
pixel_size_mm=0.150, #PILATUS 4
|
||||
detector_size_pxl=(1553, 1630),
|
||||
pixel_size_mm=0.150, # PILATUS 4
|
||||
beam_center_pxl=(750, 750),
|
||||
detector_description="PILATUS 4",
|
||||
detector_serial_number="1",
|
||||
@@ -77,7 +123,7 @@ class MainWindow(QMainWindow):
|
||||
poni_rot2_rad=-0.003839724
|
||||
)
|
||||
|
||||
geom = SampleGeometryModel(beam_location_pxl=Coordinate(x=1000,y=1000),
|
||||
geom = SampleGeometryModel(beam_location_pxl=Coordinate(x=1000, y=1000),
|
||||
pixel_in_mm=0.001,
|
||||
aerotech=Coordinate(),
|
||||
smargon=SmargonCoordinate(sh_mm=Coordinate(), phi_deg=0, chi_deg=0),
|
||||
@@ -96,7 +142,6 @@ class MainWindow(QMainWindow):
|
||||
raster_mgr=self.raster,
|
||||
diffraction=diffraction)
|
||||
|
||||
|
||||
top_widget_layout.addWidget(collection_controls_scroll)
|
||||
collection_controls_scroll.setWidget(self.data_collection)
|
||||
collection_controls_scroll.setHorizontalScrollBarPolicy(
|
||||
@@ -104,14 +149,14 @@ class MainWindow(QMainWindow):
|
||||
)
|
||||
collection_controls_scroll.setFixedWidth(self.data_collection.set_width + 10)
|
||||
|
||||
|
||||
self.video_tab = QTabWidget(parent=top_widget)
|
||||
|
||||
self.sample_camera = SampleCameraImageLabel(geom=geom, raster=self.raster, parent=top_widget, default_image=default_image)
|
||||
self.sample_camera = SampleCameraImageLabel(geom=geom, raster=self.raster, parent=top_widget,
|
||||
default_image=default_image)
|
||||
|
||||
self.beamline_view_container = QWidget(parent=top_widget)
|
||||
self.beamline_view_layout = QVBoxLayout(self.beamline_view_container)
|
||||
self.beamline_view_layout.setContentsMargins(0,0,0,0)
|
||||
self.beamline_view_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.beamline_view_layout.setSpacing(6)
|
||||
|
||||
self.beamline_view_1_combined = VideoGraphicsView()
|
||||
@@ -120,18 +165,20 @@ class MainWindow(QMainWindow):
|
||||
self.beamline_view_layout.addWidget(self.beamline_view_2_combined)
|
||||
|
||||
self.beamline_view = VideoGraphicsView()
|
||||
self.beamline_camera_thread = VideoThread(ip="x06da-axis-1.psi.ch")
|
||||
self.beamline_camera_thread.frame_ready.connect(self.beamline_view.update_frame)
|
||||
self.beamline_camera_thread.frame_ready.connect(self.beamline_view_2_combined.update_frame)
|
||||
self.beamline_camera_thread.start()
|
||||
if beamline_cam_addr:
|
||||
self.beamline_camera_thread = VideoThread(ip=beamline_cam_addr)
|
||||
self.beamline_camera_thread.frame_ready.connect(self.beamline_view.update_frame)
|
||||
self.beamline_camera_thread.frame_ready.connect(self.beamline_view_2_combined.update_frame)
|
||||
self.beamline_camera_thread.start()
|
||||
self.beamline_view_layout.addWidget(self.beamline_view)
|
||||
|
||||
self.gonio_view = VideoGraphicsView()
|
||||
#TODO add option to change cameras for gonio_camera_thread
|
||||
self.gonio_camera_thread = VideoThread(ip="axis-accc8ed2972e.psi.ch", camera=3)
|
||||
self.gonio_camera_thread.frame_ready.connect(self.gonio_view.update_frame)
|
||||
self.gonio_camera_thread.frame_ready.connect(self.beamline_view_1_combined.update_frame)
|
||||
self.gonio_camera_thread.start()
|
||||
# TODO add option to change cameras for gonio_camera_thread
|
||||
if gonio_cam_addr and gonio_cam_id:
|
||||
self.gonio_camera_thread = VideoThread(ip=gonio_cam_addr, camera=gonio_cam_id)
|
||||
self.gonio_camera_thread.frame_ready.connect(self.gonio_view.update_frame)
|
||||
self.gonio_camera_thread.frame_ready.connect(self.beamline_view_1_combined.update_frame)
|
||||
self.gonio_camera_thread.start()
|
||||
self.beamline_view_layout.addWidget(self.gonio_view)
|
||||
|
||||
self.video_tab.addTab(self.sample_camera, "Sample camera")
|
||||
@@ -151,8 +198,8 @@ class MainWindow(QMainWindow):
|
||||
)
|
||||
beamline_controls_scroll.setFixedWidth(self.beamline.set_width + 10)
|
||||
|
||||
self.tell_samples = TellSamplePanel(samples=SampleShortInfoList(s = []))
|
||||
self.ref_tools_panel = ReferenceToolsPanel(samples=SampleShortInfoList(s = []))
|
||||
self.tell_samples = TellSamplePanel(samples=SampleShortInfoList(s=[]))
|
||||
self.ref_tools_panel = ReferenceToolsPanel(samples=SampleShortInfoList(s=[]))
|
||||
self.job_list_panel = SampleQueuePanel()
|
||||
|
||||
self.tell_samples_dock = QDockWidget("Sample List", self)
|
||||
@@ -162,6 +209,7 @@ class MainWindow(QMainWindow):
|
||||
self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.tell_samples_dock)
|
||||
|
||||
self.ref_tools_dock = QDockWidget("Reference Tools", self)
|
||||
self.ref_tools_dock.setObjectName("ref_tools_dock")
|
||||
self.ref_tools_dock.setWidget(self.ref_tools_panel)
|
||||
self.ref_tools_dock.setAllowedAreas(Qt.DockWidgetArea.BottomDockWidgetArea)
|
||||
self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.ref_tools_dock)
|
||||
@@ -170,6 +218,7 @@ class MainWindow(QMainWindow):
|
||||
self.sample_logic = SampleMountLogic()
|
||||
|
||||
self.job_list_dock = QDockWidget("Automation list", self)
|
||||
self.job_list_dock.setObjectName("job_list_dock")
|
||||
self.job_list_dock.setWidget(self.job_list_panel)
|
||||
self.job_list_dock.setAllowedAreas(Qt.DockWidgetArea.BottomDockWidgetArea)
|
||||
self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.job_list_dock)
|
||||
@@ -177,36 +226,58 @@ class MainWindow(QMainWindow):
|
||||
self.manual_sample_panel = ManualSamplePanel()
|
||||
|
||||
self.manual_sample_dock = QDockWidget("Manual sample", self)
|
||||
self.manual_sample_dock.setObjectName("manual_sample_dock")
|
||||
self.manual_sample_dock.setWidget(self.manual_sample_panel)
|
||||
self.manual_sample_dock.setAllowedAreas(Qt.DockWidgetArea.BottomDockWidgetArea)
|
||||
self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.manual_sample_dock)
|
||||
|
||||
self.face_panel = FaceDetectionPanel()
|
||||
self.face_panel_dock = QDockWidget("Face detection", self)
|
||||
self.face_panel_dock.setObjectName("face_panel_dock")
|
||||
self.face_panel_dock.setWidget(self.face_panel)
|
||||
self.face_panel_dock.setAllowedAreas(Qt.DockWidgetArea.RightDockWidgetArea | Qt.DockWidgetArea.LeftDockWidgetArea)
|
||||
self.face_panel_dock.setAllowedAreas(
|
||||
Qt.DockWidgetArea.RightDockWidgetArea | Qt.DockWidgetArea.LeftDockWidgetArea)
|
||||
self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.face_panel_dock)
|
||||
self.face_panel_dock.hide()
|
||||
|
||||
self.fluor_panel = FluorescencePanel()
|
||||
self.fluor_panel_dock = QDockWidget("Fluorescence", self)
|
||||
self.fluor_panel_dock.setObjectName("fluor_panel_dock")
|
||||
self.fluor_panel_dock.setWidget(self.fluor_panel)
|
||||
self.fluor_panel_dock.setAllowedAreas(Qt.DockWidgetArea.TopDockWidgetArea | Qt.DockWidgetArea.BottomDockWidgetArea | Qt.DockWidgetArea.RightDockWidgetArea)
|
||||
self.fluor_panel_dock.setAllowedAreas(
|
||||
Qt.DockWidgetArea.TopDockWidgetArea | Qt.DockWidgetArea.BottomDockWidgetArea | Qt.DockWidgetArea.RightDockWidgetArea)
|
||||
self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.fluor_panel_dock)
|
||||
self.fluor_panel_dock.hide()
|
||||
|
||||
# Create and add the dock to your main window
|
||||
self.log_dock = LogDock("Console Log", self)
|
||||
self.log_dock.setObjectName("log_dock")
|
||||
self.addDockWidget(Qt.BottomDockWidgetArea, self.log_dock)
|
||||
self.log_dock.attach_logger("")
|
||||
self.log_dock.attach_logger("aareDAQ")
|
||||
self.log_dock.attach_logger("aareGUI")
|
||||
self.log_dock.hide()
|
||||
|
||||
self.setCentralWidget(top_widget)
|
||||
self.smargon_trace_panel = SmargonTracePanel()
|
||||
self.smargon_trace_dock = QDockWidget("Smargon trace", self)
|
||||
self.smargon_trace_dock.setObjectName("smargon_trace_dock")
|
||||
self.smargon_trace_dock.setWidget(self.smargon_trace_panel)
|
||||
self.smargon_trace_dock.setAllowedAreas(
|
||||
Qt.DockWidgetArea.RightDockWidgetArea
|
||||
| Qt.DockWidgetArea.LeftDockWidgetArea
|
||||
| Qt.DockWidgetArea.BottomDockWidgetArea
|
||||
)
|
||||
self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.smargon_trace_dock)
|
||||
self.smargon_trace_dock.hide()
|
||||
|
||||
root_layout.addWidget(top_widget)
|
||||
self.setCentralWidget(root_widget)
|
||||
|
||||
self.setWindowTitle("AareGUI")
|
||||
self.create_menu_bar()
|
||||
self._restore_window_state()
|
||||
|
||||
# Define tutorials now that the UI exists
|
||||
|
||||
self.status_bar = StatusBar(self.__decoded_token, parent=self)
|
||||
self.setStatusBar(self.status_bar)
|
||||
@@ -217,6 +288,7 @@ class MainWindow(QMainWindow):
|
||||
self.daq.reference_tools.connect(self.ref_tools_panel.new_list)
|
||||
|
||||
self.beamline.samcam.changed.connect(self.daq.samcam_settings)
|
||||
self.beamline.samcam.screenshot_requested.connect(self.daq.send_screenshot_db)
|
||||
self.beamline.loopctr.background.clicked.connect(self.daq.alc_background)
|
||||
self.beamline.loopctr.find_tip.clicked.connect(self.daq.center_loop)
|
||||
self.beamline.loopctr.bounding_box.clicked.connect(self.daq.ml_bounding_box)
|
||||
@@ -229,10 +301,13 @@ class MainWindow(QMainWindow):
|
||||
self.raster.omega.connect(self.daq.set_omega)
|
||||
self.raster.smargon.connect(self.daq.move_smargon)
|
||||
|
||||
self.beamline.omega_panel.set_omega_rel.connect(self.daq.set_omega_rel)
|
||||
self.beamline.omega_panel.set_omega.connect(self.daq.set_omega)
|
||||
self.sample_camera.set_omega.connect(self.daq.set_omega)
|
||||
self.beamline.zoom_panel.zoom.connect(self.daq.zoom)
|
||||
self.beamline.illumination_panel.light.connect(self.daq.light)
|
||||
self.beamline.illumination_panel.front_light.connect(self.daq.front_light)
|
||||
self.beamline.illumination_panel.back_light.connect(self.daq.back_light)
|
||||
|
||||
if self.__decoded_token.staff:
|
||||
self.beamline.abr_tweak.abr_tweak.connect(self.daq.abr_tweak)
|
||||
self.beamline.abr_tweak.abr_save.connect(self.daq.abr_save)
|
||||
@@ -248,25 +323,38 @@ class MainWindow(QMainWindow):
|
||||
|
||||
if zmq_addr is not None:
|
||||
self.camera_thread = SampleCameraThread(zmq_url=zmq_addr)
|
||||
self.camera_thread.camera_image.connect(self.sample_camera.update_pixmap)
|
||||
self.camera_thread.start()
|
||||
self.camera_thread.focus_measure.connect(self.status_bar.update_sharpness)
|
||||
self.camera_thread.fps_measure.connect(self.status_bar.update_samcam_fps)
|
||||
else:
|
||||
self.camera_thread = None
|
||||
|
||||
# Prediction subscriber thread
|
||||
if pred_zmq_addr is not None:
|
||||
logger.debug(f"Starting prediction subscriber thread {pred_zmq_addr}")
|
||||
self.prediction_thread = PredictionSubscriber(pred_zmq_url=pred_zmq_addr)
|
||||
self.prediction_thread = PredictionSubscriber(pred_zmq_url=pred_zmq_addr, topic="detections")
|
||||
self.prediction_thread.prediction.connect(self.sample_camera.update_detections)
|
||||
self.prediction_thread.start()
|
||||
else:
|
||||
self.prediction_thread = None
|
||||
|
||||
self._last_pred_image_ts: float | None = None
|
||||
self._pred_preferred_timeout_s: float = 0.7 # tune: how long we "trust" prediction images
|
||||
self._pred_is_preferred: bool = False
|
||||
|
||||
QApplication.instance().aboutToQuit.connect(self.cleanup)
|
||||
if self.camera_thread is not None:
|
||||
self.camera_thread.camera_image.connect(self._on_samcam_camera_pixmap)
|
||||
|
||||
if self.prediction_thread is not None:
|
||||
self.prediction_thread.image.connect(self._on_samcam_prediction_pixmap)
|
||||
|
||||
self._samcam_source_timer = QTimer(self)
|
||||
self._samcam_source_timer.setInterval(200) # ms
|
||||
self._samcam_source_timer.timeout.connect(self._update_samcam_source_preference)
|
||||
self._samcam_source_timer.start()
|
||||
|
||||
#
|
||||
# self.data_collection.helical.helical_scan.connect(self.worker.helical_scan)
|
||||
# self.data_collection.helical.helical_scan.connect(self.worker.helical_scan)
|
||||
# self.data_collection.helical.update_bookmarks.connect(
|
||||
# self.camera_image.update_bookmarks
|
||||
# )
|
||||
@@ -336,6 +424,7 @@ class MainWindow(QMainWindow):
|
||||
self.daq.update.connect(self.sample_camera.update_daq_status)
|
||||
self.daq.update.connect(self.tell_samples.update_daq_status)
|
||||
self.daq.update.connect(self.ref_tools_panel.update_daq_status)
|
||||
self.daq.update.connect(self.camera_thread.update_daq_status)
|
||||
|
||||
if self.__decoded_token.staff:
|
||||
self.daq.update.connect(self.beamline.beam_size.update_daq_status)
|
||||
@@ -368,6 +457,42 @@ class MainWindow(QMainWindow):
|
||||
self.daq.fluorimeter_spectrum_update.connect(self.fluor_panel.update_plot)
|
||||
self.daq.fluorimeter_spectrum_update.connect(lambda: self.fluor_panel_dock.setVisible(True))
|
||||
|
||||
self.daq.status_message.connect(self.status_bar.show_connection_message)
|
||||
self.daq.status_message.connect(self.alert_banner.show_message)
|
||||
|
||||
register_tutorials(self, self.tutorial_manager)
|
||||
|
||||
@Slot(QPixmap)
|
||||
def _on_samcam_prediction_pixmap(self, pix: QPixmap) -> None:
|
||||
self._last_pred_image_ts = time.monotonic()
|
||||
self._pred_is_preferred = True
|
||||
self.sample_camera.update_pixmap(pix)
|
||||
|
||||
@Slot(QPixmap)
|
||||
def _on_samcam_camera_pixmap(self, pix: QPixmap) -> None:
|
||||
# Only show camera frames when prediction is not currently "healthy"
|
||||
if not self._pred_is_preferred:
|
||||
self.sample_camera.update_pixmap(pix)
|
||||
|
||||
@Slot()
|
||||
def _update_samcam_source_preference(self) -> None:
|
||||
if self.prediction_thread is None:
|
||||
self._pred_is_preferred = False
|
||||
return
|
||||
|
||||
if self._last_pred_image_ts is None:
|
||||
self._pred_is_preferred = False
|
||||
return
|
||||
|
||||
age_s = time.monotonic() - self._last_pred_image_ts
|
||||
self._pred_is_preferred = age_s <= self._pred_preferred_timeout_s
|
||||
|
||||
def start_text_tutorial(self) -> None:
|
||||
self.tutorial_manager.start("intro_text")
|
||||
|
||||
def start_interactive_tutorial(self) -> None:
|
||||
self.tutorial_manager.start("intro_interactive")
|
||||
|
||||
def create_menu_bar(self):
|
||||
"""Create a menu bar with File->Quit and Help->About."""
|
||||
# Main menu bar
|
||||
@@ -420,6 +545,15 @@ class MainWindow(QMainWindow):
|
||||
self.fluor_panel_dock.visibilityChanged.connect(show_fluor_panel_action.setChecked)
|
||||
view_menu.addAction(show_fluor_panel_action)
|
||||
|
||||
show_smargon_trace_action = QAction("Show Smargon trace", self)
|
||||
show_smargon_trace_action.setCheckable(True)
|
||||
show_smargon_trace_action.setChecked(False)
|
||||
show_smargon_trace_action.triggered.connect(lambda checked: self.smargon_trace_dock.setVisible(checked))
|
||||
self.smargon_trace_dock.visibilityChanged.connect(
|
||||
lambda visible: self.smargon_trace_panel.refresh_plot(force=True) if visible else None
|
||||
)
|
||||
view_menu.addAction(show_smargon_trace_action)
|
||||
|
||||
show_log_action = QAction("Show Log", self)
|
||||
show_log_action.setCheckable(True)
|
||||
show_log_action.setChecked(False)
|
||||
@@ -432,6 +566,29 @@ class MainWindow(QMainWindow):
|
||||
about_action.triggered.connect(self.show_about_dialog)
|
||||
help_menu.addAction(about_action)
|
||||
|
||||
controls_help_action = QAction("Mouse / Keyboard Controls", self)
|
||||
controls_help_action.triggered.connect(self.show_controls_help)
|
||||
help_menu.addAction(controls_help_action)
|
||||
|
||||
dev_help_action = QAction("Developer / Help", self)
|
||||
dev_help_action.triggered.connect(self.show_developer_help)
|
||||
help_menu.addAction(dev_help_action)
|
||||
|
||||
if self.__decoded_token.staff:
|
||||
beamline_recovery_action = QAction("Beamline Recovery", self)
|
||||
beamline_recovery_action.triggered.connect(self.show_beamline_recovery)
|
||||
help_menu.addAction(beamline_recovery_action)
|
||||
|
||||
help_menu.addSeparator()
|
||||
|
||||
start_text_tutorial_action = QAction("Start Tutorial (Text)", self)
|
||||
start_text_tutorial_action.triggered.connect(self.start_text_tutorial)
|
||||
help_menu.addAction(start_text_tutorial_action)
|
||||
|
||||
start_interactive_tutorial_action = QAction("Start Tutorial (Interactive)", self)
|
||||
start_interactive_tutorial_action.triggered.connect(self.start_interactive_tutorial)
|
||||
help_menu.addAction(start_interactive_tutorial_action)
|
||||
|
||||
def show_about_dialog(self):
|
||||
QMessageBox.about(
|
||||
self,
|
||||
@@ -439,6 +596,37 @@ class MainWindow(QMainWindow):
|
||||
"Aare Macromolecular Crystallography GUI\nVersion: 1.0\nCopyright: Paul Scherrer Institute 2024-2025",
|
||||
)
|
||||
|
||||
def show_controls_help(self) -> None:
|
||||
if self._controls_help_dialog is None:
|
||||
self._controls_help_dialog = ControlsHelpDialog(parent=self)
|
||||
self._controls_help_dialog.show()
|
||||
self._controls_help_dialog.raise_()
|
||||
self._controls_help_dialog.activateWindow()
|
||||
|
||||
def show_developer_help(self) -> None:
|
||||
if self._dev_help_dialog is None:
|
||||
self._dev_help_dialog = DeveloperHelpDialog(
|
||||
daq=self.daq,
|
||||
is_staff=bool(getattr(self.__decoded_token, "staff", False)),
|
||||
parent=self,
|
||||
)
|
||||
self._dev_help_dialog.refresh()
|
||||
self._dev_help_dialog.show()
|
||||
self._dev_help_dialog.raise_()
|
||||
self._dev_help_dialog.activateWindow()
|
||||
|
||||
def show_beamline_recovery(self) -> None:
|
||||
if not bool(getattr(self.__decoded_token, "staff", False)):
|
||||
return
|
||||
if self._beamline_recovery_dialog is None:
|
||||
self._beamline_recovery_dialog = BeamlineRecoveryDialog(
|
||||
daq=self.daq,
|
||||
parent=self,
|
||||
)
|
||||
self._beamline_recovery_dialog.show()
|
||||
self._beamline_recovery_dialog.raise_()
|
||||
self._beamline_recovery_dialog.activateWindow()
|
||||
|
||||
@Slot(str)
|
||||
def show_sample_missing_dialog(self, msg: str):
|
||||
if self.job_list_panel.is_running():
|
||||
@@ -456,8 +644,12 @@ class MainWindow(QMainWindow):
|
||||
|
||||
@Slot(DAQStatusModel)
|
||||
def update_daq_status(self, s: DAQStatusModel):
|
||||
self.beamline_camera_thread.set_busy(s.busy)
|
||||
self.gonio_camera_thread.set_busy(s.busy)
|
||||
if hasattr(self, "beamline_camera_thread") and self.beamline_camera_thread is not None:
|
||||
self.beamline_camera_thread.set_busy(s.busy)
|
||||
|
||||
if hasattr(self, "gonio_camera_thread") and self.gonio_camera_thread is not None:
|
||||
self.gonio_camera_thread.set_busy(s.busy)
|
||||
|
||||
if not self.__mounting and s.state == BeamlineStateEnum.RobotSampleExchange:
|
||||
self.__mounting = True
|
||||
self.video_tab.setCurrentIndex(3)
|
||||
@@ -465,17 +657,57 @@ class MainWindow(QMainWindow):
|
||||
self.__mounting = False
|
||||
self.video_tab.setCurrentIndex(0)
|
||||
|
||||
@Slot(str)
|
||||
def display_error(self, msg: str):
|
||||
self.status_bar.setStyleSheet("color: red;")
|
||||
self.status_bar.showMessage(f'<span style="color: red; "> {msg} </span>', 10000)
|
||||
def _restore_window_state(self) -> None:
|
||||
settings = QSettings()
|
||||
geometry = settings.value("main_window/geometry")
|
||||
state = settings.value("main_window/state")
|
||||
|
||||
if geometry is not None:
|
||||
self.restoreGeometry(geometry)
|
||||
if state is not None:
|
||||
self.restoreState(state)
|
||||
|
||||
def closeEvent(self, event) -> None:
|
||||
try:
|
||||
settings = QSettings()
|
||||
settings.setValue("main_window/geometry", self.saveGeometry())
|
||||
settings.setValue("main_window/state", self.saveState())
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to save main window state: {e}")
|
||||
|
||||
try:
|
||||
self.cleanup()
|
||||
except Exception as e:
|
||||
logger.warning(f"Cleanup during closeEvent failed: {e}")
|
||||
|
||||
super().closeEvent(event)
|
||||
|
||||
def cleanup(self):
|
||||
if self.camera_thread is not None:
|
||||
self.camera_thread.stop()
|
||||
if self.prediction_thread is not None:
|
||||
self.prediction_thread.stop()
|
||||
if self.beamline_camera_thread is not None:
|
||||
self.beamline_camera_thread.stop()
|
||||
if self.gonio_camera_thread is not None:
|
||||
self.gonio_camera_thread.stop()
|
||||
if getattr(self, "_cleanup_done", False):
|
||||
return
|
||||
self._cleanup_done = True
|
||||
|
||||
try:
|
||||
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}")
|
||||
|
||||
for attr_name in (
|
||||
"camera_thread",
|
||||
"prediction_thread",
|
||||
"beamline_camera_thread",
|
||||
"gonio_camera_thread",
|
||||
):
|
||||
thread = getattr(self, attr_name, None)
|
||||
if thread is None:
|
||||
continue
|
||||
|
||||
logger.debug(f"Stopping {attr_name}")
|
||||
|
||||
try:
|
||||
thread.stop()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to stop {attr_name}: {e}")
|
||||
|
||||
setattr(self, attr_name, None)
|
||||
@@ -2,7 +2,7 @@ from typing import Literal
|
||||
|
||||
from PySide6.QtGui import QColor
|
||||
|
||||
from aaredaqlib.coordinate import SmargonCoordinate
|
||||
from aare.common.coordinate import SmargonCoordinate
|
||||
|
||||
|
||||
class SmargonBookmark:
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
from PySide6.QtCore import QAbstractTableModel, Qt, Slot
|
||||
from PySide6.QtCore import QAbstractTableModel, Qt
|
||||
from PySide6.QtGui import QBrush, QColor
|
||||
|
||||
from aaredaqlib.models import SampleShortInfo, SampleShortInfoList
|
||||
from aare.common.models import SampleShortInfo, SampleShortInfoList
|
||||
|
||||
|
||||
def get_entry(sample: SampleShortInfo, column: int):
|
||||
+1
-1
@@ -3,7 +3,7 @@ import re
|
||||
from PySide6.QtCore import QAbstractTableModel, Qt, QMimeData
|
||||
from PySide6.QtGui import QBrush, QColor
|
||||
|
||||
from aaredaqlib.models import SampleShortInfo, SampleShortInfoList
|
||||
from aare.common.models import SampleShortInfo, SampleShortInfoList
|
||||
|
||||
|
||||
def get_entry(sample: SampleShortInfo, column: int):
|
||||
@@ -1,9 +1,9 @@
|
||||
# Python
|
||||
|
||||
from PySide6.QtCore import Qt, Signal, QObject
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QDockWidget, QPlainTextEdit
|
||||
|
||||
from aaredaqlib.logger_config import QtLogEmitter, QtLogHandler, find_existing_formatter, attach_to_logger
|
||||
from aare.common.logger_config import QtLogEmitter, QtLogHandler, find_existing_formatter, attach_to_logger
|
||||
|
||||
|
||||
class LogDock(QDockWidget):
|
||||
@@ -1,12 +1,12 @@
|
||||
from PySide6.QtCore import Signal, Slot
|
||||
from PySide6.QtGui import Qt
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QLabel, QPushButton
|
||||
from aaredaqlib.coordinate import Coordinate
|
||||
from aaredaqlib.models import DAQStatusModel
|
||||
from aare.common.coordinate import Coordinate
|
||||
from aare.common.models import DAQStatusModel
|
||||
|
||||
from aaregui.widgets.button_with_payload import ButtonWithPayload
|
||||
from aaregui.widgets.number_line_edit import NumberLineEdit
|
||||
from aaregui.widgets.title_label import TitleLabel
|
||||
from aare.gui.widgets.button_with_payload import ButtonWithPayload
|
||||
from aare.gui.widgets.number_line_edit import NumberLineEdit
|
||||
from aare.gui.widgets.title_label import TitleLabel
|
||||
|
||||
DEFAULT_ABR_STEP_UM = 5
|
||||
|
||||
@@ -103,7 +103,7 @@ class AbrTweakWidget(QWidget):
|
||||
|
||||
goto_button = QPushButton("Go to meas.")
|
||||
grid_layout.addWidget(goto_button, 4, 0, 1, 3)
|
||||
save_button.pressed.connect(self.goto_button_pressed)
|
||||
goto_button.pressed.connect(self.goto_button_pressed)
|
||||
|
||||
@Slot()
|
||||
def goto_button_pressed(self):
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
from PySide6.QtCore import Signal, Slot
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QLabel
|
||||
|
||||
from aaredaqlib.models import DAQStatusModel
|
||||
from aaregui.widgets.number_line_edit import NumberLineEdit
|
||||
from aaregui.widgets.title_label import TitleLabel
|
||||
from aare.common.models import DAQStatusModel
|
||||
from aare.gui.widgets.number_line_edit import NumberLineEdit
|
||||
from aare.gui.widgets.title_label import TitleLabel
|
||||
|
||||
|
||||
class BeamCenterWidget(QWidget):
|
||||
@@ -1,8 +1,8 @@
|
||||
from PySide6.QtCore import Signal, Slot
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QPushButton, QLabel
|
||||
|
||||
from aaredaqlib.models import DAQStatusModel
|
||||
from aaregui.widgets.title_label import TitleLabel
|
||||
from aare.common.models import DAQStatusModel
|
||||
from aare.gui.widgets.title_label import TitleLabel
|
||||
|
||||
|
||||
class BeamMarkWidget(QWidget):
|
||||
@@ -1,9 +1,9 @@
|
||||
from PySide6.QtCore import Signal, Slot
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QLabel
|
||||
|
||||
from aaredaqlib.models import DAQStatusModel
|
||||
from aaregui.widgets.number_line_edit import NumberLineEdit
|
||||
from aaregui.widgets.title_label import TitleLabel
|
||||
from aare.common.models import DAQStatusModel
|
||||
from aare.gui.widgets.number_line_edit import NumberLineEdit
|
||||
from aare.gui.widgets.title_label import TitleLabel
|
||||
|
||||
|
||||
class BeamSizeWidget(QWidget):
|
||||
+10
-11
@@ -1,16 +1,15 @@
|
||||
from PySide6.QtWidgets import QFrame, QVBoxLayout
|
||||
|
||||
from aaregui.panels.abr_tweak_panel import AbrTweakWidget
|
||||
from aaregui.panels.beam_center_panel import BeamCenterWidget
|
||||
from aaregui.panels.beam_mark_panel import BeamMarkWidget
|
||||
from aaregui.panels.beam_size_panel import BeamSizeWidget
|
||||
from aaregui.panels.beamline_state_panel import BeamlineStatePanel
|
||||
from aaregui.panels.illumination_panel import IlluminationPanel
|
||||
from aaregui.panels.loop_centering_panel import LoopCenteringPanel
|
||||
from aaregui.panels.omega_panel import OmegaPanel
|
||||
from aaregui.panels.samcam_panel import SamcamPanel
|
||||
from aaregui.panels.smargon_panel import SmargonPanel
|
||||
from aaregui.panels.zoom_panel import ZoomPanel
|
||||
from aare.gui.panels.abr_tweak_panel import AbrTweakWidget
|
||||
from aare.gui.panels.beam_center_panel import BeamCenterWidget
|
||||
from aare.gui.panels.beam_mark_panel import BeamMarkWidget
|
||||
from aare.gui.panels.beam_size_panel import BeamSizeWidget
|
||||
from aare.gui.panels.illumination_panel import IlluminationPanel
|
||||
from aare.gui.panels.loop_centering_panel import LoopCenteringPanel
|
||||
from aare.gui.panels.omega_panel import OmegaPanel
|
||||
from aare.gui.panels.samcam_panel import SamcamPanel
|
||||
from aare.gui.panels.smargon_panel import SmargonPanel
|
||||
from aare.gui.panels.zoom_panel import ZoomPanel
|
||||
|
||||
|
||||
class BeamlineControls(QFrame):
|
||||
@@ -0,0 +1,327 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtCore import Slot
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QVBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QWidget,
|
||||
QDialogButtonBox,
|
||||
QInputDialog,
|
||||
QLineEdit,
|
||||
QMessageBox,
|
||||
)
|
||||
|
||||
from aare.common.models import DAQStatusModel
|
||||
from aare.gui.threads.daq_worker import DAQWorker
|
||||
|
||||
|
||||
class RecoveryPanel(QWidget):
|
||||
def __init__(self, *, daq: DAQWorker, parent=None):
|
||||
super().__init__(parent)
|
||||
self._daq = daq
|
||||
self._last_status: DAQStatusModel | None = None
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(10)
|
||||
|
||||
self._warning_primary = QLabel(
|
||||
"⚠ Recovery actions are staff-only and intentionally dangerous.",
|
||||
self,
|
||||
)
|
||||
self._warning_primary.setWordWrap(True)
|
||||
self._warning_primary.setStyleSheet(
|
||||
"QLabel {"
|
||||
" background: #fff3cd;"
|
||||
" color: #7a4b00;"
|
||||
" border: 1px solid #f0c36d;"
|
||||
" border-radius: 6px;"
|
||||
" padding: 8px;"
|
||||
" font-weight: 600;"
|
||||
"}"
|
||||
)
|
||||
layout.addWidget(self._warning_primary)
|
||||
|
||||
self._warning_secondary = QLabel(
|
||||
"Only use these commands when beamline is stuck and certain beamline is unrecoverable through normal operation.",
|
||||
self,
|
||||
)
|
||||
self._warning_secondary.setWordWrap(True)
|
||||
self._warning_secondary.setStyleSheet(
|
||||
"QLabel {"
|
||||
" background: #fdeaea;"
|
||||
" color: #8b1e1e;"
|
||||
" border: 1px solid #e6a8a8;"
|
||||
" border-radius: 6px;"
|
||||
" padding: 8px;"
|
||||
" font-weight: 600;"
|
||||
"}"
|
||||
)
|
||||
layout.addWidget(self._warning_secondary)
|
||||
|
||||
self._status = QLabel("Current status: waiting for DAQ status update…", self)
|
||||
self._status.setWordWrap(True)
|
||||
self._status.setStyleSheet(
|
||||
"QLabel {"
|
||||
" background: #fafafa;"
|
||||
" border: 1px solid #d0d0d0;"
|
||||
" border-radius: 6px;"
|
||||
" padding: 8px;"
|
||||
"}"
|
||||
)
|
||||
layout.addWidget(self._status)
|
||||
|
||||
self._last_action = QLabel("Last action: -", self)
|
||||
self._last_action.setWordWrap(True)
|
||||
self._last_action.setStyleSheet(
|
||||
"QLabel {"
|
||||
" background: #eef6ff;"
|
||||
" color: #12406a;"
|
||||
" border: 1px solid #a8c7e6;"
|
||||
" border-radius: 6px;"
|
||||
" padding: 8px;"
|
||||
" font-weight: 600;"
|
||||
"}"
|
||||
)
|
||||
layout.addWidget(self._last_action)
|
||||
|
||||
self._take_over_btn = QPushButton("Take over beamline", self)
|
||||
self._take_over_btn.setStyleSheet(
|
||||
"QPushButton {"
|
||||
" background: #fff3cd;"
|
||||
" border: 1px solid #f0c36d;"
|
||||
" border-radius: 6px;"
|
||||
" padding: 8px;"
|
||||
" font-weight: 600;"
|
||||
"}"
|
||||
)
|
||||
self._take_over_btn.clicked.connect(self._take_over_beamline)
|
||||
layout.addWidget(self._take_over_btn)
|
||||
|
||||
self._free_beamline_btn = QPushButton("Free beamline", self)
|
||||
self._free_beamline_btn.setStyleSheet(
|
||||
"QPushButton {"
|
||||
" background: #fff3cd;"
|
||||
" border: 1px solid #f0c36d;"
|
||||
" border-radius: 6px;"
|
||||
" padding: 8px;"
|
||||
" font-weight: 600;"
|
||||
"}"
|
||||
)
|
||||
self._free_beamline_btn.clicked.connect(self._free_beamline)
|
||||
layout.addWidget(self._free_beamline_btn)
|
||||
|
||||
self._recover_beamline_btn = QPushButton("Recover beamline", self)
|
||||
self._recover_beamline_btn.setStyleSheet(
|
||||
"QPushButton {"
|
||||
" background: #fdeaea;"
|
||||
" color: #8b1e1e;"
|
||||
" border: 1px solid #e6a8a8;"
|
||||
" border-radius: 6px;"
|
||||
" padding: 8px;"
|
||||
" font-weight: 700;"
|
||||
"}"
|
||||
)
|
||||
self._recover_beamline_btn.clicked.connect(self._recover_beamline)
|
||||
layout.addWidget(self._recover_beamline_btn)
|
||||
|
||||
self._recovery_unmount_btn = QPushButton("Unmount sample (recovery)", self)
|
||||
self._recovery_unmount_btn.setStyleSheet(
|
||||
"QPushButton {"
|
||||
" background: #fdeaea;"
|
||||
" color: #8b1e1e;"
|
||||
" border: 1px solid #e6a8a8;"
|
||||
" border-radius: 6px;"
|
||||
" padding: 8px;"
|
||||
" font-weight: 700;"
|
||||
"}"
|
||||
)
|
||||
self._recovery_unmount_btn.clicked.connect(self._recovery_unmount_sample)
|
||||
layout.addWidget(self._recovery_unmount_btn)
|
||||
|
||||
self._resync_sample_btn = QPushButton("Resync sample from TELL", self)
|
||||
self._resync_sample_btn.setStyleSheet(
|
||||
"QPushButton {"
|
||||
" background: #eef6ff;"
|
||||
" color: #12406a;"
|
||||
" border: 1px solid #a8c7e6;"
|
||||
" border-radius: 6px;"
|
||||
" padding: 8px;"
|
||||
" font-weight: 600;"
|
||||
"}"
|
||||
)
|
||||
self._resync_sample_btn.clicked.connect(self._resync_sample)
|
||||
layout.addWidget(self._resync_sample_btn)
|
||||
|
||||
layout.addStretch(1)
|
||||
|
||||
self._daq.update.connect(self._set_daq_status)
|
||||
self._daq.sample_resync_completed.connect(self._set_last_action)
|
||||
self._refresh_buttons()
|
||||
|
||||
def _prompt_recovery_code(self, action_name: str) -> str | None:
|
||||
code, ok = QInputDialog.getText(
|
||||
self,
|
||||
action_name,
|
||||
"Enter recovery confirmation code:",
|
||||
QLineEdit.EchoMode.Password,
|
||||
)
|
||||
if not ok:
|
||||
return None
|
||||
code = code.strip()
|
||||
return code or None
|
||||
|
||||
def _sample_appears_mounted(self) -> bool:
|
||||
try:
|
||||
return self._last_status is not None and self._last_status.sample is not None
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _beamline_appears_busy(self) -> bool:
|
||||
try:
|
||||
return self._last_status is not None and bool(self._last_status.busy)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _status_text(self) -> str:
|
||||
if self._last_status is None:
|
||||
return "Current status: waiting for DAQ status update…"
|
||||
|
||||
state_name = getattr(self._last_status.state, "name", str(self._last_status.state))
|
||||
busy = bool(getattr(self._last_status, "busy", False))
|
||||
sample_mounted = self._sample_appears_mounted()
|
||||
tell_connected = bool(getattr(self._last_status, "tell_connected", False))
|
||||
|
||||
return (
|
||||
f"State: {state_name}\n"
|
||||
f"Busy: {busy}\n"
|
||||
f"Sample mounted: {sample_mounted}\n"
|
||||
f"TELL connected: {tell_connected}"
|
||||
)
|
||||
|
||||
def _refresh_buttons(self) -> None:
|
||||
sample_mounted = self._sample_appears_mounted()
|
||||
beamline_busy = self._beamline_appears_busy()
|
||||
|
||||
self._recovery_unmount_btn.setEnabled(sample_mounted)
|
||||
self._recovery_unmount_btn.setToolTip(
|
||||
"" if sample_mounted else "Disabled because no mounted sample is visible in current status."
|
||||
)
|
||||
|
||||
self._free_beamline_btn.setEnabled(beamline_busy)
|
||||
self._free_beamline_btn.setToolTip(
|
||||
"" if beamline_busy else "Disabled because beamline does not currently appear busy."
|
||||
)
|
||||
|
||||
self._resync_sample_btn.setEnabled(True)
|
||||
self._resync_sample_btn.setToolTip("Force a one-shot sample reconciliation against TELL.")
|
||||
|
||||
def _confirm(self, title: str, msg: str) -> bool:
|
||||
reply = QMessageBox.warning(
|
||||
self,
|
||||
title,
|
||||
msg,
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No,
|
||||
)
|
||||
return reply == QMessageBox.StandardButton.Yes
|
||||
|
||||
@Slot(DAQStatusModel)
|
||||
def _set_daq_status(self, s: DAQStatusModel) -> None:
|
||||
self._last_status = s
|
||||
self._status.setText(self._status_text())
|
||||
self._refresh_buttons()
|
||||
|
||||
@Slot()
|
||||
def _take_over_beamline(self) -> None:
|
||||
if not self._confirm(
|
||||
"Take over beamline",
|
||||
"This will forcefully grab the active beamline session.\n\nDo you want to continue?",
|
||||
):
|
||||
return
|
||||
code = self._prompt_recovery_code("Take over beamline")
|
||||
if not code:
|
||||
return
|
||||
self._last_action.setText("Last action: Taking over beamline session...")
|
||||
self._daq.take_over_beamline(code)
|
||||
|
||||
@Slot()
|
||||
def _free_beamline(self) -> None:
|
||||
if not self._confirm(
|
||||
"Free beamline",
|
||||
"This will clear the beamline busy flag.\n\nDo you want to continue?",
|
||||
):
|
||||
return
|
||||
code = self._prompt_recovery_code("Free beamline")
|
||||
if not code:
|
||||
return
|
||||
self._last_action.setText("Last action: Clearing beamline busy flag...")
|
||||
self._daq.free_beamline(code)
|
||||
|
||||
@Slot()
|
||||
def _recover_beamline(self) -> None:
|
||||
if self._sample_appears_mounted():
|
||||
if not self._confirm(
|
||||
"Recover beamline",
|
||||
"A sample appears to be mounted.\n\n"
|
||||
"Recovering the beamline may damage the sample or leave hardware in an unsafe state.\n\n"
|
||||
"Only continue if you are sure this is the correct recovery action.",
|
||||
):
|
||||
return
|
||||
else:
|
||||
if not self._confirm(
|
||||
"Recover beamline",
|
||||
"This will take over the beamline, clear the busy flag, and set the state to Maintenance.\n\n"
|
||||
"Do you want to continue?",
|
||||
):
|
||||
return
|
||||
|
||||
code = self._prompt_recovery_code("Recover beamline")
|
||||
if not code:
|
||||
return
|
||||
self._last_action.setText("Last action: Recovering beamline to Maintenance...")
|
||||
self._daq.recover_beamline(code)
|
||||
|
||||
@Slot()
|
||||
def _recovery_unmount_sample(self) -> None:
|
||||
if not self._confirm(
|
||||
"Unmount sample (recovery)",
|
||||
"This will force-take the session and attempt a controlled recovery unmount.\n\n"
|
||||
"Use this only if normal unmount is not possible.",
|
||||
):
|
||||
return
|
||||
|
||||
code = self._prompt_recovery_code("Unmount sample (recovery)")
|
||||
if not code:
|
||||
return
|
||||
self._last_action.setText("Last action: Performing recovery sample unmount...")
|
||||
self._daq.recovery_unmount_sample(code)
|
||||
|
||||
@Slot()
|
||||
def _resync_sample(self) -> None:
|
||||
self._last_action.setText("Last action: Resyncing sample cache from TELL...")
|
||||
self._daq.resync_sample()
|
||||
|
||||
@Slot(str)
|
||||
def _set_last_action(self, message: str) -> None:
|
||||
self._last_action.setText(f"Last action: {message}")
|
||||
|
||||
|
||||
class BeamlineRecoveryDialog(QDialog):
|
||||
def __init__(self, *, daq: DAQWorker, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Beamline Recovery")
|
||||
self.setMinimumSize(560, 420)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(12, 12, 12, 12)
|
||||
layout.setSpacing(8)
|
||||
|
||||
self._panel = RecoveryPanel(daq=daq, parent=self)
|
||||
layout.addWidget(self._panel, 1)
|
||||
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close, parent=self)
|
||||
buttons.rejected.connect(self.reject)
|
||||
buttons.accepted.connect(self.accept)
|
||||
layout.addWidget(buttons)
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QPushButton
|
||||
|
||||
from aaregui.widgets.title_label import TitleLabel
|
||||
from aare.gui.widgets.title_label import TitleLabel
|
||||
|
||||
|
||||
class BeamlineStatePanel(QWidget):
|
||||
+9
-13
@@ -3,23 +3,19 @@ from PySide6.QtWidgets import (
|
||||
QFrame,
|
||||
QVBoxLayout,
|
||||
QTabWidget,
|
||||
QGridLayout,
|
||||
QPushButton,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from aaredaqlib.diffraction_geometry import DiffractionGeometry
|
||||
from aaredaqlib.models import DAQStatusModel
|
||||
from aaredaqlib.rotation_scan import RotationScanRequest
|
||||
from aaredaqlib.sample_geometry import SampleGeometryModel
|
||||
from aare.common.diffraction_geometry import DiffractionGeometry
|
||||
from aare.common.models import DAQStatusModel
|
||||
from aare.common.sample_geometry import SampleGeometryModel
|
||||
|
||||
from aaregui.panels.file_path_panel import FilePathPanel
|
||||
from aaregui.panels.fluorescence_data_collection import FluorescenceDataCollectionPanel
|
||||
from aaregui.panels.raster_data_collection import RasterDataCollectionPanel
|
||||
from aaregui.panels.rotation_data_collection import RotationDataCollectionPanel
|
||||
from aaregui.panels.smart_rotation_panel import SimpleRotationSettingsPanel
|
||||
from aaregui.scan_logic.raster_grid_manager import RasterGridManager
|
||||
from aaregui.widgets.title_label import TitleLabel
|
||||
from aare.gui.panels.file_path_panel import FilePathPanel
|
||||
from aare.gui.panels.fluorescence_data_collection import FluorescenceDataCollectionPanel
|
||||
from aare.gui.panels.raster_data_collection import RasterDataCollectionPanel
|
||||
from aare.gui.panels.rotation_data_collection import RotationDataCollectionPanel
|
||||
from aare.gui.panels.smart_rotation_panel import SimpleRotationSettingsPanel
|
||||
from aare.gui.scan_logic.raster_grid_manager import RasterGridManager
|
||||
|
||||
|
||||
class DataCollectionSettings(QFrame):
|
||||
@@ -0,0 +1,494 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from PySide6.QtCore import Qt, Slot, QUrl
|
||||
from PySide6.QtGui import QGuiApplication, QDesktopServices
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QVBoxLayout,
|
||||
QHBoxLayout,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QTabWidget,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
QTextEdit,
|
||||
QLabel,
|
||||
QWidget,
|
||||
QCheckBox,
|
||||
QDialogButtonBox,
|
||||
QFormLayout,
|
||||
QFrame,
|
||||
)
|
||||
|
||||
from aare.common.error_codes import error_code_help
|
||||
from aare.common.logger_config import QtLogEmitter, QtLogHandler, find_existing_formatter, attach_to_logger
|
||||
from aare.gui.threads.daq_worker import DAQWorker
|
||||
|
||||
|
||||
|
||||
class DeveloperHelpDialog(QDialog):
|
||||
def __init__(self, *, daq: DAQWorker, is_staff: bool, parent=None):
|
||||
super().__init__(parent)
|
||||
self._daq = daq
|
||||
self._is_staff = bool(is_staff)
|
||||
|
||||
self._codes: Dict[str, str] = {}
|
||||
self._last_payload: dict = {}
|
||||
self._freeze_payload: bool = False
|
||||
self._always_highlight_last_error: bool = True
|
||||
|
||||
self.setWindowTitle("Developer / Help")
|
||||
self.setMinimumSize(860, 560)
|
||||
|
||||
root = QVBoxLayout(self)
|
||||
root.setContentsMargins(12, 12, 12, 12)
|
||||
root.setSpacing(8)
|
||||
|
||||
# Compact banner (staff only)
|
||||
self._banner = QLabel(self)
|
||||
self._banner.setVisible(self._is_staff)
|
||||
self._banner.setWordWrap(True)
|
||||
self._banner.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||
self._banner.setStyleSheet(
|
||||
"QLabel {"
|
||||
" background: #f6f6f6;"
|
||||
" border: 1px solid #d0d0d0;"
|
||||
" border-radius: 6px;"
|
||||
" padding: 6px 8px;"
|
||||
"}"
|
||||
)
|
||||
root.addWidget(self._banner)
|
||||
|
||||
# Top controls
|
||||
top = QHBoxLayout()
|
||||
top.setSpacing(8)
|
||||
root.addLayout(top)
|
||||
|
||||
top.addWidget(QLabel("Filter:", self))
|
||||
|
||||
self._filter = QLineEdit(self)
|
||||
self._filter.setPlaceholderText("Type to filter (matches name, value, or help)…")
|
||||
self._filter.setClearButtonEnabled(True)
|
||||
self._filter.setMinimumHeight(28)
|
||||
self._filter.setStyleSheet(
|
||||
"QLineEdit {"
|
||||
" background: white;"
|
||||
" border: 1px solid #bdbdbd;"
|
||||
" border-radius: 6px;"
|
||||
" padding: 4px 8px;"
|
||||
"}"
|
||||
)
|
||||
self._filter.textChanged.connect(self._apply_filter)
|
||||
top.addWidget(self._filter, 1)
|
||||
|
||||
self._refresh_btn = QPushButton("Refresh", self)
|
||||
self._refresh_btn.clicked.connect(self.refresh)
|
||||
top.addWidget(self._refresh_btn)
|
||||
|
||||
self._freeze_cb = QCheckBox("Freeze payload", self)
|
||||
self._freeze_cb.setVisible(self._is_staff)
|
||||
self._freeze_cb.toggled.connect(self._set_freeze_payload)
|
||||
top.addWidget(self._freeze_cb)
|
||||
|
||||
self._highlight_cb = QCheckBox("Always highlight last error code", self)
|
||||
self._highlight_cb.setVisible(self._is_staff)
|
||||
self._highlight_cb.setChecked(True)
|
||||
self._highlight_cb.toggled.connect(self._set_always_highlight)
|
||||
top.addWidget(self._highlight_cb)
|
||||
|
||||
self._copy_selected_btn = QPushButton("Copy code", self)
|
||||
self._copy_selected_btn.clicked.connect(self._copy_selected_code)
|
||||
top.addWidget(self._copy_selected_btn)
|
||||
|
||||
self._copy_all_btn = QPushButton("Copy all (filtered)", self)
|
||||
self._copy_all_btn.clicked.connect(self._copy_all_filtered)
|
||||
top.addWidget(self._copy_all_btn)
|
||||
|
||||
self._copy_payload_btn = QPushButton("Copy payload", self)
|
||||
self._copy_payload_btn.setVisible(self._is_staff)
|
||||
self._copy_payload_btn.clicked.connect(self._copy_payload)
|
||||
top.addWidget(self._copy_payload_btn)
|
||||
|
||||
# Staff utilities: open log files
|
||||
self._open_gui_log_btn = QPushButton("Open GUI log", self)
|
||||
self._open_gui_log_btn.setVisible(self._is_staff)
|
||||
self._open_gui_log_btn.clicked.connect(lambda: self._open_log_file_for_logger("aareGUI"))
|
||||
top.addWidget(self._open_gui_log_btn)
|
||||
|
||||
self._open_daq_log_btn = QPushButton("Open DAQ log", self)
|
||||
self._open_daq_log_btn.setVisible(self._is_staff)
|
||||
self._open_daq_log_btn.clicked.connect(lambda: self._open_log_file_for_logger("aareDAQ"))
|
||||
top.addWidget(self._open_daq_log_btn)
|
||||
|
||||
# Tabs
|
||||
self._tabs = QTabWidget(self)
|
||||
root.addWidget(self._tabs, 1)
|
||||
|
||||
# Tab: error codes + details pane
|
||||
self._codes_table = QTableWidget(self)
|
||||
self._codes_table.setColumnCount(1)
|
||||
self._codes_table.setHorizontalHeaderLabels(["Name"])
|
||||
self._codes_table.setSortingEnabled(True)
|
||||
self._codes_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
||||
self._codes_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
||||
self._codes_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection)
|
||||
self._codes_table.itemSelectionChanged.connect(self._update_code_details)
|
||||
self._codes_table.horizontalHeader().setStretchLastSection(True)
|
||||
|
||||
self._details_frame = QFrame(self)
|
||||
self._details_frame.setFrameShape(QFrame.Shape.StyledPanel)
|
||||
self._details_frame.setStyleSheet(
|
||||
"QFrame {"
|
||||
" background: #fafafa;"
|
||||
" border: 1px solid #d0d0d0;"
|
||||
" border-radius: 6px;"
|
||||
"}"
|
||||
)
|
||||
|
||||
details_layout = QVBoxLayout(self._details_frame)
|
||||
details_layout.setContentsMargins(10, 10, 10, 10)
|
||||
details_layout.setSpacing(8)
|
||||
|
||||
form = QFormLayout()
|
||||
form.setLabelAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
||||
details_layout.addLayout(form)
|
||||
|
||||
self._detail_name = QLabel("-", self)
|
||||
self._detail_name.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||
form.addRow("Name:", self._detail_name)
|
||||
|
||||
value_row = QHBoxLayout()
|
||||
value_row.setSpacing(8)
|
||||
self._detail_value = QLabel("-", self)
|
||||
self._detail_value.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||
self._copy_value_btn = QPushButton("Copy value", self)
|
||||
self._copy_value_btn.clicked.connect(self._copy_selected_value)
|
||||
value_row.addWidget(self._detail_value, 1)
|
||||
value_row.addWidget(self._copy_value_btn, 0)
|
||||
value_row_widget = QWidget(self)
|
||||
value_row_widget.setLayout(value_row)
|
||||
form.addRow("Value:", value_row_widget)
|
||||
|
||||
self._detail_help = QLabel("-", self)
|
||||
self._detail_help.setWordWrap(True)
|
||||
self._detail_help.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||
self._detail_help.setStyleSheet(
|
||||
"QLabel {"
|
||||
" background: white;"
|
||||
" border: 1px solid #e0e0e0;"
|
||||
" border-radius: 6px;"
|
||||
" padding: 8px;"
|
||||
"}"
|
||||
)
|
||||
details_layout.addWidget(QLabel("Help:", self))
|
||||
details_layout.addWidget(self._detail_help, 1)
|
||||
|
||||
codes_container = QWidget(self)
|
||||
codes_layout = QVBoxLayout(codes_container)
|
||||
codes_layout.setContentsMargins(0, 0, 0, 0)
|
||||
codes_layout.setSpacing(8)
|
||||
codes_layout.addWidget(self._codes_table, 1)
|
||||
codes_layout.addWidget(self._details_frame, 0)
|
||||
|
||||
self._tabs.addTab(codes_container, "Error codes")
|
||||
|
||||
# Tab: last error payload (staff only) + summary header
|
||||
self._payload_summary = QLabel(self)
|
||||
self._payload_summary.setVisible(self._is_staff)
|
||||
self._payload_summary.setWordWrap(True)
|
||||
self._payload_summary.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||
|
||||
self._payload_text = QTextEdit(self)
|
||||
self._payload_text.setReadOnly(True)
|
||||
self._payload_text.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap)
|
||||
|
||||
if self._is_staff:
|
||||
payload_container = QWidget(self)
|
||||
payload_layout = QVBoxLayout(payload_container)
|
||||
payload_layout.setContentsMargins(0, 0, 0, 0)
|
||||
payload_layout.addWidget(self._payload_summary, 0)
|
||||
payload_layout.addWidget(self._payload_text, 1)
|
||||
self._tabs.addTab(payload_container, "Last error payload")
|
||||
else:
|
||||
self._payload_text.setPlainText("Hidden (staff only).")
|
||||
|
||||
# Tab: recent payloads (staff only)
|
||||
self._payloads_text = QTextEdit(self)
|
||||
self._payloads_text.setReadOnly(True)
|
||||
self._payloads_text.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap)
|
||||
if self._is_staff:
|
||||
payloads_container = QWidget(self)
|
||||
payloads_layout = QVBoxLayout(payloads_container)
|
||||
payloads_layout.setContentsMargins(0, 0, 0, 0)
|
||||
payloads_layout.addWidget(self._payloads_text, 1)
|
||||
self._tabs.addTab(payloads_container, "Recent payloads")
|
||||
|
||||
# Tab: tracebacks (staff only)
|
||||
self._tracebacks_text = QTextEdit(self)
|
||||
self._tracebacks_text.setReadOnly(True)
|
||||
self._tracebacks_text.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap)
|
||||
self._tracebacks_text.setPlainText("Tracebacks will appear here (last 10).")
|
||||
|
||||
if self._is_staff:
|
||||
tb_container = QWidget(self)
|
||||
tb_layout = QVBoxLayout(tb_container)
|
||||
tb_layout.setContentsMargins(0, 0, 0, 0)
|
||||
tb_layout.addWidget(self._tracebacks_text, 1)
|
||||
self._tabs.addTab(tb_container, "Tracebacks")
|
||||
|
||||
# Tab: error log (staff only) – live view from python logging
|
||||
self._error_log_text = QTextEdit(self)
|
||||
self._error_log_text.setReadOnly(True)
|
||||
self._error_log_text.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap)
|
||||
if self._is_staff:
|
||||
log_container = QWidget(self)
|
||||
log_layout = QVBoxLayout(log_container)
|
||||
log_layout.setContentsMargins(0, 0, 0, 0)
|
||||
log_layout.addWidget(self._error_log_text, 1)
|
||||
self._tabs.addTab(log_container, "Error log")
|
||||
|
||||
|
||||
# Bottom button box
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close, parent=self)
|
||||
buttons.rejected.connect(self.reject)
|
||||
buttons.accepted.connect(self.accept)
|
||||
root.addWidget(buttons)
|
||||
|
||||
# Wire signals
|
||||
self._daq.error_codes_loaded.connect(self.set_error_codes)
|
||||
self._daq.last_error_payload_changed.connect(self.set_last_error_payload)
|
||||
self._daq.last_error_payloads_changed.connect(self.set_last_error_payloads)
|
||||
|
||||
# Hook a Qt logging handler to show ERROR+ messages in the dialog
|
||||
if self._is_staff:
|
||||
self._qt_log_emitter = QtLogEmitter()
|
||||
self._qt_log_emitter.message.connect(self._append_error_log_line)
|
||||
|
||||
self._qt_log_handler = QtLogHandler(self._qt_log_emitter)
|
||||
self._qt_log_handler.setLevel(logging.ERROR)
|
||||
self._qt_log_handler.setFormatter(find_existing_formatter())
|
||||
|
||||
attach_to_logger("aareGUI", self._qt_log_handler)
|
||||
attach_to_logger("aareDAQ", self._qt_log_handler)
|
||||
|
||||
self._update_banner()
|
||||
self._update_code_details()
|
||||
|
||||
def _open_log_file_for_logger(self, logger_name: str) -> None:
|
||||
"""
|
||||
Opens the first FileHandler path attached to the given logger name.
|
||||
"""
|
||||
log = logging.getLogger(logger_name)
|
||||
paths: list[str] = []
|
||||
for h in getattr(log, "handlers", []) or []:
|
||||
p = getattr(h, "baseFilename", None)
|
||||
if p and isinstance(p, str):
|
||||
paths.append(p)
|
||||
|
||||
if not paths:
|
||||
self._banner.setText(f"No file handler found for logger '{logger_name}'.")
|
||||
return
|
||||
|
||||
QDesktopServices.openUrl(QUrl.fromLocalFile(paths[0]))
|
||||
|
||||
@Slot(bool)
|
||||
def _set_freeze_payload(self, enabled: bool) -> None:
|
||||
self._freeze_payload = bool(enabled)
|
||||
|
||||
@Slot(bool)
|
||||
def _set_always_highlight(self, enabled: bool) -> None:
|
||||
self._always_highlight_last_error = bool(enabled)
|
||||
|
||||
@Slot()
|
||||
def refresh(self) -> None:
|
||||
self._daq.get_error_codes()
|
||||
self.set_last_error_payload(self._daq.get_last_error_payload())
|
||||
if self._is_staff:
|
||||
self.set_last_error_payloads(self._daq.get_last_error_payloads())
|
||||
|
||||
@Slot(dict)
|
||||
def set_error_codes(self, codes: dict) -> None:
|
||||
self._codes = {str(k): str(v) for k, v in (codes or {}).items()}
|
||||
self._apply_filter()
|
||||
|
||||
@Slot(dict)
|
||||
def set_last_error_payload(self, payload: dict) -> None:
|
||||
if not self._is_staff:
|
||||
return
|
||||
if self._freeze_payload:
|
||||
return
|
||||
|
||||
self._last_payload = payload or {}
|
||||
pretty = json.dumps(self._last_payload, indent=2, sort_keys=True, default=str)
|
||||
self._payload_text.setPlainText(pretty)
|
||||
|
||||
code, msg = self._extract_code_message()
|
||||
url = str((self._last_payload or {}).get("url") or "")
|
||||
status = (self._last_payload or {}).get("http_status")
|
||||
self._payload_summary.setText(
|
||||
f"URL: {url}\nHTTP: {status}\nCode: {code or '-'}\nMessage: {msg or '-'}"
|
||||
)
|
||||
|
||||
self._update_banner()
|
||||
self._select_code_from_last_error()
|
||||
|
||||
@Slot(list)
|
||||
def set_last_error_payloads(self, payloads: list) -> None:
|
||||
if not self._is_staff:
|
||||
return
|
||||
blocks: list[str] = []
|
||||
for i, p in enumerate(payloads[-10:], start=max(1, len(payloads) - 9)):
|
||||
try:
|
||||
pretty = json.dumps(p or {}, indent=2, sort_keys=True, default=str)
|
||||
except Exception:
|
||||
pretty = str(p)
|
||||
blocks.append(f"#{i}\n{pretty}")
|
||||
self._payloads_text.setPlainText("\n\n".join(blocks) if blocks else "(none captured yet)")
|
||||
|
||||
@Slot(str)
|
||||
def _append_error_log_line(self, line: str) -> None:
|
||||
if not self._is_staff:
|
||||
return
|
||||
self._error_log_text.append(line)
|
||||
|
||||
def _code_name_for_value(self, value: str) -> str | None:
|
||||
for k, v in self._codes.items():
|
||||
if v == value:
|
||||
return k
|
||||
return None
|
||||
|
||||
def _extract_code_message(self) -> tuple[str | None, str | None]:
|
||||
body = (self._last_payload or {}).get("body_json")
|
||||
if isinstance(body, dict):
|
||||
code = body.get("code")
|
||||
msg = body.get("message")
|
||||
return (str(code) if code is not None else None, str(msg) if msg is not None else None)
|
||||
return (None, None)
|
||||
|
||||
def _update_banner(self) -> None:
|
||||
if not self._is_staff:
|
||||
return
|
||||
|
||||
code, msg = self._extract_code_message()
|
||||
if code or msg:
|
||||
parts = []
|
||||
if code:
|
||||
parts.append(f"Last error: {code}")
|
||||
if msg:
|
||||
parts.append(msg)
|
||||
self._banner.setText(" — ".join(parts))
|
||||
else:
|
||||
self._banner.setText("Last error: (none captured yet)")
|
||||
|
||||
def _code_name_for_value(self, value: str) -> str | None:
|
||||
for name, v in (self._codes or {}).items():
|
||||
if str(v) == str(value):
|
||||
return str(name)
|
||||
return None
|
||||
|
||||
def _select_code_by_name(self, name: str) -> None:
|
||||
if not name:
|
||||
return
|
||||
for row in range(self._codes_table.rowCount()):
|
||||
item = self._codes_table.item(row, 0)
|
||||
if item and item.text() == name:
|
||||
self._codes_table.setCurrentCell(row, 0)
|
||||
self._codes_table.scrollToItem(item)
|
||||
return
|
||||
|
||||
def _select_code_from_last_error(self) -> None:
|
||||
if not self._codes:
|
||||
return
|
||||
code_value, _ = self._extract_code_message()
|
||||
if not code_value:
|
||||
return
|
||||
name = self._code_name_for_value(code_value)
|
||||
if name:
|
||||
self._select_code_by_name(name)
|
||||
|
||||
@Slot()
|
||||
def _apply_filter(self) -> None:
|
||||
term = (self._filter.text() or "").strip().lower()
|
||||
|
||||
items = sorted(self._codes.items(), key=lambda kv: kv[0])
|
||||
|
||||
if term:
|
||||
def _match(name: str, value: str) -> bool:
|
||||
h = error_code_help(value) or ""
|
||||
return term in f"{name}\n{value}\n{h}".lower()
|
||||
items = [(k, v) for (k, v) in items if _match(k, v)]
|
||||
|
||||
self._codes_table.setRowCount(len(items))
|
||||
for row, (k, v) in enumerate(items):
|
||||
item = QTableWidgetItem(k)
|
||||
item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
||||
self._codes_table.setItem(row, 0, item)
|
||||
|
||||
self._codes_table.resizeColumnsToContents()
|
||||
# Keep selection sensible (don’t resize columns here; avoids jitter)
|
||||
if self._codes_table.rowCount() > 0 and self._codes_table.currentRow() < 0:
|
||||
self._codes_table.setCurrentCell(0, 0)
|
||||
|
||||
self._update_code_details()
|
||||
|
||||
# If enabled, re-try selection after the table content changes
|
||||
if self._is_staff and self._always_highlight_last_error and not self._freeze_payload:
|
||||
self._select_code_from_last_error()
|
||||
|
||||
def _selected_code(self) -> tuple[str | None, str | None]:
|
||||
row = self._codes_table.currentRow()
|
||||
if row < 0:
|
||||
return (None, None)
|
||||
name_item = self._codes_table.item(row, 0)
|
||||
if not name_item:
|
||||
return (None, None)
|
||||
name = name_item.text()
|
||||
value = self._codes.get(name)
|
||||
return (name, value)
|
||||
|
||||
@Slot()
|
||||
def _update_code_details(self) -> None:
|
||||
name, value = self._selected_code()
|
||||
if not name or not value:
|
||||
self._detail_name.setText("-")
|
||||
self._detail_value.setText("-")
|
||||
self._detail_help.setText("Select an error code to see details.")
|
||||
self._copy_value_btn.setEnabled(False)
|
||||
return
|
||||
|
||||
self._detail_name.setText(name)
|
||||
self._detail_value.setText(value)
|
||||
self._detail_help.setText(error_code_help(value) or "(no help text defined yet)")
|
||||
self._copy_value_btn.setEnabled(True)
|
||||
|
||||
@Slot()
|
||||
def _copy_selected_code(self) -> None:
|
||||
name, value = self._selected_code()
|
||||
if not name or not value:
|
||||
QGuiApplication.clipboard().setText("")
|
||||
return
|
||||
QGuiApplication.clipboard().setText(f"{name}={value}")
|
||||
|
||||
@Slot()
|
||||
def _copy_selected_value(self) -> None:
|
||||
_, value = self._selected_code()
|
||||
QGuiApplication.clipboard().setText(value or "")
|
||||
|
||||
@Slot()
|
||||
def _copy_all_filtered(self) -> None:
|
||||
rows = self._codes_table.rowCount()
|
||||
out = {}
|
||||
for r in range(rows):
|
||||
name = self._codes_table.item(r, 0).text()
|
||||
out[name] = self._codes.get(name, "")
|
||||
QGuiApplication.clipboard().setText(json.dumps(out, indent=2, sort_keys=True))
|
||||
|
||||
@Slot()
|
||||
def _copy_payload(self) -> None:
|
||||
if not self._is_staff:
|
||||
return
|
||||
QGuiApplication.clipboard().setText(self._payload_text.toPlainText())
|
||||
+54
-33
@@ -1,18 +1,17 @@
|
||||
# Python
|
||||
from PySide6.QtCore import Slot, Qt, Signal
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QLabel, QFrame, QPushButton, QSpacerItem, QSizePolicy, QVBoxLayout, \
|
||||
QHBoxLayout
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QLabel, QPushButton, QVBoxLayout
|
||||
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
||||
from matplotlib.figure import Figure
|
||||
import numpy as np
|
||||
|
||||
from aaregui.widgets.number_line_edit import NumberLineEdit
|
||||
from aaregui.widgets.title_label import TitleLabel
|
||||
from aare.gui.widgets.number_line_edit import NumberLineEdit
|
||||
from aare.gui.widgets.title_label import TitleLabel
|
||||
|
||||
from aaredaqlib.logger_config import setup_logger
|
||||
from aare.common.logger_config import setup_logger
|
||||
|
||||
logger = setup_logger("aareGUI")
|
||||
|
||||
|
||||
class FaceDetectionPanel(QWidget):
|
||||
|
||||
face_detection = Signal(int, int)
|
||||
@@ -21,6 +20,7 @@ class FaceDetectionPanel(QWidget):
|
||||
super().__init__(parent)
|
||||
self.steps = 14
|
||||
self.step_size = 15
|
||||
self._manual_run_requested = False
|
||||
|
||||
def _set_steps(val: float):
|
||||
self.steps = int(val)
|
||||
@@ -28,29 +28,31 @@ class FaceDetectionPanel(QWidget):
|
||||
def _set_step_size(val: float):
|
||||
self.step_size = int(val)
|
||||
|
||||
self.fig = Figure(figsize=(5, 4), tight_layout=True)
|
||||
self.fig = Figure(figsize=(5, 4))
|
||||
self.fig.subplots_adjust(
|
||||
left=0.12,
|
||||
right=0.97,
|
||||
bottom=0.10,
|
||||
top=0.95,
|
||||
hspace=0.45,
|
||||
)
|
||||
self.canvas = FigureCanvas(self.fig)
|
||||
self.ax1 = self.fig.add_subplot(2, 1, 1) # Height vs angle
|
||||
self.ax2 = self.fig.add_subplot(2, 1, 2) # Area vs angle
|
||||
self.ax1 = self.fig.add_subplot(2, 1, 1)
|
||||
self.ax2 = self.fig.add_subplot(2, 1, 2)
|
||||
|
||||
self._top_layout = QGridLayout()
|
||||
self._top_layout.addWidget(TitleLabel("TELL sample changer", self), 0, 0, 1, 3)
|
||||
self.status_lbl = QLabel("")
|
||||
self.status_lbl = QLabel("Idle")
|
||||
self._top_layout.addWidget(self.status_lbl, 0, 2)
|
||||
|
||||
self._top_layout.addWidget(QLabel("step size", parent=self), 1, 0)
|
||||
self.step_size_enter = NumberLineEdit(
|
||||
0, 50, 15, decimals=4, parent=self
|
||||
)
|
||||
|
||||
self.step_size_enter = NumberLineEdit(0, 50, 15, decimals=4, parent=self)
|
||||
self._top_layout.addWidget(self.step_size_enter, 1, 1, 1, 3)
|
||||
self.step_size_enter.newValue.connect(_set_step_size)
|
||||
self._top_layout.addWidget(QLabel("°", parent=self), 1, 4)
|
||||
|
||||
self._top_layout.addWidget(QLabel("number of steps", parent=self), 2, 0)
|
||||
self.steps_enter = NumberLineEdit(
|
||||
0, 50, 14, decimals=4, parent=self
|
||||
)
|
||||
self.steps_enter = NumberLineEdit(0, 50, 14, decimals=4, parent=self)
|
||||
self._top_layout.addWidget(self.steps_enter, 2, 1, 1, 3)
|
||||
self._top_layout.addWidget(QLabel("", parent=self), 2, 4)
|
||||
self.steps_enter.newValue.connect(_set_steps)
|
||||
@@ -70,49 +72,68 @@ class FaceDetectionPanel(QWidget):
|
||||
|
||||
def run_and_refresh(self):
|
||||
try:
|
||||
self._manual_run_requested = True
|
||||
self.status_lbl.setText("Starting...")
|
||||
self.face_detection_button.setEnabled(False)
|
||||
self.face_detection.emit(int(self.steps), int(self.step_size))
|
||||
except Exception as e:
|
||||
self._manual_run_requested = False
|
||||
self.status_lbl.setText(f"Error: {e}")
|
||||
self.face_detection_button.setEnabled(True)
|
||||
|
||||
def update_plot(self, data):
|
||||
samples = data.get("samples", [])
|
||||
print(samples)
|
||||
samples = data.get("samples", []) or []
|
||||
running = bool(data.get("running", False))
|
||||
angle = data.get("current_angle_deg")
|
||||
status = data.get("status", "")
|
||||
|
||||
if running:
|
||||
if self._manual_run_requested:
|
||||
self.status_lbl.setText(f"Running... angle {angle}" if angle is not None else "Running...")
|
||||
else:
|
||||
self.status_lbl.setText(f"Automation running... angle {angle}" if angle is not None else "Automation running...")
|
||||
else:
|
||||
if self._manual_run_requested:
|
||||
self.status_lbl.setText("Done")
|
||||
self.face_detection_button.setEnabled(True)
|
||||
self._manual_run_requested = False
|
||||
elif samples:
|
||||
self.status_lbl.setText("Showing latest result")
|
||||
else:
|
||||
self.status_lbl.setText("Idle")
|
||||
|
||||
self.ax1.clear()
|
||||
self.ax2.clear()
|
||||
|
||||
if not samples:
|
||||
self.ax1.clear()
|
||||
self.ax2.clear()
|
||||
self.ax1.text(0.5, 0.5, "No data", ha="center", va="center")
|
||||
self.ax2.text(0.5, 0.5, "No data", ha="center", va="center")
|
||||
self.canvas.draw_idle()
|
||||
logger.info("No data")
|
||||
return
|
||||
|
||||
angles = np.array([s["angle_deg"] for s in samples], dtype=float)
|
||||
heights = np.array([s["height"] for s in samples], dtype=float)
|
||||
areas = np.array([s["area"] for s in samples], dtype=float)
|
||||
self.ax1.clear()
|
||||
self.ax2.clear()
|
||||
|
||||
self.ax1.scatter(angles, heights, s=16, c="tab:blue", label="Height")
|
||||
self.ax2.scatter(angles, areas, s=16, c="tab:green", label="Area")
|
||||
|
||||
ang_grid = np.linspace(angles.min(),angles.max(), 400)
|
||||
#ang_grid_wrapped = ((ang_grid + 180) % 360) - 180
|
||||
hf = data.get("height_fit", {})
|
||||
ang_grid = np.linspace(angles.min(), angles.max(), 400)
|
||||
|
||||
hf = data.get("height_fit", {}) or {}
|
||||
if {"A", "B", "phi_rad", "C"} <= hf.keys():
|
||||
A, B, phi, C = hf["A"], hf["B"], hf["phi_rad"], hf["C"]
|
||||
height_fit = A + B * np.cos(C*np.deg2rad(ang_grid) - phi)
|
||||
height_fit = A + B * np.cos(C * np.deg2rad(ang_grid) - phi)
|
||||
self.ax1.plot(ang_grid, height_fit, color="tab:orange", label="Height fit")
|
||||
if "best_angle_deg" in hf:
|
||||
logger.info(f"best angle: {hf['best_angle_deg']}")
|
||||
self.ax1.axvline(hf["best_angle_deg"], color="tab:orange", ls="--", alpha=0.6)
|
||||
|
||||
af = data.get("area_fit", {})
|
||||
if {"A", "B", "phi_rad", "C"} <= hf.keys():
|
||||
af = data.get("area_fit", {}) or {}
|
||||
if {"A", "B", "phi_rad", "C"} <= af.keys():
|
||||
A2, B2, phi2, C2 = af["A"], af["B"], af["phi_rad"], af["C"]
|
||||
area_fit = A2 + B2 * np.cos(C2 * np.deg2rad(ang_grid) - phi2)
|
||||
self.ax2.plot(ang_grid, area_fit, color="tab:red", label="Area fit")
|
||||
if "best_angle_deg" in af:
|
||||
logger.info(f"best angle: {af['best_angle_deg']}")
|
||||
self.ax2.axvline(af["best_angle_deg"], color="tab:red", ls="--", alpha=0.6)
|
||||
|
||||
self.ax1.set_xlabel("Angle (deg)")
|
||||
@@ -1,13 +1,12 @@
|
||||
import copy
|
||||
import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import Signal, Slot, Qt
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QLabel, QLineEdit, QSpinBox, QCheckBox, QMessageBox
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QLabel, QLineEdit, QSpinBox, QMessageBox
|
||||
|
||||
from aaredaqlib.models import SampleShortInfo, DAQStatusModel
|
||||
from aaregui.widgets.title_label import TitleLabel
|
||||
from aare.common.models import SampleShortInfo, DAQStatusModel
|
||||
from aare.gui.widgets.title_label import TitleLabel
|
||||
|
||||
## Logic for filenames:
|
||||
## 1. For rasters 'raster/' subfolder is added at the top level of the path (managed by DAQ) - e.g. raster/20250101/PX-456/01/dataset
|
||||
@@ -183,6 +182,7 @@ class FilePathPanel(QWidget):
|
||||
self.__puck_pos = 99
|
||||
self.directory_edit.setText(f"{self.__formatted_date}/test")
|
||||
else:
|
||||
self.__sample_id = sample.db_id
|
||||
self.__sample_name = sample.sample_name
|
||||
self.__dewar_pos = sample.loc_str()
|
||||
self.__puck_name = sample.puck_name
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
from PySide6.QtCore import Signal, Slot
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QLabel, QPushButton, QCheckBox
|
||||
|
||||
from aaredaqlib.models import FluorescenceSpectrumParameterModel
|
||||
from aaregui.widgets.number_line_edit import NumberLineEdit
|
||||
from aare.common.models import FluorescenceSpectrumParameterModel
|
||||
from aare.gui.widgets.number_line_edit import NumberLineEdit
|
||||
|
||||
|
||||
class FluorescenceDataCollectionPanel(QWidget):
|
||||
+3
-3
@@ -1,11 +1,11 @@
|
||||
import numpy as np
|
||||
from PySide6.QtCharts import QChart, QChartView, QLineSeries, QValueAxis
|
||||
from PySide6.QtCore import QPointF, Qt, Slot, Signal, QEvent
|
||||
from PySide6.QtCore import QPointF, Qt, Slot, QEvent
|
||||
from PySide6.QtGui import QPainter, QColor, QPen
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QGraphicsSimpleTextItem, QLabel
|
||||
|
||||
from aaredaqlib.logger_config import setup_logger
|
||||
from aaredaqlib.models import FluorescenceSpectrumOutputModel, DAQStatusModel
|
||||
from aare.common.logger_config import setup_logger
|
||||
from aare.common.models import FluorescenceSpectrumOutputModel, DAQStatusModel
|
||||
|
||||
logger = setup_logger("aareGUI")
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
from PySide6.QtCore import Qt, Signal, Slot
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QSlider, QLabel
|
||||
from aare.common.models import DAQStatusModel
|
||||
|
||||
from aare.gui.widgets.title_label import TitleLabel
|
||||
|
||||
|
||||
class IlluminationPanel(QWidget):
|
||||
front_light = Signal(int)
|
||||
back_light = Signal(int)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
grid_layout = QGridLayout(self)
|
||||
|
||||
grid_layout.addWidget(TitleLabel("Light", self), 0, 0, 1, 2)
|
||||
|
||||
front_label = QLabel("Front light", parent=self)
|
||||
front_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
grid_layout.addWidget(front_label, 1, 0, 1, 2)
|
||||
self.is_sliding = False
|
||||
|
||||
self.front_light_slider = QSlider(orientation=Qt.Orientation.Horizontal, parent=self)
|
||||
self.front_light_slider.setRange(0, 100)
|
||||
self.front_light_slider.sliderPressed.connect(self.on_slider_pressed)
|
||||
self.front_light_slider.sliderReleased.connect(self.on_front_slider_released)
|
||||
grid_layout.addWidget(self.front_light_slider, 2, 0, 1, 2)
|
||||
|
||||
back_label = QLabel("Back light", parent=self)
|
||||
back_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
grid_layout.addWidget(back_label, 3, 0, 1, 2)
|
||||
|
||||
self.back_light_slider = QSlider(orientation=Qt.Orientation.Horizontal, parent=self)
|
||||
self.back_light_slider.setRange(0, 100)
|
||||
self.back_light_slider.sliderPressed.connect(self.on_slider_pressed)
|
||||
self.back_light_slider.sliderReleased.connect(self.on_back_slider_released)
|
||||
grid_layout.addWidget(self.back_light_slider, 4, 0, 1, 2)
|
||||
|
||||
@Slot()
|
||||
def on_slider_pressed(self):
|
||||
self.is_sliding = True
|
||||
|
||||
@Slot()
|
||||
def on_front_slider_released(self):
|
||||
self.is_sliding = False
|
||||
self.front_light.emit(self.front_light_slider.value())
|
||||
|
||||
@Slot()
|
||||
def on_back_slider_released(self):
|
||||
self.is_sliding = False
|
||||
self.back_light.emit(self.back_light_slider.value())
|
||||
|
||||
@Slot(DAQStatusModel)
|
||||
def update_daq_status(self, s: DAQStatusModel):
|
||||
if not self.is_sliding: # Update only if not sliding
|
||||
self.front_light_slider.setValue(round(s.bl.front_light))
|
||||
self.back_light_slider.setValue(round(s.bl.back_light))
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QPushButton
|
||||
|
||||
from aaregui.widgets.title_label import TitleLabel
|
||||
from aare.gui.widgets.title_label import TitleLabel
|
||||
|
||||
|
||||
class LoopCenteringPanel(QWidget):
|
||||
+5
-5
@@ -1,10 +1,10 @@
|
||||
from PySide6.QtCore import Signal, Slot
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QLabel, QTextEdit, QPushButton, QCheckBox, QLineEdit
|
||||
from aareDBclient import DataCollectionParameters
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QLabel, QPushButton, QCheckBox, QLineEdit
|
||||
from aareDB import DataCollectionParameters
|
||||
|
||||
from aaredaqlib.models import SampleShortInfo, DAQStatusModel
|
||||
from aaregui.widgets.number_line_edit import NumberLineEdit
|
||||
from aaregui.widgets.title_label import TitleLabel
|
||||
from aare.common.models import SampleShortInfo, DAQStatusModel
|
||||
from aare.gui.widgets.number_line_edit import NumberLineEdit
|
||||
from aare.gui.widgets.title_label import TitleLabel
|
||||
|
||||
|
||||
class ManualSamplePanel(QWidget):
|
||||
@@ -1,10 +1,10 @@
|
||||
from PySide6.QtCore import Signal, Slot
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QLabel, QHBoxLayout
|
||||
from aaredaqlib.models import DAQStatusModel
|
||||
from aare.common.models import DAQStatusModel
|
||||
|
||||
from aaregui.widgets.button_with_payload import ButtonWithPayload
|
||||
from aaregui.widgets.number_line_edit import NumberLineEdit
|
||||
from aaregui.widgets.title_label import TitleLabel
|
||||
from aare.gui.widgets.button_with_payload import ButtonWithPayload
|
||||
from aare.gui.widgets.number_line_edit import NumberLineEdit
|
||||
from aare.gui.widgets.title_label import TitleLabel
|
||||
|
||||
|
||||
class OmegaEntryWidget(QWidget):
|
||||
@@ -21,6 +21,7 @@ class OmegaEntryWidget(QWidget):
|
||||
|
||||
class OmegaPanel(QWidget):
|
||||
set_omega = Signal(float)
|
||||
set_omega_rel = Signal(float)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -57,7 +58,7 @@ class OmegaPanel(QWidget):
|
||||
@Slot(dict)
|
||||
def omega_button_pressed(self, payload: dict):
|
||||
if "rel" in payload:
|
||||
self.set_omega.emit(self.__omega + payload["rel"])
|
||||
self.set_omega_rel.emit(payload["rel"])
|
||||
elif "abs" in payload:
|
||||
self.set_omega.emit(payload["abs"])
|
||||
|
||||
+8
-9
@@ -1,14 +1,13 @@
|
||||
from PySide6.QtCore import Signal, Slot, Qt
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QLabel, QSizePolicy, QSpacerItem, QPushButton, QHBoxLayout, \
|
||||
QComboBox, QSlider, QMessageBox
|
||||
from PySide6.QtWidgets import QLabel, QSizePolicy, QSpacerItem, QPushButton, QComboBox, QSlider, QMessageBox
|
||||
|
||||
from aaredaqlib.diffraction_geometry import DiffractionGeometry
|
||||
from aaredaqlib.models import DAQStatusModel, BeamlineStateEnum
|
||||
from aaregui.panels.scan_settings_panel import ScanSettingsPanel
|
||||
from aaregui.scan_logic.raster_grid_manager import RasterGridManager, RasterGridMetric
|
||||
from aaregui.widgets.number_line_edit import NumberLineEdit, CheckedLineEdit
|
||||
from aaregui.widgets.raster_grid_table import RasterGridTable
|
||||
from aaredaqlib.logger_config import setup_logger
|
||||
from aare.common.diffraction_geometry import DiffractionGeometry
|
||||
from aare.common.models import DAQStatusModel, BeamlineStateEnum
|
||||
from aare.gui.panels.scan_settings_panel import ScanSettingsPanel
|
||||
from aare.gui.scan_logic.raster_grid_manager import RasterGridManager, RasterGridMetric
|
||||
from aare.gui.widgets.number_line_edit import CheckedLineEdit
|
||||
from aare.gui.widgets.raster_grid_table import RasterGridTable
|
||||
from aare.common.logger_config import setup_logger
|
||||
|
||||
logger = setup_logger("aareGUI")
|
||||
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
# reference_tools_panel.py
|
||||
from typing import Callable, List, Dict, Optional
|
||||
from typing import Optional
|
||||
|
||||
from PySide6.QtCore import Qt, QAbstractTableModel, QModelIndex, QTimer, Slot, Signal
|
||||
from PySide6.QtCore import Qt, QAbstractTableModel, QModelIndex, Slot, Signal
|
||||
from PySide6.QtGui import QBrush, QColor
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame,
|
||||
@@ -14,9 +14,9 @@ from PySide6.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
)
|
||||
|
||||
from aaredaqlib.logger_config import setup_logger
|
||||
from aaredaqlib.models import SampleShortInfoList, SampleShortInfo, BeamlineStateEnum, DAQStatusModel
|
||||
from aaregui.widgets.title_label import TitleLabel
|
||||
from aare.common.logger_config import setup_logger
|
||||
from aare.common.models import SampleShortInfoList, SampleShortInfo, DAQStatusModel
|
||||
from aare.gui.widgets.title_label import TitleLabel
|
||||
|
||||
logger = setup_logger("aareGUI")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user