DAQ: Implementation of reference-tools get methods from redis
GUI: Adding reference tools panel with mount and unmount functions
This commit is contained in:
@@ -324,6 +324,25 @@ class BeamlineConfig:
|
||||
pubsub.psubscribe(f"__keyspace@0__:{self.__bl}:sample_spreadsheet")
|
||||
return pubsub
|
||||
|
||||
@property
|
||||
def reference_tools(self) -> SampleShortInfoList:
|
||||
tmp = self.__client.get(f"{self.__bl}:reference-tools")
|
||||
if tmp is None:
|
||||
return SampleShortInfoList(s=[])
|
||||
|
||||
data_dict = json.loads(tmp)
|
||||
return SampleShortInfoList(**data_dict)
|
||||
|
||||
@reference_tools.setter
|
||||
def reference_tools(self, data: SampleShortInfoList):
|
||||
self.__client.set(f"{self.__bl}:reference-tools", data.model_dump_json())
|
||||
|
||||
def listen_changes_reference_tools(self) -> redis.client.PubSub:
|
||||
self.__client.config_set("notify-keyspace-events", "KEA")
|
||||
pubsub = self.__client.pubsub()
|
||||
pubsub.psubscribe(f"__keyspace@0__:{self.__bl}:reference-tools")
|
||||
return pubsub
|
||||
|
||||
@property
|
||||
def current_sample(self) -> SampleShortInfo | None:
|
||||
tmp = self.__client.get(f"{self.__bl}:current_sample")
|
||||
|
||||
@@ -26,6 +26,8 @@ from aaredaq.daq import AareDAQ
|
||||
|
||||
from aaredaq.camera_stat_thread import start_image_stats_receiver, stop_image_stats_receiver
|
||||
|
||||
from aaregui.panels.reference_tools_panel import ReferenceToolsModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# OAuth2 setup
|
||||
@@ -241,6 +243,9 @@ def get_spreadsheet(data: TokenData) -> SampleShortInfoList:
|
||||
else:
|
||||
return cfg.spreadsheet_pgroup(data.pgroups)
|
||||
|
||||
def get_reference_tools() -> SampleShortInfoList:
|
||||
return cfg.reference_tools
|
||||
|
||||
|
||||
async def spreadsheet_event_stream(data: TokenData) -> AsyncGenerator[str, None]:
|
||||
try:
|
||||
@@ -270,6 +275,11 @@ async def spreadsheet_sse(token: str = Depends(oauth2_scheme)):
|
||||
async def spreadsheet(token: str = Depends(oauth2_scheme)) -> SampleShortInfoList:
|
||||
return get_spreadsheet(auth.parse_token(token))
|
||||
|
||||
@app.get("/sample/reference_tools")
|
||||
async def reference_tools(token: str = Depends(oauth2_scheme)) -> SampleShortInfoList:
|
||||
auth.check_jwt_ro(cfg, auth.parse_token(token))
|
||||
return get_reference_tools()
|
||||
|
||||
|
||||
# State transitions
|
||||
@app.post("/state/dewar_exchange")
|
||||
|
||||
@@ -19,6 +19,7 @@ from aaredaqlib.sample_geometry import SampleGeometryModel
|
||||
from aaregui.panels.beamline_controls import BeamlineControls
|
||||
from aaregui.panels.data_collection_settings import DataCollectionSettings
|
||||
from aaregui.panels.manual_sample_panel import ManualSamplePanel
|
||||
from aaregui.panels.reference_tools_panel import ReferenceToolsPanel
|
||||
from aaregui.panels.sample_queue_panel import SampleQueuePanel
|
||||
from aaregui.panels.tell_sample_panel import TellSamplePanel
|
||||
from aaregui.scan_logic.raster_grid_manager import RasterGridManager
|
||||
@@ -122,7 +123,14 @@ class MainWindow(QMainWindow):
|
||||
)
|
||||
beamline_controls_scroll.setFixedWidth(self.beamline.set_width + 10)
|
||||
|
||||
self.ref_tools_panel = ReferenceToolsPanel()
|
||||
# add dock or add to left column as you prefer; e.g. dock:
|
||||
ref_dock = QDockWidget("Reference Tools", self)
|
||||
ref_dock.setWidget(self.ref_tools_panel)
|
||||
self.addDockWidget(Qt.LeftDockWidgetArea, ref_dock)
|
||||
|
||||
self.tell_samples = TellSamplePanel(samples=SampleShortInfoList(s = []))
|
||||
self.ref_tools = ReferenceToolsPanel(samples=SampleShortInfoList(s = []))
|
||||
self.job_list_panel = SampleQueuePanel()
|
||||
|
||||
self.tell_samples_dock = QDockWidget("Sample List", self)
|
||||
@@ -154,6 +162,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
self.daq = DAQWorker(base_url=self.__base_url, token=self.__token)
|
||||
self.daq.spreadsheet.connect(self.tell_samples.new_sample_list)
|
||||
self.daq.reference_tools.connect(self.ref_tools_panel.new_list)
|
||||
|
||||
self.beamline.samcam.changed.connect(self.daq.samcam_settings)
|
||||
self.beamline.loopctr.background.clicked.connect(self.daq.alc_background)
|
||||
@@ -215,6 +224,8 @@ class MainWindow(QMainWindow):
|
||||
|
||||
self.tell_samples.mount.connect(self.daq.mount)
|
||||
self.tell_samples.unmount.connect(self.daq.unmount)
|
||||
self.ref_tools.mount.connect(self.daq.mount)
|
||||
self.ref_tools.unmount.connect(self.daq.unmount)
|
||||
|
||||
self.data_collection.raster.grid_size_updated.connect(self.raster.update_grid_size)
|
||||
self.data_collection.raster.exp_time_updated.connect(self.raster.update_exposure_time)
|
||||
@@ -319,7 +330,7 @@ class MainWindow(QMainWindow):
|
||||
QMessageBox.about(
|
||||
self,
|
||||
"About",
|
||||
"Aare Macromolecular Crystallography GUI\nVersion: 1.0\nCopyright: Paul Scherrer Instiute 2024-2025",
|
||||
"Aare Macromolecular Crystallography GUI\nVersion: 1.0\nCopyright: Paul Scherrer Institute 2024-2025",
|
||||
)
|
||||
|
||||
@Slot()
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
# reference_tools_panel.py
|
||||
from typing import Callable, List, Dict, Optional
|
||||
|
||||
from PySide6.QtCore import Qt, QAbstractTableModel, QModelIndex, QTimer, Slot, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame,
|
||||
QGridLayout,
|
||||
QTableView,
|
||||
QHeaderView,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QMenu,
|
||||
QAbstractItemView,
|
||||
)
|
||||
from aaredaqlib.models import SampleShortInfoList, SampleShortInfo, BeamlineStateEnum
|
||||
|
||||
|
||||
class ReferenceToolsModel(QAbstractTableModel):
|
||||
def __init__(self, rows: Optional[List[SampleShortInfo]] = None, parent=None):
|
||||
super().__init__(parent)
|
||||
self._rows: List[SampleShortInfo] = rows or []
|
||||
|
||||
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
||||
return len(self._rows)
|
||||
|
||||
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
||||
return 3 # position, sample_name, mount_count
|
||||
|
||||
def data(self, index: QModelIndex, role: int = Qt.DisplayRole):
|
||||
if not index.isValid() or role != Qt.DisplayRole:
|
||||
return None
|
||||
s: SampleShortInfo = self._rows[index.row()]
|
||||
if index.column() == 0:
|
||||
# use loc_str if present, fallback to "-"
|
||||
return s.loc_str() if getattr(s, "loc_str", None) else "-"
|
||||
if index.column() == 1:
|
||||
return s.sample_name
|
||||
if index.column() == 2:
|
||||
return str(getattr(s, "mount_count", 0))
|
||||
return None
|
||||
|
||||
def headerData(self, section: int, orientation, role: int = Qt.DisplayRole):
|
||||
if role != Qt.DisplayRole:
|
||||
return None
|
||||
if orientation == Qt.Horizontal:
|
||||
return ("position", "sample_name", "mount_count")[section]
|
||||
return str(section + 1)
|
||||
|
||||
def update_rows(self, rows: List[SampleShortInfo]):
|
||||
self.beginResetModel()
|
||||
self._rows = rows
|
||||
self.endResetModel()
|
||||
|
||||
def get_item(self, row: int) -> Optional[SampleShortInfo]:
|
||||
try:
|
||||
return self._rows[row]
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
class ReferenceToolsPanel(QFrame):
|
||||
mount = Signal(SampleShortInfo)
|
||||
unmount = Signal(SampleShortInfo)
|
||||
|
||||
def __init__(self, samples: SampleShortInfoList | None = None, parent=None, refresh_interval_ms: int = 5000):
|
||||
"""
|
||||
:param samples: optional initial SampleShortInfoList to populate the table
|
||||
:param parent: Qt parent
|
||||
:param refresh_interval_ms: how often to call request_refresh (panel doesn't implement the request itself)
|
||||
"""
|
||||
super().__init__(parent)
|
||||
|
||||
if samples is None:
|
||||
samples = SampleShortInfoList(s=[])
|
||||
|
||||
self.setFrameShape(QFrame.Shape.StyledPanel)
|
||||
self.setFrameShadow(QFrame.Shadow.Raised)
|
||||
|
||||
layout = QGridLayout(self)
|
||||
self.setLayout(layout)
|
||||
|
||||
layout.addWidget(QLabel("Reference tools", parent=self), 0, 0, 1, 2)
|
||||
|
||||
self.table_view = QTableView(parent=self)
|
||||
# initialize model with provided samples
|
||||
self.table_model = ReferenceToolsModel(samples.s)
|
||||
self.table_view.setModel(self.table_model)
|
||||
header = self.table_view.horizontalHeader()
|
||||
header.setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
|
||||
header.setStretchLastSection(True)
|
||||
self.table_view.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
|
||||
self.table_view.setSelectionMode(QTableView.SelectionMode.SingleSelection)
|
||||
self.table_view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
self.table_view.customContextMenuRequested.connect(self._context_menu)
|
||||
|
||||
layout.addWidget(self.table_view, 1, 0, 1, 2)
|
||||
|
||||
self.unmount_button = QPushButton("Unmount", parent=self)
|
||||
layout.addWidget(self.unmount_button, 2, 0)
|
||||
self.unmount_button.clicked.connect(self._on_unmount_clicked)
|
||||
|
||||
self._timer = QTimer(self)
|
||||
self._timer.setInterval(refresh_interval_ms)
|
||||
self._timer.timeout.connect(self.request_refresh) # implement to call DAQ/adapter
|
||||
self._timer.start()
|
||||
|
||||
def _selected_item(self) -> Optional[SampleShortInfo]:
|
||||
idx = self.table_view.currentIndex()
|
||||
if not idx.isValid():
|
||||
return None
|
||||
return self.table_model.get_item(idx.row())
|
||||
|
||||
def _on_unmount_clicked(self):
|
||||
item = self._selected_item()
|
||||
if item is not None:
|
||||
self.unmount.emit(item)
|
||||
|
||||
def _context_menu(self, position):
|
||||
idx = self.table_view.indexAt(position)
|
||||
if not idx.isValid():
|
||||
return
|
||||
item = self.table_model.get_item(idx.row())
|
||||
if item is None:
|
||||
return
|
||||
menu = QMenu()
|
||||
act_mount = menu.addAction("Mount")
|
||||
act_unmount = menu.addAction("Unmount")
|
||||
action = menu.exec_(self.table_view.viewport().mapToGlobal(position))
|
||||
if action == act_mount:
|
||||
self.mount.emit(item)
|
||||
elif action == act_unmount:
|
||||
self.unmount.emit(item)
|
||||
|
||||
@Slot(SampleShortInfoList)
|
||||
def new_list(self, lst: SampleShortInfoList):
|
||||
# signal from DAQWorker will call this with the model
|
||||
self.table_model.update_rows(lst.s)
|
||||
@@ -17,6 +17,7 @@ SPREADHSEET_FREQUENCY = 25 # Every 5 seconds
|
||||
class DAQWorker(QObject):
|
||||
update = Signal(DAQStatusModel)
|
||||
spreadsheet = Signal(SampleShortInfoList)
|
||||
reference_tools = Signal(SampleShortInfoList)
|
||||
http_error = Signal(str)
|
||||
auth_error = Signal()
|
||||
automated_scan_done = Signal(int, bool) # sample ID, success
|
||||
@@ -82,6 +83,16 @@ class DAQWorker(QObject):
|
||||
print(e)
|
||||
self.http_error.emit(str(e))
|
||||
|
||||
@Slot(QNetworkReply)
|
||||
def handle_reference_tools_response(self, reply: QNetworkReply):
|
||||
try:
|
||||
response_data = self.handle_response(reply)
|
||||
parsed_response = SampleShortInfoList.model_validate_json(response_data)
|
||||
self.reference_tools.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:
|
||||
@@ -301,6 +312,16 @@ class DAQWorker(QObject):
|
||||
reply = self.__net_manager.get(request)
|
||||
reply.finished.connect(lambda: self.handle_spreadsheet_response(reply))
|
||||
|
||||
@Slot()
|
||||
def load_reference_tools(self):
|
||||
if self.__base_url is None:
|
||||
print(f"GET /sample/reference_tools")
|
||||
return
|
||||
request = QNetworkRequest(QUrl(f"{self.__base_url}/sample/reference_tools"))
|
||||
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
|
||||
reply = self.__net_manager.get(request)
|
||||
reply.finished.connect(lambda: self.handle_reference_tools_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")
|
||||
|
||||
Reference in New Issue
Block a user