# Conflicts: # common/pyproject.toml # daq/src/aaredaq/spreadsheetupdater.py # gui/pyproject.toml # pyproject.toml # src/aare/daq/tellupdater.py
This commit is contained in:
commit
67d682c39c
143 files changed
+9252
-7262
No files matched your search
@@ -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
|
||||
File renamed without changes.
@@ -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]
|
||||
@@ -1,804 +0,0 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"id": "initial_id",
|
||||
"metadata": {
|
||||
"collapsed": true,
|
||||
"ExecuteTime": {
|
||||
"end_time": "2025-09-09T06:54:20.171830792Z",
|
||||
"start_time": "2025-09-03T09:37:36.977231Z"
|
||||
}
|
||||
},
|
||||
"source": [
|
||||
"import cv2\n",
|
||||
"import numpy as np\n",
|
||||
"import matplotlib.pyplot as plt\n",
|
||||
"import seaborn as sns"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": 47
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2025-09-03T09:20:49.172543Z",
|
||||
"start_time": "2025-09-03T09:20:49.169238Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": "threshold = 5",
|
||||
"id": "19360e6dda22efb5",
|
||||
"outputs": [],
|
||||
"execution_count": 30
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"jupyter": {
|
||||
"is_executing": true
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"bkg = cv2.imread(\"/home/leonarski_f/aaredaq/daq/src/aaredaq/bkg1_50_50.jpg\")\n",
|
||||
"curr_img = cv2.imread(\"/home/leonarski_f/aaredaq/daq/src/aaredaq/1002_0_1_50_50_curr_image.tiff\")"
|
||||
],
|
||||
"id": "9fac6cc561e58b70",
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2025-09-05T11:27:26.501898Z",
|
||||
"start_time": "2025-09-05T11:27:26.496995Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"def flatfield_correction(raw_image, flat_image, dark_image=None):\n",
|
||||
" \"\"\"\n",
|
||||
" Apply flat-field correction to remove uneven illumination\n",
|
||||
"\n",
|
||||
" Args:\n",
|
||||
" raw_image: Your actual image with crystal\n",
|
||||
" flat_image: Image of uniform illumination (no sample)\n",
|
||||
" dark_image: Dark frame (camera with no light), optional\n",
|
||||
" \"\"\"\n",
|
||||
" # Convert to float for calculations\n",
|
||||
" raw = raw_image.astype(np.float32)\n",
|
||||
" flat = flat_image.astype(np.float32)\n",
|
||||
"\n",
|
||||
" if dark_image is not None:\n",
|
||||
" dark = dark_image.astype(np.float32)\n",
|
||||
" # Subtract dark frame from both\n",
|
||||
" raw_corrected = raw - dark\n",
|
||||
" flat_corrected = flat - dark\n",
|
||||
" else:\n",
|
||||
" raw_corrected = raw\n",
|
||||
" flat_corrected = flat\n",
|
||||
"\n",
|
||||
" # Avoid division by zero\n",
|
||||
" flat_corrected[flat_corrected == 0] = 1\n",
|
||||
"\n",
|
||||
" # Apply correction\n",
|
||||
" mean_flat = np.mean(flat_corrected)\n",
|
||||
" corrected = (raw_corrected / flat_corrected) * mean_flat\n",
|
||||
"\n",
|
||||
" # Convert back to original dtype\n",
|
||||
" return np.clip(corrected, 0, 255).astype(raw_image.dtype)"
|
||||
],
|
||||
"id": "114345da7d31b16d",
|
||||
"outputs": [],
|
||||
"execution_count": 139
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2025-09-05T11:43:24.969687Z",
|
||||
"start_time": "2025-09-05T11:43:24.954049Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"def create_ring_mask(image, center=None, inner_radius=50, outer_radius=100):\n",
|
||||
" \"\"\"\n",
|
||||
" Create a mask to exclude a ring region\n",
|
||||
"\n",
|
||||
" Args:\n",
|
||||
" image_shape: (height, width) of your image\n",
|
||||
" center: (cx, cy) center of ring, None for image center\n",
|
||||
" inner_radius: Inner radius of ring to mask out\n",
|
||||
" outer_radius: Outer radius of ring to mask out\n",
|
||||
"\n",
|
||||
" Returns:\n",
|
||||
" mask: Binary mask (0 = exclude ring, 255 = use for correction)\n",
|
||||
" \"\"\"\n",
|
||||
"\n",
|
||||
" if len(image.shape) == 3:\n",
|
||||
" h, w, _ = image.shape\n",
|
||||
" else:\n",
|
||||
" h, w = image.shape\n",
|
||||
"\n",
|
||||
"\n",
|
||||
" if center is None:\n",
|
||||
" center = (w // 2, h // 2)\n",
|
||||
"\n",
|
||||
" # Create coordinate arrays\n",
|
||||
" cx, cy = center\n",
|
||||
"\n",
|
||||
" w = int(w)\n",
|
||||
" h = int(h)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
" x = np.arange(w)\n",
|
||||
" y = np.arange(h)\n",
|
||||
" X, Y = np.meshgrid(x, y)\n",
|
||||
"\n",
|
||||
" # Calculate distances from center\n",
|
||||
" distances = np.sqrt((X - cx)**2 + (Y - cy)**2)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
" # Create mask: exclude the ring region\n",
|
||||
" mask = np.ones((h, w), dtype=np.uint8) * 255\n",
|
||||
" ring_region = (distances >= inner_radius) & (distances <= outer_radius)\n",
|
||||
" mask[ring_region] = 0\n",
|
||||
"\n",
|
||||
" return mask\n",
|
||||
"\n",
|
||||
"def flatfield_correction_with_mask(raw_image, flat_image, mask, dark_image=None):\n",
|
||||
" \"\"\"\n",
|
||||
" Apply flat-field correction with mask to exclude certain regions\n",
|
||||
"\n",
|
||||
" Args:\n",
|
||||
" raw_image: Your camera image\n",
|
||||
" flat_image: Flat-field reference\n",
|
||||
" mask: Binary mask (0 = ignore, 255 = use)\n",
|
||||
" dark_image: Dark frame (optional)\n",
|
||||
" \"\"\"\n",
|
||||
" # Convert to float\n",
|
||||
" raw = raw_image.astype(np.float32)\n",
|
||||
" flat = flat_image.astype(np.float32)\n",
|
||||
"\n",
|
||||
" if dark_image is not None:\n",
|
||||
" dark = dark_image.astype(np.float32)\n",
|
||||
" raw_corrected = raw - dark\n",
|
||||
" flat_corrected = flat - dark\n",
|
||||
" else:\n",
|
||||
" raw_corrected = raw\n",
|
||||
" flat_corrected = flat\n",
|
||||
"\n",
|
||||
" # Apply mask to flat field - only use unmasked regions for correction\n",
|
||||
" mask_float = mask.astype(np.float32) / 255.0\n",
|
||||
"\n",
|
||||
" # Calculate mean only from unmasked regions\n",
|
||||
" if len(flat_corrected.shape) == 3:\n",
|
||||
" # Color image\n",
|
||||
" mean_flat = []\n",
|
||||
" for channel in range(flat_corrected.shape[2]):\n",
|
||||
" masked_flat = flat_corrected[:,:,channel] * mask_float\n",
|
||||
" valid_pixels = masked_flat[mask > 0]\n",
|
||||
" mean_flat.append(np.mean(valid_pixels) if len(valid_pixels) > 0 else 1.0)\n",
|
||||
" mean_flat = np.array(mean_flat).reshape(1, 1, -1)\n",
|
||||
" else:\n",
|
||||
" # Grayscale\n",
|
||||
" masked_flat = flat_corrected * mask_float\n",
|
||||
" valid_pixels = masked_flat[mask > 0]\n",
|
||||
" mean_flat = np.mean(valid_pixels) if len(valid_pixels) > 0 else 1.0\n",
|
||||
"\n",
|
||||
" # Avoid division by zero\n",
|
||||
" flat_corrected[flat_corrected <= 0] = 1\n",
|
||||
"\n",
|
||||
" # Apply correction\n",
|
||||
" corrected = (raw_corrected / flat_corrected) * mean_flat\n",
|
||||
"\n",
|
||||
" # In masked regions, keep original values\n",
|
||||
" mask_3d = mask_float\n",
|
||||
" if len(corrected.shape) == 3:\n",
|
||||
" mask_3d = np.stack([mask_float] * corrected.shape[2], axis=2)\n",
|
||||
"\n",
|
||||
" # Blend: use corrected where mask=1, original where mask=0\n",
|
||||
" final_result = corrected * mask_3d + raw_corrected * (1 - mask_3d)\n",
|
||||
"\n",
|
||||
" return np.clip(final_result, 0, 255).astype(np.uint8)\n",
|
||||
"\n",
|
||||
"def create_interactive_ring_mask(image):\n",
|
||||
" \"\"\"\n",
|
||||
" Interactively create a ring mask by clicking on the image\n",
|
||||
" Click center, then inner radius point, then outer radius point\n",
|
||||
" \"\"\"\n",
|
||||
" points = []\n",
|
||||
"\n",
|
||||
" def mouse_callback(event, x, y, flags, param):\n",
|
||||
" if event == cv2.EVENT_LBUTTONDOWN:\n",
|
||||
" points.append((x, y))\n",
|
||||
" cv2.circle(image, (x, y), 3, (0, 255, 0), -1)\n",
|
||||
" cv2.imshow('Select Ring', image)\n",
|
||||
"\n",
|
||||
" if len(points) == 1:\n",
|
||||
" print(\"Click on inner radius of ring\")\n",
|
||||
" elif len(points) == 2:\n",
|
||||
" print(\"Click on outer radius of ring\")\n",
|
||||
"\n",
|
||||
" cv2.imshow('Select Ring', image)\n",
|
||||
" print(\"Click on center of ring\")\n",
|
||||
" cv2.setMouseCallback('Select Ring', mouse_callback)\n",
|
||||
" cv2.waitKey(0)\n",
|
||||
" cv2.destroyAllWindows()\n",
|
||||
"\n",
|
||||
" if len(points) == 3:\n",
|
||||
" center = points[0]\n",
|
||||
" inner_radius = np.sqrt((points[1][0] - center[0])**2 + (points[1][1] - center[1])**2)\n",
|
||||
" outer_radius = np.sqrt((points[2][0] - center[0])**2 + (points[2][1] - center[1])**2)\n",
|
||||
"\n",
|
||||
" mask = create_ring_mask(image.shape, center, inner_radius, outer_radius)\n",
|
||||
" return mask\n",
|
||||
" else:\n",
|
||||
" print(\"Need 3 points!\")\n",
|
||||
" return None\n"
|
||||
],
|
||||
"id": "a1ae8c16d1cb31f7",
|
||||
"outputs": [],
|
||||
"execution_count": 154
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2025-09-08T14:23:06.438502Z",
|
||||
"start_time": "2025-09-08T14:23:02.773985Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"min_contour_area = 4\n",
|
||||
"mask = create_ring_mask(curr_img, inner_radius=1000,outer_radius=2000)\n",
|
||||
"diff_image = cv2.absdiff(curr_img, bkg)\n",
|
||||
"diff_image = cv2.cvtColor(diff_image, cv2.COLOR_RGB2GRAY)\n",
|
||||
"thresh_value, binary_image = cv2.threshold(diff_image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)\n",
|
||||
"thresh_value, thresh_normal = cv2.threshold(diff_image, 50, 255, cv2.THRESH_BINARY)\n",
|
||||
"ff = flatfield_correction(curr_img, bkg, dark_image=None)\n",
|
||||
"ff = cv2.cvtColor(ff, cv2.COLOR_RGB2GRAY)\n",
|
||||
"ff_masked = flatfield_correction_with_mask(curr_img, bkg, mask, dark_image=None)\n",
|
||||
"ff_masked = cv2.cvtColor(ff_masked, cv2.COLOR_RGB2GRAY)\n",
|
||||
"thresh_value, thresh_mask = cv2.threshold(ff_masked, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)\n",
|
||||
"kernel = np.ones((3,3), np.uint8)\n",
|
||||
"thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)\n",
|
||||
"thresh = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)\n",
|
||||
"\n",
|
||||
"contours, _ = cv2.findContours(thresh_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n",
|
||||
"valid_contours = [cnt for cnt in contours if cv2.contourArea(cnt) > min_contour_area]\n",
|
||||
"\n",
|
||||
"largest_contour = max(valid_contours, key=cv2.contourArea)\n",
|
||||
"\n",
|
||||
"# Step 6: Find leftmost point\n",
|
||||
"leftmost_point = tuple(largest_contour[largest_contour[:,:,0].argmin()][0])\n",
|
||||
"print(leftmost_point)\n",
|
||||
"\n",
|
||||
"leftmost_point = None\n",
|
||||
"\n",
|
||||
"for contour in contours:\n",
|
||||
" # Loop through each point in the current contour\n",
|
||||
" for point in contour:\n",
|
||||
" x, y = point[0] # Point is a nested array [ [x, y] ]\n",
|
||||
"\n",
|
||||
" # Check if this is the leftmost point\n",
|
||||
" if leftmost_point is None or x < leftmost_point[0]:\n",
|
||||
" leftmost_point = (x, y)\n",
|
||||
" plot_contour = contour\n",
|
||||
"print(leftmost_point)\n",
|
||||
"\n",
|
||||
"c_img = cv2.drawContours(thresh, contours, -1, (0, 255, 0), 1)\n",
|
||||
"plt.subplot(2, 2, 1)\n",
|
||||
"plt.imshow(diff_image)\n",
|
||||
"plt.subplot(2, 2, 2)\n",
|
||||
"plt.imshow(thresh_normal)\n",
|
||||
"#plt.subplot(2, 2, 4)\n",
|
||||
"#plt.imshow(ff_masked)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"result_image = cv2.cvtColor(thresh, cv2.COLOR_GRAY2BGR) if len(thresh.shape) == 2 else thresh.copy()\n",
|
||||
"\n",
|
||||
"# Draw contour\n",
|
||||
"result_image = cv2.drawContours(result_image, [largest_contour], -1, (0, 255, 0), 2)\n",
|
||||
"\n",
|
||||
"# Mark leftmost point\n",
|
||||
"result_image = cv2.circle(result_image, leftmost_point, 100, (0, 0, 255), -1)\n",
|
||||
"result_image = cv2.putText(result_image, f'Leftmost: {leftmost_point}',\n",
|
||||
" (leftmost_point[0] + 10, leftmost_point[1] - 10),\n",
|
||||
" cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)\n",
|
||||
"\n",
|
||||
"print(f'Leftmost point: {leftmost_point}')\n",
|
||||
"\n",
|
||||
"plt.subplot(2, 2, 3)\n",
|
||||
"plt.imshow(binary_image)\n"
|
||||
],
|
||||
"id": "a5363e8d3c71448",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"(np.int32(24), np.int32(979))\n",
|
||||
"(np.int32(24), np.int32(979))\n",
|
||||
"Leftmost point: (np.int32(24), np.int32(979))\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"<matplotlib.image.AxesImage at 0x7f97e92802c0>"
|
||||
]
|
||||
},
|
||||
"execution_count": 354,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"<Figure size 640x480 with 3 Axes>"
|
||||
],
|
||||
"image/png": "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 truncated
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"execution_count": 354
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2025-09-08T14:02:01.581369Z",
|
||||
"start_time": "2025-09-08T14:01:56.983788Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"# Load image and convert\n",
|
||||
"img = cv2.imread('/home/leonarski_f/aaredaq/daq/src/aaredaq/991_0_1_50_50_curr_image.tiff', cv2.IMREAD_COLOR)\n",
|
||||
"#img = cv2.imread('/home/leonarski_f/aaredaq/daq/src/aaredaq/0_280_100_50_curr_image_colour.jpg', cv2.IMREAD_COLOR)\n",
|
||||
"\n",
|
||||
"bkg = cv2.imread('/home/leonarski_f/aaredaq/daq/src/aaredaq/bkg1_50_50.jpg')\n",
|
||||
"# Convert to grayscale & enhance\n",
|
||||
"gray_bkg = cv2.cvtColor(bkg, cv2.COLOR_BGR2GRAY)\n",
|
||||
"gray = cv2.subtract(gray, gray_bkg)\n",
|
||||
"gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)\n",
|
||||
"gray = cv2.GaussianBlur(gray, (5, 5), 0)\n",
|
||||
"gray = cv2.equalizeHist(gray)\n",
|
||||
"\n",
|
||||
"# Mask out camera-case ring\n",
|
||||
"#gray = cv2.subtract(gray, gray_bkg)\n",
|
||||
"mask = np.zeros_like(gray, dtype=np.uint8)\n",
|
||||
"h, w = gray.shape\n",
|
||||
"radius = min(h, w)//2 - 20\n",
|
||||
"center = (w//2, h//2)\n",
|
||||
"\n",
|
||||
"norm=gray\n",
|
||||
"print(radius)\n",
|
||||
"\n",
|
||||
"#norm = flatfield_correction_with_mask(gray, gray_bkg, mask)\n",
|
||||
"\n",
|
||||
"#radius = 500#radius#-factor\n",
|
||||
"print(gray.shape)\n",
|
||||
"# # gray = cv2.bitwise_and(gray, gray, mask=mask)\n",
|
||||
"# # x, y = center[0] - radius, center[1] - radius\n",
|
||||
"# gray_cropped = gray[y:y+2*radius, x:x+2*radius]\n",
|
||||
"# #gray_bkg_cropped = gray[y:y+2*radius, x:x+2*radius]\n",
|
||||
"# #cv2.circle(mask, center, radius, 93, -1)\n",
|
||||
"# print(gray_cropped.shape)\n",
|
||||
"#\n",
|
||||
"# #masked = cv2.bitwise_and(gray, gray, mask=mask)\n",
|
||||
"# #norm = gray_cropped\n",
|
||||
"# masked=gray_cropped\n",
|
||||
"# background= cv2.medianBlur(masked, 51)\n",
|
||||
"#\n",
|
||||
"# norm = cv2.subtract(masked, background)\n",
|
||||
"#norm = cv2.absdiff(masked,gray_bkg)\n",
|
||||
"kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (25,25))\n",
|
||||
"tophat = cv2.morphologyEx(norm, cv2.MORPH_TOPHAT, kernel)\n",
|
||||
"\n",
|
||||
"enhanced = cv2.addWeighted(norm, 0.7, tophat, 0.3, 0)\n",
|
||||
"# Adaptive thresholding\n",
|
||||
"thresh = cv2.adaptiveThreshold(enhanced, 255, cv2.ADAPTIVE_THRESH_MEAN_C,\n",
|
||||
" cv2.THRESH_BINARY_INV, 41, 2)\n",
|
||||
"\n",
|
||||
"# Find contours\n",
|
||||
"contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n",
|
||||
"\n",
|
||||
"best_point = None\n",
|
||||
"min_x = w + 1\n",
|
||||
"\n",
|
||||
"print(len(contours))\n",
|
||||
"for cnt in contours:\n",
|
||||
" x,y,wc,hc = cv2.boundingRect(cnt)\n",
|
||||
"\n",
|
||||
" # Skip very large contours (likely mask boundary)\n",
|
||||
" if cv2.contourArea(cnt) > 0.2 * h * w:\n",
|
||||
" print(\"Skipping contour with area\", cv2.contourArea(cnt))\n",
|
||||
" continue\n",
|
||||
"\n",
|
||||
" # Compute distance of contour centroid from image center\n",
|
||||
" M = cv2.moments(cnt)\n",
|
||||
" if M[\"m00\"] != 0:\n",
|
||||
" cx = int(M[\"m10\"]/M[\"m00\"])\n",
|
||||
" cy = int(M[\"m01\"]/M[\"m00\"])\n",
|
||||
" dist = np.sqrt((cx-center[0])**2 + (cy-center[1])**2)\n",
|
||||
" # If centroid is near mask radius, skip (mask edge)\n",
|
||||
" #if abs(dist - radius) < 60:\n",
|
||||
" # print(\"Skipping contour near mask radius\")\n",
|
||||
" # continue\n",
|
||||
"\n",
|
||||
" # Skip vertical pin\n",
|
||||
" if hc > 3*wc:\n",
|
||||
" print(\"Skipping vertical pin\")\n",
|
||||
" continue\n",
|
||||
"\n",
|
||||
" leftmost = tuple(cnt[cnt[:, :, 0].argmin()][0])\n",
|
||||
" if leftmost[0] < min_x:\n",
|
||||
" min_x = leftmost[0]\n",
|
||||
" best_point = leftmost\n",
|
||||
"\n",
|
||||
"# Draw result\n",
|
||||
"if best_point is not None:\n",
|
||||
" print(f\"best_point = {best_point}\")\n",
|
||||
" #cv2.circle(thresh, best_point, 100, (0, 0, 255), -1)\n",
|
||||
" print(radius)\n",
|
||||
" corrected_point = (best_point[0]+factor, best_point[1] +factor)\n",
|
||||
" print(f\"corrected_point {corrected_point}\")\n",
|
||||
" cv2.circle(img, corrected_point, 100, (0, 0, 255), -1)\n",
|
||||
" print(\"Loop tip (leftmost point):\", best_point)\n",
|
||||
"else:\n",
|
||||
" print(\"No suitable contour found\")\n",
|
||||
"\n",
|
||||
"plt.figure(figsize=(10,8))\n",
|
||||
"plt.figure(figsize=(14,10))\n",
|
||||
"plt.subplot(2,4,1); plt.title(\"Original\"); plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))#; plt.axis(\"off\")\n",
|
||||
"plt.subplot(2,4,2); plt.title(\"Cropped\"); plt.imshow(gray_cropped, cmap=\"gray\"); plt.axis(\"off\")\n",
|
||||
"plt.subplot(2,4,3); plt.title(\"Background\"); plt.imshow(norm, cmap=\"gray\"); plt.axis(\"off\")\n",
|
||||
"plt.subplot(2,4,4); plt.title(\"Top-hat\"); plt.imshow(tophat, cmap=\"gray\"); plt.axis(\"off\")\n",
|
||||
"plt.subplot(2,4,5); plt.title(\"Enhanced\"); plt.imshow(enhanced, cmap=\"gray\"); plt.axis(\"off\")\n",
|
||||
"plt.subplot(2,4,6); plt.title(\"Threshold\"); plt.imshow(thresh, cmap=\"gray\")#; plt.axis(\"off\")\n",
|
||||
"plt.subplot(2,4,7); plt.title(\"image with contours\"); #plt.imshow(img)#; plt.axis(\"off\")\n",
|
||||
"contour_vis = cv2.cvtColor(masked, cv2.COLOR_GRAY2BGR)\n",
|
||||
"cv2.drawContours(contour_vis, contours, -1, (0,255,0), 1)\n",
|
||||
"cv2.circle(contour_vis, best_point, 100, (0, 0, 255), -1)\n",
|
||||
"plt.imshow(contour_vis)\n",
|
||||
"plt.show()\n",
|
||||
"\n",
|
||||
"cv2.imwrite('masked.tiff', enhanced)\n",
|
||||
"cv2.imwrite('thresh.tiff', thresh)"
|
||||
],
|
||||
"id": "d98c070865b11f36",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"1003\n",
|
||||
"(2046, 2046)\n",
|
||||
"10291\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping contour with area 2054203.5\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"Skipping vertical pin\n",
|
||||
"best_point = (np.int32(0), np.int32(1421))\n",
|
||||
"1003\n",
|
||||
"corrected_point (np.int32(250), np.int32(1671))\n",
|
||||
"Loop tip (leftmost point): (np.int32(0), np.int32(1421))\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"<Figure size 1000x800 with 0 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"<Figure size 1400x1000 with 7 Axes>"
|
||||
],
|
||||
"image/png": "iVBORw0KGgoAAAANSUhEUgAABH0AAALRCAYAAADGGN8bAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzsvXeUXNWVPbwr5+rKVV2dg7rViq2ABJIQEkk2JjobgxGDxwkcZjzO8xkbe/CMzTgMY/jBGINtjLFxwAZMFlFIAhRoZXUO1aGqK+f4vj96neNXpepWSwYj2W+vpaWuF+67L91377777CMTBEGABAkSJEiQIEGCBAkSJEiQIEGChL8ryN/uCkiQIEGCBAkSJEiQIEGCBAkSJEh48yGRPhIkSJAgQYIECRIkSJAgQYIECX+HkEgfCRIkSJAgQYIECRIkSJAgQYKEv0NIpI8ECRIkSJAgQYIECRIkSJAgQcLfISTSR4IECRIkSJAgQYIECRIkSJAg4e8QEukjQYIECRIkSJAgQYIECRIkSJDwdwiJ9JEgQYIECRIkSJAgQYIECRIkSPg7hET6SJAgQYIECRIkSJAgQYIECRIk/B1CIn0kSJAgQYIECRIkSJAgQYIECRL+DiGRPhLednzjG9+ATCY7pX3vu+8+yGQyDA0NvbmVEmFoaAgymQz33XffW3YMCRIkSBBDanckSDj90dzcjEsvvfTtrsbbBplMhm984xtvdzUkSJBwiti6dSuMRuPbXQ0JfwNIpI+EvwoHDx7ENddcg7q6Omg0Gni9Xnz4wx/GwYMH3+6qSZAg4e8A/f39+PjHP47W1lZotVqYzWasX78eP/rRj5BOp9/u6kmQIOE0AE0Aif+5XC5s3rwZjz/++NtdPQkSJEhgVLZVs/17/vnn3+6qnhJuvfVWPPzww293NSRUQPl2V0DCmYvf//73+NCHPgSbzYYbbrgBLS0tGBoawj333IPf/va3ePDBB3HVVVedsJx///d/x5e//OVTqsO1116LD37wg9BoNKe0vwQJEk5fPPbYY3jf+94HjUaDj3zkI1iyZAlyuRxefvllfOELX8DBgwdx9913v93VlCBBwmmCW265BS0tLRAEAVNTU7jvvvtwySWX4JFHHvmHVuRIkCDh9MEvfvGLst8///nP8fTTTx+3vKur629ZrTcNt956K9773vfiyiuvfLurIkEEifSRcEro7+/Htddei9bWVrz44otwOp287rOf/SzOPfdcXHvttejp6UFra2vVMpLJJAwGA5RKJZTKU3sUFQoFFArFKe0rQYKE0xeDg4P44Ac/iKamJmzbtg21tbW87sYbb0RfXx8ee+yxqvuWSiXkcjlotdq/VXUlSJBwGuCd73wnVq9ezb9vuOEGuN1u/OpXvzpjSR/qK0mQIOHvA9dcc03Z7507d+Lpp58+brkECW8mpPAuCaeE733ve0ilUrj77rvLCB8AcDgcuOuuu5BMJvHd734XwF98ew4dOoSrr74aVqsVGzZsKFsnRjqdxmc+8xk4HA6YTCZcfvnl8Pl8x8WPV/P0oRj7l19+GWvWrIFWq0Vrayt+/vOflx0jFArh3/7t37B06VIYjUaYzWa8853vxBtvvPEmXikJEiScCr773e8ikUjgnnvuKSN8CO3t7fjsZz8LYEYqfdNNN+GXv/wlFi9eDI1GgyeeeAIAsHfvXrzzne+E2WyG0WjEBRdcgJ07d5aVRe3Iiy++iI9//OOw2+0wm834yEc+gnA4XLYttS9PPfUUuru7odVqsWjRIvz+978/ro6RSASf+9zn0NDQAI1Gg/b2dvzXf/0XSqXScdtt3boVNTU1sFgsuO666xCJRP6ayydBggQAFosFOp2ubGLptttuw7p162C326HT6bBq1Sr89re/rbr//fffjzVr1kCv18NqtWLjxo146qmn5jzmz372MyiVSnzhC1/gZcFgENdeey3MZjO/42+88cZxvl3kr9Hf349LLrkEJpMJH/7whwHMkD+f//znuT3p7OzEbbfdBkEQeP+5vMAq+0/U9+rr68PWrVthsVhQU1OD66+/HqlUqmzfbDaLf/mXf4HT6eQ+2djY2JzXQYIECaeO+bzvQHn/p7OzE1qtFqtWrcKLL754Usfz+Xy48sorYTQa4XQ68W//9m8oFotl28yn7ZTJZEgmk/jZz37GYWpbt249pWsg4c2FpPSRcEp45JFH0NzcjHPPPbfq+o0bN6K5ufm4mfj3ve99WLBgAW699dbjGi4xtm7dit/85je49tprcfbZZ+OFF17Au971rnnXr6+vD+9973txww034LrrrsNPf/pTbN26FatWrcLixYsBAAMDA3j44Yfxvve9Dy0tLZiamsJdd92F8847D4cOHYLX65338SRIkPDm4pFH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 truncated
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"True"
|
||||
]
|
||||
},
|
||||
"execution_count": 350,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"execution_count": 350
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2025-09-08T11:21:42.726663Z",
|
||||
"start_time": "2025-09-08T11:21:41.711795Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"x#bkg_old = cv2.imread(\"/home/leonarski_f/aaredaq/daq/src/aaredaq/alc_test_images/bkg280_020925.jpg\")\n",
|
||||
"#bkg_new = cv2.imread(\"/home/leonarski_f/aaredaq/daq/src/aaredaq/alc_test_images/bkg280_03092025.jpg\")\n",
|
||||
"bkg_old = cv2.imread(\"/home/leonarski_f/aaredaq/daq/src/aaredaq/bkg280_50_50_old.jpg\")\n",
|
||||
"bkg_new = cv2.imread(\"/home/leonarski_f/aaredaq/daq/src/aaredaq/bkg280_50_50.jpg\")\n",
|
||||
"#$bkg_newest = cv2.imread(\"/home/leonarski_f/aaredaq/daq/src/aaredaq/bkg280.jpg\")\n",
|
||||
"grey_old = cv2.cvtColor(bkg_old, cv2.COLOR_RGB2GRAY)\n",
|
||||
"grey_new = cv2.cvtColor(bkg_new, cv2.COLOR_RGB2GRAY)\n",
|
||||
"#grey_newest = cv2.cvtColor(bkg_newest, cv2.COLOR_RGB2GRAY)\n",
|
||||
"diff_image = cv2.absdiff(grey_old, grey_new)\n",
|
||||
"_, thresh_diff = cv2.threshold(diff_image, threshold, 255, cv2.THRESH_BINARY)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Simple subtraction (may result in negative values)\n",
|
||||
"diff_simple = cv2.subtract(bkg_new, bkg_old)\n",
|
||||
"\n",
|
||||
"# Weighted difference for motion detection\n",
|
||||
"diff_weighted = cv2.addWeighted(bkg_new, 0.5, bkg_old, -0.5, 128)\n",
|
||||
"\n",
|
||||
"# Using numpy for difference\n",
|
||||
"diff_numpy = np.abs(bkg_new.astype(np.int16) - bkg_old.astype(np.int16)).astype(np.uint8)\n",
|
||||
"\n",
|
||||
"plt.subplot(1,2,1)\n",
|
||||
"plt.imshow(bkg_old)\n",
|
||||
"plt.subplot(1,2,2)\n",
|
||||
"plt.imshow(bkg_new)"
|
||||
],
|
||||
"id": "5c26fa11f85e6483",
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"<matplotlib.image.AxesImage at 0x7f97bc9dc7a0>"
|
||||
]
|
||||
},
|
||||
"execution_count": 275,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"<Figure size 640x480 with 2 Axes>"
|
||||
],
|
||||
"image/png": "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 truncated
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"execution_count": 275
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2025-09-08T08:13:17.148953Z",
|
||||
"start_time": "2025-09-08T08:13:17.046977Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"bkg_old_np = bkg_old.astype(np.float32)\n",
|
||||
"bkg_old_np_normalized = bkg_old.astype(np.float32) / 255.0\n",
|
||||
"bkg_new_np = bkg_old.astype(np.float32)\n",
|
||||
"bkg_new_np_normalized = bkg_old.astype(np.float32) / 255.0\n",
|
||||
"diff_np=bkg_old_np-bkg_new_np\n",
|
||||
"diff_np_normalized=bkg_old_np_normalized-bkg_new_np_normalized"
|
||||
],
|
||||
"id": "95a0ac65d0013e8",
|
||||
"outputs": [],
|
||||
"execution_count": 196
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2025-09-08T08:14:30.738697Z",
|
||||
"start_time": "2025-09-08T08:14:30.671257Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"print(\"mean: \", np.mean(grey_old.astype(np.float32)-grey_new.astype(np.float32)))\n",
|
||||
"print(\"std: \", np.std(grey_old.astype(np.float32)-grey_new.astype(np.float32)))\n",
|
||||
"diff_abs = np.abs(grey_old.astype(np.float32)-grey_new.astype(np.float32))\n",
|
||||
"print(np.percentile(diff_abs, 99))"
|
||||
],
|
||||
"id": "83aae96e9df6c807",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"mean: -6.184164\n",
|
||||
"std: 5.93317\n",
|
||||
"21.0\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"execution_count": 200
|
||||
},
|
||||
{
|
||||
"metadata": {},
|
||||
"cell_type": "code",
|
||||
"outputs": [],
|
||||
"execution_count": null,
|
||||
"source": [
|
||||
"threshold_old = cv2.imread(\"/home/leonarski_f/aaredaq/daq/src/aaredaq/alc_test_images/bkg280_020925.jpg\")\n",
|
||||
"threshold_new = cv2.imread(\"/home/leonarski_f/aaredaq/daq/src/aaredaq/alc_test_images/bkg280_03092025.jpg\")"
|
||||
],
|
||||
"id": "258a3d679516f15c"
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2025-09-05T13:33:03.863166Z",
|
||||
"start_time": "2025-09-05T13:33:03.839938Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"import numpy as np\n",
|
||||
"from scipy import ndimage\n",
|
||||
"import matplotlib.pyplot as plt\n",
|
||||
"\n",
|
||||
"# Example showing the difference\n",
|
||||
"image = np.random.rand(50, 50) * 100\n",
|
||||
"# Add a bright spot\n",
|
||||
"image[20:30, 35:45] = 200\n",
|
||||
"\n",
|
||||
"# Without thresholding - all pixels contribute\n",
|
||||
"com_no_threshold = ndimage.center_of_mass(image)\n",
|
||||
"print(f\"No threshold COM: ({com_no_threshold[1]:.2f}, {com_no_threshold[0]:.2f})\")\n",
|
||||
"\n",
|
||||
"# With thresholding - only bright pixels contribute\n",
|
||||
"threshold = 150\n",
|
||||
"thresholded = image.copy()\n",
|
||||
"thresholded[thresholded < threshold] = 0\n",
|
||||
"com_with_threshold = ndimage.center_of_mass(thresholded)\n",
|
||||
"print(f\"With threshold COM: ({com_with_threshold[1]:.2f}, {com_with_threshold[0]:.2f})\")\n"
|
||||
],
|
||||
"id": "d70801a606268e76",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"No threshold COM: (26.11, 24.33)\n",
|
||||
"With threshold COM: (39.50, 24.50)\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"execution_count": 189
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2025-09-05T13:33:18.254639Z",
|
||||
"start_time": "2025-09-05T13:33:18.248074Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"def center_of_mass_with_threshold(image, threshold):\n",
|
||||
" \"\"\"\n",
|
||||
" Apply threshold before center of mass calculation\n",
|
||||
" \"\"\"\n",
|
||||
" # Create binary mask\n",
|
||||
" binary = (image >= threshold).astype(float)\n",
|
||||
"\n",
|
||||
" # Calculate center of mass on thresholded image\n",
|
||||
" com = ndimage.center_of_mass(binary)\n",
|
||||
"\n",
|
||||
" # Return as (x, y) coordinates\n",
|
||||
" return (com[1], com[0]) if com[0] is not np.nan else None\n"
|
||||
],
|
||||
"id": "1ba37750a7b66b23",
|
||||
"outputs": [],
|
||||
"execution_count": 190
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2025-09-05T13:33:27.236834Z",
|
||||
"start_time": "2025-09-05T13:33:27.231958Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"def weighted_center_of_mass_threshold(image, threshold):\n",
|
||||
" \"\"\"\n",
|
||||
" Keep original intensities above threshold, zero below\n",
|
||||
" \"\"\"\n",
|
||||
" weighted_image = image.copy()\n",
|
||||
" weighted_image[weighted_image < threshold] = 0\n",
|
||||
"\n",
|
||||
" com = ndimage.center_of_mass(weighted_image)\n",
|
||||
" return (com[1], com[0]) if com[0] is not np.nan else None\n"
|
||||
],
|
||||
"id": "59452341d64bc800",
|
||||
"outputs": [],
|
||||
"execution_count": 191
|
||||
},
|
||||
{
|
||||
"metadata": {},
|
||||
"cell_type": "code",
|
||||
"outputs": [],
|
||||
"execution_count": null,
|
||||
"source": "",
|
||||
"id": "8cf8041e736e5a43"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 2
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython2",
|
||||
"version": "2.7.6"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -1,509 +0,0 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"metadata": {},
|
||||
"cell_type": "markdown",
|
||||
"source": "**Import Packages**",
|
||||
"id": "1e84a74bce983a48"
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2025-07-09T15:21:27.478441Z",
|
||||
"start_time": "2025-07-09T15:21:26.516951Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"from aaredaq.devices import BeamlineDevices\n",
|
||||
"from aaredaqlib.beamline import MXBeamline"
|
||||
],
|
||||
"id": "d2efa08de2dbd46b",
|
||||
"outputs": [],
|
||||
"execution_count": 1
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2025-07-09T15:21:29.814198Z",
|
||||
"start_time": "2025-07-09T15:21:28.052349Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"from epics import PV\n",
|
||||
"import numpy as np\n",
|
||||
"import time\n",
|
||||
"import matplotlib.pyplot as plt\n",
|
||||
"from scipy.optimize import curve_fit\n",
|
||||
"from scipy.signal import peak_widths, find_peaks"
|
||||
],
|
||||
"id": "f8305237c9f98964",
|
||||
"outputs": [],
|
||||
"execution_count": 2
|
||||
},
|
||||
{
|
||||
"metadata": {},
|
||||
"cell_type": "markdown",
|
||||
"source": "**Config**",
|
||||
"id": "de33ac6389ffef80"
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2025-07-09T15:21:33.454435Z",
|
||||
"start_time": "2025-07-09T15:21:31.742983Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"d = BeamlineDevices(MXBeamline.X06DA)\n",
|
||||
"b = MXBeamline.X06DA\n",
|
||||
"beamline = b.value.upper()"
|
||||
],
|
||||
"id": "5407e46ef4c2a26d",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Connecting TELL p-shell service at http://x06da-tell.psi.ch:22222 ..."
|
||||
]
|
||||
}
|
||||
],
|
||||
"execution_count": 3
|
||||
},
|
||||
{
|
||||
"metadata": {},
|
||||
"cell_type": "markdown",
|
||||
"source": "**Functions for fitting**",
|
||||
"id": "ad205cd8366be8b5"
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2025-07-09T15:21:35.539527Z",
|
||||
"start_time": "2025-07-09T15:21:35.532072Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"def gaus(x, *p):\n",
|
||||
" A, mu, sigma = p\n",
|
||||
" return A * np.exp(-(x-mu)**2/(2.*sigma**2))\n",
|
||||
"\n",
|
||||
"def sigmoid(x, *p):\n",
|
||||
" L, x0, k, c = p\n",
|
||||
" return L / (1 + np.exp(-k * (x -x0))) + c\n",
|
||||
"\n",
|
||||
"def unpack_data(data):\n",
|
||||
" x_vals, y_vals = zip(*data)\n",
|
||||
" x_vals = list(x_vals)\n",
|
||||
" y_vals = list(y_vals)\n",
|
||||
" return x_vals, y_vals\n",
|
||||
"\n",
|
||||
"def sigmoid_fit(x_vals, y_vals):\n",
|
||||
" L_guess = np.max(y_vals)\n",
|
||||
" b_guess = np.min(y_vals)\n",
|
||||
" x0_guess = x_vals[np.argmin(np.abs(y_vals - (L_guess + b_guess) / 2))]\n",
|
||||
" k_guess = 1 / (x_vals[-1] - x_vals[0])\n",
|
||||
"\n",
|
||||
" s0 = [L_guess, x0_guess, k_guess, b_guess]\n",
|
||||
" parameters, params_covariance = curve_fit(sigmoid, x_vals, y_vals, p0=s0)\n",
|
||||
" return parameters\n",
|
||||
"\n",
|
||||
"def gauss_fit(x_vals, dydx):\n",
|
||||
" mean = x_vals[np.argmax(dydx)]\n",
|
||||
" sigma = (np.max(x_vals) - np.min(x_vals))/4\n",
|
||||
" A= np.max(dydx)\n",
|
||||
" dydx[np.isnan(dydx)] = 0\n",
|
||||
"\n",
|
||||
" p0 = [A, mean, sigma]\n",
|
||||
" parameters, covariance = curve_fit( gaus, x_vals, dydx, p0=p0)\n",
|
||||
" return parameters\n",
|
||||
"\n",
|
||||
"def calc_fwhm(gauss_y, x_vals):\n",
|
||||
" peaks = find_peaks( gauss_y )\n",
|
||||
" fwhm_peak = peak_widths( gauss_y, peaks[0], rel_height=0.5 )\n",
|
||||
"\n",
|
||||
" fwhm_str = x_vals[int( round( fwhm_peak[2][0], 0 ))]\n",
|
||||
" fwhm_end = x_vals[int( round( fwhm_peak[3][0], 0 ))]\n",
|
||||
"\n",
|
||||
" fwhm = fwhm_str - fwhm_end\n",
|
||||
" fwhm_height = gauss_y[int( round( fwhm_peak[2][0], 0 ))]\n",
|
||||
"\n",
|
||||
" # find 1/e2 peak width\n",
|
||||
" full_peak = peak_widths( gauss_y, peaks[0], rel_height=0.865 )\n",
|
||||
" full_str = x_vals[int( round( full_peak[2][0], 0 ))]\n",
|
||||
" full_end = x_vals[int( round( full_peak[3][0], 0 ))]\n",
|
||||
"\n",
|
||||
" full = full_str - full_end\n",
|
||||
" full_height = gauss_y[int( round( full_peak[2][0], 0 ))]\n",
|
||||
" return fwhm, full"
|
||||
],
|
||||
"id": "d92115ec3d76d57d",
|
||||
"outputs": [],
|
||||
"execution_count": 4
|
||||
},
|
||||
{
|
||||
"metadata": {},
|
||||
"cell_type": "markdown",
|
||||
"source": "**Function for performing knife_edge then fitting and plotting**",
|
||||
"id": "323bcc0c8ba19afa"
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2025-07-09T16:34:57.207683Z",
|
||||
"start_time": "2025-07-09T16:34:57.203084Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"def knife_edge(motor: str, dev: BeamlineDevices, beamline: str, steps: int = 40, start: float = 0.0, step_size: float = 0.002, offset: float = 0.0, sleep = 0.5):\n",
|
||||
" diode = PV(f\"{beamline}-ES-BS:READOUT\")#X06DA-ES-BS:READOUT\n",
|
||||
" dev.shutter = True\n",
|
||||
" time.sleep(2.0)\n",
|
||||
"\n",
|
||||
" data = []\n",
|
||||
"\n",
|
||||
" for i in range(steps): #40\n",
|
||||
" if motor == 'X':\n",
|
||||
" dev.gmx.move(start-offset+step_size*i, wait=True)\n",
|
||||
" time.sleep(sleep)\n",
|
||||
" print(f\"{dev.gmx.value:.3f} {diode.get()}\")\n",
|
||||
" data.append([dev.gmx.value, diode.get()])\n",
|
||||
" else:\n",
|
||||
" dev.gmy.move(start-offset+step_size*i, wait=True)\n",
|
||||
" time.sleep(sleep)\n",
|
||||
" print(f\"{dev.gmy.value:.3f} {diode.get()}\")\n",
|
||||
" data.append([dev.gmy.value, diode.get()])\n",
|
||||
"\n",
|
||||
" if motor == 'X':\n",
|
||||
" dev.gmx.move(start, wait=True)\n",
|
||||
" #else:\n",
|
||||
" # dev.gmy.move(start, wait=True)\n",
|
||||
"\n",
|
||||
" dev.shutter = False\n",
|
||||
" return data\n"
|
||||
],
|
||||
"id": "512dc280e62e4ed4",
|
||||
"outputs": [],
|
||||
"execution_count": 41
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2025-07-09T16:35:13.891315Z",
|
||||
"start_time": "2025-07-09T16:35:13.885335Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"def fit_knife_edge(data, motor: str):\n",
|
||||
"\n",
|
||||
" x_vals, y_vals = unpack_data(data)\n",
|
||||
"\n",
|
||||
" plt.plot(x_vals, y_vals, label = 'edge_scan')\n",
|
||||
" plt.ylabel('Intensity (counts)')\n",
|
||||
" plt.xlabel('Motor position (mm)')\n",
|
||||
"\n",
|
||||
" params=sigmoid_fit(x_vals, y_vals)\n",
|
||||
"\n",
|
||||
" plt.plot(x_vals, y_vals)\n",
|
||||
" plt.plot(x_vals, sigmoid(x_vals, *params))\n",
|
||||
" plt.show()\n",
|
||||
"\n",
|
||||
" y_fit = sigmoid(x_vals, *params)\n",
|
||||
"\n",
|
||||
" dydx = np.gradient(y_fit, x_vals)\n",
|
||||
"\n",
|
||||
" if motor == 'X':\n",
|
||||
" dydx=-dydx\n",
|
||||
"\n",
|
||||
" parameters = gauss_fit(x_vals, dydx)\n",
|
||||
"\n",
|
||||
" x0_op = parameters[1]\n",
|
||||
" sigma_op = parameters[2]\n",
|
||||
" print(parameters)\n",
|
||||
"\n",
|
||||
" gauss_y = gaus(x_vals,*parameters)\n",
|
||||
" fwhm = np.abs(2*np.sqrt(2*np.log(2))*sigma_op)\n",
|
||||
" print(f\"Manually calculated FWHM to {fwhm} mm\")\n",
|
||||
" plt.plot(x_vals, dydx, label = 'derivative')\n",
|
||||
" plt.plot(x_vals, gauss_y,label='Gaussian fit',color ='orange')\n",
|
||||
" plt.fill_between(x_vals,gauss_y,color='orange',alpha=0.5)\n",
|
||||
"\n",
|
||||
" try:\n",
|
||||
" fwhm, full = calc_fwhm(gauss_y, x_vals)\n",
|
||||
" print( \"Scipy calculated FWHM = {0} mm\".format( fwhm ) )\n",
|
||||
" print( \"Scipy calculated 1/e2 = {0} mm\".format( full ) )\n",
|
||||
" except:\n",
|
||||
" print('scipy peak finding failed')\n",
|
||||
"\n",
|
||||
" plt.axvspan(x0_op+fwhm/2,x0_op-fwhm/2, color='green', alpha=0.75, lw=0, label='FWHM = {0} mm'.format(fwhm))\n",
|
||||
" plt.legend()\n",
|
||||
" plt.show()\n",
|
||||
" #plt.savefig(output_name+filename.split('/')[-1]+'FWHM_{0}.png'.format(fwhm))\n"
|
||||
],
|
||||
"id": "80460b35a33f1ecf",
|
||||
"outputs": [],
|
||||
"execution_count": 42
|
||||
},
|
||||
{
|
||||
"metadata": {},
|
||||
"cell_type": "markdown",
|
||||
"source": "**Knife Edge scan in X**",
|
||||
"id": "b063ff6432f42184"
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2025-07-09T17:51:02.087583Z",
|
||||
"start_time": "2025-07-09T17:49:59.206783Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"steps_x = 50\n",
|
||||
"start_pos_x = -17.94\n",
|
||||
"step_size_x = 0.002\n",
|
||||
"offset_x = 0.0 # offset subtracted from start position\n",
|
||||
"sleep = 1 # wait after move before recording diode\n",
|
||||
"x_data=knife_edge(motor='X', dev=d, beamline=beamline, steps=steps_x, start=start_pos_x, step_size=step_size_x, offset=offset_x, sleep = sleep)\n",
|
||||
"fit_knife_edge(x_data, motor='X')"
|
||||
],
|
||||
"id": "5454eb901526eb3",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"-17.940 8.959981e-06\n",
|
||||
"-17.938 8.944953e-06\n",
|
||||
"-17.936 8.937235e-06\n",
|
||||
"-17.934 8.946256e-06\n",
|
||||
"-17.932 8.870569e-06\n",
|
||||
"-17.930 8.943733e-06\n",
|
||||
"-17.928 8.932527e-06\n",
|
||||
"-17.926 8.931002e-06\n",
|
||||
"-17.924 8.912179e-06\n",
|
||||
"-17.922 8.929313e-06\n",
|
||||
"-17.920 8.917321e-06\n",
|
||||
"-17.918 8.937232e-06\n",
|
||||
"-17.916 8.892818e-06\n",
|
||||
"-17.914 8.824038e-06\n",
|
||||
"-17.912 8.808735e-06\n",
|
||||
"-17.910 8.837087e-06\n",
|
||||
"-17.908 8.671096e-06\n",
|
||||
"-17.906 8.606978e-06\n",
|
||||
"-17.904 8.432655e-06\n",
|
||||
"-17.902 8.189082e-06\n",
|
||||
"-17.900 8.250342e-06\n",
|
||||
"-17.898 7.650508e-06\n",
|
||||
"-17.896 7.203671e-06\n",
|
||||
"-17.894 6.735723e-06\n",
|
||||
"-17.892 6.1932e-06\n",
|
||||
"-17.890 6.269931e-06\n",
|
||||
"-17.888 5.137038e-06\n",
|
||||
"-17.886 4.552743e-06\n",
|
||||
"-17.884 3.875157e-06\n",
|
||||
"-17.882 3.34356e-06\n",
|
||||
"-17.880 3.422219e-06\n",
|
||||
"-17.878 2.437191e-06\n",
|
||||
"-17.876 2.017403e-06\n",
|
||||
"-17.874 1.662639e-06\n",
|
||||
"-17.872 1.323577e-06\n",
|
||||
"-17.870 1.346216e-06\n",
|
||||
"-17.868 9.006973e-07\n",
|
||||
"-17.866 7.306933e-07\n",
|
||||
"-17.864 5.915996e-07\n",
|
||||
"-17.862 4.870485e-07\n",
|
||||
"-17.860 4.98184e-07\n",
|
||||
"-17.858 3.496483e-07\n",
|
||||
"-17.856 2.945638e-07\n",
|
||||
"-17.854 2.461977e-07\n",
|
||||
"-17.852 2.114641e-07\n",
|
||||
"-17.850 2.139551e-07\n",
|
||||
"-17.848 1.598115e-07\n",
|
||||
"-17.846 1.397988e-07\n",
|
||||
"-17.844 1.199971e-07\n",
|
||||
"-17.842 1.045175e-07\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"<Figure size 640x480 with 1 Axes>"
|
||||
],
|
||||
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAjYAAAHACAYAAACxlSKsAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAPYQAAD2EBqD+naQAAXndJREFUeJzt3Xd8FHXixvHP7GbTKymEEgjd0LsUCyhFBQ/sFUEU7+zlp3eoZzsVLIcdxQrYQREroIII0nsPnVACIYH0ttnszu+PaM4I0SwkmZTn/XrNK9nZ2dlnxrJPZr47Y5imaSIiIiJSB9isDiAiIiJSWVRsREREpM5QsREREZE6Q8VGRERE6gwVGxEREakzVGxERESkzlCxERERkTpDxUZERETqDBUbERERqTNUbERERKTOqDPFZvHixVx88cU0btwYwzD48ssvq/w9k5OTuf7664mMjCQgIIBOnTqxZs2aKn9fERERObk6U2zy8vLo0qULkydPrpb3y8jIoH///jgcDubOncu2bduYNGkSERER1fL+IiIiciKjLt4E0zAMZs+ezciRI0vnOZ1OHn74YT755BMyMzPp2LEjzz77LAMGDDil9xg/fjxLly7ll19+qZzQIiIictrqzBGbv3LHHXewfPlyPv30UzZt2sQVV1zBBRdcwK5du05pfV9//TU9e/bkiiuuICYmhm7duvH2229XcmoRERHxRr04YnPgwAFatmzJgQMHaNy4celygwYNonfv3kyYMMHr9/D39wfgvvvu44orrmD16tXcfffdTJkyhdGjR1fKdoiIiIh3fKwOUB02b96M2+2mbdu2ZeY7nU4iIyMB2L59OwkJCX+6nn/9618888wzAHg8Hnr27Flairp168aWLVtUbERERCxUL4pNbm4udrudtWvXYrfbyzwXHBwMQMuWLUlMTPzT9fxWggAaNWpE+/btyzyfkJDArFmzKim1iIiIeKteFJtu3brhdrtJTU3l7LPPPukyvr6+nHHGGRVeZ//+/dmxY0eZeTt37qR58+anlVVEREROXZ0pNrm5uezevbv08b59+9iwYQMNGjSgbdu2XHfdddxwww1MmjSJbt26kZaWxoIFC+jcuTPDhg3z+v3uvfde+vXrx4QJE7jyyitZtWoVb731Fm+99VZlbpaIiIh4oc4MHv75558ZOHDgCfNHjx7NtGnTcLlc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 truncated
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"[ 2.92142522e-04 -1.78855968e+01 1.17269351e-02]\n",
|
||||
"Manually calculated FWHM to 0.02761482178449844 mm\n",
|
||||
"Scipy calculated FWHM = -0.02800645446777139 mm\n",
|
||||
"Scipy calculated 1/e2 = -0.04800543212890318 mm\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"<Figure size 640x480 with 1 Axes>"
|
||||
],
|
||||
"image/png": "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 truncated
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"execution_count": 63
|
||||
},
|
||||
{
|
||||
"metadata": {},
|
||||
"cell_type": "code",
|
||||
"source": "fit_knife_edge(x_data, motor='X')",
|
||||
"id": "41b7430b4ce4727",
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"metadata": {},
|
||||
"cell_type": "markdown",
|
||||
"source": "**Knife edge scan in Y**",
|
||||
"id": "f6c950fbb9299d42"
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2025-07-09T18:01:19.109015Z",
|
||||
"start_time": "2025-07-09T18:00:32.468027Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"steps_y = 40\n",
|
||||
"start_pos_y = -0.20\n",
|
||||
"step_size_y = 0.002\n",
|
||||
"offset_y = 0.00\n",
|
||||
"sleep = 1.0 # wait after move before recording diode\n",
|
||||
"y_data=knife_edge(motor='Y', dev=d, beamline=beamline, steps=steps_y, start=start_pos_y, step_size=step_size_y, offset=offset_y, sleep=sleep)\n",
|
||||
"fit_knife_edge(y_data, motor='Y')"
|
||||
],
|
||||
"id": "f8e0541c9e92e4e6",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"-0.200 2.360645e-08\n",
|
||||
"-0.198 2.811506e-08\n",
|
||||
"-0.196 2.810113e-08\n",
|
||||
"-0.194 3.39931e-08\n",
|
||||
"-0.192 4.061688e-08\n",
|
||||
"-0.190 6.625245e-08\n",
|
||||
"-0.188 8.658314e-08\n",
|
||||
"-0.186 1.106975e-07\n",
|
||||
"-0.184 1.428539e-07\n",
|
||||
"-0.182 1.863939e-07\n",
|
||||
"-0.180 1.863939e-07\n",
|
||||
"-0.178 2.407869e-07\n",
|
||||
"-0.176 2.862987e-07\n",
|
||||
"-0.174 3.589597e-07\n",
|
||||
"-0.172 4.629766e-07\n",
|
||||
"-0.170 7.34634e-07\n",
|
||||
"-0.168 9.591903e-07\n",
|
||||
"-0.166 1.226978e-06\n",
|
||||
"-0.164 1.638941e-06\n",
|
||||
"-0.162 2.070232e-06\n",
|
||||
"-0.160 2.070232e-06\n",
|
||||
"-0.158 2.719557e-06\n",
|
||||
"-0.156 3.40311e-06\n",
|
||||
"-0.154 4.3336e-06\n",
|
||||
"-0.152 5.30817e-06\n",
|
||||
"-0.150 7.05408e-06\n",
|
||||
"-0.148 7.882228e-06\n",
|
||||
"-0.146 8.391648e-06\n",
|
||||
"-0.144 8.645698e-06\n",
|
||||
"-0.142 8.800723e-06\n",
|
||||
"-0.140 8.833474e-06\n",
|
||||
"-0.138 8.667967e-06\n",
|
||||
"-0.136 8.724034e-06\n",
|
||||
"-0.134 8.751249e-06\n",
|
||||
"-0.132 8.949502e-06\n",
|
||||
"-0.130 8.983748e-06\n",
|
||||
"-0.128 8.932714e-06\n",
|
||||
"-0.126 8.97258e-06\n",
|
||||
"-0.124 8.960077e-06\n",
|
||||
"-0.122 9.000123e-06\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"<Figure size 640x480 with 1 Axes>"
|
||||
],
|
||||
"image/png": "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 truncated
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"[ 0.00048058 -0.15422773 0.00713343]\n",
|
||||
"Manually calculated FWHM to 0.016797947350266428 mm\n",
|
||||
"Scipy calculated FWHM = -0.015900000000000247 mm\n",
|
||||
"Scipy calculated 1/e2 = -0.02798400000000001 mm\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"<Figure size 640x480 with 1 Axes>"
|
||||
],
|
||||
"image/png": "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 truncated
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"execution_count": 68
|
||||
},
|
||||
{
|
||||
"metadata": {},
|
||||
"cell_type": "code",
|
||||
"outputs": [],
|
||||
"execution_count": null,
|
||||
"source": "fit_knife_edge(y_data, motor='Y')",
|
||||
"id": "d6dd221d89d3b71a"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 2
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython2",
|
||||
"version": "2.7.6"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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):
|
||||
File renamed without changes.
@@ -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)
|
||||
File renamed without changes.
File renamed without changes.
@@ -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)
|
||||
File renamed without changes.
@@ -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)
|
||||
File renamed without changes.
File renamed without changes.
@@ -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)
|
||||
File renamed without changes.
@@ -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):
|
||||
File renamed without changes.
@@ -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")
|
||||
|
||||
Loaded 100 of 143 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user