diff --git a/pyobs_gui/__init__.py b/pyobs_gui/__init__.py index 80f3ae3..537153b 100644 --- a/pyobs_gui/__init__.py +++ b/pyobs_gui/__init__.py @@ -1 +1,6 @@ +""" +TODO: write doc +""" +__title__ = 'GUI' + from .gui import GUI diff --git a/pyobs_gui/basewidget.py b/pyobs_gui/basewidget.py index d93325d..ee556c4 100644 --- a/pyobs_gui/basewidget.py +++ b/pyobs_gui/basewidget.py @@ -1,5 +1,6 @@ import threading import logging +from typing import List, Dict, Tuple, Any from PyQt5 import QtWidgets, QtGui, QtCore from PyQt5.QtCore import pyqtSignal @@ -23,11 +24,11 @@ def __init__(self, update_func=None, update_interval: float = 1, *args, **kwargs # update thread self._update_func = update_func self._update_interval = update_interval - self._update_thread = None - self._update_thread_event = None + self._update_thread = threading.Thread() + self._update_thread_event = threading.Event() # sidebar - self.sidebar_widgets = [] + self.sidebar_widgets: List[BaseWidget] = [] self.sidebar_layout = None # has it been initialized? @@ -65,18 +66,17 @@ def hideEvent(self, event: QtGui.QHideEvent) -> None: self._update_thread_event.set() # wait for it - if self._update_thread is not None: + if self._update_thread.is_alive(): self._update_thread.join() - self._update_thread = None - self._update_thread_event = None def _update_loop_thread(self): while not self._update_thread_event.is_set(): try: # call update function self._update_func() - except: - pass + except Exception as e: + log.warning("Exception during GUIs update function: %s", + str(e)) # sleep a little self._update_thread_event.wait(self._update_interval) @@ -107,7 +107,7 @@ def show_error(self, message): def enable_buttons(self, widgets, enable): [w.setEnabled(enable) for w in widgets] - def get_fits_headers(self, namespaces: list = None, *args, **kwargs) -> dict: + def get_fits_headers(self, namespaces: List[str] = None, *args, **kwargs) -> Dict[str, Tuple[Any, str]]: """Returns FITS header for the current status of this module. Args: diff --git a/pyobs_gui/gui.py b/pyobs_gui/gui.py index bcf782c..c8eb4b0 100644 --- a/pyobs_gui/gui.py +++ b/pyobs_gui/gui.py @@ -1,3 +1,5 @@ +from typing import List, Dict, Tuple, Any + from PyQt5 import QtWidgets from pyobs.interfaces import IFitsHeaderProvider @@ -6,6 +8,8 @@ class GUI(Module, IFitsHeaderProvider): + __module__ = 'pyobs_gui' + def __init__(self, *args, **kwargs): Module.__init__(self, *args, **kwargs) self._window = None @@ -21,7 +25,7 @@ def main(self): # run app.exec() - def get_fits_headers(self, namespaces: list = None, *args, **kwargs) -> dict: + def get_fits_headers(self, namespaces: List[str] = None, *args, **kwargs) -> Dict[str, Tuple[Any, str]]: """Returns FITS header for the current status of this module. Args: diff --git a/pyobs_gui/mainwindow.py b/pyobs_gui/mainwindow.py index dabe062..e21664b 100644 --- a/pyobs_gui/mainwindow.py +++ b/pyobs_gui/mainwindow.py @@ -20,6 +20,29 @@ from pyobs_gui.widgetweather import WidgetWeather +class PagesListWidgetItem(QtWidgets.QListWidgetItem): + """ListWidgetItem for the pages list. Always sorts Shell and Events first""" + def __lt__(self, other): + """Compare two items.""" + + # special cases? + if self.text() == 'Shell': + # if self is 'Shell', it always goes first + return True + elif other.text() == 'Shell': + # if other is 'Shell', it always goes later + return False + elif self.text() == 'Events': + # if self is 'Events', it only goes first if other is not 'Shell' + return other.text() != 'Shell' + elif other.text() == 'Events': + # if other is 'Events', self always goes later, since case of 'Shell' as self has always been dealt with + return False + else: + # default case + return QtWidgets.QListWidgetItem.__lt__(self, other) + + class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): add_log = pyqtSignal(list) add_command_log = pyqtSignal(str) @@ -109,12 +132,13 @@ def _add_client(self, client: str, icon: QtGui.QIcon, widget: QtWidgets.QWidget) """ # add list item - item = QtWidgets.QListWidgetItem() + item = PagesListWidgetItem() item.setIcon(icon) item.setText(client) - # add to list + # add to list and sort self.listPages.addItem(item) + self.listPages.sortItems() # add widget self.stackedWidget.addWidget(widget) diff --git a/pyobs_gui/qt/mainwindow.py b/pyobs_gui/qt/mainwindow.py index da307c7..d3b0872 100644 --- a/pyobs_gui/qt/mainwindow.py +++ b/pyobs_gui/qt/mainwindow.py @@ -74,9 +74,16 @@ def setupUi(self, MainWindow): sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.listPages.sizePolicy().hasHeightForWidth()) self.listPages.setSizePolicy(sizePolicy) + self.listPages.setMinimumSize(QtCore.QSize(100, 0)) self.listPages.setMaximumSize(QtCore.QSize(100, 16777215)) self.listPages.setFrameShape(QtWidgets.QFrame.NoFrame) + self.listPages.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.listPages.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + self.listPages.setProperty("showDropIndicator", False) + self.listPages.setDragDropMode(QtWidgets.QAbstractItemView.NoDragDrop) + self.listPages.setDefaultDropAction(QtCore.Qt.IgnoreAction) self.listPages.setIconSize(QtCore.QSize(64, 64)) + self.listPages.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) self.listPages.setViewMode(QtWidgets.QListView.IconMode) self.listPages.setObjectName("listPages") self.horizontalLayout.addWidget(self.listPages) diff --git a/pyobs_gui/qt/mainwindow.ui b/pyobs_gui/qt/mainwindow.ui index c72c5ee..cc4666f 100644 --- a/pyobs_gui/qt/mainwindow.ui +++ b/pyobs_gui/qt/mainwindow.ui @@ -153,6 +153,12 @@ 0 + + + 100 + 0 + + 100 @@ -162,12 +168,30 @@ QFrame::NoFrame + + Qt::ScrollBarAlwaysOff + + + QAbstractItemView::NoEditTriggers + + + false + + + QAbstractItemView::NoDragDrop + + + Qt::IgnoreAction + 64 64 + + QAbstractItemView::ScrollPerPixel + QListView::IconMode diff --git a/pyobs_gui/qt/widgetcamera.py b/pyobs_gui/qt/widgetcamera.py index 472ab74..4d05a7b 100644 --- a/pyobs_gui/qt/widgetcamera.py +++ b/pyobs_gui/qt/widgetcamera.py @@ -13,7 +13,7 @@ class Ui_WidgetCamera(object): def setupUi(self, WidgetCamera): WidgetCamera.setObjectName("WidgetCamera") - WidgetCamera.resize(967, 657) + WidgetCamera.resize(1269, 810) self.horizontalLayout_2 = QtWidgets.QHBoxLayout(WidgetCamera) self.horizontalLayout_2.setObjectName("horizontalLayout_2") self.scrollArea = QtWidgets.QScrollArea(WidgetCamera) @@ -26,7 +26,7 @@ def setupUi(self, WidgetCamera): self.scrollArea.setWidgetResizable(True) self.scrollArea.setObjectName("scrollArea") self.scrollAreaWidgetContents = QtWidgets.QWidget() - self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 183, 641)) + self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 264, 794)) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -80,45 +80,30 @@ def setupUi(self, WidgetCamera): self.gridLayout_3 = QtWidgets.QGridLayout(self.groupBinning) self.gridLayout_3.setSpacing(0) self.gridLayout_3.setObjectName("gridLayout_3") - self.spinBinningY = QtWidgets.QSpinBox(self.groupBinning) - self.spinBinningY.setMinimum(1) - self.spinBinningY.setMaximum(3) - self.spinBinningY.setObjectName("spinBinningY") - self.gridLayout_3.addWidget(self.spinBinningY, 1, 1, 1, 1) - self.spinBinningX = QtWidgets.QSpinBox(self.groupBinning) - self.spinBinningX.setMinimum(1) - self.spinBinningX.setMaximum(3) - self.spinBinningX.setObjectName("spinBinningX") - self.gridLayout_3.addWidget(self.spinBinningX, 0, 1, 1, 1) - self.label_6 = QtWidgets.QLabel(self.groupBinning) - self.label_6.setObjectName("label_6") - self.gridLayout_3.addWidget(self.label_6, 1, 0, 1, 1) self.label_5 = QtWidgets.QLabel(self.groupBinning) self.label_5.setObjectName("label_5") self.gridLayout_3.addWidget(self.label_5, 0, 0, 1, 1) + self.comboBinning = QtWidgets.QComboBox(self.groupBinning) + self.comboBinning.setObjectName("comboBinning") + self.gridLayout_3.addWidget(self.comboBinning, 0, 1, 1, 1) self.verticalLayout.addWidget(self.groupBinning) + self.groupImageFormat = QtWidgets.QGroupBox(self.scrollAreaWidgetContents) + self.groupImageFormat.setObjectName("groupImageFormat") + self.gridLayout_6 = QtWidgets.QGridLayout(self.groupImageFormat) + self.gridLayout_6.setSpacing(0) + self.gridLayout_6.setObjectName("gridLayout_6") + self.label_10 = QtWidgets.QLabel(self.groupImageFormat) + self.label_10.setObjectName("label_10") + self.gridLayout_6.addWidget(self.label_10, 0, 0, 1, 1) + self.comboImageFormat = QtWidgets.QComboBox(self.groupImageFormat) + self.comboImageFormat.setObjectName("comboImageFormat") + self.gridLayout_6.addWidget(self.comboImageFormat, 0, 1, 1, 1) + self.verticalLayout.addWidget(self.groupImageFormat) self.groupExposure = QtWidgets.QGroupBox(self.scrollAreaWidgetContents) self.groupExposure.setObjectName("groupExposure") self.gridLayout_4 = QtWidgets.QGridLayout(self.groupExposure) self.gridLayout_4.setSpacing(0) self.gridLayout_4.setObjectName("gridLayout_4") - self.label_7 = QtWidgets.QLabel(self.groupExposure) - self.label_7.setObjectName("label_7") - self.gridLayout_4.addWidget(self.label_7, 1, 0, 1, 1) - self.comboImageType = QtWidgets.QComboBox(self.groupExposure) - self.comboImageType.setObjectName("comboImageType") - self.gridLayout_4.addWidget(self.comboImageType, 0, 1, 1, 1) - self.label_8 = QtWidgets.QLabel(self.groupExposure) - self.label_8.setObjectName("label_8") - self.gridLayout_4.addWidget(self.label_8, 2, 0, 1, 1) - self.spinCount = QtWidgets.QSpinBox(self.groupExposure) - self.spinCount.setMinimum(1) - self.spinCount.setMaximum(9999) - self.spinCount.setObjectName("spinCount") - self.gridLayout_4.addWidget(self.spinCount, 2, 1, 1, 1) - self.label_9 = QtWidgets.QLabel(self.groupExposure) - self.label_9.setObjectName("label_9") - self.gridLayout_4.addWidget(self.label_9, 0, 0, 1, 1) self.butExpose = QtWidgets.QPushButton(self.groupExposure) palette = QtGui.QPalette() brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) @@ -267,7 +252,21 @@ def setupUi(self, WidgetCamera): palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.PlaceholderText, brush) self.butExpose.setPalette(palette) self.butExpose.setObjectName("butExpose") - self.gridLayout_4.addWidget(self.butExpose, 3, 0, 1, 2) + self.gridLayout_4.addWidget(self.butExpose, 4, 0, 1, 2) + self.spinCount = QtWidgets.QSpinBox(self.groupExposure) + self.spinCount.setMinimum(1) + self.spinCount.setMaximum(9999) + self.spinCount.setObjectName("spinCount") + self.gridLayout_4.addWidget(self.spinCount, 2, 1, 1, 1) + self.comboImageType = QtWidgets.QComboBox(self.groupExposure) + self.comboImageType.setObjectName("comboImageType") + self.gridLayout_4.addWidget(self.comboImageType, 0, 1, 1, 1) + self.labelImageType = QtWidgets.QLabel(self.groupExposure) + self.labelImageType.setObjectName("labelImageType") + self.gridLayout_4.addWidget(self.labelImageType, 0, 0, 1, 1) + self.labelExpTime = QtWidgets.QLabel(self.groupExposure) + self.labelExpTime.setObjectName("labelExpTime") + self.gridLayout_4.addWidget(self.labelExpTime, 1, 0, 1, 1) self.butAbort = QtWidgets.QPushButton(self.groupExposure) palette = QtGui.QPalette() brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) @@ -416,12 +415,32 @@ def setupUi(self, WidgetCamera): palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.PlaceholderText, brush) self.butAbort.setPalette(palette) self.butAbort.setObjectName("butAbort") - self.gridLayout_4.addWidget(self.butAbort, 4, 0, 1, 2) + self.gridLayout_4.addWidget(self.butAbort, 5, 0, 1, 2) + self.label_8 = QtWidgets.QLabel(self.groupExposure) + self.label_8.setObjectName("label_8") + self.gridLayout_4.addWidget(self.label_8, 2, 0, 1, 1) + self.checkBroadcast = QtWidgets.QCheckBox(self.groupExposure) + self.checkBroadcast.setChecked(True) + self.checkBroadcast.setObjectName("checkBroadcast") + self.gridLayout_4.addWidget(self.checkBroadcast, 3, 1, 1, 1) + self.horizontalLayout_3 = QtWidgets.QHBoxLayout() + self.horizontalLayout_3.setObjectName("horizontalLayout_3") self.spinExpTime = QtWidgets.QDoubleSpinBox(self.groupExposure) - self.spinExpTime.setMaximum(99999.0) + self.spinExpTime.setSuffix("") + self.spinExpTime.setDecimals(3) + self.spinExpTime.setMaximum(999.0) self.spinExpTime.setProperty("value", 1.0) self.spinExpTime.setObjectName("spinExpTime") - self.gridLayout_4.addWidget(self.spinExpTime, 1, 1, 1, 1) + self.horizontalLayout_3.addWidget(self.spinExpTime) + self.comboExpTimeUnit = QtWidgets.QComboBox(self.groupExposure) + self.comboExpTimeUnit.setMinimumContentsLength(2) + self.comboExpTimeUnit.setObjectName("comboExpTimeUnit") + self.comboExpTimeUnit.addItem("") + self.comboExpTimeUnit.addItem("") + self.comboExpTimeUnit.addItem("") + self.horizontalLayout_3.addWidget(self.comboExpTimeUnit) + self.horizontalLayout_3.setStretch(0, 1) + self.gridLayout_4.addLayout(self.horizontalLayout_3, 1, 1, 1, 1) self.verticalLayout.addWidget(self.groupExposure) spacerItem = QtWidgets.QSpacerItem(20, 26, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) self.verticalLayout.addItem(spacerItem) @@ -508,20 +527,20 @@ def setupUi(self, WidgetCamera): WidgetCamera.setTabOrder(self.spinWindowTop, self.spinWindowWidth) WidgetCamera.setTabOrder(self.spinWindowWidth, self.spinWindowHeight) WidgetCamera.setTabOrder(self.spinWindowHeight, self.butFullFrame) - WidgetCamera.setTabOrder(self.butFullFrame, self.spinBinningX) - WidgetCamera.setTabOrder(self.spinBinningX, self.spinBinningY) - WidgetCamera.setTabOrder(self.spinBinningY, self.comboImageType) - WidgetCamera.setTabOrder(self.comboImageType, self.spinExpTime) - WidgetCamera.setTabOrder(self.spinExpTime, self.spinCount) - WidgetCamera.setTabOrder(self.spinCount, self.butExpose) + WidgetCamera.setTabOrder(self.butFullFrame, self.comboBinning) + WidgetCamera.setTabOrder(self.comboBinning, self.comboImageFormat) + WidgetCamera.setTabOrder(self.comboImageFormat, self.comboImageType) + WidgetCamera.setTabOrder(self.comboImageType, self.spinCount) + WidgetCamera.setTabOrder(self.spinCount, self.checkBroadcast) + WidgetCamera.setTabOrder(self.checkBroadcast, self.butExpose) WidgetCamera.setTabOrder(self.butExpose, self.butAbort) - WidgetCamera.setTabOrder(self.butAbort, self.tabWidget) - WidgetCamera.setTabOrder(self.tabWidget, self.checkAutoUpdate) + WidgetCamera.setTabOrder(self.butAbort, self.checkAutoUpdate) WidgetCamera.setTabOrder(self.checkAutoUpdate, self.checkAutoSave) WidgetCamera.setTabOrder(self.checkAutoSave, self.textAutoSavePath) WidgetCamera.setTabOrder(self.textAutoSavePath, self.butAutoSave) WidgetCamera.setTabOrder(self.butAutoSave, self.butSaveTo) - WidgetCamera.setTabOrder(self.butSaveTo, self.tableFitsHeader) + WidgetCamera.setTabOrder(self.butSaveTo, self.tabWidget) + WidgetCamera.setTabOrder(self.tabWidget, self.tableFitsHeader) def retranslateUi(self, WidgetCamera): _translate = QtCore.QCoreApplication.translate @@ -532,16 +551,20 @@ def retranslateUi(self, WidgetCamera): self.label.setText(_translate("WidgetCamera", "Left:")) self.label_4.setText(_translate("WidgetCamera", "Height:")) self.butFullFrame.setText(_translate("WidgetCamera", "Full Frame")) - self.groupBinning.setTitle(_translate("WidgetCamera", "Binning:")) - self.label_6.setText(_translate("WidgetCamera", "Y:")) - self.label_5.setText(_translate("WidgetCamera", "X:")) + self.groupBinning.setTitle(_translate("WidgetCamera", "Binning")) + self.label_5.setText(_translate("WidgetCamera", "XxY:")) + self.groupImageFormat.setTitle(_translate("WidgetCamera", "Image format")) + self.label_10.setText(_translate("WidgetCamera", "Format:")) self.groupExposure.setTitle(_translate("WidgetCamera", "Exposure")) - self.label_7.setText(_translate("WidgetCamera", "ExpTime:")) - self.label_8.setText(_translate("WidgetCamera", "Count:")) - self.label_9.setText(_translate("WidgetCamera", "Type:")) self.butExpose.setText(_translate("WidgetCamera", "Expose")) + self.labelImageType.setText(_translate("WidgetCamera", "Type:")) + self.labelExpTime.setText(_translate("WidgetCamera", "ExpTime:")) self.butAbort.setText(_translate("WidgetCamera", "Abort")) - self.spinExpTime.setSuffix(_translate("WidgetCamera", "s")) + self.label_8.setText(_translate("WidgetCamera", "Count:")) + self.checkBroadcast.setText(_translate("WidgetCamera", "Broadcast")) + self.comboExpTimeUnit.setItemText(0, _translate("WidgetCamera", "s")) + self.comboExpTimeUnit.setItemText(1, _translate("WidgetCamera", "ms")) + self.comboExpTimeUnit.setItemText(2, _translate("WidgetCamera", "µs")) self.labelStatus.setText(_translate("WidgetCamera", "IDLE")) self.labelExposuresLeft.setText(_translate("WidgetCamera", "IDLE")) self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabImage), _translate("WidgetCamera", "Image")) diff --git a/pyobs_gui/qt/widgetcamera.ui b/pyobs_gui/qt/widgetcamera.ui index 1ddfb46..b85e56a 100644 --- a/pyobs_gui/qt/widgetcamera.ui +++ b/pyobs_gui/qt/widgetcamera.ui @@ -6,8 +6,8 @@ 0 0 - 967 - 657 + 1269 + 810 @@ -33,8 +33,8 @@ 0 0 - 183 - 641 + 264 + 794 @@ -131,46 +131,44 @@ - Binning: + Binning 0 - - - - 1 - - - 3 + + + + XxY: - - - 1 - - - 3 - - - - - - - Y: - - + + + + + + + + Image format + + + + 0 + - + - X: + Format: + + + @@ -183,41 +181,7 @@ 0 - - - - ExpTime: - - - - - - - - - - Count: - - - - - - - 1 - - - 9999 - - - - - - - Type: - - - - + @@ -666,7 +630,34 @@ - + + + + 1 + + + 9999 + + + + + + + + + + Type: + + + + + + + ExpTime: + + + + @@ -1115,19 +1106,65 @@ - - - - s + + + + Count: - - 99999.000000000000000 + + + + + + Broadcast - - 1.000000000000000 + + true + + + + + + + + + 3 + + + 999.000000000000000 + + + 1.000000000000000 + + + + + + + 2 + + + + s + + + + + ms + + + + + µs + + + + + + @@ -1325,19 +1362,19 @@ spinWindowWidth spinWindowHeight butFullFrame - spinBinningX - spinBinningY + comboBinning + comboImageFormat comboImageType - spinExpTime spinCount + checkBroadcast butExpose butAbort - tabWidget checkAutoUpdate checkAutoSave textAutoSavePath butAutoSave butSaveTo + tabWidget tableFitsHeader diff --git a/pyobs_gui/widgetcamera.py b/pyobs_gui/widgetcamera.py index fb69fb0..f769f88 100644 --- a/pyobs_gui/widgetcamera.py +++ b/pyobs_gui/widgetcamera.py @@ -1,14 +1,15 @@ import logging import os import threading -from PyQt5 import QtWidgets, QtGui -from PyQt5.QtCore import pyqtSignal +from PyQt5 import QtWidgets, QtCore +from PyQt5.QtCore import pyqtSignal, pyqtSlot from PyQt5.QtWidgets import QMessageBox from pyobs.events import ExposureStatusChangedEvent, NewImageEvent from pyobs.interfaces import ICamera, ICameraBinning, ICameraWindow, ICooling, IFilters, ITemperatures, \ - ICameraExposureTime, IImageType -from pyobs.utils.enums import ImageType + ICameraExposureTime, IImageType, IImageFormat +from pyobs.utils.enums import ImageType, ImageFormat, ExposureStatus +from pyobs.images import Image from pyobs.vfs import VirtualFileSystem from pyobs_gui.basewidget import BaseWidget from pyobs_gui.widgetcooling import WidgetCooling @@ -22,8 +23,51 @@ log = logging.getLogger(__name__) +class DownloadThread(QtCore.QThread): + """Worker thread for downloading images.""" + + """Signal emitted when the image is downloaded.""" + imageReady = pyqtSignal(Image, str) + + def __init__(self, vfs: VirtualFileSystem, filename: str, autosave: str = None, *args, **kwargs): + """Init a new worker thread. + + Args: + vfs: VFS to use for download + filename: File to download + autosave: Path for autosave or None. + """ + QtCore.QThread.__init__(self, *args, **kwargs) + self.vfs = vfs + self.filename = filename + self.autosave = autosave + + def run(self): + """Run method in thread.""" + + # download image + image = self.vfs.read_image(self.filename) + + # auto save? + if self.autosave is not None: + # get path and check + path = self.autosave + if not os.path.exists(path): + log.warning('Invalid path for auto-saving.') + + else: + # save image + filename = os.path.join(path, os.path.basename(self.filename.replace('.fits.gz', '.fits'))) + log.info('Saving image as %s...', filename) + image.writeto(filename, overwrite=True) + + # update GUI + self.imageReady.emit(image, self.filename) + + class WidgetCamera(BaseWidget, Ui_WidgetCamera): signal_update_gui = pyqtSignal() + signal_new_image = pyqtSignal(NewImageEvent, str) def __init__(self, module, comm, vfs, parent=None): BaseWidget.__init__(self, parent=parent, update_func=self._update) @@ -37,10 +81,11 @@ def __init__(self, module, comm, vfs, parent=None): self.image_filename = None self.image = None self.status = None - self.exposure_status = ICamera.ExposureStatus.IDLE + self.exposure_status = ExposureStatus.IDLE self.exposures_left = 0 self.exposure_time_left = 0 self.exposure_progress = 0 + self.download_threads = [] # set exposure types image_types = ['OBJECT', 'BIAS', 'DARK'] @@ -52,6 +97,14 @@ def __init__(self, module, comm, vfs, parent=None): # hide groups, if necessary self.groupWindowing.setVisible(isinstance(self.module, ICameraWindow)) self.groupBinning.setVisible(isinstance(self.module, ICameraBinning)) + self.groupImageFormat.setVisible(isinstance(self.module, IImageFormat)) + + # and single controls + self.labelImageType.setVisible(isinstance(self.module, IImageType)) + self.comboImageType.setVisible(isinstance(self.module, IImageType)) + self.labelExpTime.setVisible(isinstance(self.module, ICameraExposureTime)) + self.spinExpTime.setVisible(isinstance(self.module, ICameraExposureTime)) + self.comboExpTimeUnit.setVisible(isinstance(self.module, ICameraExposureTime)) # add image panel self.imageLayout = QtWidgets.QVBoxLayout(self.tabImage) @@ -63,13 +116,8 @@ def __init__(self, module, comm, vfs, parent=None): self.tableFitsHeader.setHorizontalHeaderLabels(['Key', 'Value', 'Comment']) # connect signals - self.butFullFrame.clicked.connect(self.set_full_frame) - self.comboImageType.currentTextChanged.connect(self.image_type_changed) - self.butExpose.clicked.connect(self.expose) - self.butAbort.clicked.connect(self.abort) self.signal_update_gui.connect(self.update_gui) - self.butAutoSave.clicked.connect(self.select_autosave_path) - self.butSaveTo.clicked.connect(self.save_image) + self.signal_new_image.connect(self._on_new_image) self.checkAutoSave.stateChanged.connect(lambda x: self.textAutoSavePath.setEnabled(x)) # initial values @@ -89,22 +137,79 @@ def __init__(self, module, comm, vfs, parent=None): self.add_to_sidebar(WidgetTemperatures(module, comm)) def _init(self): - # get status and update gui - self.exposure_status = ICamera.ExposureStatus(self.module.get_exposure_status().wait()) + # get status + self.exposure_status = ExposureStatus(self.module.get_exposure_status().wait()) + + # get binnings + if isinstance(self.module, ICameraBinning): + # get binnings + binnings = ['%dx%d' % tuple(binning) for binning in self.module.list_binnings().wait()] + + # set it + self.comboBinning.clear() + self.comboBinning.addItems(binnings) + + # set default value + self.comboBinning.setCurrentIndex(0) + + # get image formats + if isinstance(self.module, IImageFormat): + # get formats + image_formats = [ImageFormat(f) for f in self.module.list_image_formats().wait()] + + # set it + self.comboImageFormat.clear() + self.comboImageFormat.addItems([f.name for f in image_formats]) + + # find default value + if ImageFormat.INT16 in image_formats: + self.comboImageFormat.setCurrentText('INT16') + elif ImageFormat.INT8 in image_formats: + self.comboImageFormat.setCurrentText('INT8') + else: + self.comboImageFormat.setCurrentIndex(0) + + # set full frame self.set_full_frame() + + # update GUI self.signal_update_gui.emit() + @pyqtSlot(name='on_butFullFrame_clicked') def set_full_frame(self): if isinstance(self.module, ICameraWindow): # get full frame left, top, width, height = self.module.get_full_frame().wait() + # get binning + binning = int(self.comboBinning.currentText()[0]) if isinstance(self.module, ICameraBinning) else 1 + + # max values + self.spinWindowLeft.setMaximum(width / binning) + self.spinWindowTop.setMaximum(height / binning) + self.spinWindowWidth.setMaximum(width / binning) + self.spinWindowHeight.setMaximum(height / binning) + # set it self.spinWindowLeft.setValue(left) self.spinWindowTop.setValue(top) - self.spinWindowWidth.setValue(width / self.spinBinningX.value()) - self.spinWindowHeight.setValue(height / self.spinBinningY.value()) + self.spinWindowWidth.setValue(width / binning) + self.spinWindowHeight.setValue(height / binning) + + @pyqtSlot(str, name='on_comboBinning_currentTextChanged') + def binning_changed(self, binning): + self.set_full_frame() + + @pyqtSlot(int, name='on_checkBroadcast_stateChanged') + def broadcast_changed(self, state): + if state == 0: + r = QMessageBox.question(self, 'pyobs', 'When disabling the broadcast, new images will not processed (and ' + 'saved) within the pyobs network. Continue?', + QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if r == QMessageBox.No: + self.checkBroadcast.setChecked(True) + @pyqtSlot(str, name='on_comboImageType_currentTextChanged') def image_type_changed(self, image_type): if image_type == 'BIAS': self.spinExpTime.setValue(0) @@ -112,28 +217,35 @@ def image_type_changed(self, image_type): else: self.spinExpTime.setEnabled(True) + @pyqtSlot(name='on_butExpose_clicked') def expose(self): # set binning if isinstance(self.module, ICameraBinning): - binx, biny = self.spinBinningX.value(), self.spinBinningY.value() + binning = int(self.comboBinning.currentText()[0]) try: - self.module.set_binning(binx, biny).wait() + self.module.set_binning(binning, binning).wait() except: + log.exception('bla') QMessageBox.information(self, 'Error', 'Could not set binning.') return else: - binx, biny = 1, 1 + binning = 1 # set window if isinstance(self.module, ICameraWindow): left, top = self.spinWindowLeft.value(), self.spinWindowTop.value() width, height = self.spinWindowWidth.value(), self.spinWindowHeight.value() try: - self.module.set_window(left, top, width * binx, height * biny).wait() + self.module.set_window(left, top, width * binning, height * binning).wait() except: QMessageBox.information(self, 'Error', 'Could not set window.') return + # set image format + if isinstance(self.module, IImageFormat): + image_format = ImageFormat[self.comboImageFormat.currentText()] + self.module.set_image_format(image_format) + # set initial image count self.exposures_left = self.spinCount.value() @@ -146,23 +258,44 @@ def _expose_thread_func(self): # do exposure(s) while self.exposures_left > 0: - # set exposure time and expose + # set exposure time if isinstance(self.module, ICameraExposureTime): - self.module.set_exposure_time(self.spinExpTime.value()).wait() + # get exp_time + exp_time = self.spinExpTime.value() + + # unit + if self.comboExpTimeUnit.currentText() == 'ms': + exp_time /= 1e3 + elif self.comboExpTimeUnit.currentText() == 'µs': + exp_time /= 1e6 + + # set it + self.module.set_exposure_time(exp_time).wait() + + # set image type if isinstance(self.module, IImageType): self.module.set_image_type(image_type) - self.module.expose().wait() + + # expose + broadcast = self.checkBroadcast.isChecked() + filename = self.module.expose(broadcast=broadcast).wait() # decrement number of exposures left self.exposures_left -= 1 + # if we're not broadcasting the filename, we need to signal it manually + if not broadcast: + ev = NewImageEvent(filename, image_type) + self.signal_new_image.emit(ev, self.module.name) + # signal GUI update self.signal_update_gui.emit() def plot(self): """Show image.""" self.imageView.display(self.image) - + + @pyqtSlot(name='on_butAbort_clicked') def abort(self): """Abort exposure.""" @@ -179,7 +312,7 @@ def abort(self): def _update(self): # are we exposing? - if self.exposure_status == ICamera.ExposureStatus.EXPOSING: + if self.exposure_status == ExposureStatus.EXPOSING: # get camera status exposure_time_left = self.module.get_exposure_time_left() exposure_progress = self.module.get_exposure_progress() @@ -203,8 +336,8 @@ def update_gui(self): self.setEnabled(True) # enable/disable buttons - self.butExpose.setEnabled(self.exposure_status == ICamera.ExposureStatus.IDLE) - self.butAbort.setEnabled(self.exposure_status != ICamera.ExposureStatus.IDLE) + self.butExpose.setEnabled(self.exposure_status == ExposureStatus.IDLE) + self.butAbort.setEnabled(self.exposure_status != ExposureStatus.IDLE) # set abort text if self.exposures_left > 1: @@ -214,13 +347,13 @@ def update_gui(self): # set progress msg = '' - if self.exposure_status == ICamera.ExposureStatus.IDLE: + if self.exposure_status == ExposureStatus.IDLE: self.progressExposure.setValue(0) msg = 'IDLE' - elif self.exposure_status == ICamera.ExposureStatus.EXPOSING: + elif self.exposure_status == ExposureStatus.EXPOSING: self.progressExposure.setValue(self.exposure_progress) msg = 'EXPOSING %.1fs' % self.exposure_time_left - elif self.exposure_status == ICamera.ExposureStatus.READOUT: + elif self.exposure_status == ExposureStatus.READOUT: self.progressExposure.setValue(100) msg = 'READOUT' @@ -300,27 +433,34 @@ def _on_new_image(self, event: NewImageEvent, sender: str): if not self.checkAutoUpdate.isChecked(): return - # download image - self.image = self.vfs.read_image(event.filename) - self.image_filename = event.filename - self.new_image = True + # autosave? + autosave = self.textAutoSavePath.text() if self.checkAutoSave.isChecked() else None - # auto save? - if self.checkAutoSave.isChecked(): - # get path and check - path = self.textAutoSavePath.text() - if not os.path.exists(path): - log.warning('Invalid path for auto-saving.') + # create thread for download + thread = DownloadThread(self.vfs, event.filename, autosave) + thread.imageReady.connect(self._image_downloaded) + thread.start() - else: - # save image - filename = os.path.join(path, os.path.basename(self.image_filename.replace('.fits.gz', '.fits'))) - log.info('Saving image as %s...', filename) - self.image.writeto(filename, overwrite=True) + self.download_threads.append(thread) - # update GUI - self.signal_update_gui.emit() + def _image_downloaded(self, image, filename): + """Called, when image is downloaded. + """ + + # store image and filename + self.image = image + self.image_filename = filename + self.new_image = True + + # find finished threads and delete them + finished_threads = [t for t in self.download_threads if not t.isRunning()] + for t in finished_threads: + self.download_threads.remove(t) + + # show image + self.update_gui() + @pyqtSlot(name='on_butAutoSave_clicked') def select_autosave_path(self): """Select path for auto-saving.""" @@ -333,6 +473,7 @@ def select_autosave_path(self): else: self.textAutoSavePath.clear() + @pyqtSlot(name='on_butSaveTo_clicked') def save_image(self): """Save image.""" diff --git a/pyobs_gui/widgetfilter.py b/pyobs_gui/widgetfilter.py index dd99768..68a95d4 100644 --- a/pyobs_gui/widgetfilter.py +++ b/pyobs_gui/widgetfilter.py @@ -1,10 +1,10 @@ from PyQt5 import QtWidgets, QtCore from PyQt5.QtCore import pyqtSignal, pyqtSlot -import threading from pyobs.comm import Comm from pyobs.events import FilterChangedEvent, MotionStatusChangedEvent -from pyobs.interfaces import IFilters, IMotion +from pyobs.interfaces import IFilters +from pyobs.utils.enums import MotionStatus from pyobs_gui.basewidget import BaseWidget from .qt.widgetfilter import Ui_WidgetFilter @@ -20,7 +20,7 @@ def __init__(self, module: IFilters, comm: Comm, parent=None): # variables self._filter = None - self._motion_status = IMotion.Status.UNKNOWN + self._motion_status = MotionStatus.UNKNOWN # connect signals self.signal_update_gui.connect(self.update_gui) @@ -45,8 +45,8 @@ def update_gui(self): self.setEnabled(True) self.textStatus.setText(self._motion_status.name) self.textFilter.setText('' if self._filter is None else self._filter) - initialized = self._motion_status in [IMotion.Status.SLEWING, IMotion.Status.TRACKING, - IMotion.Status.IDLE, IMotion.Status.POSITIONED] + initialized = self._motion_status in [MotionStatus.SLEWING, MotionStatus.TRACKING, + MotionStatus.IDLE, MotionStatus.POSITIONED] self.buttonSetFilter.setEnabled(initialized) def _on_filter_changed(self, event: FilterChangedEvent, sender: str): diff --git a/pyobs_gui/widgetfocus.py b/pyobs_gui/widgetfocus.py index b08dc1a..bc50fac 100644 --- a/pyobs_gui/widgetfocus.py +++ b/pyobs_gui/widgetfocus.py @@ -5,6 +5,7 @@ from pyobs.events import MotionStatusChangedEvent from pyobs.interfaces import IFocuser, IMotion +from pyobs.utils.enums import MotionStatus from pyobs_gui.basewidget import BaseWidget from .qt.widgetfocus import Ui_WidgetFocus @@ -24,7 +25,7 @@ def __init__(self, module, comm, parent=None): # variables self._focus = None self._focus_offset = None - self._motion_status = IMotion.Status.UNKNOWN + self._motion_status = MotionStatus.UNKNOWN # connect signals self.signal_update_gui.connect(self.update_gui) @@ -60,10 +61,17 @@ def _set_focus(self, offset: bool = False): self.run_async(setter, new_value) def _init(self): - # get current filter - self._focus = self.module.get_focus().wait() - self._focus_offset = self.module.get_focus_offset().wait() - self._motion_status = self.module.get_motion_status().wait() + # get status + try: + self._focus = self.module.get_focus().wait() + self._focus_offset = self.module.get_focus_offset().wait() + self._motion_status = self.module.get_motion_status().wait() + except: + self._focus = None + self._focus_offset = None + self._motion_status = MotionStatus.UNKNOWN + + # update GUI self.signal_update_gui.emit() def update_gui(self): @@ -74,8 +82,8 @@ def update_gui(self): self.labelCurFocusOffset.setText('' if self._focus_offset is None else '%.3f' % self._focus_offset) self.labelCurFocus.setText('' if self._focus is None or self._focus_offset is None else '%.3f' % (self._focus + self._focus_offset,)) - initialized = self._motion_status in [IMotion.Status.SLEWING, IMotion.Status.TRACKING, - IMotion.Status.IDLE, IMotion.Status.POSITIONED] + initialized = self._motion_status in [MotionStatus.SLEWING, MotionStatus.TRACKING, + MotionStatus.IDLE, MotionStatus.POSITIONED] self.buttonResetFocusOffset.setEnabled(initialized) self.butSetFocusOffset.setEnabled(initialized) self.butSetFocusBase.setEnabled(initialized) diff --git a/pyobs_gui/widgettelescope.py b/pyobs_gui/widgettelescope.py index 5d59384..bbc68e8 100644 --- a/pyobs_gui/widgettelescope.py +++ b/pyobs_gui/widgettelescope.py @@ -12,6 +12,7 @@ from pyobs.comm import Comm from pyobs.events import MotionStatusChangedEvent from pyobs.interfaces import ITelescope, IFilters, IFocuser, ITemperatures, IMotion, IAltAzOffsets, IRaDecOffsets +from pyobs.utils.enums import MotionStatus from pyobs.utils.time import Time from pyobs_gui.widgetfilter import WidgetFilter from pyobs_gui.widgetfocus import WidgetFocus @@ -34,7 +35,7 @@ def __init__(self, module, comm, observer, parent=None): self.observer = observer # type: Observer # variables - self._motion_status = IMotion.Status.UNKNOWN + self._motion_status = MotionStatus.UNKNOWN self._ra_dec = None self._alt_az = None self._off_ra = None @@ -158,12 +159,12 @@ def update_gui(self): self.labelStatus.setText(self._motion_status.value.upper()) # (de)activate buttons - self.buttonInit.setEnabled(self._motion_status == IMotion.Status.PARKED) - self.buttonPark.setEnabled(self._motion_status not in [IMotion.Status.PARKED, IMotion.Status.ERROR, - IMotion.Status.PARKING, IMotion.Status.INITIALIZING]) - self.buttonStop.setEnabled(self._motion_status in [IMotion.Status.SLEWING, IMotion.Status.TRACKING]) - initialized = self._motion_status in [IMotion.Status.SLEWING, IMotion.Status.TRACKING, - IMotion.Status.IDLE, IMotion.Status.POSITIONED] + self.buttonInit.setEnabled(self._motion_status == MotionStatus.PARKED) + self.buttonPark.setEnabled(self._motion_status not in [MotionStatus.PARKED, MotionStatus.ERROR, + MotionStatus.PARKING, MotionStatus.INITIALIZING]) + self.buttonStop.setEnabled(self._motion_status in [MotionStatus.SLEWING, MotionStatus.TRACKING]) + initialized = self._motion_status in [MotionStatus.SLEWING, MotionStatus.TRACKING, + MotionStatus.IDLE, MotionStatus.POSITIONED] self.buttonMove.setEnabled(initialized) self.buttonOffsetNorth.setEnabled(initialized) self.buttonOffsetSouth.setEnabled(initialized) @@ -396,7 +397,7 @@ def _set_offset(self): self.run_async(self.module.set_radec_offsets, self._off_ra, 0.) else: # now the sets, ask for value - new_value, ok = QtWidgets.QInputDialog.getDouble(self, 'Set offset', 'New offset ["]', 0, 0, 999) + new_value, ok = QtWidgets.QInputDialog.getDouble(self, 'Set offset', 'New offset ["]', 0, -999, 999) if ok: if self.sender() == self.buttonSetAltOffset: self.run_async(self.module.set_altaz_offsets, new_value / 3600., self._off_az) diff --git a/setup.py b/setup.py index 4267aff..c851758 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='pyobs-gui', - version='0.12.2', + version='0.13', description='GUI for pyobs', author='Tim-Oliver Husser', author_email='thusser@uni-goettingen.de',