DAQ: Implementation of reference-tools get methods from redis

GUI: Adding reference tools panel with mount and unmount functions
This commit is contained in:
GotthardG
2025-10-15 15:18:29 +02:00
parent bb476669cc
commit aabd435c6f
5 changed files with 198 additions and 1 deletions
+19
View File
@@ -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")
+10
View File
@@ -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")
+12 -1
View File
@@ -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)
+21
View File
@@ -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")