pyobs-gui 1.8.7__tar.gz → 2.0.0.dev1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. pyobs_gui-2.0.0.dev1/.github/workflows/pyrefly.yml +23 -0
  2. pyobs_gui-2.0.0.dev1/.github/workflows/ruff.yml +23 -0
  3. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/.pre-commit-config.yaml +10 -3
  4. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/PKG-INFO +4 -2
  5. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/base.py +24 -34
  6. pyobs_gui-2.0.0.dev1/pyobs_gui/camerawidget.py +307 -0
  7. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/commandinputwidget.py +1 -1
  8. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/compassmovewidget.py +24 -13
  9. pyobs_gui-2.0.0.dev1/pyobs_gui/coolingwidget.py +55 -0
  10. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/datadisplaywidget.py +21 -23
  11. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/eventswidget.py +9 -7
  12. pyobs_gui-2.0.0.dev1/pyobs_gui/filterwidget.py +69 -0
  13. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/fitsheaderswidget.py +1 -1
  14. pyobs_gui-2.0.0.dev1/pyobs_gui/focuswidget.py +91 -0
  15. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/gui.py +1 -1
  16. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/logmodel.py +1 -1
  17. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/mainwindow.py +30 -32
  18. pyobs_gui-2.0.0.dev1/pyobs_gui/modewidget.py +119 -0
  19. pyobs_gui-2.0.0.dev1/pyobs_gui/modifiedmixin.py +86 -0
  20. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/modulegui.py +10 -8
  21. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/camerawidget.ui +165 -98
  22. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/camerawidget_ui.py +125 -80
  23. pyobs_gui-2.0.0.dev1/pyobs_gui/roofwidget.py +69 -0
  24. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/shellwidget.py +6 -10
  25. pyobs_gui-2.0.0.dev1/pyobs_gui/spectrographwidget.py +88 -0
  26. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/statuswidget.py +45 -110
  27. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/telescopewidget.py +170 -269
  28. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/temperaturesplotwidget.py +5 -7
  29. pyobs_gui-2.0.0.dev1/pyobs_gui/temperatureswidget.py +67 -0
  30. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/videowidget.py +77 -43
  31. pyobs_gui-2.0.0.dev1/pyobs_gui/watchedlabel.py +22 -0
  32. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/weatherwidget.py +1 -0
  33. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyproject.toml +34 -3
  34. pyobs_gui-2.0.0.dev1/test/camera.yaml +32 -0
  35. pyobs_gui-2.0.0.dev1/test/full.yaml +66 -0
  36. pyobs_gui-2.0.0.dev1/test/mode.yaml +18 -0
  37. pyobs_gui-2.0.0.dev1/test/roof.yaml +18 -0
  38. pyobs_gui-2.0.0.dev1/test/spectrograph.yaml +18 -0
  39. pyobs_gui-2.0.0.dev1/test/telescope.yaml +23 -0
  40. pyobs_gui-2.0.0.dev1/test/video.yaml +31 -0
  41. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/uv.lock +94 -51
  42. pyobs_gui-1.8.7/.flake8 +0 -12
  43. pyobs_gui-1.8.7/pyobs_gui/camerawidget.py +0 -378
  44. pyobs_gui-1.8.7/pyobs_gui/coolingwidget.py +0 -74
  45. pyobs_gui-1.8.7/pyobs_gui/filterwidget.py +0 -124
  46. pyobs_gui-1.8.7/pyobs_gui/focuswidget.py +0 -147
  47. pyobs_gui-1.8.7/pyobs_gui/modewidget.py +0 -160
  48. pyobs_gui-1.8.7/pyobs_gui/roofwidget.py +0 -80
  49. pyobs_gui-1.8.7/pyobs_gui/spectrographwidget.py +0 -163
  50. pyobs_gui-1.8.7/pyobs_gui/temperatureswidget.py +0 -80
  51. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/.github/workflows/pypi.yml +0 -0
  52. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/.readthedocs.yml +0 -0
  53. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/CHANGELOG.rst +0 -0
  54. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/LICENSE +0 -0
  55. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/README.md +0 -0
  56. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/bin/mypyuic5 +0 -0
  57. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/docs/Makefile +0 -0
  58. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/docs/make.bat +0 -0
  59. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/docs/requirements.txt +0 -0
  60. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/docs/source/_static/pyobs.gif +0 -0
  61. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/docs/source/conf.py +0 -0
  62. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/docs/source/index.rst +0 -0
  63. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/mypy.ini +0 -0
  64. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/__init__.py +0 -0
  65. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/__init__.py +0 -0
  66. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/compassmoveplugin.py +0 -0
  67. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/compassmovewidget.py +0 -0
  68. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/compassmovewidget.ui +0 -0
  69. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/compassmovewidget_ui.py +0 -0
  70. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/compile.sh +0 -0
  71. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/coolingwidget.ui +0 -0
  72. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/coolingwidget_ui.py +0 -0
  73. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/datadisplayplugin.py +0 -0
  74. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/datadisplaywidget.ui +0 -0
  75. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/datadisplaywidget_ui.py +0 -0
  76. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/designer.sh +0 -0
  77. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/eventswidget.ui +0 -0
  78. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/eventswidget_ui.py +0 -0
  79. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/filterwidget.ui +0 -0
  80. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/filterwidget_ui.py +0 -0
  81. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/fitsheaderswidget.ui +0 -0
  82. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/fitsheaderswidget_ui.py +0 -0
  83. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/focuswidget.ui +0 -0
  84. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/focuswidget_ui.py +0 -0
  85. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/mainwindow.ui +0 -0
  86. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/mainwindow_ui.py +0 -0
  87. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/modewidget.ui +0 -0
  88. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/modewidget_ui.py +0 -0
  89. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/resources/arrow-alt-circle-down-solid.svg +0 -0
  90. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/resources/arrow-alt-circle-left-solid.svg +0 -0
  91. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/resources/arrow-alt-circle-right-solid.svg +0 -0
  92. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/resources/arrow-alt-circle-up-solid.svg +0 -0
  93. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/resources/arrow-up-right-from-square-solid.svg +0 -0
  94. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/resources/arrows-to-eye-solid.svg +0 -0
  95. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/resources/chart-line-solid.svg +0 -0
  96. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/resources/circle-question-solid.svg +0 -0
  97. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/resources/cloud-sun-solid.svg +0 -0
  98. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/resources/edit-solid.svg +0 -0
  99. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/resources/search-solid.svg +0 -0
  100. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/resources/undo-solid.svg +0 -0
  101. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/resources.qrc +0 -0
  102. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/resources_rc.py +0 -0
  103. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/roofwidget.ui +0 -0
  104. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/roofwidget_ui.py +0 -0
  105. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/shellwidget.ui +0 -0
  106. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/shellwidget_ui.py +0 -0
  107. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/spectrographwidget.ui +0 -0
  108. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/spectrographwidget_ui.py +0 -0
  109. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/telescopewidget.ui +0 -0
  110. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/telescopewidget_ui.py +0 -0
  111. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/temperaturesplotwidget.ui +0 -0
  112. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/temperaturesplotwidget_ui.py +0 -0
  113. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/temperatureswidget.ui +0 -0
  114. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/temperatureswidget_ui.py +0 -0
  115. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/videowidget.ui +0 -0
  116. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/videowidget_ui.py +0 -0
  117. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/weatherwidget.ui +0 -0
  118. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/qt/weatherwidget_ui.py +0 -0
  119. {pyobs_gui-1.8.7 → pyobs_gui-2.0.0.dev1}/pyobs_gui/utils.py +0 -0
@@ -0,0 +1,23 @@
1
+ name: pyrefly
2
+ on: push
3
+
4
+ jobs:
5
+ pyrefly:
6
+ runs-on: ubuntu-latest
7
+ timeout-minutes: 10
8
+
9
+ steps:
10
+ - name: Check out repository code
11
+ uses: actions/checkout@v4
12
+
13
+ - name: Set up Python
14
+ uses: astral-sh/setup-uv@v5
15
+ with:
16
+ enable-cache: true
17
+ python-version: "3.13"
18
+
19
+ - name: Install packages
20
+ run: uv sync --all-extras --dev
21
+
22
+ - name: Run pyrefly
23
+ run: uv run pyrefly check
@@ -0,0 +1,23 @@
1
+ name: ruff
2
+ on: push
3
+
4
+ jobs:
5
+ ruff:
6
+ runs-on: ubuntu-latest
7
+ timeout-minutes: 10
8
+
9
+ steps:
10
+ - name: Check out repository code
11
+ uses: actions/checkout@v4
12
+
13
+ - name: Set up Python
14
+ uses: astral-sh/setup-uv@v5
15
+ with:
16
+ enable-cache: true
17
+ python-version: "3.13"
18
+
19
+ - name: Install packages
20
+ run: uv sync --all-extras --dev
21
+
22
+ - name: Run ruff
23
+ run: uv run ruff check pyobs_gui/
@@ -9,7 +9,14 @@ repos:
9
9
  # pre-commit's default_language_version, see
10
10
  # https://pre-commit.com/#top_level-default_language_version
11
11
  language_version: python3.11
12
- - repo: https://github.com/pycqa/flake8
13
- rev: '7.3.0' # pick a git hash / tag to point to
12
+ - repo: https://github.com/astral-sh/ruff-pre-commit
13
+ rev: v0.11.0
14
14
  hooks:
15
- - id: flake8
15
+ - id: ruff
16
+ - repo: local
17
+ hooks:
18
+ - id: pyrefly
19
+ name: pyrefly
20
+ entry: uv run pyrefly check
21
+ language: system
22
+ pass_filenames: false
@@ -1,14 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyobs-gui
3
- Version: 1.8.7
3
+ Version: 2.0.0.dev1
4
4
  Summary: A remote GUI for pyobs
5
5
  Author-email: Tim-Oliver Husser <thusser@uni-goettingen.de>
6
6
  License-Expression: MIT
7
7
  License-File: LICENSE
8
8
  Requires-Python: <3.14,>=3.11
9
+ Requires-Dist: astroplan>=0.10.1
10
+ Requires-Dist: astropy<8,>=7.0.1
9
11
  Requires-Dist: colour<0.2,>=0.1.5
10
12
  Requires-Dist: matplotlib<4,>=3.10.1
11
- Requires-Dist: pyobs-core<2,>=1.49.0
13
+ Requires-Dist: pyobs-core<3,>=2.0.0.dev6
12
14
  Requires-Dist: pyside6>=6.10.1
13
15
  Requires-Dist: qasync<0.28,>=0.27.1
14
16
  Requires-Dist: qfitswidget>=0.13.1
@@ -2,28 +2,31 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import logging
5
- from collections.abc import Coroutine
6
- from typing import Any, TypeVar, Callable, Type
7
- from PySide6 import QtWidgets, QtGui, QtCore # type: ignore
8
- from astroplan import Observer
5
+ from typing import TYPE_CHECKING, Any, Callable, Type, TypeVar, overload
9
6
 
10
- from pyobs.comm import Comm, Proxy
11
- from pyobs.interfaces import IModule
7
+ import pyobs.utils.exceptions as exc
12
8
  from pyobs.object import create_object
13
9
  from pyobs.utils.enums import ModuleState
14
- from pyobs.vfs import VirtualFileSystem
15
- import pyobs.utils.exceptions as exc
10
+ from PySide6 import QtCore, QtGui, QtWidgets # type: ignore
11
+
16
12
  from .utils import QAsyncMessageBox
17
13
 
14
+ if TYPE_CHECKING:
15
+ from collections.abc import Coroutine
16
+
17
+ from astroplan import Observer
18
+ from pyobs.comm import Comm
19
+ from pyobs.vfs import VirtualFileSystem
20
+
18
21
  log = logging.getLogger(__name__)
19
22
 
20
- WidgetClass = TypeVar("WidgetClass")
23
+ WidgetClass = TypeVar("WidgetClass", bound="BaseWidget")
21
24
 
22
25
 
23
26
  class BaseWindow:
24
27
  def __init__(self) -> None:
25
28
  """Base class for MainWindow and all widgets."""
26
- self.modules: list[Proxy] = []
29
+ self.modules: list[str] = []
27
30
  self._comm: Comm | None = None
28
31
  self.observer: Observer | None = None
29
32
  self.vfs: VirtualFileSystem | dict[str, Any] | None = None
@@ -36,29 +39,11 @@ class BaseWindow:
36
39
  return self._comm
37
40
 
38
41
  @property
39
- def module(self) -> Proxy:
42
+ def module(self) -> str:
40
43
  """Returns the first module in the list or None, if list is empty"""
41
44
  return self.modules[0]
42
45
 
43
- def module_by_name(self, name: str) -> Proxy | None:
44
- """Return the module with the given name or None, if not exists.
45
-
46
- Args:
47
- name: Name of module to return.
48
-
49
- Returns:
50
- Module or None.
51
- """
52
-
53
- # loop all modules and check name
54
- for module in self.modules:
55
- if module.name == name:
56
- return module
57
-
58
- # nothing found
59
- return None
60
-
61
- def modules_by_interface(self, interface: Any) -> list[Proxy]:
46
+ def modules_by_interface(self, interface: Any) -> list[str]:
62
47
  """Returns all modules that implement the given interface.
63
48
 
64
49
  Args:
@@ -69,7 +54,7 @@ class BaseWindow:
69
54
  """
70
55
  return list(filter(lambda m: isinstance(m, interface), self.modules))
71
56
 
72
- def module_by_interface(self, interface: Any) -> Proxy | None:
57
+ def module_by_interface(self, interface: Any) -> str | None:
73
58
  """Returns first modules that implement the given interface, or None, if no exist.
74
59
 
75
60
  Args:
@@ -81,6 +66,10 @@ class BaseWindow:
81
66
  modules = self.modules_by_interface(interface)
82
67
  return None if len(modules) == 0 else modules[0]
83
68
 
69
+ @overload
70
+ def create_widget(self, config: type[WidgetClass], **kwargs: Any) -> WidgetClass: ...
71
+ @overload
72
+ def create_widget(self, config: dict[str, Any], **kwargs: Any) -> "BaseWidget": ...
84
73
  def create_widget(self, config: dict[str, Any] | type, **kwargs: Any) -> "BaseWidget":
85
74
  """Creates new widget.
86
75
 
@@ -108,7 +97,7 @@ class BaseWindow:
108
97
 
109
98
  async def open(
110
99
  self,
111
- modules: list[Proxy] | None = None,
100
+ modules: list[str] | None = None,
112
101
  comm: Comm | None = None,
113
102
  observer: Observer | None = None,
114
103
  vfs: VirtualFileSystem | dict[str, Any] | None = None,
@@ -247,8 +236,9 @@ class BaseWidget(BaseWindow, QtWidgets.QWidget): # type: ignore
247
236
  try:
248
237
  # get module state
249
238
  module = self.module
250
- if isinstance(module, IModule):
251
- state = await module.get_state()
239
+ client_state = self.comm.get_client_state(module)
240
+ if client_state is not None:
241
+ state, _ = client_state
252
242
  self.setEnabled(state == ModuleState.READY)
253
243
  if state != ModuleState.READY:
254
244
  return
@@ -0,0 +1,307 @@
1
+ import logging
2
+ from typing import Any
3
+ import qasync # type: ignore
4
+ from PySide6 import QtWidgets, QtCore # type: ignore
5
+ from astroplan import Observer
6
+
7
+ from pyobs.comm import Comm
8
+ from pyobs.events import NewImageEvent
9
+ from pyobs.interfaces import (
10
+ IAbortable,
11
+ IExposureTime,
12
+ ExposureTimeState,
13
+ IImageType,
14
+ ImageTypeState,
15
+ IImageFormat,
16
+ ImageFormatState,
17
+ IBinning,
18
+ BinningState,
19
+ IWindow,
20
+ WindowState,
21
+ IFilters,
22
+ ICooling,
23
+ ITemperatures,
24
+ IGain,
25
+ GainState,
26
+ IExposure,
27
+ ExposureState,
28
+ )
29
+ from pyobs.utils.enums import ImageType, ImageFormat, ExposureStatus
30
+ from pyobs.vfs import VirtualFileSystem
31
+ from .base import BaseWidget
32
+ from .coolingwidget import CoolingWidget
33
+ from .filterwidget import FilterWidget
34
+ from .temperatureswidget import TemperaturesWidget
35
+ from .fitsheaderswidget import FitsHeadersWidget
36
+ from .qt.camerawidget_ui import Ui_CameraWidget
37
+
38
+ log = logging.getLogger(__name__)
39
+
40
+
41
+ class CameraWidget(BaseWidget, Ui_CameraWidget):
42
+ signal_update_gui = QtCore.Signal()
43
+ signal_new_image = QtCore.Signal(NewImageEvent, str)
44
+
45
+ def __init__(self, **kwargs: Any):
46
+ BaseWidget.__init__(self, **kwargs)
47
+ self.setupUi(self) # type: ignore
48
+
49
+ # variables
50
+ self.new_image = False
51
+ self.image_filename = None
52
+ self.image = None
53
+ self.status = None
54
+ self.exposure_status = ExposureStatus.IDLE
55
+ self.exposures_left = 0
56
+ self.exposure_time_left = 0.0
57
+ self.exposure_progress = 0.0
58
+
59
+ self.spinWindowLeft.init_modified(self.labelWindowLeft).committed.connect(self._window_changed)
60
+ self.spinWindowTop.init_modified(self.labelWindowTop).committed.connect(self._window_changed)
61
+ self.spinWindowWidth.init_modified(self.labelWindowWidth).committed.connect(self._window_changed)
62
+ self.spinWindowHeight.init_modified(self.labelWindowHeight).committed.connect(self._window_changed)
63
+ self.spinGain.init_modified(self.labelGain).committed.connect(self._gain_changed)
64
+ self.spinGainOffset.init_modified(self.labelGain).committed.connect(self._gain_changed)
65
+
66
+ async def open(
67
+ self,
68
+ modules: list[str] | None = None,
69
+ comm: Comm | None = None,
70
+ observer: Observer | None = None,
71
+ vfs: VirtualFileSystem | dict[str, Any] | None = None,
72
+ ) -> None:
73
+ """Open module."""
74
+ await BaseWidget.open(self, modules=modules, comm=comm, observer=observer, vfs=vfs)
75
+ await self.datadisplay.open(modules=modules, comm=comm, observer=observer, vfs=vfs)
76
+
77
+ # set exposure types
78
+ image_types = sorted([it.name for it in ImageType])
79
+ self.comboImageType.addItems(image_types)
80
+
81
+ # before first update, disable mys
82
+ self.setEnabled(False)
83
+
84
+ # hide groups, if necessary
85
+ self.groupWindowing.setVisible(await self.comm.has_proxy(self.module, IWindow))
86
+ self.groupBinning.setVisible(await self.comm.has_proxy(self.module, IBinning))
87
+ self.groupImageFormat.setVisible(await self.comm.has_proxy(self.module, IImageFormat))
88
+ self.groupExpTime.setVisible(await self.comm.has_proxy(self.module, IExposureTime))
89
+ self.groupGain.setVisible(await self.comm.has_proxy(self.module, IGain))
90
+
91
+ # and single controls
92
+ self.labelImageType.setVisible(await self.comm.has_proxy(self.module, IImageType))
93
+ self.comboImageType.setVisible(await self.comm.has_proxy(self.module, IImageType))
94
+ self.butAbort.setVisible(await self.comm.has_proxy(self.module, IAbortable))
95
+
96
+ # initial values
97
+ self.comboImageType.setCurrentIndex(image_types.index("OBJECT"))
98
+
99
+ # connect signals
100
+ self.signal_update_gui.connect(self.update_gui)
101
+ self.butFullFrame.clicked.connect(self.set_full_frame)
102
+ self.comboBinning.currentTextChanged.connect(self.set_full_frame)
103
+ self.checkBroadcast.stateChanged.connect(self.broadcast_changed)
104
+ self.comboImageType.currentTextChanged.connect(self.image_type_changed)
105
+ self.butExpose.clicked.connect(self.expose)
106
+ self.butAbort.clicked.connect(self.abort)
107
+
108
+ # fill sidebar
109
+ await self.add_to_sidebar(self.create_widget(FitsHeadersWidget, module=self.module))
110
+ if await self.comm.has_proxy(self.module, IFilters):
111
+ await self.add_to_sidebar(self.create_widget(FilterWidget, module=self.module))
112
+ if await self.comm.has_proxy(self.module, ICooling):
113
+ await self.add_to_sidebar(self.create_widget(CoolingWidget, module=self.module))
114
+ if await self.comm.has_proxy(self.module, ITemperatures):
115
+ await self.add_to_sidebar(self.create_widget(TemperaturesWidget, module=self.module))
116
+
117
+ async def _init(self) -> None:
118
+ # window
119
+ async with self.comm.safe_proxy(self.module, IWindow) as proxy:
120
+ if proxy is not None:
121
+ await self.comm.subscribe_state(self.module, IWindow, self._update_window)
122
+ await self.set_full_frame()
123
+
124
+ # binning
125
+ binning_caps = await self.comm.get_capabilities(self.module, IBinning)
126
+ if binning_caps is not None:
127
+ binnings = [f"{b.x}x{b.y}" for b in binning_caps.binnings]
128
+ self.comboBinning.clear()
129
+ self.comboBinning.addItems(binnings)
130
+ await self.comm.subscribe_state(self.module, IBinning, self._update_binning)
131
+
132
+ # gain — initial values delivered by subscription callback
133
+ if await self.comm.has_proxy(self.module, IGain):
134
+ await self.comm.subscribe_state(self.module, IGain, self._update_gain)
135
+
136
+ # image format
137
+ image_format_caps = await self.comm.get_capabilities(self.module, IImageFormat)
138
+ if image_format_caps is not None:
139
+ image_formats = [ImageFormat(f) for f in image_format_caps.image_formats]
140
+ self.comboImageFormat.clear()
141
+ self.comboImageFormat.addItems([f.name for f in image_formats])
142
+ await self.comm.subscribe_state(self.module, IImageFormat, self._update_image_format)
143
+
144
+ async with self.comm.safe_proxy(self.module, IImageType) as proxy:
145
+ if proxy is not None:
146
+ await self.comm.subscribe_state(self.module, IImageType, self._update_image_type)
147
+
148
+ async with self.comm.safe_proxy(self.module, IExposure) as proxy:
149
+ if proxy is not None:
150
+ await self.comm.subscribe_state(self.module, IExposure, self._update_exposure)
151
+
152
+ async with self.comm.safe_proxy(self.module, IExposureTime) as proxy:
153
+ if proxy is not None:
154
+ await self.comm.subscribe_state(self.module, IExposureTime, self._update_exposure_time)
155
+
156
+ # update GUI
157
+ self.signal_update_gui.emit()
158
+
159
+ def _update_window(self, state: WindowState):
160
+ self.labelWindowLeft.setText(str(state.x))
161
+ self.labelWindowTop.setText(str(state.y))
162
+ self.labelWindowWidth.setText(str(state.width))
163
+ self.labelWindowHeight.setText(str(state.height))
164
+
165
+ def _update_binning(self, state: BinningState):
166
+ self.comboBinning.setCurrentText(f"{state.x}x{state.y}")
167
+
168
+ def _update_gain(self, state: GainState):
169
+ self.labelGain.setText(str(state.gain))
170
+ self.labelGainOffset.setText(str(state.offset))
171
+
172
+ def _update_image_format(self, state: ImageFormatState):
173
+ self.comboImageFormat.setCurrentText(state.image_format.name)
174
+
175
+ def _update_image_type(self, state: ImageTypeState):
176
+ self.comboImageType.setCurrentText(state.image_type.name)
177
+
178
+ def _update_exposure(self, state: ExposureState):
179
+ self.exposure_status = state.status
180
+ self.exposure_progress = state.progress
181
+ self.exposure_time_left = state.exposure_time_left
182
+ self.update_gui()
183
+
184
+ def _update_exposure_time(self, state: ExposureTimeState):
185
+ self.spinExpTime.setValue(state.exposure_time)
186
+ self.update_gui()
187
+
188
+ @qasync.asyncSlot()
189
+ async def _window_changed(self):
190
+ print("ok")
191
+
192
+ @qasync.asyncSlot()
193
+ async def _gain_changed(self):
194
+ print("ok")
195
+
196
+ @qasync.asyncSlot() # type: ignore
197
+ async def set_full_frame(self) -> None:
198
+ caps = await self.comm.get_capabilities(self.module, IWindow)
199
+ if caps is not None:
200
+ # get binning
201
+ binning = 1
202
+ if self.comboBinning.count() > 0 and await self.comm.has_proxy(self.module, IBinning):
203
+ binning = int(self.comboBinning.currentText()[0])
204
+
205
+ # max values
206
+ self.spinWindowLeft.setMaximum(int(caps.full_frame_width / binning))
207
+ self.spinWindowTop.setMaximum(int(caps.full_frame_height / binning))
208
+ self.spinWindowWidth.setMaximum(int(caps.full_frame_width / binning))
209
+ self.spinWindowHeight.setMaximum(int(caps.full_frame_height / binning))
210
+
211
+ # set it
212
+ self.spinWindowLeft.setValue(caps.full_frame_x)
213
+ self.spinWindowTop.setValue(caps.full_frame_y)
214
+ self.spinWindowWidth.setValue(int(caps.full_frame_width / binning))
215
+ self.spinWindowHeight.setValue(int(caps.full_frame_height / binning))
216
+
217
+ @QtCore.Slot(int) # type: ignore
218
+ def broadcast_changed(self, state: int) -> None:
219
+ if state == 0:
220
+ r = QtWidgets.QMessageBox.question(
221
+ self,
222
+ "pyobs",
223
+ "When disabling the broadcast, new images will not processed (and "
224
+ "saved) within the pyobs network. Continue?",
225
+ QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
226
+ QtWidgets.QMessageBox.StandardButton.No,
227
+ )
228
+ if r == QtWidgets.QMessageBox.StandardButton.No:
229
+ self.checkBroadcast.setChecked(True)
230
+
231
+ @QtCore.Slot(str) # type: ignore
232
+ def image_type_changed(self, image_type: str) -> None:
233
+ if image_type == "BIAS":
234
+ self.spinExpTime.setValue(0)
235
+ self.spinExpTime.setEnabled(False)
236
+ else:
237
+ self.spinExpTime.setEnabled(True)
238
+
239
+ @qasync.asyncSlot() # type: ignore
240
+ async def expose(self) -> None:
241
+ # set initial image count
242
+ self.exposures_left = self.spinCount.value()
243
+
244
+ # do exposure(s)
245
+ while self.exposures_left > 0:
246
+ # expose
247
+ broadcast = self.checkBroadcast.isChecked()
248
+ await self.datadisplay.grab_data(broadcast)
249
+
250
+ # decrement number of exposures left
251
+ self.exposures_left -= 1
252
+
253
+ # signal GUI update
254
+ self.signal_update_gui.emit()
255
+
256
+ @qasync.asyncSlot() # type: ignore
257
+ async def abort(self) -> None:
258
+ """Abort exposure."""
259
+ # do we have a running exposure?
260
+ if self.exposures_left == 0:
261
+ return
262
+
263
+ # got exposures left?
264
+ if self.exposures_left > 1:
265
+ # abort sequence
266
+ self.exposures_left = 0
267
+ else:
268
+ async with self.comm.safe_proxy(self.module, IAbortable) as proxy:
269
+ if proxy is not None:
270
+ await proxy.abort()
271
+
272
+ def update_gui(self) -> None:
273
+ """Update the GUI."""
274
+
275
+ # enable myself
276
+ self.setEnabled(True)
277
+
278
+ # enable/disable buttons
279
+ self.butExpose.setEnabled(self.exposure_status == ExposureStatus.IDLE)
280
+ self.butAbort.setEnabled(self.exposure_status != ExposureStatus.IDLE)
281
+
282
+ # set abort text
283
+ if self.exposures_left > 1:
284
+ self.butAbort.setText("Abort sequence")
285
+ else:
286
+ self.butAbort.setText("Abort exposure")
287
+
288
+ # set progress
289
+ msg = ""
290
+ if self.exposure_status == ExposureStatus.IDLE:
291
+ self.progressExposure.setValue(0)
292
+ msg = "IDLE"
293
+ elif self.exposure_status == ExposureStatus.EXPOSING:
294
+ self.progressExposure.setValue(int(self.exposure_progress))
295
+ msg = "EXPOSING %.1fs" % self.exposure_time_left
296
+ elif self.exposure_status == ExposureStatus.READOUT:
297
+ self.progressExposure.setValue(100)
298
+ msg = "READOUT"
299
+
300
+ # set message
301
+ self.labelStatus.setText(msg)
302
+
303
+ # exposures left
304
+ if self.exposures_left > 0:
305
+ self.labelExposuresLeft.setText("%d exposure(s) left" % self.exposures_left)
306
+ else:
307
+ self.labelExposuresLeft.setText("")
@@ -13,7 +13,7 @@ class CommandInputWidget(QtWidgets.QLineEdit): # type: ignore
13
13
  def keyPressEvent(self, event: QtGui.QKeyEvent) -> None:
14
14
  if event.key() == QtCore.Qt.Key.Key_Return:
15
15
  # get command
16
- cmd = str(self.text())
16
+ cmd = self.text()
17
17
  # emit command
18
18
  self.commandExecuted.emit(cmd)
19
19
  # clear text field
@@ -1,6 +1,5 @@
1
1
  from typing import Any
2
2
  from PySide6 import QtWidgets, QtCore # type: ignore
3
- import qasync # type: ignore
4
3
  import astropy.units as u
5
4
  from astropy.coordinates import SkyCoord, ICRS
6
5
 
@@ -33,20 +32,30 @@ class CompassMoveWidget(BaseWidget, Ui_CompassMoveWidget):
33
32
  self.run_background(self.__move_offset, self.sender())
34
33
 
35
34
  async def __move_offset(self, sender: QtWidgets.QWidget) -> None:
35
+ if self.observer is None:
36
+ return
37
+
36
38
  # get offsets
37
- if isinstance(self.module, IOffsetsAltAz) and isinstance(self.module, IPointingAltAz):
38
- alt, az = await self.module.get_altaz()
39
+ altaz = None
40
+ has_offsets_altaz = await self.comm.has_proxy(self.module, IOffsetsAltAz)
41
+ has_pointing_altaz = await self.comm.has_proxy(self.module, IPointingAltAz)
42
+ has_offsets_radec = await self.comm.has_proxy(self.module, IOffsetsRaDec)
43
+ if has_offsets_altaz and has_pointing_altaz:
44
+ async with self.comm.proxy(self.module, IPointingAltAz) as p:
45
+ alt, az = await p.get_altaz() # type: ignore[attr-defined]
39
46
  altaz = SkyCoord(
40
- alt=alt * u.degree,
41
- az=alt * u.degree,
47
+ alt=alt * u.degree, # type: ignore[attr-defined]
48
+ az=az * u.degree, # type: ignore[attr-defined]
42
49
  obstime=Time.now(),
43
50
  location=self.observer.location,
44
51
  frame="altaz",
45
52
  )
46
- off_alt, off_az = await self.module.get_offsets_altaz()
53
+ async with self.comm.proxy(self.module, IOffsetsAltAz) as p:
54
+ off_alt, off_az = await p.get_offsets_altaz() # type: ignore[attr-defined]
47
55
  off_ra, off_dec = offset_altaz_to_radec(altaz, off_alt, off_az)
48
- elif isinstance(self.module, IOffsetsRaDec):
49
- off_ra, off_dec = await self.module.get_offsets_radec()
56
+ elif has_offsets_radec:
57
+ async with self.comm.proxy(self.module, IOffsetsRaDec) as p:
58
+ off_ra, off_dec = await p.get_offsets_radec() # type: ignore[attr-defined]
50
59
  else:
51
60
  return
52
61
 
@@ -64,11 +73,13 @@ class CompassMoveWidget(BaseWidget, Ui_CompassMoveWidget):
64
73
  off_ra -= user_offset
65
74
 
66
75
  # move
67
- if isinstance(self.module, IOffsetsRaDec):
68
- self.run_background(self.module.set_offsets_radec, off_ra, off_dec)
69
- elif isinstance(self.module, IOffsetsAltAz):
70
- off_alt, off_az = offset_radec_to_altaz(altaz.transform_to(ICRS()), off_ra, off_dec, self.observer.location)
71
- self.run_background(self.module.set_offsets_altaz, off_alt, off_az)
76
+ if has_offsets_radec:
77
+ async with self.comm.proxy(self.module, IOffsetsRaDec) as p:
78
+ await p.set_offsets_radec(off_ra, off_dec)
79
+ elif has_offsets_altaz and altaz is not None:
80
+ off_alt, off_az = offset_radec_to_altaz(altaz.transform_to(ICRS()), off_ra, off_dec, self.observer.location) # noqa: E501
81
+ async with self.comm.proxy(self.module, IOffsetsAltAz) as p:
82
+ await p.set_offsets_altaz(off_alt, off_az)
72
83
  else:
73
84
  raise ValueError
74
85
 
@@ -0,0 +1,55 @@
1
+ import qasync
2
+ import logging
3
+ from typing import Any
4
+ from PySide6 import QtCore # type: ignore
5
+
6
+ from pyobs.interfaces import ICooling, CoolingState
7
+ from .base import BaseWidget
8
+ from .qt.coolingwidget_ui import Ui_CoolingWidget
9
+
10
+ log = logging.getLogger(__name__)
11
+
12
+
13
+ class CoolingWidget(BaseWidget, Ui_CoolingWidget):
14
+ signal_update_gui = QtCore.Signal()
15
+
16
+ def __init__(self, **kwargs: Any):
17
+ BaseWidget.__init__(self, **kwargs)
18
+ self.setupUi(self) # type: ignore
19
+
20
+ # state
21
+ self.state: CoolingState | None = None
22
+
23
+ # connect signals
24
+ self.signal_update_gui.connect(self.update_gui)
25
+ self.checkEnabled.toggled.connect(self.checkEnabled_toggled)
26
+ self.buttonApply.clicked.connect(self.buttonApply_clicked)
27
+
28
+ async def _init(self) -> None:
29
+ await self.comm.subscribe_state(self.module, ICooling, self._update_state)
30
+
31
+ def _update_state(self, state: CoolingState) -> None:
32
+ self.state = state
33
+ self.signal_update_gui.emit()
34
+
35
+ def update_gui(self) -> None:
36
+ if self.state is not None:
37
+ self.setEnabled(True)
38
+
39
+ if self.state.enabled:
40
+ self.labelStatus.setText("N/A" if self.state.setpoint is None else f"{self.state.setpoint:.1f}°C")
41
+ self.labelPower.setText("N/A" if self.state.power is None else f"{self.state.power:d}%")
42
+ else:
43
+ self.labelStatus.setText("OFF")
44
+ self.labelPower.clear()
45
+
46
+ @QtCore.Slot(bool) # type: ignore
47
+ def checkEnabled_toggled(self, enabled: bool) -> None:
48
+ self.spinSetPoint.setEnabled(enabled)
49
+
50
+ @qasync.asyncSlot() # type: ignore
51
+ async def buttonApply_clicked(self) -> None:
52
+ enabled = self.checkEnabled.isChecked()
53
+ temp = self.spinSetPoint.value()
54
+ async with self.comm.proxy(self.module, ICooling) as proxy:
55
+ await proxy.set_cooling(enabled, temp)