DAQ/GUI: feature added - fluorimeter panel and device
This commit is contained in:
@@ -1524,3 +1524,18 @@ class AareDAQ:
|
||||
self.__devs.anneal(time_s)
|
||||
finally:
|
||||
self.__cfg.state_busy = False
|
||||
|
||||
def fluorimeter_start(self, erase: bool = False):
|
||||
self.__devs.fluorimeter.start_acquisition(erase=erase)
|
||||
|
||||
def fluorimeter_stop(self):
|
||||
self.__devs.fluorimeter.stop_acquisition()
|
||||
|
||||
def fluorimeter_status(self) -> int | None:
|
||||
return self.__devs.fluorimeter.check_status()
|
||||
|
||||
def fluorimeter_data(self):
|
||||
return self.__devs.fluorimeter.get_current_data()
|
||||
|
||||
def fluorimeter_background(self):
|
||||
return self.__devs.fluorimeter.get_current_background()
|
||||
@@ -9,6 +9,7 @@ from mxlibs3 import aerotech, smargon
|
||||
from mxlibs3.area_detector import epicsAD
|
||||
from mxlibs3.enum_pv import EnumPv
|
||||
from mxlibs3.filter_transmission import FilterTransmission
|
||||
from mxlibs3.fluorimeter import Fluorimeter
|
||||
from mxlibs3.my_motor import MyMotor
|
||||
from mxlibs3.non_standard import NonStandard
|
||||
from mxlibs3.tell_client import TellClient
|
||||
@@ -175,6 +176,9 @@ class BeamlineDevices:
|
||||
|
||||
self.sample_cam = epicsAD(f"{BEAMLINE}-SAMCAM:")
|
||||
|
||||
self.fluorimeter = Fluorimeter(beamline)
|
||||
|
||||
|
||||
@property
|
||||
def lamp_light(self) -> float:
|
||||
return self.__lamp_light.value
|
||||
|
||||
@@ -2,7 +2,7 @@ import asyncio
|
||||
import io
|
||||
import os, time
|
||||
from typing import Tuple, AsyncGenerator
|
||||
|
||||
import json
|
||||
import cv2
|
||||
import urllib3
|
||||
import uvicorn
|
||||
@@ -546,6 +546,78 @@ async def get_all_pgroups(token: str = Depends(oauth2_scheme)):
|
||||
logger.error(f"Failed to list pgroups: {e}")
|
||||
return []
|
||||
|
||||
@app.post("/fluorimeter/start")
|
||||
async def fluorimeter_start(erase: bool = False, token: str = Depends(oauth2_scheme)) -> str:
|
||||
auth.check_jwt_rw(cfg, auth.parse_token(token))
|
||||
daq.fluorimeter_start(erase=erase)
|
||||
return "OK"
|
||||
|
||||
@app.post("/fluorimeter/stop")
|
||||
async def fluorimeter_stop(token: str = Depends(oauth2_scheme)) -> str:
|
||||
auth.check_jwt_rw(cfg, auth.parse_token(token))
|
||||
daq.fluorimeter_stop()
|
||||
return "OK"
|
||||
|
||||
@app.get("/fluorimeter/status")
|
||||
async def fluorimeter_status(token: str = Depends(oauth2_scheme)) -> int | None:
|
||||
auth.check_jwt_ro(cfg, auth.parse_token(token))
|
||||
return daq.fluorimeter_status()
|
||||
|
||||
@app.get("/fluorimeter/data")
|
||||
async def fluorimeter_data(token: str = Depends(oauth2_scheme)) -> list[int] | None:
|
||||
auth.check_jwt_ro(cfg, auth.parse_token(token))
|
||||
return daq.fluorimeter_data()
|
||||
|
||||
@app.get("/fluorimeter/background")
|
||||
async def fluorimeter_background(token: str = Depends(oauth2_scheme)) -> list[int] | None:
|
||||
auth.check_jwt_ro(cfg, auth.parse_token(token))
|
||||
return daq.fluorimeter_background()
|
||||
|
||||
# Optional: SSE stream for live spectra while acquiring
|
||||
async def fluorimeter_stream() -> AsyncGenerator[str, None]:
|
||||
logger.debug("start fluorimeter stream...")
|
||||
try:
|
||||
while True:
|
||||
s = daq.fluorimeter_status()
|
||||
d = daq.fluorimeter_data()
|
||||
b = daq.fluorimeter_background()
|
||||
# Ensure JSON-serializable lists; avoid numpy truthiness
|
||||
status = "stopped" if s == 0 else "running"
|
||||
logger.debug(f"fluorimeter status: {status}. got data from fluorimeter: {d[0]} and background: {b[0]}")
|
||||
data = d.tolist() if hasattr(d, "tolist") else (list(d) if isinstance(d, (tuple, list)) else ([] if d is None else [d]))
|
||||
bkg = b.tolist() if hasattr(b, "tolist") else (list(b) if isinstance(b, (tuple, list)) else ([] if b is None else [b]))
|
||||
logger.debug(f"fluorimeter data: {data[0]}. fluorimeter background: {bkg[0]}")
|
||||
payload = json.dumps({"status": s, "data": data, "background": bkg}, separators=(",", ":"))
|
||||
yield f"data: {payload}\n\n"
|
||||
if s == 0:
|
||||
await asyncio.sleep(0.2)
|
||||
logger.debug("fluorimeter stream stopped")
|
||||
break
|
||||
await asyncio.sleep(0.2)
|
||||
except asyncio.CancelledError as e:
|
||||
logger.error(f">>> Fluorimeter stream cancelled: {e}")
|
||||
return
|
||||
|
||||
@app.get("/sse/fluorimeter")
|
||||
async def sse_fluorimeter(token: str = Depends(oauth2_scheme)):
|
||||
auth.check_jwt_ro(cfg, auth.parse_token(token))
|
||||
try:
|
||||
daq.fluorimeter_start(erase=False)
|
||||
except Exception:
|
||||
# Ignore if already running or start not needed
|
||||
pass
|
||||
return StreamingResponse(
|
||||
fluorimeter_stream(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Headers": "Cache-Control"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
LOGGING_CONFIG = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
import time
|
||||
from typing import Callable, cast
|
||||
|
||||
from epics import PV, poll
|
||||
from epics.ca import pend_io
|
||||
|
||||
from aaredaqlib.beamline import MXBeamline
|
||||
|
||||
from aaredaqlib.logger_config import setup_logger
|
||||
|
||||
logger = setup_logger("aareaDAQ")
|
||||
|
||||
class Fluorimeter(object):
|
||||
|
||||
def __init__(self, beamline: MXBeamline, **kwargs):
|
||||
BEAMLINE = beamline.value.upper()
|
||||
|
||||
self.__start = PV(f"{BEAMLINE}-ES-SiD:mca1Start") #0 done ,1 start
|
||||
self.__stop = PV(f"{BEAMLINE}-ES-SiD:mca1Stop") #0 done ,1 stop
|
||||
self.__erase_and_start = PV(f"{BEAMLINE}-ES-SiD:mca1EraseStart") #0 done,1 start
|
||||
self.__erase = PV(f"{BEAMLINE}-ES-SiD:mca1Erase") #0 done,1 erase
|
||||
|
||||
self.__preset_mode = PV(f"{BEAMLINE}-ES-SiD:dxp1:PresetMode") # set mode
|
||||
self.__status = PV(f"{BEAMLINE}-ES-SiD:mca1.ACQG") # 0 done, 1 acquire
|
||||
|
||||
self.__real_time = PV(f"{BEAMLINE}-ES-SiD:mca1.PRTM") #float
|
||||
self.__live_time = PV(f"{BEAMLINE}-ES-SiD:mca1.PLTM") #float
|
||||
self.__elapsed_real_time = PV(f"{BEAMLINE}-ES-SiD:mca1.ERTM") #float
|
||||
self.__elapsed_live_time = PV(f"{BEAMLINE}-ES-SiD:mca1.ELTM") #float
|
||||
self.__elapsed_trigger_live_time = PV(f"{BEAMLINE}-ES-SiD:dxp1:ElapsedTriggerLiveTime")
|
||||
|
||||
self.__instant_dead_time = PV(f"{BEAMLINE}-ES-SiD:mca1.IDTIM")
|
||||
self.__average_dead_time = PV(f"{BEAMLINE}-ES-SiD:mca1.DTIM")
|
||||
|
||||
self.__poll_time_rbv = PV(f"{BEAMLINE}-ES-SiD:PollTime_RBV")
|
||||
self.__poll_time = PV(f"{BEAMLINE}-ES-SiD:PollTime")
|
||||
|
||||
self.__mca = PV(f"{BEAMLINE}-ES-SiD:mca1")
|
||||
self.__data = PV(f"{BEAMLINE}-ES-SiD:mca1.VAL")
|
||||
self.__background = PV(f"{BEAMLINE}-ES-SiD:mca1.BG")
|
||||
|
||||
self.__saveFile = PV(f"{BEAMLINE}-ES-SiD:SaveSystemFile")
|
||||
self.__saveFile_name = PV(f"{BEAMLINE}-ES-SiD:SaveSystem")
|
||||
self.__saveFile_rbv = PV(f"{BEAMLINE}-ES-SiD:SaveSystem_RBV") #1 ssave, 0 done
|
||||
self.__roi_1 = self.get_roi(BEAMLINE, 1) #currently SiEsc
|
||||
self.__roi_2 = self.get_roi(BEAMLINE, 2) #currently MnKa
|
||||
|
||||
def get_roi(self, bl, roi_num:int):
|
||||
if roi_num not in [1, 2]:
|
||||
raise ValueError(f"Invalid ROI number: {roi_num}")
|
||||
roi_name = f"{bl}-ES-SiD:mca1.R{roi_num}NM"
|
||||
return PV(roi_name)
|
||||
|
||||
def start_acquisition(self, erase: bool = False):
|
||||
if erase:
|
||||
self.__erase_and_start.put(1)
|
||||
else:
|
||||
self.__start.put(1)
|
||||
|
||||
def stop_acquisition(self):
|
||||
self.__stop.put(1)
|
||||
|
||||
def erase(self):
|
||||
self.__erase.put(1)
|
||||
|
||||
def poll_data(self, timeout_s: float = 30.0):
|
||||
timeout = time.time() + timeout_s
|
||||
while not self.check_status_done():
|
||||
if time.time() > timeout:
|
||||
self.stop_acquisition()
|
||||
raise TimeoutError("timeout waiting for data to be acquired.")
|
||||
self.__mca.get()
|
||||
poll(0.1)
|
||||
|
||||
def get_current_data(self):
|
||||
return self.__data.get()
|
||||
|
||||
def get_current_background(self):
|
||||
return self.__background.get()
|
||||
|
||||
def check_status(self):
|
||||
return self.__status.get()
|
||||
|
||||
def check_status_done(self):
|
||||
if self.check_status() == 0:
|
||||
return True
|
||||
|
||||
def check_status_acquiring(self):
|
||||
if self.check_status() == 1:
|
||||
return True
|
||||
|
||||
def wait_till_done(self, timeout_s: float = 30.0):
|
||||
timeout = time.time() + timeout_s
|
||||
while not self.check_status_done():
|
||||
if time.time() > timeout:
|
||||
raise TimeoutError("timeout waiting for done.")
|
||||
poll(0.1)
|
||||
|
||||
def wait_till_acquiring(self, timeout_s: float = 30.0):
|
||||
timeout = time.time() + timeout_s
|
||||
while not self.check_status_acquiring():
|
||||
if time.time() > timeout:
|
||||
raise TimeoutError("timeout waiting for acquiring.")
|
||||
poll(0.1)
|
||||
|
||||
def set_preset_mode(self, mode: int):
|
||||
""" 0 No preset, 1 Real time, 2 Live time, 3 Events, 4 Triggers"""
|
||||
if mode not in [0, 1, 2, 3, 4]:
|
||||
raise ValueError(f"Invalid preset mode: {mode}")
|
||||
try:
|
||||
self.__preset_mode.put(mode)
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting preset mode to {mode}: {e}")
|
||||
|
||||
def save_file(self, filename:str|None = None, timeout_s: float = 30.0):
|
||||
if not filename:
|
||||
filename = f"KETEK_{time.strftime('%Y%m%d_%H%M%S')}.txt"
|
||||
self.__saveFile_name.put(filename)
|
||||
try:
|
||||
self.__saveFile.put(1)
|
||||
timeout = time.time() + timeout_s
|
||||
while self.__saveFile_rbv.get() == 1:
|
||||
if time.time() > timeout:
|
||||
raise TimeoutError("Timed out waiting for save to complete")
|
||||
poll(0.1)
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving file {filename}: {e}")
|
||||
|
||||
@property
|
||||
def real_time(self):
|
||||
return self.__real_time.get()
|
||||
|
||||
@real_time.setter
|
||||
def real_time(self, value):
|
||||
self.__real_time.put(value)
|
||||
|
||||
@property
|
||||
def live_time(self):
|
||||
return self.__live_time.get()
|
||||
|
||||
@live_time.setter
|
||||
def live_time(self, value):
|
||||
self.__live_time.put(value)
|
||||
|
||||
def elapsed_real_time(self):
|
||||
return self.__elapsed_real_time.get()
|
||||
|
||||
def elapsed_live_time(self):
|
||||
return self.__elapsed_live_time.get()
|
||||
|
||||
def elapsed_trigger_live_time(self):
|
||||
return self.__elapsed_trigger_live_time.get()
|
||||
|
||||
def instant_dead_time(self):
|
||||
return self.__instant_dead_time.get()
|
||||
|
||||
def average_dead_time(self):
|
||||
return self.__average_dead_time.get()
|
||||
|
||||
@property
|
||||
def poll_time(self):
|
||||
return self.__poll_time_rbv.get()
|
||||
|
||||
@poll_time.setter
|
||||
def poll_time(self, value):
|
||||
self.__poll_time.put(value)
|
||||
|
||||
@@ -36,6 +36,7 @@ from aaregui.widgets.camera_image import SampleCameraImageLabel
|
||||
from aaregui.widgets.no_wheel_scroll_area import NoWheelScrollArea
|
||||
from aaregui.widgets.status_bar import StatusBar
|
||||
from aaregui.widgets.video_image import VideoGraphicsView
|
||||
from aaregui.panels.fluoresence_panel import FluorescencePanel
|
||||
|
||||
logger = setup_logger("aareGUI")
|
||||
|
||||
@@ -186,6 +187,13 @@ class MainWindow(QMainWindow):
|
||||
self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.face_panel_dock)
|
||||
self.face_panel_dock.hide()
|
||||
|
||||
self.fluor_panel = FluorescencePanel()
|
||||
self.fluor_panel_dock = QDockWidget("Fluorescence", self)
|
||||
self.fluor_panel_dock.setWidget(self.fluor_panel)
|
||||
self.fluor_panel_dock.setAllowedAreas(Qt.DockWidgetArea.RightDockWidgetArea | Qt.DockWidgetArea.LeftDockWidgetArea)
|
||||
self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.fluor_panel_dock)
|
||||
self.fluor_panel_dock.hide()
|
||||
|
||||
# Create and add the dock to your main window
|
||||
self.log_dock = LogDock("Console Log", self)
|
||||
self.addDockWidget(Qt.BottomDockWidgetArea, self.log_dock)
|
||||
@@ -347,6 +355,14 @@ class MainWindow(QMainWindow):
|
||||
self.status_bar.get_all_pgroups.connect(self.daq.get_all_pgroups)
|
||||
self.daq.staff_pgroups_loaded.connect(self.status_bar.staff_pgroups_loaded)
|
||||
|
||||
self.fluor_panel.start.connect(self.daq.fluorimeter_start)
|
||||
self.fluor_panel.start_erase.connect(self.daq.fluorimeter_start_erase)
|
||||
self.fluor_panel.stop.connect(self.daq.fluorimeter_stop)
|
||||
self.fluor_panel.snapshot.connect(self.daq.fluorimeter_request_snapshot)
|
||||
self.fluor_panel.live.connect(self.daq.fluorimeter_start_erase)
|
||||
self.fluor_panel.live.connect(self.daq.start_fluorimeter_stream)
|
||||
self.daq.fluorimeter_update.connect(self.fluor_panel.update_plot)
|
||||
|
||||
def create_menu_bar(self):
|
||||
"""Create a menu bar with File->Quit and Help->About."""
|
||||
# Main menu bar
|
||||
@@ -392,6 +408,13 @@ class MainWindow(QMainWindow):
|
||||
self.face_panel_dock.visibilityChanged.connect(show_face_panel_action.setChecked)
|
||||
view_menu.addAction(show_face_panel_action)
|
||||
|
||||
show_fluor_panel_action = QAction("Show fluorescence", self)
|
||||
show_fluor_panel_action.setCheckable(True)
|
||||
show_fluor_panel_action.setChecked(False)
|
||||
show_fluor_panel_action.triggered.connect(lambda checked: self.fluor_panel_dock.setVisible(checked))
|
||||
self.fluor_panel_dock.visibilityChanged.connect(show_fluor_panel_action.setChecked)
|
||||
view_menu.addAction(show_fluor_panel_action)
|
||||
|
||||
show_log_action = QAction("Show Log", self)
|
||||
show_log_action.setCheckable(True)
|
||||
show_log_action.setChecked(False)
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
from PySide6.QtCore import Slot, QTimer, Signal
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QPushButton
|
||||
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
||||
from matplotlib.figure import Figure
|
||||
import numpy as np
|
||||
|
||||
from aaregui.widgets.number_line_edit import NumberLineEdit
|
||||
from aaregui.widgets.title_label import TitleLabel
|
||||
from aaredaqlib.logger_config import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
class FluorescencePanel(QWidget):
|
||||
|
||||
start = Signal()
|
||||
start_erase = Signal()
|
||||
stop = Signal()
|
||||
snapshot = Signal()
|
||||
live = Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
grid = QGridLayout(self)
|
||||
grid.addWidget(TitleLabel("Fluorescence", self), 0, 0, 1, 4)
|
||||
|
||||
self.btn_start = QPushButton("Start")
|
||||
self.btn_start_erase = QPushButton("Erase+Start")
|
||||
self.btn_stop = QPushButton("Stop")
|
||||
self.btn_live = QPushButton("Start Live")
|
||||
self.btn_snapshot = QPushButton("Snapshot")
|
||||
for i, b in enumerate((self.btn_start, self.btn_start_erase, self.btn_stop, self.btn_live, self.btn_snapshot), start=1):
|
||||
grid.addWidget(b, 1, i - 1)
|
||||
|
||||
self.fig = Figure(figsize=(5, 3), tight_layout=True)
|
||||
self.ax = self.fig.add_subplot(111)
|
||||
self.canvas = FigureCanvas(self.fig)
|
||||
grid.addWidget(self.canvas, 2, 0, 1, 4)
|
||||
|
||||
# Wire buttons
|
||||
self.btn_start.clicked.connect(self.start.emit)
|
||||
self.btn_start_erase.clicked.connect(self.start_erase.emit)
|
||||
self.btn_stop.clicked.connect(self.stop.emit)
|
||||
self.btn_snapshot.clicked.connect(self.snapshot.emit)
|
||||
self.btn_live.clicked.connect(self.live.emit)
|
||||
|
||||
|
||||
@Slot(list, list, int)
|
||||
def update_plot(self, data, background, status):
|
||||
try:
|
||||
self.ax.clear()
|
||||
|
||||
if data:
|
||||
y = np.array(data, dtype=float)
|
||||
x = np.arange(len(y), dtype=int)
|
||||
self.ax.plot(x, y, lw=1.0, label="Data")
|
||||
|
||||
if background:
|
||||
b = np.array(background, dtype=float)
|
||||
xb = np.arange(len(b), dtype=int)
|
||||
self.ax.plot(xb, b, lw=1.0, label="Background")
|
||||
|
||||
self.ax.set_xlabel("Channel")
|
||||
self.ax.set_ylabel("Counts")
|
||||
if self.ax.has_data():
|
||||
self.ax.legend(loc="best")
|
||||
self.ax.grid(True, alpha=0.3)
|
||||
self.canvas.draw_idle()
|
||||
except Exception as e:
|
||||
logger.error(f"Update plot error: {e}")
|
||||
|
||||
|
||||
# class FluorescencePanel(QWidget):
|
||||
# def __init__(self, daq_worker, parent=None):
|
||||
# super().__init__(parent)
|
||||
# self._w = daq_worker
|
||||
#
|
||||
# lay = QGridLayout(self)
|
||||
# self.btn_start = QPushButton("Start")
|
||||
# self.btn_start_erase = QPushButton("Erase+Start")
|
||||
# self.btn_stop = QPushButton("Stop")
|
||||
# self.btn_live = QPushButton("Start Live")
|
||||
# self.btn_snapshot = QPushButton("Snapshot")
|
||||
# lay.addWidget(self.btn_start, 0, 0)
|
||||
# lay.addWidget(self.btn_start_erase, 0, 1)
|
||||
# lay.addWidget(self.btn_stop, 0, 2)
|
||||
# lay.addWidget(self.btn_live, 0, 3)
|
||||
# lay.addWidget(self.btn_snapshot, 0, 4)
|
||||
#
|
||||
# self.fig = Figure(figsize=(5, 3), tight_layout=True)
|
||||
# self.ax = self.fig.add_subplot(111)
|
||||
# self.canvas = FigureCanvas(self.fig)
|
||||
# lay.addWidget(self.canvas, 1, 0, 1, 5)
|
||||
#
|
||||
# # Polling timer
|
||||
# self._timer = QTimer(self)
|
||||
# self._timer.setInterval(200)
|
||||
# self._timer.timeout.connect(self._request_snapshot)
|
||||
#
|
||||
# # Wire buttons
|
||||
# self.btn_start.clicked.connect(self._w.fluorimeter_start)
|
||||
# self.btn_start_erase.clicked.connect(self._w.fluorimeter_start_erase)
|
||||
# self.btn_stop.clicked.connect(self._on_stop_clicked)
|
||||
# self.btn_snapshot.clicked.connect(self._request_snapshot)
|
||||
# self.btn_live.clicked.connect(self._on_start_live)
|
||||
#
|
||||
# # Subscribe to data updates
|
||||
# # Expected signal: fluorimeter_update(list data, list background, int status)
|
||||
# self._w.fluorimeter_update.connect(self.update_plot)
|
||||
#
|
||||
# @Slot()
|
||||
# def _on_start_live(self):
|
||||
# # Start acquisition if not already running, then begin polling
|
||||
# self._w.fluorimeter_start()
|
||||
# self._timer.start()
|
||||
#
|
||||
# @Slot()
|
||||
# def _on_stop_clicked(self):
|
||||
# self._timer.stop()
|
||||
# self._w.fluorimeter_stop()
|
||||
#
|
||||
# @Slot()
|
||||
# def _request_snapshot(self):
|
||||
# # One-shot HTTP GET to fetch data+status+background (worker emits fluorimeter_update)
|
||||
# self._w.fluorimeter_request_snapshot()
|
||||
#
|
||||
# @Slot(list, list, int)
|
||||
# def update_plot(self, data, background, status):
|
||||
# # Plot and stop timer automatically when acquisition ends
|
||||
# self.ax.clear()
|
||||
# if data:
|
||||
# y = np.array(data, dtype=float)
|
||||
# x = np.arange(len(y))
|
||||
# self.ax.plot(x, y, lw=1.0, label="Data")
|
||||
# if background:
|
||||
# b = np.array(background, dtype=float)
|
||||
# xb = np.arange(len(b))
|
||||
# self.ax.plot(xb, b, lw=1.0, label="Background")
|
||||
# if self.ax.has_data():
|
||||
# self.ax.legend(loc="best")
|
||||
# self.ax.set_xlabel("Channel")
|
||||
# self.ax.set_ylabel("Counts")
|
||||
# self.ax.grid(True, alpha=0.3)
|
||||
# self.canvas.draw_idle()
|
||||
#
|
||||
# # status: 1 acquiring, 0 done, other -> unknown
|
||||
# if status == 0:
|
||||
# self._timer.stop()
|
||||
@@ -1,6 +1,7 @@
|
||||
import copy
|
||||
import random
|
||||
import time
|
||||
import json
|
||||
|
||||
from PySide6.QtCore import Signal, QUrl, Slot, QTimer, QObject, QByteArray
|
||||
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
|
||||
@@ -32,6 +33,7 @@ class DAQWorker(QObject):
|
||||
raster_generated_by_ml = Signal(RasterGridRequest)
|
||||
face_detection_result = Signal(dict)
|
||||
staff_pgroups_loaded = Signal(list)
|
||||
fluorimeter_update = Signal(list, list, int)
|
||||
|
||||
def __init__(self, base_url: str | None, token: str, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -507,4 +509,87 @@ class DAQWorker(QObject):
|
||||
reply = self.__net_manager.post(request, QByteArray(b""))
|
||||
reply.finished.connect(lambda: self._handle_face_detection_response(reply))
|
||||
|
||||
@Slot()
|
||||
def fluorimeter_start(self):
|
||||
self.generic_post("fluorimeter/start")
|
||||
|
||||
@Slot()
|
||||
def fluorimeter_start_erase(self):
|
||||
self.generic_post("fluorimeter/start?erase=true")
|
||||
|
||||
@Slot()
|
||||
def fluorimeter_stop(self):
|
||||
self.generic_post("fluorimeter/stop")
|
||||
|
||||
def _handle_fluorimeter_data(self, reply: QNetworkReply, emit_status: bool = False):
|
||||
try:
|
||||
response_data = self.handle_response(reply)
|
||||
import json
|
||||
data = json.loads(response_data) if response_data else []
|
||||
if emit_status:
|
||||
# fetch status and bkg in parallel (simple sequential here)
|
||||
status_req = QNetworkRequest(QUrl(f"{self.__base_url}/fluorimeter/status"))
|
||||
status_req.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
|
||||
status_reply = self.__net_manager.get(status_req)
|
||||
status_reply.finished.connect(lambda: self._handle_fluorimeter_status_and_emit(data, status_reply))
|
||||
else:
|
||||
self.fluorimeter_update.emit(data, [], -1)
|
||||
except Exception as e:
|
||||
logger.error(f"Fluorimeter data error: {e}")
|
||||
self.http_error.emit(str(e))
|
||||
|
||||
def _handle_fluorimeter_status_and_emit(self, data, status_reply: QNetworkReply):
|
||||
try:
|
||||
s_payload = self.handle_response(status_reply)
|
||||
s = int(s_payload) if s_payload not in ("", "null") else -1
|
||||
b_req = QNetworkRequest(QUrl(f"{self.__base_url}/fluorimeter/background"))
|
||||
b_req.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
|
||||
b_reply = self.__net_manager.get(b_req)
|
||||
b_reply.finished.connect(lambda: self._emit_fluorimeter_with_bkg(data, s, b_reply))
|
||||
except Exception as e:
|
||||
logger.error(f"Fluorimeter status error: {e}")
|
||||
self.http_error.emit(str(e))
|
||||
|
||||
def _emit_fluorimeter_with_bkg(self, data, s, b_reply: QNetworkReply):
|
||||
try:
|
||||
bkg_json = self.handle_response(b_reply)
|
||||
import json
|
||||
bkg = json.loads(bkg_json) if bkg_json else []
|
||||
self.fluorimeter_update.emit(data, bkg, s)
|
||||
except Exception as e:
|
||||
logger.error(f"Fluorimeter background error: {e}")
|
||||
self.http_error.emit(str(e))
|
||||
|
||||
@Slot()
|
||||
def fluorimeter_request_snapshot(self):
|
||||
if self.__base_url is None:
|
||||
return
|
||||
req = QNetworkRequest(QUrl(f"{self.__base_url}/fluorimeter/data"))
|
||||
req.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
|
||||
reply = self.__net_manager.get(req)
|
||||
reply.finished.connect(lambda: self._handle_fluorimeter_data(reply, emit_status=True))
|
||||
|
||||
# Optional: SSE listener for live updates
|
||||
def start_fluorimeter_stream(self):
|
||||
if self.__base_url is None:
|
||||
return
|
||||
request = QNetworkRequest(QUrl(f"{self.__base_url}/sse/fluorimeter"))
|
||||
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
|
||||
reply = self.__net_manager.get(request)
|
||||
reply.readyRead.connect(lambda: self._read_fluorimeter_stream(reply))
|
||||
reply.finished.connect(lambda: reply.deleteLater())
|
||||
|
||||
def _read_fluorimeter_stream(self, reply: QNetworkReply):
|
||||
try:
|
||||
chunk = reply.readAll().data().decode("utf-8")
|
||||
for line in chunk.splitlines():
|
||||
if line.startswith("data:"):
|
||||
payload = line[5:].strip()
|
||||
if payload:
|
||||
obj = json.loads(payload)
|
||||
data = obj.get("data") or []
|
||||
bkg = obj.get("background") or []
|
||||
status = obj.get("status", -1)
|
||||
self.fluorimeter_update.emit(data, bkg, status)
|
||||
except Exception as e:
|
||||
logger.error(f"SSE parse error: {e}")
|
||||
Reference in New Issue
Block a user