Include everything in one repo, for better development
Build and Publish / build (push) Successful in 26s
Build and Publish / build (push) Successful in 26s
This commit is contained in:
1 parent
6be6ae1843
commit
a38b3b055a
78 files changed
+3362
-7
No files matched your search
@@ -19,11 +19,28 @@ jobs:
|
||||
python3 -m venv venv # Create a virtual environment
|
||||
source venv/bin/activate
|
||||
pip install twine build
|
||||
mkdir dist/
|
||||
|
||||
- name: Build the Wheel
|
||||
- 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
|
||||
|
||||
- name: Upload Package to Gitea PyPI
|
||||
env:
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
.idea/
|
||||
.venv/
|
||||
@@ -0,0 +1,17 @@
|
||||
[project]
|
||||
name = "aaredaqlib"
|
||||
version = "0.1.0"
|
||||
description = "Libraries shared between AareDAQ and AareGUI"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"pydantic==2.11.4",
|
||||
"numpy==2.2.5",
|
||||
]
|
||||
|
||||
[lint]
|
||||
ignore = ["F401", "F541", "W503", "W504"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=75.6.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
File renamed without changes.
@@ -0,0 +1,104 @@
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Coordinate(BaseModel):
|
||||
x: float = 0.0
|
||||
y: float = 0.0
|
||||
z: float = 0.0
|
||||
|
||||
# Overload +
|
||||
def __add__(self, other: "Coordinate") -> "Coordinate":
|
||||
if isinstance(other, Coordinate):
|
||||
return Coordinate(
|
||||
x=self.x + other.x, y=self.y + other.y, z=self.z + other.z
|
||||
)
|
||||
return NotImplemented
|
||||
|
||||
# Overload -
|
||||
def __sub__(self, other: "Coordinate") -> "Coordinate":
|
||||
if isinstance(other, Coordinate):
|
||||
return Coordinate(
|
||||
x=self.x - other.x, y=self.y - other.y, z=self.z - other.z
|
||||
)
|
||||
return NotImplemented
|
||||
|
||||
# Overload *
|
||||
def __mul__(self, other):
|
||||
if isinstance(other, (int, float)): # Scalar multiplication
|
||||
return Coordinate(x=self.x * other, y=self.y * other, z=self.z * other)
|
||||
elif isinstance(other, Coordinate): # Dot product
|
||||
return self.x * other.x + self.y * other.y + self.z * other.z
|
||||
return NotImplemented
|
||||
|
||||
# Overload /
|
||||
def __truediv__(self, scalar: float) -> "Coordinate":
|
||||
if isinstance(scalar, (int, float)) and scalar != 0: # Avoid division by zero
|
||||
return Coordinate(x=self.x / scalar, y=self.y / scalar, z=self.z / scalar)
|
||||
elif scalar == 0:
|
||||
raise ValueError("Cannot divide by zero")
|
||||
return NotImplemented
|
||||
|
||||
def normalize(self) -> "Coordinate":
|
||||
magnitude = np.sqrt(self.x**2 + self.y**2 + self.z**2)
|
||||
if magnitude == 0:
|
||||
raise ValueError("Cannot normalize a zero-magnitude vector.")
|
||||
return self / magnitude
|
||||
|
||||
def rotate(self, angle_deg: float, axis: str) -> "Coordinate":
|
||||
"""
|
||||
Rotate the coordinate around a specified axis by a given angle in degrees.
|
||||
|
||||
:param angle_deg: The angle of rotation in degrees.
|
||||
:param axis: The axis to rotate around ('x', 'y', or 'z').
|
||||
:return: A new Coordinate after rotation.
|
||||
"""
|
||||
angle_rad = np.radians(angle_deg) # Convert angle to radians
|
||||
c = np.cos(angle_rad)
|
||||
s = np.sin(angle_rad)
|
||||
|
||||
if axis == "x":
|
||||
# Rotate around x-axis (affects y, z)
|
||||
y_new = self.y * c - self.z * s
|
||||
z_new = self.y * s + self.z * c
|
||||
return Coordinate(x=self.x, y=y_new, z=z_new)
|
||||
elif axis == "y":
|
||||
# Rotate around y-axis (affects x, z)
|
||||
x_new = self.x * c + self.z * s
|
||||
z_new = -self.x * s + self.z * c
|
||||
return Coordinate(x=x_new, y=self.y, z=z_new)
|
||||
elif axis == "z":
|
||||
# Rotate around z-axis (affects x, y)
|
||||
x_new = self.x * c - self.y * s
|
||||
y_new = self.x * s + self.y * c
|
||||
return Coordinate(x=x_new, y=y_new, z=self.z)
|
||||
else:
|
||||
raise ValueError("Invalid axis. Choose 'x', 'y', or 'z'.")
|
||||
|
||||
|
||||
class SmargonCoordinate(BaseModel):
|
||||
sh_mm: Optional[Coordinate] = None # SH coordinate in Smargon
|
||||
phi_deg: Optional[float] = None
|
||||
chi_deg: Optional[float] = None
|
||||
|
||||
def eq(self, other: "SmargonCoordinate", tol: float) -> bool:
|
||||
return (
|
||||
abs(self.sh_mm.x - other.sh_mm.x) < tol
|
||||
and abs(self.sh_mm.y - other.sh_mm.y) < tol
|
||||
and abs(self.sh_mm.z - other.sh_mm.z) < tol
|
||||
and abs(self.phi_deg - other.phi_deg) < tol
|
||||
and abs(self.chi_deg - other.chi_deg) < tol
|
||||
)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if isinstance(other, SmargonCoordinate):
|
||||
return self.eq(other, tol=0.1)
|
||||
return NotImplemented
|
||||
|
||||
|
||||
def positive_coords(value: Coordinate) -> Coordinate:
|
||||
if value.x <= 0 or value.y <= 0:
|
||||
raise ValueError("Coordinates must be positive")
|
||||
return value
|
||||
@@ -0,0 +1,6 @@
|
||||
from aaredaqlib import SmargonCoordinate
|
||||
from aaredaqlib.rotation_scan import RotationScanRequest
|
||||
|
||||
|
||||
class HelicalScanRequest(RotationScanRequest):
|
||||
end: SmargonCoordinate
|
||||
@@ -0,0 +1,171 @@
|
||||
from enum import Enum
|
||||
from typing import Annotated, Literal, Tuple, List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from aaredaqlib import Coordinate
|
||||
from aaredaqlib import SampleGeometryModel
|
||||
|
||||
class TokenData(BaseModel):
|
||||
sub: str # Username
|
||||
pgroups: List[str]
|
||||
session: int
|
||||
staff: bool = False
|
||||
|
||||
class DewarAddress(BaseModel):
|
||||
segment: Literal["A", "B", "C", "D", "E", "F", "X", "R"]
|
||||
pos: Annotated[int, Field(ge=1, le=5)]
|
||||
|
||||
|
||||
class SampleDewarAddress(BaseModel):
|
||||
puck: DewarAddress
|
||||
pin: Annotated[int, Field(ge=1, le=16)]
|
||||
|
||||
|
||||
# From the database for puck loading
|
||||
class PuckInfo(BaseModel):
|
||||
db_id: int
|
||||
puck_name: str
|
||||
dewar_name: str
|
||||
user: str = ""
|
||||
location: Optional[DewarAddress] = None
|
||||
|
||||
|
||||
# From TELL to database after loading
|
||||
class PuckLoadedInfo(BaseModel):
|
||||
puck_name: str
|
||||
location: DewarAddress
|
||||
|
||||
|
||||
# From database to TELL after loading
|
||||
class SampleShortInfo(BaseModel):
|
||||
db_id: int
|
||||
puck_name: str
|
||||
dewar_name: str
|
||||
sample_name: str
|
||||
user: str = ""
|
||||
pin: Annotated[int, Field(ge=1, le=16)]
|
||||
location: DewarAddress | None = None
|
||||
priority: Optional[float] = 1.0
|
||||
comment: Optional[str] = None
|
||||
mount_count: int = 0
|
||||
def tell_address(self) -> SampleDewarAddress:
|
||||
return SampleDewarAddress(puck=self.location, pin=self.pin)
|
||||
|
||||
def loc_str(self) -> str:
|
||||
if self.location is None:
|
||||
return "-"
|
||||
else:
|
||||
return f"{self.location.segment}{self.location.pos}-{self.pin}"
|
||||
|
||||
def loc_str_sort(self) -> str:
|
||||
if self.location is None:
|
||||
return ""
|
||||
else:
|
||||
return f"{self.location.segment}{self.location.pos}-{self.pin:02d}"
|
||||
|
||||
|
||||
class SampleShortInfoList(BaseModel):
|
||||
s: List[SampleShortInfo]
|
||||
|
||||
|
||||
class BeamCenterCoeffModel(BaseModel):
|
||||
"""
|
||||
Model to calculate beam center for a given zoom level based on a quadratic approximation
|
||||
For both x and y it contains tuple of coefficients a, b, c (a*zoom^2 + b*zoom + c)
|
||||
Default is beam center in 1000,1000 at any zoom level
|
||||
"""
|
||||
|
||||
coeff_x: Tuple[float, float, float] = (0, 0, 1000.0)
|
||||
coeff_y: Tuple[float, float, float] = (0, 0, 1000.0)
|
||||
|
||||
def apply(self, zoom: float):
|
||||
return Coordinate(
|
||||
x=self.coeff_x[0] * zoom**2 + self.coeff_x[1] * zoom + self.coeff_x[2],
|
||||
y=self.coeff_y[0] * zoom**2 + self.coeff_y[1] * zoom + self.coeff_y[2],
|
||||
)
|
||||
|
||||
|
||||
class LoopCenteringZoomModelElem(BaseModel):
|
||||
zoom_value: float
|
||||
sam_cam_gain: float
|
||||
sam_cam_exp: float
|
||||
|
||||
|
||||
class LoopCenteringZoomModel(BaseModel):
|
||||
z: List[LoopCenteringZoomModelElem]
|
||||
|
||||
|
||||
# zoom gain exp (X06DA)
|
||||
# 1 50 0.05
|
||||
# 200 50 0.05
|
||||
# 500 70 0.05
|
||||
# 700 150 0.05
|
||||
# 800 100 0.10
|
||||
# 1000 200 0.10
|
||||
|
||||
|
||||
def def_loop_centering_zoom() -> LoopCenteringZoomModel:
|
||||
return LoopCenteringZoomModel(
|
||||
z=[
|
||||
LoopCenteringZoomModelElem(
|
||||
zoom_value=200, sam_cam_gain=50.0, sam_cam_exp=0.05
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class BeamlineStateEnum(Enum):
|
||||
Maintenance = 1
|
||||
SampleExchange = 2
|
||||
SampleAlignment = 3
|
||||
DataCollection = 4
|
||||
DewarTransfer = 5
|
||||
XrayFluorescence = 6
|
||||
BeamLocation = 7
|
||||
Moving = 8
|
||||
RobotSampleExchange = 9
|
||||
# Busy state is used whenever transition between two states happen or mounting/data collection procedure happens
|
||||
|
||||
|
||||
class SessionsStateEnum(Enum):
|
||||
OwnedByYou = 1
|
||||
OwnedByElse = 2
|
||||
Vacant = 3
|
||||
|
||||
class BeamlineStatus(BaseModel):
|
||||
energy_keV: float
|
||||
detector_distance_mm: float
|
||||
ring_current_mA: float
|
||||
light: Annotated[float, Field(ge=0.0, le=100.0)]
|
||||
cryojet_K: float
|
||||
|
||||
class SessionStatus(BaseModel):
|
||||
session: SessionsStateEnum = SessionsStateEnum.Vacant
|
||||
current_pgroup: str | None
|
||||
staff: bool = False
|
||||
|
||||
class DAQStatusModel(BaseModel):
|
||||
state: BeamlineStateEnum
|
||||
busy: bool
|
||||
geom: SampleGeometryModel
|
||||
bl: BeamlineStatus
|
||||
sample: SampleShortInfo | None = None
|
||||
session: SessionStatus
|
||||
|
||||
class BeamlineSettingsModel(BaseModel):
|
||||
# Float properties
|
||||
cryojet_park_position: float = 12.0
|
||||
cryojet_measurement_position: float = 5.0
|
||||
dtz_collection: float = 130
|
||||
dtz_park: float = 150.0
|
||||
dtz_wash_sample_distance: float = 150.0
|
||||
dtz_bsz_safety_margin: float = 50.0
|
||||
bsz: float = 25.0
|
||||
|
||||
camera_max_magnification: float = 1.0
|
||||
camera_min_magnification: float = 500.0
|
||||
camera_translation_factor_a: float = 0.00253
|
||||
camera_translation_factor_b: float = 255.0
|
||||
|
||||
cryojet_in_use: bool = True
|
||||
@@ -0,0 +1,54 @@
|
||||
import math
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import Field, AfterValidator
|
||||
|
||||
from aaredaqlib import Coordinate, positive_coords, SmargonCoordinate
|
||||
from aaredaqlib import SampleGeometryModel
|
||||
from aaredaqlib import ScanRequest
|
||||
|
||||
|
||||
def normalize_angle(angle_deg: float) -> float:
|
||||
normalized_angle = math.fmod(angle_deg + 180, 360) - 180.0
|
||||
return normalized_angle
|
||||
|
||||
|
||||
class RasterGridRequest(ScanRequest):
|
||||
# Number of grid elements
|
||||
n_x: Annotated[int, Field(ge=1)]
|
||||
n_y: Annotated[int, Field(ge=1)]
|
||||
|
||||
# Size of grid elements
|
||||
grid_size_mm: Annotated[Coordinate, AfterValidator(positive_coords)]
|
||||
|
||||
# Coordinates of top left corner in Smargon (SH...) coordinates
|
||||
smargon: SmargonCoordinate
|
||||
|
||||
# omega angle
|
||||
omega_deg: float = 0.0
|
||||
|
||||
def get_image_number(self) -> int:
|
||||
return self.n_x * self.n_y
|
||||
|
||||
def grid_size_pxl(self, geom: SampleGeometryModel) -> Coordinate:
|
||||
return self.grid_size_mm / geom.pixel_in_mm
|
||||
|
||||
def start_pxl(self, geom: SampleGeometryModel) -> Coordinate:
|
||||
return geom.smargon_to_beamline(self.smargon.sh_mm)
|
||||
|
||||
def point_sample_belongs_to_grid(
|
||||
self, geom: SampleGeometryModel, i: Coordinate
|
||||
) -> bool:
|
||||
point = i - geom.smargon_to_beamline(self.smargon.sh_mm)
|
||||
return (0 <= point.x < self.n_x * self.grid_size_mm.x) and (
|
||||
0 <= point.y < self.n_y * self.grid_size_mm.y
|
||||
)
|
||||
|
||||
def grid_visible(self, geom: SampleGeometryModel) -> bool:
|
||||
if (
|
||||
abs(normalize_angle(self.omega_deg - geom.omega_deg)) < 1.0
|
||||
and abs(self.smargon.phi_deg - geom.smargon.phi_deg) < 1.0
|
||||
and abs(self.smargon.chi_deg - geom.smargon.chi_deg) < 1.0
|
||||
):
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,13 @@
|
||||
from aaredaqlib import SmargonCoordinate
|
||||
from aaredaqlib import ScanRequest
|
||||
|
||||
|
||||
class RotationScanRequest(ScanRequest):
|
||||
start: SmargonCoordinate | None = None # None = start where you are
|
||||
|
||||
start_omega_deg: float = 0
|
||||
omega_incr_deg: float
|
||||
steps: int
|
||||
|
||||
def get_image_number(self) -> int:
|
||||
return self.steps
|
||||
@@ -0,0 +1,87 @@
|
||||
from typing import Annotated
|
||||
|
||||
import numpy as np
|
||||
from pydantic import BaseModel, Field, AfterValidator
|
||||
|
||||
from aaredaqlib import Coordinate, SmargonCoordinate, positive_coords
|
||||
|
||||
|
||||
class SampleGeometryModel(BaseModel):
|
||||
# Beam location in the camera coordinates
|
||||
beam_location_pxl: Coordinate
|
||||
|
||||
# 1 pixel in mm
|
||||
pixel_in_mm: Annotated[float, Field(gt=0.0)]
|
||||
|
||||
aerotech: Coordinate
|
||||
|
||||
aerotech_meas: Coordinate
|
||||
# Current smargon coordinates/target
|
||||
smargon: SmargonCoordinate
|
||||
|
||||
omega_deg: float
|
||||
|
||||
# Beam size in mm
|
||||
beam_size_mm: Annotated[Coordinate, AfterValidator(positive_coords)]
|
||||
|
||||
@property
|
||||
def beam_size_pxl(self) -> Coordinate:
|
||||
return self.beam_size_mm / self.pixel_in_mm
|
||||
|
||||
# This goes from image coordinates to relative beam coordinates in image plane
|
||||
def picture_to_sample(self, pic_pxl: Coordinate) -> Coordinate:
|
||||
# Relative to beam location
|
||||
beam_mm = (pic_pxl - self.beam_location_pxl) * self.pixel_in_mm + self.aerotech
|
||||
return beam_mm
|
||||
|
||||
# This goes from relative beam coordinates in image plane to image
|
||||
def sample_to_picture(self, beam_mm: Coordinate) -> Coordinate:
|
||||
beam_pxl = (beam_mm - self.aerotech) / self.pixel_in_mm
|
||||
return beam_pxl + self.beam_location_pxl
|
||||
|
||||
def smargon_nudge(self, coord: Coordinate) -> Coordinate:
|
||||
phi = np.radians(np.around(self.smargon.phi_deg, decimals=1))
|
||||
chi = np.radians(np.around(self.smargon.chi_deg, decimals=1))
|
||||
omega = np.radians(np.around(self.omega_deg, decimals=1))
|
||||
|
||||
offset = Coordinate()
|
||||
co = np.cos(omega)
|
||||
so = np.sin(omega)
|
||||
cp = np.cos(phi)
|
||||
sp = np.sin(phi)
|
||||
cc = np.cos(chi)
|
||||
sc = np.sin(chi)
|
||||
|
||||
offset.x = (
|
||||
-coord.z * co * sp
|
||||
- coord.y * so * sp
|
||||
+ coord.x * cp * sc
|
||||
- coord.y * co * cc * cp
|
||||
+ coord.z * so * cc * cp
|
||||
)
|
||||
offset.y = (
|
||||
-coord.z * co * cp
|
||||
- coord.y * so * cp
|
||||
- coord.x * sc * sp
|
||||
+ coord.y * co * cc * sp
|
||||
- coord.z * so * cc * sp
|
||||
)
|
||||
offset.z = -coord.x * cc - coord.y * co * sc + coord.z * so * sc
|
||||
return offset
|
||||
|
||||
def beamline_to_smargon(self, coord: Coordinate) -> Coordinate:
|
||||
return self.smargon.sh_mm + self.smargon_nudge(coord)
|
||||
|
||||
def smargon_to_beamline(self, coord: Coordinate) -> Coordinate:
|
||||
rel = coord - self.smargon.sh_mm
|
||||
|
||||
vec_x = self.smargon_nudge(Coordinate(x=1)).normalize()
|
||||
vec_y = self.smargon_nudge(Coordinate(y=1)).normalize()
|
||||
|
||||
return Coordinate(x=rel * vec_x, y=rel * vec_y)
|
||||
|
||||
def picture_to_smargon(self, coord: Coordinate) -> Coordinate:
|
||||
return self.beamline_to_smargon(self.picture_to_sample(coord))
|
||||
|
||||
def smargon_to_picture(self, coord: Coordinate) -> Coordinate:
|
||||
return self.sample_to_picture(self.smargon_to_beamline(coord))
|
||||
@@ -0,0 +1,15 @@
|
||||
from abc import abstractmethod, ABC
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ScanRequest(ABC, BaseModel):
|
||||
dtz: float
|
||||
transmission: float
|
||||
file_prefix: str
|
||||
exp_time_s: float
|
||||
|
||||
@abstractmethod
|
||||
def get_image_number(self) -> int:
|
||||
pass
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
from aaredaqlib import ScanRequest
|
||||
|
||||
|
||||
class ScreeningScanRequest(ScanRequest):
|
||||
omega_start_deg: float
|
||||
omega_step_deg: float
|
||||
steps: int
|
||||
oscrange_deg: float
|
||||
|
||||
def get_image_number(self) -> int:
|
||||
return self.steps
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "aaredaq"
|
||||
version = "0.0.1"
|
||||
version = "0.1.0"
|
||||
description = "AareDAQ data acquisition server"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
@@ -14,7 +14,7 @@ dependencies = [
|
||||
"uvicorn==0.34.2",
|
||||
"ultralytics==8.3.133",
|
||||
"aareDB==0.1.1a4",
|
||||
"aaredaqlib==0.0.2",
|
||||
"aaredaqlib==0.1.0",
|
||||
"jfjoch_client==1.0.0rc37"
|
||||
]
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,7 +1,4 @@
|
||||
from xml.sax.handler import property_dom_node
|
||||
|
||||
from aaredaqlib.coordinate import Coordinate
|
||||
from aaredaqlib.models import BeamlineStatus
|
||||
from epics import PV
|
||||
|
||||
from aaredaq.aaredb import AareWrapper
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,20 @@
|
||||
[project]
|
||||
name = "aaregui"
|
||||
version = "0.1.0"
|
||||
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.1.0"
|
||||
]
|
||||
|
||||
[lint]
|
||||
ignore = ["F401", "F541", "W503", "W504"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=75.6.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
Whitespace-only changes.
@@ -0,0 +1,20 @@
|
||||
import sys
|
||||
|
||||
from PySide6.QtWidgets import QApplication, QDialog
|
||||
|
||||
from aaregui import MainWindow
|
||||
from aaregui import LoginDialog
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
base_url = "http://0.0.0.0:5210"
|
||||
dialog = LoginDialog(base_url=base_url)
|
||||
|
||||
if dialog.exec() == QDialog.DialogCode.Accepted and dialog.token:
|
||||
win = MainWindow(base_url=base_url, token=dialog.token)
|
||||
win.show()
|
||||
sys.exit(app.exec())
|
||||
else:
|
||||
print("Cancelled")
|
||||
sys.exit()
|
||||
@@ -0,0 +1,200 @@
|
||||
import jwt
|
||||
from PySide6.QtCore import Qt, Slot, Signal
|
||||
from PySide6.QtGui import QAction
|
||||
from PySide6.QtWidgets import (
|
||||
QMainWindow,
|
||||
QWidget,
|
||||
QSplitter,
|
||||
QHBoxLayout,
|
||||
QVBoxLayout,
|
||||
QMessageBox,
|
||||
QApplication, )
|
||||
from aaredaqlib.coordinate import Coordinate, SmargonCoordinate
|
||||
from aaredaqlib.models import SampleShortInfoList, TokenData
|
||||
from aaredaqlib.sample_geometry import SampleGeometryModel
|
||||
|
||||
from aaregui import BeamlineControls
|
||||
from aaregui import DataCollectionSettings
|
||||
from aaregui import SampleQueuePanel
|
||||
from aaregui import StatusPanel
|
||||
from aaregui import TellSamplePanel
|
||||
from aaregui import SampleCameraThread
|
||||
from aaregui import DAQWorker
|
||||
from aaregui import SampleCameraImageLabel
|
||||
from aaregui import NoWheelScrollArea
|
||||
from aaregui import StatusBar
|
||||
|
||||
|
||||
# Subclass QMainWindow to define the behavior of your application
|
||||
class MainWindow(QMainWindow):
|
||||
sample_geometry = Signal(SampleGeometryModel)
|
||||
|
||||
def __init__(self, base_url: str, token: str):
|
||||
super().__init__()
|
||||
self.__base_url = base_url
|
||||
self.__token = token
|
||||
|
||||
# Decode the JWT without signature verification
|
||||
self.__decoded_token = TokenData(**jwt.decode(token, options={"verify_signature": False}))
|
||||
print(self.__decoded_token)
|
||||
|
||||
self.setStyleSheet("background-color: rgb(216, 228, 253);")
|
||||
|
||||
top_widget = QWidget(parent=self)
|
||||
top_widget_layout = QHBoxLayout(top_widget)
|
||||
top_widget.setLayout(top_widget_layout)
|
||||
|
||||
geom = SampleGeometryModel(beam_location_pxl=Coordinate(x=1000,y=1000),
|
||||
pixel_in_mm=1,
|
||||
aerotech=Coordinate(),
|
||||
smargon=SmargonCoordinate(sh_mm=Coordinate(), phi_deg=0, chi_deg=0),
|
||||
omega_deg=0,
|
||||
beam_size_mm=Coordinate(x=1, y=1),
|
||||
aerotech_meas=Coordinate()
|
||||
)
|
||||
rasters = []
|
||||
|
||||
self.data_collection = DataCollectionSettings(s=geom, parent=top_widget)
|
||||
top_widget_layout.addWidget(self.data_collection)
|
||||
|
||||
self.camera_image = SampleCameraImageLabel(
|
||||
geom=geom, rasters=rasters, parent=top_widget
|
||||
)
|
||||
|
||||
top_widget_layout.addWidget(self.camera_image)
|
||||
|
||||
beamline_controls_scroll = NoWheelScrollArea(top_widget)
|
||||
|
||||
self.beamline = BeamlineControls(beamline_controls_scroll, staff=self.__decoded_token.staff)
|
||||
top_widget_layout.addWidget(beamline_controls_scroll)
|
||||
beamline_controls_scroll.setWidget(self.beamline)
|
||||
beamline_controls_scroll.setHorizontalScrollBarPolicy(
|
||||
Qt.ScrollBarPolicy.ScrollBarAlwaysOff
|
||||
)
|
||||
beamline_controls_scroll.setFixedWidth(self.beamline.set_width + 10)
|
||||
bottom_widget_splitter = QSplitter(Qt.Orientation.Horizontal, parent=self)
|
||||
|
||||
bottom_widget = QWidget(self)
|
||||
|
||||
self.tell_samples = TellSamplePanel(parent=bottom_widget, samples=SampleShortInfoList(s = []))
|
||||
bottom_widget_splitter.addWidget(self.tell_samples)
|
||||
self.job_list_panel = SampleQueuePanel(parent=bottom_widget)
|
||||
bottom_widget_splitter.addWidget(self.job_list_panel)
|
||||
|
||||
self.status_panel = StatusPanel(parent=bottom_widget)
|
||||
bottom_widget_splitter.addWidget(self.status_panel)
|
||||
|
||||
bottom_widget_layout = QVBoxLayout(bottom_widget)
|
||||
bottom_widget.setLayout(bottom_widget_layout)
|
||||
bottom_widget_layout.addWidget(bottom_widget_splitter)
|
||||
|
||||
splitter = QSplitter(Qt.Orientation.Vertical, parent=self)
|
||||
splitter.addWidget(top_widget)
|
||||
splitter.addWidget(bottom_widget)
|
||||
|
||||
self.setCentralWidget(splitter)
|
||||
|
||||
self.setWindowTitle("Aare macromolecular crystallography GUI")
|
||||
self.create_menu_bar()
|
||||
|
||||
self.status_bar = StatusBar(self.__decoded_token, parent=self)
|
||||
self.setStatusBar(self.status_bar)
|
||||
|
||||
self.daq = DAQWorker(base_url=self.__base_url, token=self.__token)
|
||||
self.daq.spreadsheet.connect(self.tell_samples.new_sample_list)
|
||||
self.daq.update.connect(self.beamline.omega_panel.update_daq_status)
|
||||
self.daq.update.connect(self.beamline.smargon_panel.update_daq_status)
|
||||
self.daq.update.connect(self.data_collection.helical.update_daq_status)
|
||||
self.daq.update.connect(self.beamline.illumination_panel.update_daq_status)
|
||||
self.daq.update.connect(self.status_panel.update_daq_status)
|
||||
self.daq.update.connect(self.camera_image.update_daq_status)
|
||||
self.daq.update.connect(self.tell_samples.update_daq_status)
|
||||
self.daq.update.connect(self.beamline.abr_tweak.update_daq_status)
|
||||
self.daq.update.connect(self.status_bar.update_daq_status)
|
||||
self.beamline.state_panel.sample_exchange.clicked.connect(self.daq.sample_exchange)
|
||||
self.beamline.state_panel.sample_alignment.clicked.connect(self.daq.sample_alignment)
|
||||
self.beamline.state_panel.dewar_exchange.clicked.connect(self.daq.dewar_exchange)
|
||||
self.beamline.loopctr.background.clicked.connect(self.daq.alc_background)
|
||||
self.beamline.loopctr.find_tip.clicked.connect(self.daq.center_loop)
|
||||
self.camera_image.smargon.connect(self.daq.move_smargon)
|
||||
self.beamline.smargon_panel.smargon.connect(self.daq.move_smargon)
|
||||
|
||||
self.beamline.omega_panel.set_omega.connect(self.daq.set_omega)
|
||||
self.camera_image.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.abr_tweak.abr_tweak.connect(self.daq.abr_tweak)
|
||||
self.beamline.abr_tweak.abr_save.connect(self.daq.abr_save)
|
||||
|
||||
self.beamline.beam_center.beam_center_clear.connect(self.daq.beam_center_clear)
|
||||
self.camera_image.update_beam_center.connect(self.daq.beam_center_mark)
|
||||
|
||||
self.camera_thread = SampleCameraThread(zmq_url="tcp://129.129.110.12:9089")
|
||||
self.camera_thread.camera_image.connect(self.camera_image.update_pixmap)
|
||||
self.camera_thread.start()
|
||||
QApplication.instance().aboutToQuit.connect(self.cleanup)
|
||||
|
||||
#
|
||||
# self.data_collection.helical.helical_scan.connect(self.worker.helical_scan)
|
||||
# self.data_collection.helical.update_bookmarks.connect(
|
||||
# self.camera_image.update_bookmarks
|
||||
# )
|
||||
#
|
||||
|
||||
self.camera_image.run_raster.connect(self.daq.raster_scan)
|
||||
self.data_collection.standard_data_collection.connect(self.daq.standard_scan)
|
||||
self.data_collection.screening_data_collection.connect(self.daq.screening_scan)
|
||||
|
||||
self.data_collection.helical.show_helical_start.connect(self.camera_image.show_helical_start)
|
||||
self.data_collection.helical.show_helical_end.connect(self.camera_image.show_helical_end)
|
||||
|
||||
self.camera_image.set_helical_start.connect(self.data_collection.helical.set_start_point)
|
||||
self.camera_image.set_helical_end.connect(self.data_collection.helical.set_end_point)
|
||||
self.job_list_panel.auto_scan.connect(self.daq.automated_scan)
|
||||
self.daq.automated_scan_done.connect(self.job_list_panel.automated_scan_done)
|
||||
self.job_list_panel.unmount.connect(self.daq.unmount)
|
||||
|
||||
self.tell_samples.mount.connect(self.daq.mount)
|
||||
self.tell_samples.unmount.connect(self.daq.unmount)
|
||||
|
||||
self.data_collection.raster.grid_size_updated.connect(
|
||||
self.camera_image.update_raster_grid_size
|
||||
)
|
||||
self.status_bar.set_pgroup.connect(self.daq.set_pgroup)
|
||||
self.status_bar.end_session.connect(self.daq.end_session)
|
||||
self.status_bar.force_session.connect(self.daq.force_session)
|
||||
self.status_bar.dewar_exchange.connect(self.daq.dewar_exchange)
|
||||
self.status_bar.sample_exchange.connect(self.daq.sample_exchange)
|
||||
self.status_bar.sample_alignment.connect(self.daq.sample_alignment)
|
||||
|
||||
def create_menu_bar(self):
|
||||
"""Create a menu bar with File->Quit and Help->About."""
|
||||
# Main menu bar
|
||||
menu_bar = self.menuBar()
|
||||
|
||||
# File menu
|
||||
file_menu = menu_bar.addMenu("File")
|
||||
quit_action = QAction("&Quit", self) # Action for 'Quit'
|
||||
quit_action.triggered.connect(self.close) # Connect to window's `close` method
|
||||
file_menu.addAction(quit_action)
|
||||
|
||||
# Help menu
|
||||
help_menu = menu_bar.addMenu("Help")
|
||||
about_action = QAction("About", self) # Action for 'About'
|
||||
about_action.triggered.connect(self.show_about_dialog)
|
||||
help_menu.addAction(about_action)
|
||||
|
||||
def show_about_dialog(self):
|
||||
QMessageBox.about(
|
||||
self,
|
||||
"About",
|
||||
"Aare Macromolecular Crystallography GUI\nVersion: 1.0\nCopyright: Paul Scherrer Insitute 2024-2025",
|
||||
)
|
||||
|
||||
@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 cleanup(self):
|
||||
self.camera_thread.stop()
|
||||
Whitespace-only changes.
@@ -0,0 +1,27 @@
|
||||
from typing import Literal
|
||||
|
||||
from PySide6.QtGui import QColor
|
||||
|
||||
from aaredaqlib.coordinate import SmargonCoordinate
|
||||
|
||||
|
||||
class SmargonBookmark:
|
||||
coord: SmargonCoordinate
|
||||
color: Literal[
|
||||
"red", "green", "blue", "indigo", "lime"
|
||||
] # Restrict to specific colors
|
||||
|
||||
def qt_color(self) -> QColor:
|
||||
"""Convert the color property to a QColor."""
|
||||
color_map = {
|
||||
"red": QColor("red"),
|
||||
"green": QColor("green"),
|
||||
"blue": QColor("blue"),
|
||||
"indigo": QColor("indigo"),
|
||||
"lime": QColor("lime"),
|
||||
}
|
||||
return color_map[self.color] # Map the color string to QColor
|
||||
|
||||
|
||||
class SmargonBookmarkList:
|
||||
b: list[SmargonBookmark]
|
||||
@@ -0,0 +1,119 @@
|
||||
from PySide6.QtCore import QAbstractTableModel, Qt, Slot
|
||||
from PySide6.QtGui import QBrush, QColor
|
||||
|
||||
from aaredaqlib.models import SampleShortInfo, SampleShortInfoList
|
||||
|
||||
|
||||
def get_entry(sample: SampleShortInfo, column: int):
|
||||
if column == 0:
|
||||
return sample.user
|
||||
elif column == 1:
|
||||
return sample.puck_name
|
||||
elif column == 2:
|
||||
return sample.sample_name
|
||||
|
||||
|
||||
class SampleQueueSpreadsheet(QAbstractTableModel):
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
samples: list[SampleShortInfo] | None = None
|
||||
):
|
||||
super().__init__(parent)
|
||||
if samples is None:
|
||||
samples = []
|
||||
self.__running = False
|
||||
self.samples: list[SampleShortInfo] = samples
|
||||
self.header = [
|
||||
"User",
|
||||
"Puck",
|
||||
"Sample",
|
||||
]
|
||||
|
||||
def flags(self, index):
|
||||
default_flags = super().flags(index)
|
||||
return Qt.ItemFlag.ItemIsDropEnabled | default_flags
|
||||
|
||||
def rowCount(self, parent=None):
|
||||
return len(self.samples)
|
||||
|
||||
def columnCount(self, parent=None):
|
||||
return len(self.header)
|
||||
|
||||
def data(self, index, role=None):
|
||||
if role == Qt.ItemDataRole.DisplayRole:
|
||||
return get_entry(self.samples[index.row()], index.column())
|
||||
elif role == Qt.ItemDataRole.TextAlignmentRole: # Align text to center
|
||||
return Qt.AlignmentFlag.AlignCenter
|
||||
elif role == Qt.ItemDataRole.BackgroundRole:
|
||||
if index.row() == 0:
|
||||
if self.__running:
|
||||
return QBrush(QColor(255, 102, 0)) # Fluorescent orange
|
||||
else:
|
||||
return QBrush(QColor(114, 159, 207)) # Dark blue
|
||||
return QBrush(QColor(255, 255, 255)) # White
|
||||
return None # For other roles, return None
|
||||
|
||||
def headerData(self, section, orientation, role=None):
|
||||
if role == Qt.ItemDataRole.DisplayRole:
|
||||
if orientation == Qt.Orientation.Horizontal: # Column header
|
||||
return self.header[section] if self.header else f"Column {section + 1}"
|
||||
if orientation == Qt.Orientation.Vertical: # Row header
|
||||
return str(section + 1) # Row numbers start from 1
|
||||
return None
|
||||
|
||||
def updateData(self, samples: list[SampleShortInfo],):
|
||||
self.beginResetModel()
|
||||
self.samples = samples
|
||||
self.endResetModel()
|
||||
|
||||
def mimeTypes(self):
|
||||
return ['text/plain']
|
||||
|
||||
def canDropMimeData(self, data, action, row, column, parent):
|
||||
if data.hasText():
|
||||
return True
|
||||
return False
|
||||
|
||||
def dropMimeData(self, data, action, row, column, parent):
|
||||
if not self.canDropMimeData(data, action, row, column, parent):
|
||||
return False
|
||||
|
||||
if parent.isValid():
|
||||
row = parent.row()
|
||||
|
||||
try:
|
||||
l = SampleShortInfoList.model_validate_json(data.text())
|
||||
self.beginResetModel()
|
||||
for sample in l.s:
|
||||
updated_row = row
|
||||
updated_samples = []
|
||||
for i in range(len(self.samples)):
|
||||
if sample != self.samples[i]:
|
||||
updated_samples.append(self.samples[i])
|
||||
else:
|
||||
if 0 < row < i:
|
||||
updated_row -= 1
|
||||
if updated_row == -1:
|
||||
updated_samples.append(sample)
|
||||
else:
|
||||
updated_samples.insert(updated_row, sample)
|
||||
self.samples = updated_samples
|
||||
self.endResetModel()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return False
|
||||
return True
|
||||
|
||||
def set_running(self, running: bool):
|
||||
self.__running = running
|
||||
|
||||
def remove_sample(self, db_id: int):
|
||||
self.beginResetModel()
|
||||
self.samples = list(filter(lambda x: x.db_id != db_id, self.samples))
|
||||
self.endResetModel()
|
||||
|
||||
def clearSamples(self):
|
||||
self.beginResetModel()
|
||||
self.samples = []
|
||||
self.endResetModel()
|
||||
@@ -0,0 +1,139 @@
|
||||
from PySide6.QtCore import QAbstractTableModel, Qt, QMimeData
|
||||
from PySide6.QtGui import QBrush, QColor
|
||||
|
||||
from aaredaqlib.models import SampleShortInfo, SampleShortInfoList
|
||||
|
||||
|
||||
def get_entry(sample: SampleShortInfo, column: int):
|
||||
if column == 0:
|
||||
return sample.sample_name
|
||||
elif column == 1:
|
||||
return sample.puck_name
|
||||
elif column == 2:
|
||||
return sample.dewar_name
|
||||
elif column == 3:
|
||||
return sample.loc_str()
|
||||
elif column == 4:
|
||||
return sample.priority
|
||||
elif column == 5:
|
||||
return sample.user
|
||||
elif column == 6:
|
||||
return sample.mount_count
|
||||
elif column == 7:
|
||||
return sample.comment
|
||||
|
||||
|
||||
|
||||
class UserSampleSpreadsheet(QAbstractTableModel):
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
samples: list[SampleShortInfo] | None = None,
|
||||
current_puck: str | None = None,
|
||||
current_sample: int | None = None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
if samples is None:
|
||||
samples = []
|
||||
self.samples: list[SampleShortInfo] = samples
|
||||
self.header = [
|
||||
"Sample name",
|
||||
"Puck",
|
||||
"Dewar",
|
||||
"Location",
|
||||
"Priority",
|
||||
"User",
|
||||
"Mount count",
|
||||
"Comment",
|
||||
]
|
||||
self.current_sample = current_sample
|
||||
self.current_puck = current_puck
|
||||
|
||||
def rowCount(self, parent=None):
|
||||
return len(self.samples)
|
||||
|
||||
def columnCount(self, parent=None):
|
||||
return len(self.header)
|
||||
|
||||
def data(self, index, role=None):
|
||||
if role == Qt.ItemDataRole.DisplayRole:
|
||||
return get_entry(self.samples[index.row()], index.column())
|
||||
elif role == Qt.ItemDataRole.TextAlignmentRole: # Align text to center
|
||||
return Qt.AlignmentFlag.AlignCenter
|
||||
elif role == Qt.ItemDataRole.BackgroundRole:
|
||||
if self.samples[index.row()].db_id == self.current_sample:
|
||||
return QBrush(QColor(114, 159, 207)) # darker blue
|
||||
if self.samples[index.row()].puck_name == self.current_puck:
|
||||
return QBrush(QColor(216, 228, 253)) # light blue
|
||||
return QBrush(QColor(255, 255, 255)) # White
|
||||
return None # For other roles, return None
|
||||
|
||||
def headerData(self, section, orientation, role=None):
|
||||
if role == Qt.ItemDataRole.DisplayRole:
|
||||
if orientation == Qt.Orientation.Horizontal: # Column header
|
||||
return self.header[section] if self.header else f"Column {section + 1}"
|
||||
if orientation == Qt.Orientation.Vertical: # Row header
|
||||
return str(section + 1) # Row numbers start from 1
|
||||
return None
|
||||
|
||||
def updateCurrentSample(
|
||||
self, current_puck: str | None = None, current_sample: int | None = None
|
||||
):
|
||||
self.current_puck = current_puck
|
||||
self.current_sample = current_sample
|
||||
|
||||
def updateData(
|
||||
self,
|
||||
samples: list[SampleShortInfo]
|
||||
):
|
||||
if self.samples != samples:
|
||||
self.beginResetModel()
|
||||
self.samples = samples
|
||||
self.endResetModel()
|
||||
|
||||
def sort(self, column, order):
|
||||
self.layoutAboutToBeChanged.emit()
|
||||
|
||||
if column == 3:
|
||||
self.samples = sorted(
|
||||
self.samples,
|
||||
key=lambda row: row.loc_str_sort(),
|
||||
reverse=(
|
||||
order == Qt.SortOrder.DescendingOrder
|
||||
), # Reverse for descending order
|
||||
)
|
||||
else:
|
||||
# Sort the samples based on the specified column and order
|
||||
self.samples = sorted(
|
||||
self.samples,
|
||||
key=lambda row: get_entry(row, column),
|
||||
reverse=(
|
||||
order == Qt.SortOrder.DescendingOrder
|
||||
), # Reverse for descending order
|
||||
)
|
||||
|
||||
# Notify views that the model layout has changed
|
||||
self.layoutChanged.emit()
|
||||
|
||||
def flags(self, index):
|
||||
default_flags = super().flags(index)
|
||||
if index.isValid():
|
||||
return Qt.ItemFlag.ItemIsDragEnabled | default_flags
|
||||
return default_flags
|
||||
|
||||
def mimeTypes(self):
|
||||
return ['text/plain']
|
||||
|
||||
def mimeData(self, indexes):
|
||||
mime_data = QMimeData()
|
||||
|
||||
l = SampleShortInfoList(s=[])
|
||||
|
||||
for i in set(index.row() for index in indexes):
|
||||
l.s.append(self.samples[i])
|
||||
|
||||
mime_data.setText(l.model_dump_json())
|
||||
return mime_data
|
||||
|
||||
def get_id(self, row: int) -> SampleShortInfo:
|
||||
return self.samples[row]
|
||||
Whitespace-only changes.
@@ -0,0 +1,129 @@
|
||||
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 aaregui import ButtonWithPayload
|
||||
from aaregui import NumberLineEdit
|
||||
from aaregui import TitleLabel
|
||||
|
||||
|
||||
class AbrTweakButtons(QWidget):
|
||||
abr_tweak = Signal(Coordinate)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.__step = 0.1
|
||||
grid_layout = QGridLayout(self)
|
||||
grid_layout.setColumnStretch(0, 1)
|
||||
grid_layout.setColumnStretch(1, 0)
|
||||
grid_layout.setColumnStretch(2, 0)
|
||||
grid_layout.setColumnStretch(3, 3)
|
||||
|
||||
grid_layout.addWidget(QLabel("GMX"), 0, 0)
|
||||
button_gmx_minus = ButtonWithPayload("←", payload={"x": -1, "y": 0, "z": 0})
|
||||
button_gmx_plus = ButtonWithPayload("→", payload={"x": 1, "y": 0, "z": 0})
|
||||
grid_layout.addWidget(button_gmx_minus, 0, 1)
|
||||
grid_layout.addWidget(button_gmx_plus, 0, 2)
|
||||
|
||||
grid_layout.addWidget(QLabel("GMY"), 1, 0)
|
||||
button_gmy_minus = ButtonWithPayload("←", payload={"x": 0, "y": -1, "z": 0})
|
||||
button_gmy_plus = ButtonWithPayload("→", payload={"x": 0, "y": 1, "z": 0})
|
||||
grid_layout.addWidget(button_gmy_minus, 1, 1)
|
||||
grid_layout.addWidget(button_gmy_plus, 1, 2)
|
||||
|
||||
grid_layout.addWidget(QLabel("GMZ"), 2, 0)
|
||||
button_gmz_minus = ButtonWithPayload("←", payload={"x": 0, "y": 0, "z": -1})
|
||||
button_gmz_plus = ButtonWithPayload("→", payload={"x": 0, "y": 0, "z": 1})
|
||||
grid_layout.addWidget(button_gmz_minus, 2, 1)
|
||||
grid_layout.addWidget(button_gmz_plus, 2, 2)
|
||||
|
||||
button_gmx_minus.pressed.connect(self.abr_button)
|
||||
button_gmx_plus.pressed.connect(self.abr_button)
|
||||
button_gmy_minus.pressed.connect(self.abr_button)
|
||||
button_gmy_plus.pressed.connect(self.abr_button)
|
||||
button_gmz_minus.pressed.connect(self.abr_button)
|
||||
button_gmz_plus.pressed.connect(self.abr_button)
|
||||
|
||||
button_gmx_minus.setFixedWidth(32)
|
||||
button_gmy_minus.setFixedWidth(32)
|
||||
button_gmz_minus.setFixedWidth(32)
|
||||
button_gmx_plus.setFixedWidth(32)
|
||||
button_gmy_plus.setFixedWidth(32)
|
||||
button_gmz_plus.setFixedWidth(32)
|
||||
self.gmx_label = QLabel("0.000")
|
||||
grid_layout.addWidget(self.gmx_label, 0, 3)
|
||||
self.gmx_label.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
self.gmy_label = QLabel("0.000")
|
||||
self.gmy_label.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
grid_layout.addWidget(self.gmy_label, 1, 3)
|
||||
self.gmz_label = QLabel("0.000")
|
||||
self.gmz_label.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
grid_layout.addWidget(self.gmz_label, 2, 3)
|
||||
|
||||
@Slot(dict)
|
||||
def abr_button(self, payload: dict):
|
||||
self.abr_tweak.emit(Coordinate(x=self.__step * payload["x"], y=self.__step * payload["y"], z=self.__step * payload["z"]))
|
||||
|
||||
@Slot(float)
|
||||
def set_step(self, val_mm: float):
|
||||
print(self.__step)
|
||||
print(val_mm)
|
||||
self.__step = val_mm / 1000.0
|
||||
|
||||
class AbrTweakWidget(QWidget):
|
||||
abr_tweak = Signal(Coordinate)
|
||||
abr_save = Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
grid_layout = QGridLayout(self)
|
||||
grid_layout.setColumnStretch(0, 0)
|
||||
grid_layout.setColumnStretch(1, 0)
|
||||
grid_layout.setColumnStretch(2, 1)
|
||||
|
||||
grid_layout.addWidget(TitleLabel("ABR meas. pos.", self), 0, 0, 1, 3)
|
||||
|
||||
self.__abr_buttons = AbrTweakButtons(parent=self)
|
||||
grid_layout.addWidget(self.__abr_buttons, 1, 0, 1 ,4)
|
||||
self.__abr_buttons.abr_tweak.connect(self.abr_button_pressed)
|
||||
|
||||
grid_layout.addWidget(QLabel("Step", parent=self), 2, 0)
|
||||
self.__step = NumberLineEdit(1, 1000, 100, 0, parent=self)
|
||||
grid_layout.addWidget(self.__step, 2, 1)
|
||||
grid_layout.addWidget(QLabel("μm", parent=self), 2, 2)
|
||||
self.__step.newValue.connect(self.__abr_buttons.set_step)
|
||||
|
||||
save_button = QPushButton("Save ABR pos.")
|
||||
grid_layout.addWidget(save_button, 3, 0, 1, 3)
|
||||
save_button.pressed.connect(self.save_button_pressed)
|
||||
|
||||
@Slot(Coordinate)
|
||||
def abr_button_pressed(self, c: Coordinate):
|
||||
self.abr_tweak.emit(c)
|
||||
|
||||
@Slot()
|
||||
def save_button_pressed(self):
|
||||
self.abr_save.emit()
|
||||
|
||||
@Slot(DAQStatusModel)
|
||||
def update_daq_status(self, s: DAQStatusModel):
|
||||
self.__abr_buttons.gmx_label.setText(f"{s.geom.aerotech_meas.x:.3f}")
|
||||
if abs(s.geom.aerotech.x) >= 0.001:
|
||||
self.__abr_buttons.gmx_label.setStyleSheet("color: rgb(255, 0, 0);")
|
||||
else:
|
||||
self.__abr_buttons.gmx_label.setStyleSheet("color: rgb(0, 0, 0);")
|
||||
self.__abr_buttons.gmy_label.setText(f"{s.geom.aerotech_meas.y:.3f}")
|
||||
if abs(s.geom.aerotech.y) >= 0.001:
|
||||
self.__abr_buttons.gmy_label.setStyleSheet("color: rgb(255, 0, 0);")
|
||||
else:
|
||||
self.__abr_buttons.gmy_label.setStyleSheet("color: rgb(0, 0, 0);")
|
||||
|
||||
self.__abr_buttons.gmz_label.setText(f"{s.geom.aerotech_meas.z:.3f}")
|
||||
if abs(s.geom.aerotech.z) >= 0.001:
|
||||
self.__abr_buttons.gmz_label.setStyleSheet("color: rgb(255, 0, 0);")
|
||||
else:
|
||||
self.__abr_buttons.gmz_label.setStyleSheet("color: rgb(0, 0, 0);")
|
||||
@@ -0,0 +1,23 @@
|
||||
from PySide6.QtCore import Signal, Slot
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QPushButton
|
||||
|
||||
from aaregui import TitleLabel
|
||||
|
||||
|
||||
class BeamCenterWidget(QWidget):
|
||||
beam_center_clear = Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
grid_layout = QGridLayout(self)
|
||||
|
||||
grid_layout.addWidget(TitleLabel("Beam center", self), 0, 0)
|
||||
|
||||
clear_button = QPushButton("Clear marks")
|
||||
grid_layout.addWidget(clear_button, 1, 0)
|
||||
clear_button.pressed.connect(self.clear_button_pressed)
|
||||
|
||||
@Slot()
|
||||
def clear_button_pressed(self):
|
||||
self.beam_center_clear.emit()
|
||||
@@ -0,0 +1,49 @@
|
||||
from PySide6.QtWidgets import QFrame, QVBoxLayout
|
||||
|
||||
from aaregui import AbrTweakWidget
|
||||
from aaregui import BeamCenterWidget
|
||||
from aaregui import BeamlineStatePanel
|
||||
from aaregui import IlluminationPanel
|
||||
from aaregui import LoopCenteringPanel
|
||||
from aaregui import OmegaPanel
|
||||
from aaregui import SmargonPanel
|
||||
from aaregui import ZoomPanel
|
||||
|
||||
|
||||
class BeamlineControls(QFrame):
|
||||
set_width = 250
|
||||
|
||||
def __init__(self, parent=None, staff : bool = True):
|
||||
super().__init__(parent)
|
||||
self.setFixedWidth(self.set_width)
|
||||
self.setFrameShape(QFrame.Shape.StyledPanel)
|
||||
self.setFrameShadow(QFrame.Shadow.Raised)
|
||||
|
||||
self.v_layout = QVBoxLayout(self)
|
||||
self.zoom_panel = ZoomPanel(self)
|
||||
self.v_layout.addWidget(self.zoom_panel)
|
||||
|
||||
self.illumination_panel = IlluminationPanel(self)
|
||||
self.v_layout.addWidget(self.illumination_panel)
|
||||
|
||||
self.omega_panel = OmegaPanel(parent=self)
|
||||
self.v_layout.addWidget(self.omega_panel)
|
||||
|
||||
self.smargon_panel = SmargonPanel(parent=self)
|
||||
self.v_layout.addWidget(self.smargon_panel)
|
||||
|
||||
self.loopctr = LoopCenteringPanel(self)
|
||||
self.v_layout.addWidget(self.loopctr)
|
||||
|
||||
self.state_panel = BeamlineStatePanel(self)
|
||||
self.v_layout.addWidget(self.state_panel)
|
||||
|
||||
self.abr_tweak = AbrTweakWidget(self)
|
||||
self.beam_center = BeamCenterWidget(self)
|
||||
|
||||
if staff:
|
||||
self.v_layout.addWidget(self.abr_tweak)
|
||||
self.v_layout.addWidget(self.beam_center)
|
||||
|
||||
self.v_layout.addStretch()
|
||||
self.setLayout(self.v_layout)
|
||||
@@ -0,0 +1,20 @@
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QPushButton
|
||||
|
||||
from aaregui import TitleLabel
|
||||
|
||||
|
||||
class BeamlineStatePanel(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
grid_layout = QGridLayout(self)
|
||||
|
||||
grid_layout.addWidget(TitleLabel("Beamline state", self), 0, 0)
|
||||
|
||||
self.sample_exchange = QPushButton("Sample exchange", parent=self)
|
||||
grid_layout.addWidget(self.sample_exchange, 1, 0)
|
||||
|
||||
self.sample_alignment = QPushButton("Sample alignment", parent=self)
|
||||
grid_layout.addWidget(self.sample_alignment, 2, 0)
|
||||
|
||||
self.dewar_exchange = QPushButton("Load/unload samples", parent=self)
|
||||
grid_layout.addWidget(self.dewar_exchange, 3, 0)
|
||||
@@ -0,0 +1,118 @@
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame,
|
||||
QVBoxLayout,
|
||||
QTabWidget,
|
||||
QGridLayout,
|
||||
QPushButton,
|
||||
QWidget,
|
||||
)
|
||||
from aaredaqlib.helical_geometry import HelicalScanRequest
|
||||
from aaredaqlib.rotation_scan import RotationScanRequest
|
||||
from aaredaqlib.sample_geometry import SampleGeometryModel
|
||||
from aaredaqlib.screening_scan import ScreeningScanRequest
|
||||
|
||||
from aaregui import FilePathPanel
|
||||
from aaregui import HelicalDataCollectionPanel
|
||||
from aaregui import RasterDataCollectionPanel
|
||||
from aaregui import ScreeningDataCollectionPanel
|
||||
from aaregui import StandardDataCollectionPanel
|
||||
from aaregui import TitleLabel
|
||||
|
||||
|
||||
class DataCollectionSettings(QFrame):
|
||||
standard_data_collection = Signal(RotationScanRequest)
|
||||
helical_data_collection = Signal(HelicalScanRequest)
|
||||
screening_data_collection = Signal(ScreeningScanRequest)
|
||||
|
||||
def __init__(self, s: SampleGeometryModel, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setFixedWidth(400)
|
||||
self.setFrameShape(QFrame.Shape.StyledPanel)
|
||||
self.setFrameShadow(QFrame.Shadow.Raised)
|
||||
|
||||
self.grid_layout = QGridLayout(self)
|
||||
self.file_path_panel = FilePathPanel(self)
|
||||
self.grid_layout.addWidget(self.file_path_panel, 0, 0, 1, 2)
|
||||
|
||||
widget = QWidget(self)
|
||||
v_layout = QVBoxLayout(widget)
|
||||
widget.setLayout(v_layout)
|
||||
|
||||
v_layout.addWidget(TitleLabel("Data collection settings"))
|
||||
|
||||
self.tab_widget = QTabWidget(parent=widget)
|
||||
|
||||
self.raster = RasterDataCollectionPanel(parent=self.tab_widget)
|
||||
self.tab_widget.addTab(self.raster, "Raster")
|
||||
|
||||
self.screening = ScreeningDataCollectionPanel(parent=self.tab_widget)
|
||||
self.tab_widget.addTab(self.screening, "Screening")
|
||||
|
||||
self.standard = StandardDataCollectionPanel(parent=self.tab_widget)
|
||||
self.tab_widget.addTab(self.standard, "Standard")
|
||||
|
||||
self.helical = HelicalDataCollectionPanel(s=s, parent=self.tab_widget)
|
||||
self.tab_widget.addTab(self.helical, "Helical")
|
||||
|
||||
v_layout.addWidget(self.tab_widget)
|
||||
v_layout.addStretch()
|
||||
self.grid_layout.addWidget(widget, 1, 0, 1, 2)
|
||||
|
||||
self.start_button = QPushButton("Start")
|
||||
self.start_button.setStyleSheet("color: rgb(78, 154, 6);")
|
||||
self.grid_layout.addWidget(self.start_button, 3, 0)
|
||||
self.start_button.clicked.connect(self.run_data_collection)
|
||||
|
||||
self.abort_button = QPushButton("Abort")
|
||||
self.abort_button.setStyleSheet("color: rgb(164, 0, 0);")
|
||||
self.grid_layout.addWidget(self.abort_button, 3, 1)
|
||||
|
||||
def run_data_collection(self):
|
||||
current_tab = self.tab_widget.currentWidget()
|
||||
|
||||
if current_tab == self.raster:
|
||||
print("Raster tab is active.")
|
||||
# Add specific action for Raster tab here
|
||||
elif current_tab == self.screening:
|
||||
data = self.screening.screening_type.itemData(
|
||||
self.screening.screening_type.currentIndex()
|
||||
)
|
||||
|
||||
if data is None:
|
||||
print("No screening type selected.")
|
||||
return
|
||||
|
||||
s = ScreeningScanRequest(
|
||||
|
||||
|
||||
|
||||
omega_start_deg=self.screening.start_angle.value,
|
||||
exp_time_s=self.screening.image_time_enter.value,
|
||||
omega_step_deg=data["omega_step_deg"],
|
||||
steps=data["steps"],
|
||||
oscrange_deg=0.1
|
||||
)
|
||||
self.screening_data_collection.emit(s)
|
||||
elif current_tab == self.standard:
|
||||
total_angle = self.standard.total_angle.value
|
||||
image_angle = self.standard.image_angle.value
|
||||
if total_angle <= 0 or total_angle < image_angle:
|
||||
print("Total angle is not valid.")
|
||||
return
|
||||
|
||||
image_number = round(total_angle / image_angle)
|
||||
total_time = image_number * self.standard.image_time_enter.value
|
||||
|
||||
r = RotationScanRequest(
|
||||
start_omega_deg=self.standard.start_angle.value,
|
||||
total_omega_deg=total_angle,
|
||||
total_time_sec=total_time
|
||||
)
|
||||
self.standard_data_collection.emit(r)
|
||||
elif current_tab == self.helical:
|
||||
r = self.helical.get_request()
|
||||
if r is not None:
|
||||
self.helical_data_collection.emit(r)
|
||||
else:
|
||||
print("No matching tab found.")
|
||||
@@ -0,0 +1,21 @@
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QLabel, QLineEdit
|
||||
|
||||
from aaregui import TitleLabel
|
||||
|
||||
|
||||
class FilePathPanel(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
grid_layout = QGridLayout(self)
|
||||
|
||||
grid_layout.addWidget(TitleLabel("Dataset path", self), 0, 0, 1, 2)
|
||||
|
||||
grid_layout.addWidget(QLabel("File prefix", parent=self), 1, 0)
|
||||
self.file_prefix_edit = QLineEdit(parent=self)
|
||||
self.file_prefix_edit.setStyleSheet("background-color: rgb(255, 255, 255);")
|
||||
grid_layout.addWidget(self.file_prefix_edit, 1, 1)
|
||||
|
||||
grid_layout.addWidget(QLabel("Directory", parent=self), 2, 0)
|
||||
self.directory_edit = QLineEdit(parent=self)
|
||||
self.directory_edit.setStyleSheet("background-color: rgb(255, 255, 255);")
|
||||
grid_layout.addWidget(self.directory_edit, 2, 1)
|
||||
@@ -0,0 +1,115 @@
|
||||
from PySide6.QtCore import Signal, Slot
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
QGridLayout,
|
||||
QLabel,
|
||||
QSizePolicy,
|
||||
QSpacerItem,
|
||||
QPushButton,
|
||||
)
|
||||
from aaredaqlib.coordinate import SmargonCoordinate
|
||||
from aaredaqlib.helical_geometry import HelicalScanRequest
|
||||
from aaredaqlib.models import DAQStatusModel
|
||||
from aaredaqlib.sample_geometry import SampleGeometryModel
|
||||
|
||||
from aaregui import NumberLineEdit
|
||||
|
||||
|
||||
class HelicalDataCollectionPanel(QWidget):
|
||||
helical_scan = Signal(HelicalScanRequest)
|
||||
show_helical_start = Signal(SmargonCoordinate)
|
||||
show_helical_end = Signal(SmargonCoordinate)
|
||||
|
||||
def __init__(self, s: SampleGeometryModel, parent=None):
|
||||
super().__init__(parent)
|
||||
grid_layout = QGridLayout(self)
|
||||
|
||||
self.__geom = s
|
||||
self.__steps = 100
|
||||
|
||||
self.__image_time = 0.005
|
||||
self.__image_angle = 0.1
|
||||
self.__total_angle = 360.0
|
||||
|
||||
self.__start = SmargonCoordinate()
|
||||
self.__end = SmargonCoordinate()
|
||||
|
||||
start_button = QPushButton("Save start point", parent=self)
|
||||
start_button.clicked.connect(self.set_start_point_curr)
|
||||
grid_layout.addWidget(start_button, 0, 0, 1, 3)
|
||||
|
||||
end_button = QPushButton("Save end point", parent=self)
|
||||
end_button.clicked.connect(self.set_end_point_curr)
|
||||
grid_layout.addWidget(end_button, 1, 0, 1, 3)
|
||||
|
||||
grid_layout.addWidget(QLabel("Start angle", parent=self), 2, 0)
|
||||
self.start_angle = NumberLineEdit(-720, 719.0, 0, decimals=3, parent=self)
|
||||
grid_layout.addWidget(self.start_angle, 2, 1)
|
||||
grid_layout.addWidget(QLabel("°", parent=self), 2, 2)
|
||||
|
||||
grid_layout.addWidget(QLabel("Total angle", parent=self), 3, 0)
|
||||
self.total_angle = NumberLineEdit(
|
||||
0, 999.0, self.__total_angle, decimals=3, parent=self
|
||||
)
|
||||
grid_layout.addWidget(self.total_angle, 3, 1)
|
||||
grid_layout.addWidget(QLabel("°", parent=self), 3, 2)
|
||||
|
||||
grid_layout.addWidget(QLabel("Image angle", parent=self), 4, 0)
|
||||
self.image_angle = NumberLineEdit(
|
||||
0, 10.0, self.__image_angle, decimals=3, parent=self
|
||||
)
|
||||
grid_layout.addWidget(self.image_angle, 4, 1)
|
||||
grid_layout.addWidget(QLabel("°", parent=self), 4, 2)
|
||||
|
||||
grid_layout.addWidget(QLabel("Image time", parent=self), 5, 0)
|
||||
self.image_time_enter = NumberLineEdit(
|
||||
0.0005, 10.0, self.__image_time, decimals=4, parent=self
|
||||
)
|
||||
grid_layout.addWidget(self.image_time_enter, 5, 1)
|
||||
grid_layout.addWidget(QLabel("s", parent=self), 5, 2)
|
||||
|
||||
horizontal_spacer = QSpacerItem(
|
||||
40, 20, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding
|
||||
)
|
||||
grid_layout.addItem(horizontal_spacer, 6, 0, 1, 3)
|
||||
|
||||
@Slot(DAQStatusModel)
|
||||
def update_daq_status(self, s: DAQStatusModel):
|
||||
self.__geom = s.geom
|
||||
|
||||
@Slot()
|
||||
def set_end_point_curr(self):
|
||||
self.set_end_point(self.__geom.smargon)
|
||||
|
||||
@Slot()
|
||||
def set_start_point_curr(self):
|
||||
self.set_start_point(self.__geom.smargon)
|
||||
|
||||
@Slot(SmargonCoordinate)
|
||||
def set_start_point(self, s: SmargonCoordinate):
|
||||
self.__start = s
|
||||
if s.sh_mm is not None:
|
||||
self.start_angle.update_value(self.__geom.omega_deg)
|
||||
self.show_helical_start.emit(self.__start)
|
||||
|
||||
@Slot(SmargonCoordinate)
|
||||
def set_end_point(self, s: SmargonCoordinate):
|
||||
self.__end = s
|
||||
self.show_helical_end.emit(self.__end)
|
||||
|
||||
|
||||
def get_request(self) -> HelicalScanRequest | None:
|
||||
if self.__start.sh_mm is None or self.__end.sh_mm is None:
|
||||
return
|
||||
|
||||
omega_step = self.total_angle.value / self.__steps
|
||||
h = HelicalScanRequest(
|
||||
start=self.__start,
|
||||
end=self.__end,
|
||||
omega_start=self.start_angle.value,
|
||||
total_omega=self.total_angle.value,
|
||||
time_sec=self.image_time_enter.value
|
||||
* (self.total_angle.value / self.image_angle.value),
|
||||
steps=self.__steps,
|
||||
)
|
||||
return h
|
||||
@@ -0,0 +1,38 @@
|
||||
from PySide6.QtCore import Qt, Signal, Slot
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QSlider
|
||||
from aaredaqlib.models import DAQStatusModel
|
||||
|
||||
from aaregui 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.light))
|
||||
@@ -0,0 +1,18 @@
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QPushButton
|
||||
|
||||
from aaregui import TitleLabel
|
||||
|
||||
|
||||
class LoopCenteringPanel(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
grid_layout = QGridLayout(self)
|
||||
|
||||
grid_layout.addWidget(TitleLabel("Loop centering", self), 0, 0, 1, 2)
|
||||
grid_layout.setColumnStretch(0, 1)
|
||||
grid_layout.setColumnStretch(1, 1)
|
||||
|
||||
self.find_tip = QPushButton("Center", parent=self)
|
||||
grid_layout.addWidget(self.find_tip, 1, 0)
|
||||
self.background = QPushButton("Background", parent=self)
|
||||
grid_layout.addWidget(self.background, 1, 1)
|
||||
@@ -0,0 +1,71 @@
|
||||
from PySide6.QtCore import Signal, Slot
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QLabel, QHBoxLayout
|
||||
from aaredaqlib.models import DAQStatusModel
|
||||
|
||||
from aaregui import ButtonWithPayload
|
||||
from aaregui import NumberLineEdit
|
||||
from aaregui import TitleLabel
|
||||
|
||||
|
||||
class OmegaEntryWidget(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
h_layout = QHBoxLayout(self)
|
||||
h_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.edit = NumberLineEdit(-720, 720, 0, parent=self)
|
||||
h_layout.addWidget(self.edit)
|
||||
h_layout.addWidget(QLabel("°", parent=self))
|
||||
h_layout.addStretch()
|
||||
|
||||
|
||||
class OmegaPanel(QWidget):
|
||||
set_omega = Signal(float)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.__omega = 0
|
||||
|
||||
grid_layout = QGridLayout(self)
|
||||
|
||||
grid_layout.addWidget(TitleLabel("Omega", self), 0, 0, 1, 2)
|
||||
grid_layout.setColumnStretch(0, 1)
|
||||
grid_layout.setColumnStretch(1, 1)
|
||||
omega_settings = [
|
||||
{"name": " 0°", "payload": {"abs": 0.0}},
|
||||
{"name": "+180°", "payload": {"rel": 180.0}},
|
||||
{"name": "-180°", "payload": {"rel": -180.0}},
|
||||
{"name": "+90°", "payload": {"rel": 90.0}},
|
||||
{"name": "-90°", "payload": {"rel": -90.0}},
|
||||
{"name": "+45°", "payload": {"rel": 45.0}},
|
||||
{"name": "-45°", "payload": {"rel": -45.0}},
|
||||
{"name": "+10°", "payload": {"rel": 10.0}},
|
||||
{"name": "-10°", "payload": {"rel": -10.0}},
|
||||
]
|
||||
|
||||
self.edit = OmegaEntryWidget(self)
|
||||
self.edit.edit.newValue.connect(self.omega_value_entered)
|
||||
grid_layout.addWidget(self.edit, 1, 0)
|
||||
|
||||
i = 1
|
||||
for x in omega_settings:
|
||||
button = ButtonWithPayload(x["name"], x["payload"], parent=self)
|
||||
grid_layout.addWidget(button, int(i / 2 + 1), int(i % 2))
|
||||
i += 1
|
||||
button.pressed.connect(self.omega_button_pressed)
|
||||
|
||||
@Slot(dict)
|
||||
def omega_button_pressed(self, payload: dict):
|
||||
if "rel" in payload:
|
||||
self.set_omega.emit(self.__omega + payload["rel"])
|
||||
elif "abs" in payload:
|
||||
self.set_omega.emit(payload["abs"])
|
||||
|
||||
@Slot(float)
|
||||
def omega_value_entered(self, f: float):
|
||||
self.set_omega.emit(f)
|
||||
|
||||
@Slot(DAQStatusModel)
|
||||
def update_daq_status(self, s: DAQStatusModel):
|
||||
self.edit.edit.update_value(s.geom.omega_deg)
|
||||
self.__omega = s.geom.omega_deg
|
||||
@@ -0,0 +1,43 @@
|
||||
from PySide6.QtCore import Signal, Slot
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QLabel, QSizePolicy, QSpacerItem
|
||||
|
||||
from aaregui import NumberLineEdit
|
||||
|
||||
|
||||
class RasterDataCollectionPanel(QWidget):
|
||||
grid_size_updated = Signal(float, float)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
grid_layout = QGridLayout(self)
|
||||
grid_layout.addWidget(QLabel("Grid size", parent=self), 0, 0)
|
||||
|
||||
self.width_enter = NumberLineEdit(5, 100, decimals=0, default=20, parent=self)
|
||||
self.width_enter.newValue.connect(self.grid_size)
|
||||
grid_layout.addWidget(self.width_enter, 0, 1)
|
||||
|
||||
grid_layout.addWidget(QLabel(" x ", parent=self), 0, 2)
|
||||
|
||||
self.height_enter = NumberLineEdit(5, 100, decimals=0, default=20, parent=self)
|
||||
self.height_enter.newValue.connect(self.grid_size)
|
||||
grid_layout.addWidget(self.height_enter, 0, 3)
|
||||
|
||||
grid_layout.addWidget(QLabel("μm", parent=self), 0, 4)
|
||||
|
||||
grid_layout.addWidget(QLabel("Image time", parent=self), 1, 0)
|
||||
self.image_time_enter = NumberLineEdit(
|
||||
0.0005, 10.0, 0.005, decimals=4, parent=self
|
||||
)
|
||||
grid_layout.addWidget(self.image_time_enter, 1, 1, 1, 3)
|
||||
grid_layout.addWidget(QLabel("s", parent=self), 1, 4)
|
||||
|
||||
horizontal_spacer = QSpacerItem(
|
||||
40, 20, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding
|
||||
)
|
||||
grid_layout.addItem(horizontal_spacer, 2, 0, 1, 4)
|
||||
|
||||
@Slot(float)
|
||||
def grid_size(self, _: float):
|
||||
width = self.width_enter.value / 1000.0
|
||||
height = self.height_enter.value / 1000.0
|
||||
self.grid_size_updated.emit(width, height)
|
||||
@@ -0,0 +1,89 @@
|
||||
from PySide6.QtCore import Signal, Slot
|
||||
from PySide6.QtWidgets import QFrame, QVBoxLayout, QHBoxLayout, QPushButton, QHeaderView, QSizePolicy, \
|
||||
QTableView
|
||||
from aaredaqlib.models import SampleShortInfoList, SampleShortInfo
|
||||
|
||||
from aaregui import SampleQueueSpreadsheet
|
||||
from aaregui import TitleLabel
|
||||
|
||||
|
||||
class SampleQueuePanel(QFrame):
|
||||
auto_scan = Signal(SampleShortInfo)
|
||||
unmount = Signal()
|
||||
|
||||
def __init__(self, parent=None, samples: SampleShortInfoList | None = None):
|
||||
super().__init__(parent)
|
||||
self.__pause = True
|
||||
self.__set_to_pause = False
|
||||
self.setFrameShape(QFrame.Shape.StyledPanel)
|
||||
self.setFrameShadow(QFrame.Shadow.Raised)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
self.setLayout(layout)
|
||||
|
||||
layout.addWidget(TitleLabel("Sample queue", self))
|
||||
# Create the table
|
||||
self.table_view = QTableView(self)
|
||||
|
||||
# Create the custom table model
|
||||
self.table_model = SampleQueueSpreadsheet()
|
||||
# Create the QTableView and set the model
|
||||
self.table_view.setModel(self.table_model)
|
||||
# Enable automatic column resizing
|
||||
header = self.table_view.horizontalHeader()
|
||||
header.setSectionResizeMode(QHeaderView.ResizeMode.Stretch) # Columns stretch to fill the table
|
||||
|
||||
# Set the table's size policy to expand
|
||||
self.table_view.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
self.table_view.setAcceptDrops(True)
|
||||
self.table_view.setDropIndicatorShown(True)
|
||||
|
||||
layout.addWidget(self.table_view) # Add table to the layout
|
||||
|
||||
# Create a horizontal layout for the buttons
|
||||
button_layout = QHBoxLayout()
|
||||
self.play_button = QPushButton("▶ Run", self)
|
||||
self.play_button.clicked.connect(self.run)
|
||||
self.clear_button = QPushButton("✖ Clear list", self)
|
||||
self.clear_button.clicked.connect(self.clear)
|
||||
|
||||
self.clear_button.clicked.connect(self.table_model.clearSamples)
|
||||
|
||||
button_layout.addWidget(self.play_button)
|
||||
button_layout.addWidget(self.clear_button)
|
||||
|
||||
# Add the button layout to the main layout
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
|
||||
def run(self):
|
||||
if self.__pause:
|
||||
if len(self.table_model.samples) > 0:
|
||||
self.table_model.set_running(True)
|
||||
self.__set_to_pause = False
|
||||
self.__pause = False
|
||||
self.play_button.setText("⏸ Pause")
|
||||
self.auto_scan.emit(self.table_model.samples[0])
|
||||
else:
|
||||
self.__set_to_pause = True
|
||||
self.__pause = True
|
||||
self.play_button.setText("▶ Run")
|
||||
|
||||
def clear(self):
|
||||
self.table_model.clearSamples()
|
||||
|
||||
@Slot(int, bool)
|
||||
def automated_scan_done(self, db_id: int, success: bool):
|
||||
if success:
|
||||
self.table_model.remove_sample(db_id)
|
||||
if not self.__pause and len(self.table_model.samples) > 0:
|
||||
self.auto_scan.emit(self.table_model.samples[0])
|
||||
else:
|
||||
self.table_model.set_running(False)
|
||||
self.__pause = True
|
||||
self.play_button.setText("▶ Run")
|
||||
self.unmount.emit()
|
||||
else:
|
||||
self.table_model.set_running(False)
|
||||
self.__pause = True
|
||||
self.play_button.setText("▶ Run")
|
||||
@@ -0,0 +1,60 @@
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
QGridLayout,
|
||||
QLabel,
|
||||
QComboBox,
|
||||
QSizePolicy,
|
||||
QSpacerItem,
|
||||
)
|
||||
|
||||
from aaregui import NumberLineEdit
|
||||
|
||||
|
||||
class ScreeningDataCollectionPanel(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
grid_layout = QGridLayout(self)
|
||||
|
||||
grid_layout.addWidget(QLabel("High resolution", parent=self), 0, 0)
|
||||
self.high_res_enter = NumberLineEdit(
|
||||
0.5, 10, decimals=2, default=2.0, parent=self
|
||||
)
|
||||
grid_layout.addWidget(self.high_res_enter, 0, 1)
|
||||
grid_layout.addWidget(QLabel("Å", parent=self), 0, 2)
|
||||
grid_layout.addWidget(QLabel("Detector distance", parent=self), 1, 0)
|
||||
self.dtz_enter = NumberLineEdit(60, 1000, decimals=2, default=120, parent=self)
|
||||
grid_layout.addWidget(self.dtz_enter, 1, 1)
|
||||
grid_layout.addWidget(QLabel("mm", parent=self), 1, 2)
|
||||
|
||||
grid_layout.addWidget(QLabel("Beam transmission", parent=self), 2, 0)
|
||||
self.transmission_enter = NumberLineEdit(
|
||||
0, 1.0, decimals=4, default=1.0, parent=self
|
||||
)
|
||||
grid_layout.addWidget(self.transmission_enter, 2, 1)
|
||||
|
||||
grid_layout.addWidget(QLabel("Start angle", parent=self), 3, 0)
|
||||
self.start_angle = NumberLineEdit(-720, 720.0, 0.0, decimals=3, parent=self)
|
||||
grid_layout.addWidget(self.start_angle, 3, 1)
|
||||
grid_layout.addWidget(QLabel("°", parent=self), 3, 2)
|
||||
|
||||
grid_layout.addWidget(QLabel("Image time", parent=self), 4, 0)
|
||||
self.image_time_enter = NumberLineEdit(
|
||||
0.0005, 10.0, 0.005, decimals=4, parent=self
|
||||
)
|
||||
grid_layout.addWidget(self.image_time_enter, 4, 1)
|
||||
grid_layout.addWidget(QLabel("s", parent=self), 4, 2)
|
||||
|
||||
self.screening_type = QComboBox(parent=self)
|
||||
self.screening_type.addItem("1 image", {"steps": 1, "omega_step_deg": 0})
|
||||
self.screening_type.addItem("2 images every 90°", {"steps": 2, "omega_step_deg": 90})
|
||||
self.screening_type.addItem("4 images every 90°", {"steps": 4, "omega_step_deg": 90})
|
||||
self.screening_type.addItem("3 images every 60°", {"steps": 3, "omega_step_deg": 60})
|
||||
self.screening_type.addItem("2 images every 45°", {"steps": 2, "omega_step_deg": 45})
|
||||
self.screening_type.addItem("4 images every 45°", {"steps": 4, "omega_step_deg": 45})
|
||||
|
||||
grid_layout.addWidget(self.screening_type, 5, 0, 1, 3)
|
||||
|
||||
horizontal_spacer = QSpacerItem(
|
||||
40, 20, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding
|
||||
)
|
||||
grid_layout.addItem(horizontal_spacer, 6, 0, 1, 3)
|
||||
@@ -0,0 +1,113 @@
|
||||
from PySide6.QtCore import Signal, Slot
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QLabel, QPushButton
|
||||
|
||||
from aaredaqlib.models import DAQStatusModel
|
||||
from aaregui import ButtonWithPayload
|
||||
from aaregui import NumberLineEdit
|
||||
from aaregui import TitleLabel
|
||||
from aaredaqlib.coordinate import SmargonCoordinate, Coordinate
|
||||
from aaredaqlib.sample_geometry import SampleGeometryModel
|
||||
|
||||
|
||||
class SmargonMoveWidget(QWidget):
|
||||
smargon_rel = Signal(Coordinate)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
grid_layout = QGridLayout(self)
|
||||
grid_layout.setColumnStretch(0, 1)
|
||||
grid_layout.setColumnStretch(1, 1)
|
||||
grid_layout.setColumnStretch(2, 1)
|
||||
|
||||
self.button_left = ButtonWithPayload("←", payload={"x": 1, "y": 0})
|
||||
self.button_right = ButtonWithPayload("→", payload={"x": -1, "y": 0})
|
||||
self.button_up = ButtonWithPayload("↑", payload={"x": 0, "y": 1})
|
||||
self.button_down = ButtonWithPayload("↓", payload={"x": 0, "y": -1})
|
||||
|
||||
self.button_left.pressed.connect(self.smargon_button)
|
||||
self.button_right.pressed.connect(self.smargon_button)
|
||||
self.button_up.pressed.connect(self.smargon_button)
|
||||
self.button_down.pressed.connect(self.smargon_button)
|
||||
|
||||
grid_layout.addWidget(self.button_up, 0, 1)
|
||||
grid_layout.addWidget(self.button_left, 1, 0)
|
||||
grid_layout.addWidget(self.button_right, 1, 2)
|
||||
grid_layout.addWidget(self.button_down, 2, 1)
|
||||
|
||||
@Slot(dict)
|
||||
def smargon_button(self, payload: dict):
|
||||
self.smargon_rel.emit(Coordinate(x=payload["x"], y=payload["y"]))
|
||||
|
||||
|
||||
class SmargonPanel(QWidget):
|
||||
smargon = Signal(SmargonCoordinate)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.__geom: SampleGeometryModel | None = None
|
||||
self.__step = 100
|
||||
|
||||
grid_layout = QGridLayout(self)
|
||||
|
||||
grid_layout.addWidget(TitleLabel("Smargon", self), 0, 0, 1, 6)
|
||||
|
||||
grid_layout.addWidget(QLabel("Chi", parent=self), 1, 0)
|
||||
self.chi_enter = NumberLineEdit(-0.2, 40, decimals=1, parent=self)
|
||||
self.chi_enter.newValue.connect(self.chi)
|
||||
grid_layout.addWidget(self.chi_enter, 1, 1)
|
||||
grid_layout.addWidget(QLabel("°", parent=self), 1, 2)
|
||||
|
||||
grid_layout.addWidget(QLabel("Phi", parent=self), 1, 3)
|
||||
self.phi_enter = NumberLineEdit(-0.2, 360, decimals=1, parent=self)
|
||||
self.phi_enter.newValue.connect(self.phi)
|
||||
grid_layout.addWidget(self.phi_enter, 1, 4)
|
||||
|
||||
grid_layout.addWidget(QLabel("°", parent=self), 1, 5)
|
||||
|
||||
self.home_button = QPushButton("Move home", parent=self)
|
||||
grid_layout.addWidget(self.home_button, 2, 0, 1, 6)
|
||||
self.home_button.clicked.connect(self.home)
|
||||
|
||||
self.move_panel = SmargonMoveWidget(parent=self)
|
||||
grid_layout.addWidget(self.move_panel, 3, 0, 1, 6)
|
||||
self.move_panel.smargon_rel.connect(self.smargon_rel)
|
||||
|
||||
grid_layout.addWidget(QLabel("Step", parent=self), 4, 0)
|
||||
self.step = NumberLineEdit(1, 1000, 100, 0, parent=self)
|
||||
grid_layout.addWidget(self.step, 4, 1)
|
||||
grid_layout.addWidget(QLabel("μm", parent=self), 4, 2)
|
||||
self.step.newValue.connect(self.step_changed)
|
||||
|
||||
@Slot(float)
|
||||
def step_changed(self, f: float):
|
||||
self.__step = f
|
||||
|
||||
@Slot()
|
||||
def home(self):
|
||||
self.smargon.emit(Smargon.SMARGON_HOME)
|
||||
|
||||
@Slot(float)
|
||||
def phi(self, f: float):
|
||||
self.smargon.emit(SmargonCoordinate(phi_deg=f))
|
||||
|
||||
@Slot(float)
|
||||
def chi(self, f: float):
|
||||
self.smargon.emit(SmargonCoordinate(chi_deg=f))
|
||||
|
||||
@Slot(DAQStatusModel)
|
||||
def update_daq_status(self, s: DAQStatusModel):
|
||||
self.chi_enter.update_value(s.geom.smargon.chi_deg)
|
||||
self.phi_enter.update_value(s.geom.smargon.phi_deg)
|
||||
self.__geom = s.geom
|
||||
|
||||
@Slot(Coordinate)
|
||||
def smargon_rel(self, c: Coordinate):
|
||||
if self.__geom is None:
|
||||
return
|
||||
|
||||
self.smargon.emit(
|
||||
SmargonCoordinate(
|
||||
sh_mm=self.__geom.beamline_to_smargon(c * self.__step / 1000.0)
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,54 @@
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QLabel, QSpacerItem, QSizePolicy
|
||||
|
||||
from aaregui import NumberLineEdit
|
||||
|
||||
|
||||
class StandardDataCollectionPanel(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
grid_layout = QGridLayout(self)
|
||||
|
||||
grid_layout.addWidget(QLabel("High resolution", parent=self), 0, 0)
|
||||
self.high_res_enter = NumberLineEdit(
|
||||
0.5, 10, decimals=2, default=2.0, parent=self
|
||||
)
|
||||
grid_layout.addWidget(self.high_res_enter, 0, 1)
|
||||
grid_layout.addWidget(QLabel("Å", parent=self), 0, 2)
|
||||
grid_layout.addWidget(QLabel("Detector distance", parent=self), 1, 0)
|
||||
self.dtz_enter = NumberLineEdit(60, 1000, decimals=2, default=120, parent=self)
|
||||
grid_layout.addWidget(self.dtz_enter, 1, 1)
|
||||
grid_layout.addWidget(QLabel("mm", parent=self), 1, 2)
|
||||
|
||||
grid_layout.addWidget(QLabel("Beam transmission", parent=self), 2, 0)
|
||||
self.transmission_enter = NumberLineEdit(
|
||||
0, 1.0, decimals=4, default=1.0, parent=self
|
||||
)
|
||||
grid_layout.addWidget(self.transmission_enter, 2, 1)
|
||||
|
||||
grid_layout.addWidget(QLabel("Start angle", parent=self), 3, 0)
|
||||
self.start_angle = NumberLineEdit(-720, 720.0, 0.0, decimals=3, parent=self)
|
||||
grid_layout.addWidget(self.start_angle, 3, 1)
|
||||
grid_layout.addWidget(QLabel("°", parent=self), 3, 2)
|
||||
|
||||
grid_layout.addWidget(QLabel("Total angle", parent=self), 4, 0)
|
||||
self.total_angle = NumberLineEdit(0, 999.0, 180.0, decimals=3, parent=self)
|
||||
grid_layout.addWidget(self.total_angle, 4, 1)
|
||||
grid_layout.addWidget(QLabel("°", parent=self), 4, 2)
|
||||
|
||||
grid_layout.addWidget(QLabel("Image angle", parent=self), 5, 0)
|
||||
self.image_angle = NumberLineEdit(0, 10.0, 0.1, decimals=3, parent=self)
|
||||
grid_layout.addWidget(self.image_angle, 5, 1)
|
||||
grid_layout.addWidget(QLabel("°", parent=self), 5, 2)
|
||||
|
||||
grid_layout.addWidget(QLabel("Image time", parent=self), 6, 0)
|
||||
self.image_time_enter = NumberLineEdit(
|
||||
0.0005, 10.0, 0.005, decimals=4, parent=self
|
||||
)
|
||||
grid_layout.addWidget(self.image_time_enter, 6, 1)
|
||||
grid_layout.addWidget(QLabel("s", parent=self), 6, 2)
|
||||
|
||||
|
||||
horizontal_spacer = QSpacerItem(
|
||||
40, 20, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding
|
||||
)
|
||||
grid_layout.addItem(horizontal_spacer, 6, 0, 1, 3)
|
||||
@@ -0,0 +1,64 @@
|
||||
from PySide6.QtCore import Qt, Slot
|
||||
from PySide6.QtGui import QPixmap
|
||||
from PySide6.QtWidgets import QFrame, QGridLayout, QLabel, QSpacerItem, QSizePolicy
|
||||
from aaredaqlib.models import DAQStatusModel
|
||||
from aaredaqlib.sample_geometry import SampleGeometryModel
|
||||
|
||||
from aaregui import StatusLabel
|
||||
from aaregui import TitleLabel
|
||||
|
||||
|
||||
class StatusPanel(QFrame):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setFrameShape(QFrame.Shape.StyledPanel)
|
||||
self.setFrameShadow(QFrame.Shadow.Raised)
|
||||
|
||||
layout = QGridLayout(self)
|
||||
self.setLayout(layout)
|
||||
|
||||
layout.addWidget(TitleLabel("Beamline status", self), 0, 0, 1, 5)
|
||||
|
||||
layout.addWidget(QLabel("Ring current", parent=self), 2, 1)
|
||||
self.ring_current = StatusLabel(val=400)
|
||||
layout.addWidget(self.ring_current, 2, 2)
|
||||
layout.addWidget(QLabel("mA", parent=self), 2, 3)
|
||||
|
||||
layout.addWidget(QLabel("Flux", parent=self), 3, 1)
|
||||
self.flux = StatusLabel(val=45300)
|
||||
layout.addWidget(self.flux, 3, 2)
|
||||
layout.addWidget(QLabel("10<sup>9</sup> ph/s", parent=self), 3, 3)
|
||||
|
||||
layout.addWidget(QLabel("Wavelength", parent=self), 4, 1)
|
||||
self.wavelength = StatusLabel(val=1.00, decimals=3)
|
||||
layout.addWidget(self.wavelength, 4, 2)
|
||||
layout.addWidget(QLabel("Å", parent=self), 4, 3)
|
||||
|
||||
layout.addWidget(QLabel("Beam size", parent=self), 5, 1)
|
||||
self.beam_size = QLabel("<b>20</b> x <b>20</b>")
|
||||
self.beam_size.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(self.beam_size, 5, 2)
|
||||
layout.addWidget(QLabel("µm", parent=self), 5, 3)
|
||||
|
||||
layout.addWidget(QLabel("Omega", parent=self), 6, 1)
|
||||
self.omega = StatusLabel(val=0.1, decimals=2)
|
||||
layout.addWidget(self.omega, 6, 2)
|
||||
layout.addWidget(QLabel("°", parent=self), 6, 3)
|
||||
|
||||
horizontal_spacer_1 = QSpacerItem(
|
||||
40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
|
||||
)
|
||||
layout.addItem(horizontal_spacer_1, 1, 0, 1, 1)
|
||||
|
||||
horizontal_spacer_2 = QSpacerItem(
|
||||
40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
|
||||
)
|
||||
|
||||
layout.addItem(horizontal_spacer_2, 7, 4, 1, 1)
|
||||
|
||||
@Slot(QPixmap, SampleGeometryModel)
|
||||
def update_daq_status(self, s: DAQStatusModel):
|
||||
self.omega.new_value(s.geom.omega_deg)
|
||||
self.beam_size.setText(
|
||||
f"<b>{s.geom.beam_size_mm.x * 1000.0}</b> x <b>{s.geom.beam_size_mm.y * 1000.0}</b>"
|
||||
)
|
||||
@@ -0,0 +1,121 @@
|
||||
from PySide6.QtCore import Qt, Signal, Slot
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame,
|
||||
QGridLayout,
|
||||
QTableView,
|
||||
QHeaderView,
|
||||
QMenu,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QAbstractItemView,
|
||||
)
|
||||
from aaredaqlib.models import BeamlineStateEnum, SampleShortInfo, SampleShortInfoList, DAQStatusModel
|
||||
|
||||
from aaregui import UserSampleSpreadsheet
|
||||
from aaregui import TitleLabel
|
||||
|
||||
|
||||
class TellSamplePanel(QFrame):
|
||||
mount = Signal(SampleShortInfo)
|
||||
unmount = Signal()
|
||||
state = Signal(BeamlineStateEnum)
|
||||
|
||||
def __init__(self, samples: SampleShortInfoList | None = None, parent=None):
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
if samples is None:
|
||||
samples = SampleShortInfoList(s=[])
|
||||
self.setFrameShape(QFrame.Shape.StyledPanel)
|
||||
self.setFrameShadow(QFrame.Shadow.Raised)
|
||||
|
||||
grid_layout = QGridLayout(self)
|
||||
self.setLayout(grid_layout)
|
||||
|
||||
grid_layout.addWidget(TitleLabel("TELL sample changer", self), 0, 0, 1, 4)
|
||||
|
||||
self.table_view = QTableView()
|
||||
grid_layout.addWidget(self.table_view, 1, 0, 1, 4)
|
||||
|
||||
self.curr_sample_label = QLabel("No sample mounted", parent=self)
|
||||
grid_layout.addWidget(self.curr_sample_label, 2, 0)
|
||||
|
||||
self.unmount_button = QPushButton("Unmount", parent=self)
|
||||
grid_layout.addWidget(self.unmount_button, 2, 1)
|
||||
self.unmount_button.clicked.connect(self.unmount_button_clicked)
|
||||
|
||||
grid_layout.setColumnStretch(0, 1)
|
||||
grid_layout.setColumnStretch(1, 0)
|
||||
|
||||
# Create the custom table model
|
||||
self.table_model = UserSampleSpreadsheet(samples=samples.s)
|
||||
# Create the QTableView and set the model
|
||||
self.table_view.setModel(self.table_model)
|
||||
self.table_view.horizontalHeader().setSectionResizeMode(
|
||||
QHeaderView.ResizeMode.Stretch
|
||||
)
|
||||
|
||||
# Enable the custom context menu
|
||||
self.table_view.setSortingEnabled(True)
|
||||
self.table_view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
self.table_view.customContextMenuRequested.connect(self.context_menu)
|
||||
self.table_view.setSelectionBehavior(
|
||||
QAbstractItemView.SelectionBehavior.SelectRows
|
||||
)
|
||||
self.table_view.setSelectionMode(
|
||||
QAbstractItemView.SelectionMode.ExtendedSelection
|
||||
)
|
||||
|
||||
self.table_view.setEditTriggers(QTableView.EditTrigger.NoEditTriggers)
|
||||
self.table_view.setDragEnabled(True)
|
||||
header = self.table_view.horizontalHeader()
|
||||
header.setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
|
||||
header.setStretchLastSection(True) # Optional: Make last column stretch to fit
|
||||
self.table_view.verticalHeader().setVisible(True)
|
||||
|
||||
def unmount_button_clicked(self):
|
||||
self.unmount.emit()
|
||||
|
||||
@Slot(SampleShortInfoList)
|
||||
def new_sample_list(self, samples: SampleShortInfoList):
|
||||
self.table_model.updateData(samples=samples.s)
|
||||
|
||||
def context_menu(self, position):
|
||||
# Determine the row where the user clicked
|
||||
index = self.table_view.indexAt(position)
|
||||
if not index.isValid():
|
||||
return # Do nothing if user didn't click on a valid item
|
||||
|
||||
row = index.row()
|
||||
sample = self.table_model.get_id(row)
|
||||
|
||||
if sample.location is None:
|
||||
return # Do nothing if sample not mounted
|
||||
|
||||
# Create the context menu
|
||||
menu = QMenu()
|
||||
mount_action = menu.addAction("Mount") # Add "Mount" to the menu
|
||||
|
||||
# Display the menu at the cursor position
|
||||
action = menu.exec_(self.table_view.viewport().mapToGlobal(position))
|
||||
|
||||
# Handle the result of the selected action
|
||||
if action == mount_action:
|
||||
self.mount.emit(sample)
|
||||
|
||||
@Slot(DAQStatusModel)
|
||||
def update_daq_status(self, status: DAQStatusModel):
|
||||
sample = status.sample
|
||||
if sample is None:
|
||||
self.curr_sample_label.setText("No sample mounted")
|
||||
self.table_model.updateCurrentSample(current_puck=None, current_sample=None)
|
||||
else:
|
||||
try:
|
||||
self.curr_sample_label.setText(
|
||||
f"Current sample: <b>{sample.sample_name} ({sample.location.segment}{sample.location.pos}-{sample.pin})</b>"
|
||||
)
|
||||
self.table_model.updateCurrentSample(
|
||||
current_puck=sample.puck_name, current_sample=sample.db_id
|
||||
)
|
||||
except Exception as e:
|
||||
self.curr_sample_label.setText(f"Confusing information :/ {e}")
|
||||
@@ -0,0 +1,33 @@
|
||||
from PySide6.QtCore import Slot, Signal
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout
|
||||
|
||||
from aaregui import ButtonWithPayload
|
||||
from aaregui import TitleLabel
|
||||
|
||||
|
||||
class ZoomPanel(QWidget):
|
||||
zoom = Signal(float)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
grid_layout = QGridLayout(self)
|
||||
zoom_settings = [
|
||||
{"name": "1x", "value": 1},
|
||||
{"name": "2.0x", "value": 280},
|
||||
{"name": "3.5x", "value": 500},
|
||||
{"name": "5.8x", "value": 700},
|
||||
{"name": "7.5x", "value": 800},
|
||||
{"name": "12.5x", "value": 1000},
|
||||
]
|
||||
grid_layout.addWidget(TitleLabel("Zoom", self), 0, 0, 1, 2)
|
||||
i = 2
|
||||
for z in zoom_settings:
|
||||
button = ButtonWithPayload(z["name"], {"zoom": z["value"]})
|
||||
grid_layout.addWidget(button, int(i / 2), int(i % 2))
|
||||
i += 1
|
||||
button.pressed.connect(self.set_zoom)
|
||||
|
||||
@Slot(dict)
|
||||
def set_zoom(self, payload: dict):
|
||||
self.zoom.emit(payload["zoom"])
|
||||
Whitespace-only changes.
@@ -0,0 +1,51 @@
|
||||
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.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_GB2RGB)
|
||||
rgb_image = rgb_image[:, ::-1, :].copy()
|
||||
qimage = QImage(rgb_image.data, 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 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.wait()
|
||||
@@ -0,0 +1,229 @@
|
||||
from PySide6.QtCore import Signal, QUrl, Slot, QTimer, QObject, QByteArray
|
||||
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
|
||||
|
||||
from aaredaqlib.models import DAQStatusModel, SampleShortInfoList, SampleShortInfo
|
||||
from aaredaqlib.coordinate import SmargonCoordinate, Coordinate
|
||||
from aaredaqlib.helical_geometry import HelicalScanRequest
|
||||
from aaredaqlib.raster_grid import RasterGridRequest
|
||||
from aaredaqlib.rotation_scan import RotationScanRequest
|
||||
from aaredaqlib.screening_scan import ScreeningScanRequest
|
||||
|
||||
SPREADHSEET_FREQUENCY = 25 # Every 5 seconds
|
||||
|
||||
class DAQWorker(QObject):
|
||||
update = Signal(DAQStatusModel)
|
||||
spreadsheet = Signal(SampleShortInfoList)
|
||||
http_error = Signal(str)
|
||||
auth_error = Signal()
|
||||
automated_scan_done = Signal(int, bool) # sample ID, success
|
||||
|
||||
def __init__(self, base_url: str, token: str, parent=None):
|
||||
super().__init__(parent)
|
||||
self.__token = token
|
||||
self.__base_url = base_url
|
||||
self.__net_manager = QNetworkAccessManager()
|
||||
self.__timer = QTimer()
|
||||
self.__timer.setInterval(200)
|
||||
self.__timer.timeout.connect(self.regular_update)
|
||||
self.__timer.start()
|
||||
self.__counter = 0
|
||||
|
||||
|
||||
@Slot()
|
||||
def regular_update(self):
|
||||
if self.__counter % SPREADHSEET_FREQUENCY == 0:
|
||||
self.load_spreadsheet()
|
||||
self.__counter = (self.__counter + 1) % SPREADHSEET_FREQUENCY
|
||||
self.send_status_request()
|
||||
|
||||
def send_status_request(self):
|
||||
request = QNetworkRequest(QUrl(f"{self.__base_url}/status"))
|
||||
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
|
||||
reply = self.__net_manager.get(request)
|
||||
reply.finished.connect(lambda: self.handle_status_response(reply))
|
||||
|
||||
@staticmethod
|
||||
def handle_response(reply: QNetworkReply):
|
||||
if reply.error() == QNetworkReply.NetworkError.NoError:
|
||||
response_data = reply.readAll().data().decode("utf-8")
|
||||
reply.deleteLater()
|
||||
return response_data
|
||||
else:
|
||||
reply.deleteLater()
|
||||
raise RuntimeError(reply.errorString())
|
||||
|
||||
@Slot(QNetworkReply)
|
||||
def handle_status_response(self, reply: QNetworkReply):
|
||||
try:
|
||||
response_data = self.handle_response(reply)
|
||||
parsed_response = DAQStatusModel.model_validate_json(response_data)
|
||||
self.update.emit(parsed_response)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
self.http_error.emit(str(e))
|
||||
|
||||
@Slot(QNetworkReply)
|
||||
def handle_spreadsheet_response(self, reply: QNetworkReply):
|
||||
try:
|
||||
response_data = self.handle_response(reply)
|
||||
parsed_response = SampleShortInfoList.model_validate_json(response_data)
|
||||
self.spreadsheet.emit(parsed_response)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
self.http_error.emit(str(e))
|
||||
|
||||
def handle_req_response(self, reply: QNetworkReply):
|
||||
if reply.error() != QNetworkReply.NetworkError.NoError:
|
||||
if reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute) == 401:
|
||||
print(reply.errorString())
|
||||
self.auth_error.emit()
|
||||
else:
|
||||
print(reply.errorString())
|
||||
self.http_error.emit(reply.errorString())
|
||||
reply.deleteLater()
|
||||
|
||||
def generic_post(self, url: str, body: str = ""):
|
||||
request = QNetworkRequest(QUrl(f"{self.__base_url}/{url}"))
|
||||
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
|
||||
if str:
|
||||
request.setRawHeader(b"Content-Type", b"application/json")
|
||||
reply = self.__net_manager.post(request, QByteArray(body.encode("utf-8")))
|
||||
reply.finished.connect(lambda: self.handle_req_response(reply))
|
||||
|
||||
def generic_put(self, url: str, body: str = ""):
|
||||
request = QNetworkRequest(QUrl(f"{self.__base_url}/{url}"))
|
||||
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
|
||||
if str:
|
||||
request.setRawHeader(b"Content-Type", b"application/json")
|
||||
reply = self.__net_manager.put(request, QByteArray(body.encode("utf-8")))
|
||||
reply.finished.connect(lambda: self.handle_req_response(reply))
|
||||
|
||||
def generic_delete(self, url: str):
|
||||
request = QNetworkRequest(QUrl(f"{self.__base_url}/{url}"))
|
||||
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
|
||||
reply = self.__net_manager.deleteResource(request)
|
||||
reply.finished.connect(lambda: self.handle_req_response(reply))
|
||||
|
||||
@Slot(float)
|
||||
def set_omega(self, f: float):
|
||||
self.generic_put(f"beamline/omega?val={f:.3f}")
|
||||
|
||||
@Slot(float)
|
||||
def zoom(self, f: float):
|
||||
self.generic_put(f"beamline/zoom?val={f:.3f}")
|
||||
|
||||
@Slot(int)
|
||||
def light(self, v: int):
|
||||
self.generic_put(f"beamline/light?val={v:d}")
|
||||
|
||||
@Slot()
|
||||
def alc_background(self):
|
||||
self.generic_post("alc/background")
|
||||
|
||||
@Slot()
|
||||
def center_loop(self):
|
||||
self.generic_post("alc/center_loop")
|
||||
|
||||
@Slot()
|
||||
def force_session(self):
|
||||
self.generic_post("access/force_current_session")
|
||||
|
||||
@Slot()
|
||||
def end_session(self):
|
||||
self.generic_post("access/end_session")
|
||||
|
||||
@Slot()
|
||||
def dewar_exchange(self):
|
||||
self.generic_post("state/dewar_exchange")
|
||||
|
||||
@Slot()
|
||||
def sample_exchange(self):
|
||||
self.generic_post("state/sample_exchange")
|
||||
|
||||
@Slot()
|
||||
def sample_alignment(self):
|
||||
self.generic_post("state/sample_alignment")
|
||||
|
||||
@Slot(str)
|
||||
def set_pgroup(self, val: str):
|
||||
if val == "":
|
||||
self.generic_delete("access/pgroup")
|
||||
else:
|
||||
self.generic_put(f"access/pgroup?val={val}")
|
||||
|
||||
@Slot(SmargonCoordinate)
|
||||
def move_smargon(self, coord: SmargonCoordinate):
|
||||
self.generic_put("beamline/smargon", coord.model_dump_json())
|
||||
|
||||
@Slot(RotationScanRequest)
|
||||
def standard_scan(self, r: RotationScanRequest):
|
||||
self.generic_post("scan/rotation", r.model_dump_json())
|
||||
|
||||
@Slot(ScreeningScanRequest)
|
||||
def screening_scan(self, r: ScreeningScanRequest):
|
||||
self.generic_post("scan/screening", r.model_dump_json())
|
||||
|
||||
@Slot(HelicalScanRequest)
|
||||
def helical_scan(self, h: HelicalScanRequest):
|
||||
self.generic_post("scan/helical", h.model_dump_json())
|
||||
|
||||
@Slot(RasterGridRequest)
|
||||
def raster_scan(self, r: RasterGridRequest):
|
||||
self.generic_post("scan/raster", r.model_dump_json())
|
||||
|
||||
@Slot()
|
||||
def load_spreadsheet(self):
|
||||
request = QNetworkRequest(QUrl(f"{self.__base_url}/tell/spreadsheet"))
|
||||
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
|
||||
reply = self.__net_manager.get(request)
|
||||
reply.finished.connect(lambda: self.handle_spreadsheet_response(reply))
|
||||
|
||||
def handle_auto_scan_response(self, reply, sample_id: int):
|
||||
if reply.error() == QNetworkReply.NetworkError.NoError:
|
||||
resp = reply.readAll().data().decode("utf-8")
|
||||
print(f"Sample time {resp} s")
|
||||
self.automated_scan_done.emit(sample_id, True)
|
||||
else:
|
||||
if reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute) == 401:
|
||||
print(reply.errorString())
|
||||
self.auth_error.emit()
|
||||
else:
|
||||
print(reply.errorString())
|
||||
self.http_error.emit(reply.errorString())
|
||||
self.automated_scan_done.emit(sample_id, False)
|
||||
reply.deleteLater()
|
||||
|
||||
@Slot(SampleShortInfo)
|
||||
def automated_scan(self, s: SampleShortInfo):
|
||||
request = QNetworkRequest(QUrl(f"{self.__base_url}/scan/auto"))
|
||||
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
|
||||
request.setRawHeader(b"Content-Type", b"application/json")
|
||||
body = s.model_dump_json()
|
||||
reply = self.__net_manager.post(request, QByteArray(body.encode("utf-8")))
|
||||
reply.finished.connect(lambda: self.handle_auto_scan_response(reply, s.db_id))
|
||||
|
||||
@Slot(Coordinate)
|
||||
def abr_tweak(self, c: Coordinate):
|
||||
self.generic_post("beamline/tweak_abr_meas_pos", c.model_dump_json())
|
||||
|
||||
@Slot()
|
||||
def abr_save(self):
|
||||
self.generic_post("beamline/save_abr_meas_pos")
|
||||
|
||||
|
||||
@Slot(float, float)
|
||||
def beam_center_mark(self, x: float, y: float):
|
||||
print(f"beam_center/mark?x={x}&y={y}")
|
||||
self.generic_post(f"beam_center/mark?x={x}&y={y}")
|
||||
|
||||
@Slot()
|
||||
def beam_center_clear(self):
|
||||
self.generic_post(f"beam_center/clear")
|
||||
|
||||
@Slot()
|
||||
def unmount(self):
|
||||
self.generic_post("tell/unmount")
|
||||
|
||||
@Slot(SampleShortInfo)
|
||||
def mount(self, s: SampleShortInfo):
|
||||
self.generic_post(f"tell/mount?dbid={s.db_id}")
|
||||
Whitespace-only changes.
@@ -0,0 +1,16 @@
|
||||
from PySide6.QtCore import Signal, Slot
|
||||
from PySide6.QtWidgets import QPushButton
|
||||
|
||||
|
||||
class ButtonWithPayload(QPushButton):
|
||||
pressed = Signal(dict)
|
||||
|
||||
def __init__(self, text: str, payload: dict, parent=None):
|
||||
super().__init__(parent)
|
||||
self.payload = payload
|
||||
self.setText(text)
|
||||
self.clicked.connect(self.button_pressed)
|
||||
|
||||
@Slot()
|
||||
def button_pressed(self):
|
||||
self.pressed.emit(self.payload)
|
||||
@@ -0,0 +1,496 @@
|
||||
from typing import List
|
||||
|
||||
from PySide6.QtCore import Qt, QTimer, QRect, QPoint, Signal, Slot, QPointF
|
||||
from PySide6.QtGui import (
|
||||
QPixmap,
|
||||
QPainter,
|
||||
QPen,
|
||||
QColor,
|
||||
QWheelEvent,
|
||||
QTransform,
|
||||
QCursor,
|
||||
QLinearGradient,
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QMenu,
|
||||
QToolTip,
|
||||
QGraphicsView,
|
||||
QGraphicsScene,
|
||||
QGraphicsPixmapItem,
|
||||
QFileDialog,
|
||||
QFrame,
|
||||
)
|
||||
|
||||
from aaredaqlib.models import DAQStatusModel
|
||||
from aaregui import SmargonBookmarkList
|
||||
from aaredaqlib.coordinate import Coordinate, SmargonCoordinate
|
||||
from aaredaqlib.raster_grid import RasterGridRequest
|
||||
from aaredaqlib.sample_geometry import SampleGeometryModel
|
||||
|
||||
|
||||
class SampleCameraImageLabel(QGraphicsView):
|
||||
smargon = Signal(SmargonCoordinate)
|
||||
run_raster = Signal(RasterGridRequest)
|
||||
zoom_change = Signal(int)
|
||||
set_omega = Signal(float)
|
||||
set_helical_start = Signal(SmargonCoordinate)
|
||||
set_helical_end = Signal(SmargonCoordinate)
|
||||
update_beam_center = Signal(float, float)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
geom: SampleGeometryModel,
|
||||
rasters: List[RasterGridRequest],
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
if rasters is None:
|
||||
self.__raster = []
|
||||
else:
|
||||
self.__raster = rasters
|
||||
|
||||
self.__pixmap = QPixmap("/home/leonarski_f/loop200.jpg")
|
||||
self.__geom = geom
|
||||
self.__bookmarks: SmargonBookmarkList = SmargonBookmarkList(b=[])
|
||||
self.__autoscale = True
|
||||
self.__show_coords = False
|
||||
self.__helical_start = SmargonCoordinate()
|
||||
self.__helical_end = SmargonCoordinate()
|
||||
|
||||
self.drawing = False # Indicates whether the mouse is currently drawing
|
||||
self.moving_raster_grid = None
|
||||
self.right_click_grid = None
|
||||
self.active_grid = None
|
||||
self.drawn_grid = None
|
||||
|
||||
self.start_point = None # Starting point of the rectangle
|
||||
self.end_point = None # Ending point of the rectangle
|
||||
self.rectangles = [] # List of all drawn rectangles
|
||||
|
||||
self.scene = QGraphicsScene(self)
|
||||
self.setScene(self.scene)
|
||||
|
||||
self.setMouseTracking(True)
|
||||
|
||||
pixmap = QPixmap("/home/leonarski_f/loop200.jpg")
|
||||
self.pixmap_item = QGraphicsPixmapItem(pixmap)
|
||||
self.scene.addItem(self.pixmap_item)
|
||||
|
||||
self.setFrameShape(QFrame.Shape.NoFrame)
|
||||
self.setRenderHints(QPainter.RenderHint.Antialiasing)
|
||||
|
||||
# Create a timer for throttling wheel events
|
||||
self.wheel_event_timer = QTimer()
|
||||
self.wheel_event_timer.setSingleShot(True)
|
||||
self.wheel_event_threshold = 500 # Minimum delay between wheel events in ms
|
||||
self.last_wheel_event = 0 # Timestamp of the last processed wheel event
|
||||
|
||||
self.click_timer = QTimer() # Timer to detect click vs hold
|
||||
self.click_timer.setSingleShot(True) # The timer runs only once
|
||||
self.left_click_hold_threshold = 200
|
||||
self.right_click_hold_threshold = 200
|
||||
|
||||
# Default raster grid size
|
||||
self.raster_grid_x = 0.02
|
||||
self.raster_grid_y = 0.02
|
||||
|
||||
def drawForeground(self, painter, rect):
|
||||
self.__draw_beam_center(painter)
|
||||
self.__draw_grids(painter)
|
||||
self.__draw_helical(painter)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
# Call the base class implementation of resizeEvent to maintain default behavior
|
||||
super().resizeEvent(event)
|
||||
self.__scaling()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
pos = self.mapToScene(event.pos())
|
||||
if event.button() == Qt.MouseButton.RightButton:
|
||||
self.__start_drawing(pos)
|
||||
self.click_timer.start(self.right_click_hold_threshold)
|
||||
elif event.button() == Qt.MouseButton.LeftButton:
|
||||
self.__start_moving(pos)
|
||||
self.click_timer.start(self.left_click_hold_threshold)
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
mouse_pos = self.mapToScene(event.pos())
|
||||
if self.__show_coords:
|
||||
x, y = mouse_pos.x(), mouse_pos.y()
|
||||
QToolTip.showText(
|
||||
self.mapToGlobal(event.pos()), f"{x:.0f}, {y:.0f} pxl", self
|
||||
)
|
||||
self.end_point = mouse_pos
|
||||
if self.drawing and not self.click_timer.isActive():
|
||||
self.__update_drawn_grid()
|
||||
self.update()
|
||||
elif self.moving_raster_grid is not None and not self.click_timer.isActive():
|
||||
self.__update_moving_raster_grid()
|
||||
self.update()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
if self.click_timer.isActive():
|
||||
self.click_timer.stop()
|
||||
self.__left_single_click(event)
|
||||
self.__end_moving(event, self.click_timer.isActive())
|
||||
|
||||
if event.button() == Qt.MouseButton.RightButton:
|
||||
if self.click_timer.isActive():
|
||||
self.click_timer.stop()
|
||||
self.__right_click_menu(event)
|
||||
else:
|
||||
self.__end_drawing(event)
|
||||
|
||||
def __right_click_menu(self, event):
|
||||
self.drawing = False
|
||||
|
||||
menu = QMenu(self)
|
||||
|
||||
scale_action = menu.addAction("Scale to width")
|
||||
scale_action.setCheckable(True)
|
||||
scale_action.setChecked(self.__autoscale)
|
||||
|
||||
show_coord_action = menu.addAction("Show coordinates")
|
||||
show_coord_action.setCheckable(True)
|
||||
show_coord_action.setChecked(self.__show_coords)
|
||||
|
||||
grab_action = menu.addAction("Grab")
|
||||
grab_with_overlay_action = menu.addAction("Grab with overlay")
|
||||
|
||||
beam_mark_action = menu.addAction("Mark beam center")
|
||||
|
||||
delete_action = None
|
||||
evaluate_action = None
|
||||
|
||||
self.active_grid = self.right_click_grid
|
||||
|
||||
if self.right_click_grid is not None:
|
||||
menu.addSection("Grid")
|
||||
delete_action = menu.addAction("Delete")
|
||||
evaluate_action = menu.addAction("Evaluate")
|
||||
|
||||
action = menu.exec_(self.mapToGlobal(event.pos()))
|
||||
|
||||
if action == show_coord_action:
|
||||
self.__show_coords = not self.__show_coords
|
||||
elif action == scale_action:
|
||||
self.__autoscale = not self.__autoscale
|
||||
self.__scaling()
|
||||
elif self.right_click_grid is not None and action == delete_action:
|
||||
self.__raster.remove(self.right_click_grid)
|
||||
self.right_click_grid = None
|
||||
elif self.right_click_grid is not None and action == evaluate_action:
|
||||
self.run_raster.emit(self.right_click_grid)
|
||||
elif action == grab_action:
|
||||
self.__screenshot_with_dialog(overlay=False)
|
||||
elif action == grab_with_overlay_action:
|
||||
self.__screenshot_with_dialog(overlay=True)
|
||||
elif action == beam_mark_action:
|
||||
c = self.mapToScene(event.pos())
|
||||
self.update_beam_center.emit(c.x(), c.y())
|
||||
self.active_grid = None
|
||||
self.update()
|
||||
|
||||
def __screenshot_with_dialog(self, overlay: bool):
|
||||
file_path, _ = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
"Save View As",
|
||||
"",
|
||||
"JPEG Files (*.jpg; *.jpeg);;All Files (*)",
|
||||
)
|
||||
|
||||
if file_path:
|
||||
self.__screenshot(file_path=file_path, overlay=overlay)
|
||||
|
||||
def __screenshot(self, file_path: str, overlay: bool):
|
||||
scene_rect = self.scene.sceneRect()
|
||||
pixmap = QPixmap(scene_rect.size().toSize())
|
||||
painter = QPainter(pixmap)
|
||||
self.scene.render(painter)
|
||||
if overlay:
|
||||
self.drawForeground(painter, scene_rect)
|
||||
painter.end()
|
||||
pixmap.save(file_path, "JPEG")
|
||||
|
||||
def __scaling(self):
|
||||
if not self.__autoscale:
|
||||
self.setTransform(QTransform())
|
||||
return
|
||||
|
||||
if self.pixmap_item.boundingRect().width() == 0:
|
||||
ratio = 1.0
|
||||
else:
|
||||
ratio = (
|
||||
self.viewport().size().width() / self.pixmap_item.boundingRect().width()
|
||||
)
|
||||
|
||||
if ratio < 0.2:
|
||||
ratio = 0.2
|
||||
|
||||
if ratio >= 1.0:
|
||||
self.setTransform(QTransform())
|
||||
else:
|
||||
matrix = QTransform()
|
||||
matrix.scale(ratio, ratio)
|
||||
self.setTransform(matrix)
|
||||
|
||||
@Slot(float, float)
|
||||
def update_raster_grid_size(self, grid_size_x: float, grid_size_y: float):
|
||||
self.raster_grid_x = grid_size_x
|
||||
self.raster_grid_y = grid_size_y
|
||||
|
||||
@Slot(QPixmap)
|
||||
def update_pixmap(self, pixmap: QPixmap):
|
||||
if self.pixmap_item is not None: # Ensure pixmap_item exists
|
||||
self.pixmap_item.setPixmap(pixmap) # Update the pixmap in the item
|
||||
else:
|
||||
# If no pixmap item exists (rare case), create one
|
||||
self.pixmap_item = QGraphicsPixmapItem(pixmap)
|
||||
self.scene.addItem(self.pixmap_item)
|
||||
self.update() # Request an update to redraw the view
|
||||
|
||||
@Slot(DAQStatusModel)
|
||||
def update_daq_status(self, s: DAQStatusModel):
|
||||
self.__geom = s.geom
|
||||
# View is redrawn when new camera image will arrive
|
||||
|
||||
def __point_is_part_of_raster(self, pos) -> RasterGridRequest | None:
|
||||
c = self.__geom.picture_to_sample(Coordinate(x=pos.x(), y=pos.y()))
|
||||
|
||||
for i in self.__raster:
|
||||
if i is not None and i.point_sample_belongs_to_grid(self.__geom, c):
|
||||
return i
|
||||
return None
|
||||
|
||||
def __update_moving_raster_grid(self):
|
||||
delta_pxl = Coordinate(
|
||||
x=self.end_point.x() - self.start_point.x(),
|
||||
y=self.end_point.y() - self.start_point.y(),
|
||||
)
|
||||
self.start_point = self.end_point
|
||||
|
||||
delta_mm = self.__geom.smargon_nudge(delta_pxl * self.__geom.pixel_in_mm)
|
||||
|
||||
self.moving_raster_grid.smargon = SmargonCoordinate(
|
||||
sh_mm=self.moving_raster_grid.smargon.sh_mm + delta_mm,
|
||||
phi_deg=self.__geom.smargon.phi_deg,
|
||||
chi_deg=self.__geom.smargon.chi_deg,
|
||||
)
|
||||
|
||||
def __left_single_click(self, event):
|
||||
self.end_point = self.mapToScene(event.pos())
|
||||
|
||||
sample_coord = self.__geom.picture_to_sample(
|
||||
Coordinate(x=self.end_point.x(), y=self.end_point.y())
|
||||
)
|
||||
smargon_coord = SmargonCoordinate(
|
||||
sh_mm=self.__geom.beamline_to_smargon(sample_coord)
|
||||
)
|
||||
|
||||
self.smargon.emit(smargon_coord)
|
||||
|
||||
def __start_moving(self, pos):
|
||||
self.moving_raster_grid = self.__point_is_part_of_raster(pos)
|
||||
self.active_grid = self.moving_raster_grid
|
||||
self.start_point = pos # Record the starting position
|
||||
self.end_point = self.start_point
|
||||
|
||||
def __end_moving(self, event, too_short: bool):
|
||||
self.end_point = self.mapToScene(event.pos())
|
||||
self.active_grid = None
|
||||
if self.moving_raster_grid is not None and not too_short:
|
||||
self.__update_moving_raster_grid()
|
||||
self.moving_raster_grid = None
|
||||
self.update()
|
||||
|
||||
def __start_drawing(self, pos):
|
||||
self.drawing = True
|
||||
self.right_click_grid = self.__point_is_part_of_raster(pos)
|
||||
if self.right_click_grid is not None:
|
||||
# Starting position is top-left corner of the old raster
|
||||
old_start_pos = self.__geom.sample_to_picture(
|
||||
self.right_click_grid.start_pxl(geom=self.__geom)
|
||||
)
|
||||
self.start_point = QPoint(int(old_start_pos.x), int(old_start_pos.y))
|
||||
else:
|
||||
self.start_point = pos
|
||||
self.end_point = pos
|
||||
|
||||
def __end_drawing(self, event):
|
||||
self.drawing = False
|
||||
self.end_point = self.mapToScene(event.pos())
|
||||
|
||||
self.__update_drawn_grid()
|
||||
|
||||
if self.right_click_grid is not None:
|
||||
self.__raster.remove(self.right_click_grid)
|
||||
if self.drawn_grid is not None:
|
||||
self.__raster.append(self.drawn_grid)
|
||||
|
||||
self.active_grid = None
|
||||
self.right_click_grid = None
|
||||
self.drawn_grid = None
|
||||
|
||||
self.update() # Repaint to permanently draw the rectangle
|
||||
|
||||
def __draw_beam_center(self, painter: QPainter):
|
||||
beam_size_pxl = self.__geom.beam_size_pxl
|
||||
|
||||
painter.setBrush(Qt.BrushStyle.NoBrush)
|
||||
painter.setPen(QPen(QColor(245, 121, 0), 3, Qt.PenStyle.SolidLine))
|
||||
painter.drawRect(
|
||||
QRect(
|
||||
int(self.__geom.beam_location_pxl.x - beam_size_pxl.x / 2),
|
||||
int(self.__geom.beam_location_pxl.y - beam_size_pxl.y / 2),
|
||||
int(beam_size_pxl.x),
|
||||
int(beam_size_pxl.y),
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def __draw_circle(painter, coord: Coordinate, color: QColor, radius=10):
|
||||
painter.setPen(QPen(Qt.PenStyle.NoPen))
|
||||
painter.setBrush(color)
|
||||
painter.drawEllipse(coord.x - radius, coord.y - radius, 2 * radius, 2 * radius)
|
||||
|
||||
@staticmethod
|
||||
def __draw_arrow(painter: QPainter, start: Coordinate, end: Coordinate):
|
||||
gradient = QLinearGradient()
|
||||
gradient.setStart(QPointF(start.x, start.y)) # Start of the gradient (green)
|
||||
gradient.setFinalStop(QPointF(end.x, end.y)) # End of the gradient (red)
|
||||
|
||||
gradient.setColorAt(0.0, QColor("green")) # Start color
|
||||
gradient.setColorAt(1.0, QColor("red")) # End color
|
||||
|
||||
pen = QPen()
|
||||
pen.setBrush(gradient) # Use gradient as the brush for the pen
|
||||
pen.setWidth(4) # Set pen width
|
||||
|
||||
painter.setPen(pen)
|
||||
|
||||
painter.drawLine(QPointF(start.x, start.y), QPointF(end.x, end.y))
|
||||
|
||||
def __draw_helical(self, painter: QPainter):
|
||||
if self.__helical_start.sh_mm is not None:
|
||||
start_pxl = self.__geom.smargon_to_picture(self.__helical_start.sh_mm)
|
||||
self.__draw_circle(painter, start_pxl, QColor("green"))
|
||||
else:
|
||||
start_pxl = None
|
||||
|
||||
if self.__helical_end.sh_mm is not None:
|
||||
end_pxl = self.__geom.smargon_to_picture(self.__helical_end.sh_mm)
|
||||
self.__draw_circle(painter, end_pxl, QColor("red"))
|
||||
else:
|
||||
end_pxl = None
|
||||
|
||||
if start_pxl is not None and end_pxl is not None:
|
||||
self.__draw_arrow(painter, start_pxl, end_pxl)
|
||||
|
||||
def __draw_grid(self, painter: QPainter, r: RasterGridRequest):
|
||||
painter.setBrush(Qt.BrushStyle.NoBrush)
|
||||
if r == self.active_grid:
|
||||
painter.setPen(QPen(QColor(114, 159, 207), 2, Qt.PenStyle.SolidLine))
|
||||
else:
|
||||
painter.setPen(QPen(QColor(255, 0, 255), 2, Qt.PenStyle.SolidLine))
|
||||
|
||||
g = r.grid_size_pxl(self.__geom)
|
||||
c0 = r.start_pxl(self.__geom)
|
||||
for x in range(r.n_x):
|
||||
for y in range(r.n_y):
|
||||
c = self.__geom.sample_to_picture(
|
||||
c0 + Coordinate(x=x * r.grid_size_mm.x, y=y * r.grid_size_mm.y)
|
||||
)
|
||||
painter.drawRect(QRect(int(c.x), int(c.y), int(g.x), int(g.y)))
|
||||
|
||||
def __draw_grids(self, painter):
|
||||
for i in self.__raster:
|
||||
if i is not None and i.grid_visible(self.__geom):
|
||||
self.__draw_grid(painter, i)
|
||||
|
||||
if self.drawn_grid is not None:
|
||||
self.__draw_grid(painter, self.drawn_grid)
|
||||
|
||||
def __update_drawn_grid(self):
|
||||
if self.right_click_grid is None:
|
||||
grid_mm = Coordinate(x=self.raster_grid_x, y=self.raster_grid_y)
|
||||
else:
|
||||
grid_mm = self.right_click_grid.grid_size_mm
|
||||
|
||||
delta_x = self.end_point.x() - self.start_point.x()
|
||||
delta_y = self.end_point.y() - self.start_point.y()
|
||||
n_x = round(abs(delta_x * self.__geom.pixel_in_mm / grid_mm.x))
|
||||
n_y = round(abs(delta_y * self.__geom.pixel_in_mm / grid_mm.y))
|
||||
|
||||
c = self.__geom.picture_to_sample(
|
||||
Coordinate(x=self.start_point.x(), y=self.start_point.y())
|
||||
)
|
||||
|
||||
if delta_x < 0:
|
||||
c.x -= n_x * grid_mm.x
|
||||
if delta_y < 0:
|
||||
c.y -= n_y * grid_mm.y
|
||||
|
||||
if n_x > 0 and n_y > 0:
|
||||
self.drawn_grid = RasterGridRequest(
|
||||
n_x=n_x,
|
||||
n_y=n_y,
|
||||
smargon=SmargonCoordinate(
|
||||
sh_mm=self.__geom.beamline_to_smargon(c),
|
||||
phi_deg=self.__geom.smargon.phi_deg,
|
||||
chi_deg=self.__geom.smargon.chi_deg,
|
||||
),
|
||||
grid_size_mm=grid_mm,
|
||||
omega_deg=self.__geom.omega_deg,
|
||||
)
|
||||
self.active_grid = self.drawn_grid
|
||||
else:
|
||||
self.drawn_grid = None
|
||||
|
||||
@Slot(SmargonCoordinate)
|
||||
def show_helical_start(self, pos: SmargonCoordinate):
|
||||
self.__helical_start = pos
|
||||
self.update()
|
||||
|
||||
@Slot(SmargonCoordinate)
|
||||
def show_helical_end(self, pos: SmargonCoordinate):
|
||||
self.__helical_end = pos
|
||||
self.update()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
mouse_global_pos = QCursor.pos()
|
||||
mouse_view_pos = self.mapFromGlobal(mouse_global_pos)
|
||||
mouse_scene_pos = self.mapToScene(mouse_view_pos)
|
||||
smargon_coord = SmargonCoordinate(
|
||||
sh_mm=self.__geom.picture_to_smargon(
|
||||
Coordinate(x=mouse_scene_pos.x(), y=mouse_scene_pos.y())
|
||||
))
|
||||
|
||||
if event.key() == Qt.Key.Key_1:
|
||||
self.set_helical_start.emit(smargon_coord)
|
||||
|
||||
elif event.key() == Qt.Key.Key_2:
|
||||
self.set_helical_end.emit(smargon_coord)
|
||||
|
||||
super().keyPressEvent(event)
|
||||
|
||||
def wheelEvent(self, event: QWheelEvent):
|
||||
# Only process the wheel event if the timer is not active or enough time has passed
|
||||
if not self.wheel_event_timer.isActive():
|
||||
delta_y = event.angleDelta().y()
|
||||
|
||||
if event.modifiers() & Qt.KeyboardModifier.ControlModifier:
|
||||
val = 180
|
||||
elif event.modifiers() & Qt.KeyboardModifier.ShiftModifier:
|
||||
val = 10
|
||||
else:
|
||||
val = 90
|
||||
|
||||
if delta_y > 0:
|
||||
self.set_omega.emit(self.__geom.omega_deg + val)
|
||||
else:
|
||||
self.set_omega.emit(self.__geom.omega_deg - val)
|
||||
|
||||
# Start the timer to throttle further events
|
||||
self.wheel_event_timer.start(self.wheel_event_threshold)
|
||||
@@ -0,0 +1,12 @@
|
||||
from PySide6.QtCore import Signal, Qt
|
||||
from PySide6.QtWidgets import QLabel
|
||||
|
||||
|
||||
class ClickableLabel(QLabel):
|
||||
clicked = Signal()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self.clicked.emit()
|
||||
else:
|
||||
super().mousePressEvent(event)
|
||||
@@ -0,0 +1,66 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from PySide6.QtCore import Slot, QByteArray, QUrl, QUrlQuery
|
||||
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QLineEdit, QPushButton, QLabel, QHBoxLayout
|
||||
)
|
||||
|
||||
|
||||
class LoginDialog(QDialog):
|
||||
def __init__(self, base_url: str):
|
||||
super().__init__()
|
||||
self.token = ""
|
||||
self.setWindowTitle("User Authentication")
|
||||
self.setMinimumWidth(400)
|
||||
self.setStyleSheet("background-color: rgb(216, 228, 253);")
|
||||
self.__base_url = base_url
|
||||
self.__reply = None
|
||||
|
||||
# Layout
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Username
|
||||
self.name_label = QLabel("Username:")
|
||||
self.name_entry = QLineEdit()
|
||||
user = os.getlogin()
|
||||
self.name_entry.setText(user)
|
||||
self.name_entry.setPlaceholderText("Enter username")
|
||||
layout.addWidget(self.name_label)
|
||||
layout.addWidget(self.name_entry)
|
||||
|
||||
# Buttons
|
||||
button_layout = QHBoxLayout()
|
||||
self.ok_button = QPushButton("OK")
|
||||
self.ok_button.clicked.connect(self.authenticate) # Close dialog with accept status
|
||||
self.cancel_button = QPushButton("Cancel")
|
||||
self.cancel_button.clicked.connect(self.reject) # Close dialog with reject status
|
||||
button_layout.addWidget(self.ok_button)
|
||||
button_layout.addWidget(self.cancel_button)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
@Slot()
|
||||
def authenticate(self):
|
||||
network_manager = QNetworkAccessManager(self)
|
||||
request = QNetworkRequest(QUrl(f"{self.__base_url}/token"))
|
||||
request.setHeader(QNetworkRequest.KnownHeaders.ContentTypeHeader, "application/x-www-form-urlencoded")
|
||||
|
||||
payload = QUrlQuery()
|
||||
payload.addQueryItem("username", f"{self.name_entry.text()}")
|
||||
payload.addQueryItem("password", "")
|
||||
payload_string = payload.toString()
|
||||
self.__reply = network_manager.post(request, QByteArray.fromStdString(payload_string))
|
||||
self.__reply.finished.connect(self.handle_token_response)
|
||||
|
||||
|
||||
@Slot()
|
||||
def handle_token_response(self):
|
||||
if self.__reply is not None and self.__reply.error() == QNetworkReply.NetworkError.NoError:
|
||||
response_data = self.__reply.readAll().data()
|
||||
response_json = json.loads(response_data.decode("utf-8"))
|
||||
if "access_token" in response_json:
|
||||
self.token = response_json["access_token"]
|
||||
self.__reply.deleteLater()
|
||||
self.accept()
|
||||
@@ -0,0 +1,10 @@
|
||||
from PySide6.QtWidgets import QScrollArea
|
||||
|
||||
|
||||
class NoWheelScrollArea(QScrollArea):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
def wheelEvent(self, event):
|
||||
# Override the wheelEvent and do nothing
|
||||
pass
|
||||
@@ -0,0 +1,65 @@
|
||||
from PySide6.QtCore import Signal, Slot, Qt
|
||||
from PySide6.QtGui import QDoubleValidator
|
||||
from PySide6.QtWidgets import QLineEdit
|
||||
|
||||
|
||||
class NumberLineEdit(QLineEdit):
|
||||
newValue = Signal(float)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
min_val: float,
|
||||
max_val: float,
|
||||
default: float = 0.0,
|
||||
decimals: int = 2,
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setStyleSheet("background-color: rgb(255, 255, 255);")
|
||||
|
||||
# Use a QDoubleValidator to only allow valid floating point numbers
|
||||
self.validator = QDoubleValidator()
|
||||
self.validator.setNotation(QDoubleValidator.Notation.StandardNotation)
|
||||
self.decimal_count = decimals
|
||||
self.validator.setRange(min_val, max_val, self.decimal_count)
|
||||
self.setValidator(self.validator)
|
||||
self.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
self.setToolTip(
|
||||
"Minimum: {:s}\nMaximum: {:s}".format(
|
||||
self.to_string(min_val), self.to_string(max_val)
|
||||
)
|
||||
)
|
||||
|
||||
# Connect the textChanged signal to a custom slot to check validity
|
||||
self.textChanged.connect(self.on_text_changed)
|
||||
self.editingFinished.connect(self.on_editing_finished)
|
||||
self.setText(self.to_string(default))
|
||||
self.saved_value = default
|
||||
|
||||
def to_string(self, i: float) -> str:
|
||||
format_string = f"{{:.{self.decimal_count}f}}"
|
||||
return format_string.format(i)
|
||||
|
||||
@Slot(str)
|
||||
def on_text_changed(self, text: str):
|
||||
if self.validator.validate(text, 0)[0] == QDoubleValidator.State.Acceptable:
|
||||
self.setStyleSheet("background-color: rgb(255, 255, 255);")
|
||||
else:
|
||||
self.setStyleSheet("background-color: rgb(255, 213, 213);")
|
||||
|
||||
@Slot()
|
||||
def on_editing_finished(self):
|
||||
val = float(self.text())
|
||||
self.newValue.emit(val)
|
||||
self.saved_value = self.saved_value
|
||||
|
||||
@property
|
||||
def value(self) -> float:
|
||||
return float(self.text())
|
||||
|
||||
@Slot(float)
|
||||
def update_value(self, val: float):
|
||||
if abs(val - self.saved_value) > 0.001:
|
||||
self.saved_value = val
|
||||
self.setText(self.to_string(val))
|
||||
@@ -0,0 +1,37 @@
|
||||
from PySide6.QtWidgets import QDialog, QLineEdit, QVBoxLayout, QPushButton, QLabel
|
||||
|
||||
|
||||
class PGroupDialog(QDialog):
|
||||
def __init__(self, curr_pgroup: str | None = None, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Change current p-group")
|
||||
self.setMinimumWidth(300)
|
||||
|
||||
# Create a layout
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Add a label
|
||||
self.label = QLabel("Set p-group:", self)
|
||||
layout.addWidget(self.label)
|
||||
|
||||
# Add a text entry field
|
||||
self.text_entry = QLineEdit(self)
|
||||
if curr_pgroup is not None:
|
||||
self.text_entry.setText(curr_pgroup)
|
||||
layout.addWidget(self.text_entry)
|
||||
|
||||
# Create buttons
|
||||
self.ok_button = QPushButton("OK", self)
|
||||
self.cancel_button = QPushButton("Cancel", self)
|
||||
|
||||
# Add buttons to the layout
|
||||
layout.addWidget(self.ok_button)
|
||||
layout.addWidget(self.cancel_button)
|
||||
|
||||
# Connect button signals
|
||||
self.ok_button.clicked.connect(self.accept)
|
||||
self.cancel_button.clicked.connect(self.reject)
|
||||
|
||||
def get_input(self):
|
||||
"""Return the input text when the dialog is accepted."""
|
||||
return self.text_entry.text()
|
||||
@@ -0,0 +1,149 @@
|
||||
from PySide6.QtCore import Signal, Slot, QPoint
|
||||
from PySide6.QtWidgets import QStatusBar, QDialog, QMenu
|
||||
|
||||
from aaredaqlib.models import TokenData, BeamlineStateEnum, DAQStatusModel, SessionsStateEnum
|
||||
from aaregui import ClickableLabel
|
||||
from aaregui import PGroupDialog
|
||||
|
||||
|
||||
class StatusBar(QStatusBar):
|
||||
set_pgroup = Signal(str)
|
||||
dewar_exchange = Signal()
|
||||
sample_alignment = Signal()
|
||||
sample_exchange = Signal()
|
||||
|
||||
force_session = Signal()
|
||||
end_session = Signal()
|
||||
|
||||
def __init__(self, token: TokenData, parent=None):
|
||||
super().__init__(parent)
|
||||
self.__decoded_token = token
|
||||
|
||||
self.pgroup_label = ClickableLabel(self)
|
||||
self.pgroup_label.clicked.connect(self.show_pgroup_menu)
|
||||
|
||||
self.state_label = ClickableLabel(parent=self)
|
||||
self.state_label.clicked.connect(self.show_state_menu)
|
||||
|
||||
self.busy_label = ClickableLabel(parent=self)
|
||||
|
||||
self.session_label = ClickableLabel(parent=self)
|
||||
self.session_label.clicked.connect(self.show_session_menu)
|
||||
|
||||
self.addPermanentWidget(self.pgroup_label)
|
||||
self.addPermanentWidget(self.state_label)
|
||||
self.addPermanentWidget(self.busy_label)
|
||||
self.addPermanentWidget(self.session_label)
|
||||
|
||||
|
||||
@Slot(DAQStatusModel)
|
||||
def update_daq_status(self, status: DAQStatusModel):
|
||||
if status.current_pgroup is not None:
|
||||
self.pgroup_label.setText(f"""p-group: {status.current_pgroup} """)
|
||||
else:
|
||||
self.pgroup_label.setText(f"Inactive p-group ")
|
||||
|
||||
state = status.state
|
||||
if state == BeamlineStateEnum.SampleExchange:
|
||||
state = "Sample exchange"
|
||||
elif state == BeamlineStateEnum.SampleAlignment:
|
||||
state = "Sample alignment"
|
||||
elif state == BeamlineStateEnum.DataCollection:
|
||||
state = "Data collection"
|
||||
elif state == BeamlineStateEnum.DewarTransfer:
|
||||
state = "Dewar transfer"
|
||||
elif state == BeamlineStateEnum.Maintenance:
|
||||
state = "Maintenance"
|
||||
elif state == BeamlineStateEnum.Moving:
|
||||
state = "Moving"
|
||||
elif state == BeamlineStateEnum.RobotSampleExchange:
|
||||
state = "Robot sample exchange"
|
||||
else:
|
||||
state = "-"
|
||||
|
||||
self.state_label.setText(f"""State: {state} """)
|
||||
|
||||
if status.busy:
|
||||
busy_flag = """ <span style="color: red; "> Busy ⬤ </span>"""
|
||||
else:
|
||||
busy_flag = """ <span style="color: green ; "> Idle ⬤ </span>"""
|
||||
|
||||
html_content = f"""Beamline: {busy_flag} """
|
||||
|
||||
self.busy_label.setText(html_content)
|
||||
|
||||
session_flag = ""
|
||||
if status.session == SessionsStateEnum.Vacant:
|
||||
session_flag = """<span style="color: yellow ; "> Vacant ⬤ </span>"""
|
||||
elif status.session == SessionsStateEnum.OwnedByYou:
|
||||
session_flag = """<span style="color: green ; "> Owned ⬤ </span>"""
|
||||
elif status.session == SessionsStateEnum.OwnedByElse:
|
||||
session_flag = """<span style="color: red ; "> Other ⬤ </span>"""
|
||||
|
||||
html_content_session = f"""Session: {session_flag}"""
|
||||
self.session_label.setText(html_content_session)
|
||||
|
||||
def show_session_menu(self):
|
||||
menu = QMenu(self)
|
||||
|
||||
action_1 = menu.addAction("Grab")
|
||||
action_1.triggered.connect(self.grab_session_clicked)
|
||||
action_2 = menu.addAction("End")
|
||||
action_2.triggered.connect(self.end_session_clicked)
|
||||
|
||||
label_geometry = self.session_label.geometry()
|
||||
menu.move(self.mapToGlobal(label_geometry.topLeft()) - QPoint(0, menu.sizeHint().height()))
|
||||
|
||||
menu.setFixedWidth(label_geometry.width())
|
||||
|
||||
menu.exec()
|
||||
|
||||
def show_pgroup_menu(self):
|
||||
if self.__decoded_token.staff:
|
||||
menu = QMenu(self)
|
||||
|
||||
action_1 = menu.addAction("Change")
|
||||
action_1.triggered.connect(self.show_change_dialog)
|
||||
|
||||
label_geometry = self.pgroup_label.geometry()
|
||||
menu.move(self.mapToGlobal(label_geometry.topLeft()) - QPoint(0, menu.sizeHint().height()))
|
||||
menu.setFixedWidth(label_geometry.width())
|
||||
|
||||
menu.exec()
|
||||
|
||||
def show_state_menu(self):
|
||||
menu = QMenu(self)
|
||||
|
||||
action_1 = menu.addAction("Sample alignment")
|
||||
action_1.triggered.connect(self.sa)
|
||||
action_2 = menu.addAction("Sample exchange")
|
||||
action_2.triggered.connect(self.se)
|
||||
action_3 = menu.addAction("Dewar load/unload")
|
||||
action_3.triggered.connect(self.dl)
|
||||
|
||||
label_geometry = self.state_label.geometry()
|
||||
menu.move(self.mapToGlobal(label_geometry.topLeft()) - QPoint(0, menu.sizeHint().height()))
|
||||
menu.setFixedWidth(label_geometry.width())
|
||||
|
||||
menu.exec()
|
||||
|
||||
def grab_session_clicked(self):
|
||||
self.force_session.emit()
|
||||
|
||||
def end_session_clicked(self):
|
||||
self.end_session.emit()
|
||||
|
||||
def sa(self):
|
||||
self.sample_alignment.emit()
|
||||
|
||||
def se(self):
|
||||
self.sample_exchange.emit()
|
||||
|
||||
def dl(self):
|
||||
self.dewar_exchange.emit()
|
||||
|
||||
def show_change_dialog(self):
|
||||
dialog = PGroupDialog()
|
||||
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||
entered_text = dialog.get_input()
|
||||
self.set_pgroup.emit(entered_text)
|
||||
@@ -0,0 +1,15 @@
|
||||
from PySide6.QtCore import Qt, Slot
|
||||
from PySide6.QtWidgets import QLabel
|
||||
|
||||
|
||||
class StatusLabel(QLabel):
|
||||
def __init__(self, val: float, decimals: int = 0, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.format_string = f"<b>{{:.{decimals}f}}</b>"
|
||||
self.setText(self.format_string.format(val))
|
||||
self.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
|
||||
@Slot()
|
||||
def new_value(self, val: float):
|
||||
self.setText(self.format_string.format(val))
|
||||
@@ -0,0 +1,12 @@
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QLabel
|
||||
|
||||
|
||||
class TitleLabel(QLabel):
|
||||
def __init__(self, text: str, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setText(f"<H3>{text}</H3>")
|
||||
self.setStyleSheet("background-color: #4B0082; color: #ffffff;")
|
||||
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
self.setFixedHeight(50)
|
||||
@@ -1 +0,0 @@
|
||||
.idea/
|
||||
Reference in new issue
Block a user