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',