pyobs-gui 2.0.0.dev19__tar.gz → 2.0.0.dev21__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.
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/PKG-INFO +2 -2
- pyobs_gui-2.0.0.dev21/README.md +28 -0
- pyobs_gui-2.0.0.dev21/docs/source/index.rst +140 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/base.py +58 -3
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/camerawidget.py +42 -10
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/mainwindow.py +143 -34
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/shellwidget.py +42 -6
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/telescopewidget.py +22 -5
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/videowidget.py +11 -1
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyproject.toml +4 -4
- pyobs_gui-2.0.0.dev21/specs/2026-08-21-gui-widget-startup-responsiveness.md +397 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/specs/index.md +7 -0
- pyobs_gui-2.0.0.dev21/tests/test_mainwindow_startup.py +322 -0
- pyobs_gui-2.0.0.dev21/tests/test_videowidget.py +82 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/uv.lock +77 -42
- pyobs_gui-2.0.0.dev19/README.md +0 -3
- pyobs_gui-2.0.0.dev19/docs/source/index.rst +0 -51
- pyobs_gui-2.0.0.dev19/tests/test_videowidget.py +0 -38
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/.claude/skills/verify/SKILL.md +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/.github/dependabot.yml +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/.github/workflows/pypi.yml +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/.github/workflows/pyrefly.yml +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/.github/workflows/ruff.yml +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/.gitignore +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/.pre-commit-config.yaml +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/.readthedocs.yml +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/CHANGELOG.rst +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/LICENSE +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/bin/mypyuic5 +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/docs/Makefile +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/docs/make.bat +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/docs/requirements.txt +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/docs/source/_static/pyobs.gif +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/docs/source/conf.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/mypy.ini +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/__init__.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/__main__.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/_nuitka_astropy_patch.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/accounts.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/acquisitionwidget.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/autofocuswidget.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/autoguidingwidget.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/commandinputwidget.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/compassmovewidget.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/coolingwidget.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/datadisplaywidget.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/eventswidget.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/filterwidget.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/fitsheaderswidget.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/focuswidget.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/gui.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/login.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/loginwindow.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/logmodel.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/modewidget.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/modifiedmixin.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/modulegui.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/__init__.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/acquisitionwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/acquisitionwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/autofocuswidget.ui +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/autofocuswidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/autoguidingwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/autoguidingwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/camerawidget.ui +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/camerawidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/compassmoveplugin.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/compassmovewidget.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/compassmovewidget.ui +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/compassmovewidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/compile.sh +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/coolingwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/coolingwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/datadisplayplugin.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/datadisplaywidget.ui +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/datadisplaywidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/designer.sh +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/eventswidget.ui +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/eventswidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/filterwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/filterwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/fitsheaderswidget.ui +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/fitsheaderswidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/focuswidget.ui +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/focuswidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/mainwindow.ui +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/mainwindow_ui.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/modewidget.ui +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/modewidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/resources/arrow-alt-circle-down-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/resources/arrow-alt-circle-left-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/resources/arrow-alt-circle-right-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/resources/arrow-alt-circle-up-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/resources/arrow-up-right-from-square-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/resources/arrows-to-eye-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/resources/chart-line-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/resources/circle-question-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/resources/cloud-sun-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/resources/edit-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/resources/search-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/resources/undo-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/resources.qrc +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/resources_rc.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/roofwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/roofwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/shellwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/shellwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/spectrographwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/spectrographwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/telescopewidget.ui +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/telescopewidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/temperaturesplotwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/temperaturesplotwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/temperatureswidget.ui +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/temperatureswidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/videowidget.ui +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/videowidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/weatherwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/qt/weatherwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/roofwidget.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/spectrographwidget.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/statuswidget.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/temperaturesplotwidget.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/temperatureswidget.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/utils.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/watchedlabel.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pyobs_gui/weatherwidget.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/pysidedeploy.spec +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/specs/2026-08-20-gui-remote-call-error-handling.md +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/spike_standalone/main.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/spike_standalone/pysidedeploy.spec +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/test/acquisition.yaml +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/test/autofocus.yaml +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/test/camera.yaml +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/test/full.yaml +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/test/guiding.yaml +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/test/mode.yaml +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/test/roof.yaml +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/test/spectrograph.yaml +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/test/telescope.yaml +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/test/telescope_acl.yaml +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/test/telescope_acl_denied.yaml +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/test/video.yaml +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/tests/__init__.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/tests/conftest.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/tests/test_accounts.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/tests/test_camerawidget.py +0 -0
- {pyobs_gui-2.0.0.dev19 → pyobs_gui-2.0.0.dev21}/tests/test_loginwindow.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pyobs-gui
|
|
3
|
-
Version: 2.0.0.
|
|
3
|
+
Version: 2.0.0.dev21
|
|
4
4
|
Summary: A remote GUI for pyobs
|
|
5
5
|
Author-email: Tim-Oliver Husser <thusser@uni-goettingen.de>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -11,7 +11,7 @@ Requires-Dist: astropy<8,>=7.0.1
|
|
|
11
11
|
Requires-Dist: colour<0.2,>=0.1.5
|
|
12
12
|
Requires-Dist: keyring<26,>=25.7.0
|
|
13
13
|
Requires-Dist: matplotlib<4,>=3.10.1
|
|
14
|
-
Requires-Dist: pyobs-core<3,>=2.0.0.
|
|
14
|
+
Requires-Dist: pyobs-core<3,>=2.0.0.dev93
|
|
15
15
|
Requires-Dist: pyside6>=6.10.1
|
|
16
16
|
Requires-Dist: qasync<0.29,>=0.27.1
|
|
17
17
|
Requires-Dist: qfitswidget>=0.13.1
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
pyobs-gui
|
|
2
|
+
=========
|
|
3
|
+
|
|
4
|
+
This is a [pyobs](https://www.pyobs.org) GUI for operating the whole system: one page per
|
|
5
|
+
connected module (camera, telescope, roof, focuser, ...), plus Shell/Events/Status tool pages.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
Install *pyobs-gui*
|
|
9
|
+
---------------------
|
|
10
|
+
Clone the repository:
|
|
11
|
+
|
|
12
|
+
git clone https://github.com/pyobs/pyobs-gui.git
|
|
13
|
+
cd pyobs-gui
|
|
14
|
+
|
|
15
|
+
Install it with [uv](https://docs.astral.sh/uv/):
|
|
16
|
+
|
|
17
|
+
uv sync
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
Running
|
|
21
|
+
-------
|
|
22
|
+
Two ways to start it:
|
|
23
|
+
|
|
24
|
+
- `uv run pyobs-gui` — standalone, interactive login. No YAML config needed; it prompts for XMPP
|
|
25
|
+
credentials (with an optional "store password") and connects directly.
|
|
26
|
+
- `uv run pyobs <config.yaml>` — the standard `pyobs` CLI, driven by a config file whose top-level
|
|
27
|
+
class is `pyobs_gui.GUI` (see `docs/source/index.rst` for a full example, including how to wire
|
|
28
|
+
in a custom widget for a module).
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
pyobs-gui
|
|
2
|
+
#########
|
|
3
|
+
|
|
4
|
+
This is a `pyobs <https://www.pyobs.org>`_ (`documentation <https://docs.pyobs.org>`_) module providing a GUI
|
|
5
|
+
for operating the whole system.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
Example configuration
|
|
9
|
+
**********************
|
|
10
|
+
|
|
11
|
+
This is an example configuration for a GUI that defines one custom widget for the ``guiding`` module::
|
|
12
|
+
|
|
13
|
+
class: pyobs_gui.GUI
|
|
14
|
+
|
|
15
|
+
widgets:
|
|
16
|
+
- module: guiding
|
|
17
|
+
overwrite: True
|
|
18
|
+
widget:
|
|
19
|
+
class: mypackage.GuidingWidget
|
|
20
|
+
|
|
21
|
+
comm:
|
|
22
|
+
jid: test@example.com
|
|
23
|
+
password: ***
|
|
24
|
+
|
|
25
|
+
timezone: Europe/Berlin
|
|
26
|
+
location:
|
|
27
|
+
longitude: 9.944333
|
|
28
|
+
latitude: 51.560583
|
|
29
|
+
elevation: 201.
|
|
30
|
+
|
|
31
|
+
vfs:
|
|
32
|
+
class: pyobs.vfs.VirtualFileSystem
|
|
33
|
+
roots:
|
|
34
|
+
cache:
|
|
35
|
+
class: pyobs.vfs.HttpFile
|
|
36
|
+
download: http://localhost:37075/
|
|
37
|
+
webcam:
|
|
38
|
+
class: pyobs.vfs.HttpFile
|
|
39
|
+
download: http://localhost:37077/
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
Available classes
|
|
43
|
+
******************
|
|
44
|
+
|
|
45
|
+
There is one single class for the GUI.
|
|
46
|
+
|
|
47
|
+
GUI
|
|
48
|
+
===
|
|
49
|
+
.. autoclass:: pyobs_gui.GUI
|
|
50
|
+
:members:
|
|
51
|
+
:show-inheritance:
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
Widgets
|
|
55
|
+
*******
|
|
56
|
+
|
|
57
|
+
The GUI shows one page per connected module, picking a widget by the module's interfaces
|
|
58
|
+
(``DEFAULT_WIDGETS`` in ``mainwindow.py``), plus three always-present "Tools" pages. No screenshots
|
|
59
|
+
yet — this section was written from source, not from a running GUI; add screenshots as a follow-up.
|
|
60
|
+
|
|
61
|
+
Shell
|
|
62
|
+
=====
|
|
63
|
+
Always present. A command console: type shell-like commands against any connected module, with
|
|
64
|
+
tab-completion driven by the module's own docstrings, and a per-module command history.
|
|
65
|
+
|
|
66
|
+
Events
|
|
67
|
+
======
|
|
68
|
+
Always present. Live feed of events broadcast by connected modules, and lets you send one back.
|
|
69
|
+
|
|
70
|
+
Status
|
|
71
|
+
======
|
|
72
|
+
Always present. Per-module connection/presence indicator plus a live, expandable tree of each
|
|
73
|
+
connected module's published state.
|
|
74
|
+
|
|
75
|
+
Camera — drives ``ICamera``
|
|
76
|
+
============================
|
|
77
|
+
Expose/abort control: binning, gain, image format/type, exposure time, full-frame vs. windowed
|
|
78
|
+
readout, and (if the module supports it) data sequences.
|
|
79
|
+
|
|
80
|
+
Telescope — drives ``ITelescope``
|
|
81
|
+
====================================
|
|
82
|
+
Init/park/stop and move-to-target, offsets in both RA/Dec and Alt/Az, Simbad and JPL Horizons
|
|
83
|
+
target lookup, and tracking of orbital elements or solar-system bodies.
|
|
84
|
+
|
|
85
|
+
Roof — drives ``IRoof``
|
|
86
|
+
==========================
|
|
87
|
+
Open/close/stop the roof, with live motion and pointing state.
|
|
88
|
+
|
|
89
|
+
Focuser — drives ``IFocuser``
|
|
90
|
+
================================
|
|
91
|
+
Set an absolute focus value, apply/reset a focus offset.
|
|
92
|
+
|
|
93
|
+
Auto Focus — drives ``IAutoFocus``
|
|
94
|
+
=====================================
|
|
95
|
+
Run or abort an autofocus sequence.
|
|
96
|
+
|
|
97
|
+
Acquisition — drives ``IAcquisition``
|
|
98
|
+
========================================
|
|
99
|
+
Acquire a target, abort an in-progress acquisition.
|
|
100
|
+
|
|
101
|
+
Auto Guiding — drives ``IAutoGuiding``
|
|
102
|
+
=========================================
|
|
103
|
+
Start/stop guiding and set the guide-exposure time.
|
|
104
|
+
|
|
105
|
+
Weather — drives ``IWeather``
|
|
106
|
+
================================
|
|
107
|
+
Live sensor values plus a history plot.
|
|
108
|
+
|
|
109
|
+
Video — drives ``IVideo``
|
|
110
|
+
============================
|
|
111
|
+
Live view (MJPEG/raw stream), grab a still, adjust exposure time/gain/image type.
|
|
112
|
+
|
|
113
|
+
Spectrograph — drives ``ISpectrograph``
|
|
114
|
+
==========================================
|
|
115
|
+
Grab a spectrum, abort an in-progress exposure.
|
|
116
|
+
|
|
117
|
+
Filter wheel — drives ``IFilters``
|
|
118
|
+
=====================================
|
|
119
|
+
Set the active filter.
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
Keyboard shortcuts
|
|
123
|
+
*******************
|
|
124
|
+
|
|
125
|
+
Global, not per-widget — bound with the ``Ctrl``/``Ctrl+Alt`` modifier specifically so no
|
|
126
|
+
text/numeric-entry widget can mistake one for ordinary input.
|
|
127
|
+
|
|
128
|
+
=============== ==========================================================
|
|
129
|
+
Shortcut Action
|
|
130
|
+
=============== ==========================================================
|
|
131
|
+
``Ctrl+1`` Switch to Shell (fixed)
|
|
132
|
+
``Ctrl+2`` Switch to Events (fixed)
|
|
133
|
+
``Ctrl+3`` Switch to Status (fixed)
|
|
134
|
+
``Ctrl+4``..``Ctrl+9``, ``Ctrl+0``
|
|
135
|
+
Switch to whichever module page is bound to that slot
|
|
136
|
+
(no-op if unbound or that module isn't connected)
|
|
137
|
+
``Ctrl+Alt+4``..``Ctrl+Alt+9``, ``Ctrl+Alt+0``
|
|
138
|
+
Bind the currently selected page to that slot, overwriting
|
|
139
|
+
any previous binding
|
|
140
|
+
=============== ==========================================================
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import contextlib
|
|
4
5
|
import logging
|
|
5
6
|
from typing import TYPE_CHECKING, Any, Callable, Type, TypeVar, overload
|
|
6
7
|
|
|
@@ -44,6 +45,13 @@ async def show_remote_error(parent: QtWidgets.QWidget, exception: Exception) ->
|
|
|
44
45
|
await QAsyncMessageBox.warning(parent, "Error", str(exception))
|
|
45
46
|
|
|
46
47
|
|
|
48
|
+
async def cancel_and_drain(task: asyncio.Task[Any]) -> None:
|
|
49
|
+
"""Cancel a task and await its unwind, swallowing the resulting CancelledError."""
|
|
50
|
+
task.cancel()
|
|
51
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
52
|
+
await task
|
|
53
|
+
|
|
54
|
+
|
|
47
55
|
class BaseWindow:
|
|
48
56
|
def __init__(self) -> None:
|
|
49
57
|
"""Base class for MainWindow and all widgets."""
|
|
@@ -169,6 +177,14 @@ class BaseWidget(BaseWindow, QtWidgets.QWidget): # type: ignore
|
|
|
169
177
|
# has it been initialized?
|
|
170
178
|
self._initialized = False
|
|
171
179
|
|
|
180
|
+
# memoized one-shot init task -- two rapid show/hide/show cycles must not run _init()
|
|
181
|
+
# twice concurrently (see _showEvent)
|
|
182
|
+
self._init_task: asyncio.Task[Any] | None = None
|
|
183
|
+
|
|
184
|
+
# keys of _init_once() sub-steps that already completed, so a retried _init() (see
|
|
185
|
+
# _showEvent) skips steps that already ran instead of re-subscribing to comm state
|
|
186
|
+
self._init_steps_done: set[str] = set()
|
|
187
|
+
|
|
172
188
|
# methods this GUI is permitted to invoke on self.module; None until fetched or if the
|
|
173
189
|
# fetch failed, meaning "treat everything as permitted"
|
|
174
190
|
self._permitted_methods: set[str] | None = None
|
|
@@ -251,6 +267,15 @@ class BaseWidget(BaseWindow, QtWidgets.QWidget): # type: ignore
|
|
|
251
267
|
in Comm._event_handlers forever, keeping this widget alive and still reacting to
|
|
252
268
|
events for as long as the app runs.
|
|
253
269
|
"""
|
|
270
|
+
# cancel a still-in-flight one-time _init() first: comm's own disconnect-triggered
|
|
271
|
+
# subscription cleanup (Comm._client_disconnected) runs before this method is called,
|
|
272
|
+
# so a subscribe_state() call inside _init_task that resolves afterwards would leak a
|
|
273
|
+
# subscription bound to this now-discarded widget -- the same stale-callback crash
|
|
274
|
+
# class this method otherwise guards against
|
|
275
|
+
if self._init_task is not None:
|
|
276
|
+
await cancel_and_drain(self._init_task)
|
|
277
|
+
self._init_task = None
|
|
278
|
+
|
|
254
279
|
for event_class, handler in self._registered_event_handlers:
|
|
255
280
|
await self.comm.unregister_event(event_class, handler)
|
|
256
281
|
self._registered_event_handlers.clear()
|
|
@@ -263,14 +288,44 @@ class BaseWidget(BaseWindow, QtWidgets.QWidget): # type: ignore
|
|
|
263
288
|
asyncio.create_task(self._showEvent(event))
|
|
264
289
|
|
|
265
290
|
async def _showEvent(self, event: QtGui.QShowEvent) -> None:
|
|
266
|
-
if self._initialized is False
|
|
267
|
-
|
|
268
|
-
|
|
291
|
+
if self._initialized is False:
|
|
292
|
+
if self._init_task is None:
|
|
293
|
+
self._init_task = asyncio.create_task(self._run_init())
|
|
294
|
+
try:
|
|
295
|
+
await self._init_task
|
|
296
|
+
except Exception:
|
|
297
|
+
# transient init failure (e.g. a comm RPC hiccup): log, and allow a retry on
|
|
298
|
+
# the next show instead of leaving the widget permanently marked initialized
|
|
299
|
+
log.exception("Init of %s failed; will retry on next show.", type(self).__name__)
|
|
300
|
+
self._init_task = None
|
|
269
301
|
|
|
270
302
|
if self._update_func:
|
|
271
303
|
# start update task
|
|
272
304
|
self._update_task = asyncio.create_task(self._update_loop())
|
|
273
305
|
|
|
306
|
+
async def _run_init(self) -> None:
|
|
307
|
+
await self._init()
|
|
308
|
+
self._initialized = True
|
|
309
|
+
|
|
310
|
+
async def _init(self) -> None:
|
|
311
|
+
"""Default no-op init. Widgets with one-time subscription/state setup override this;
|
|
312
|
+
_showEvent memoizes the call so it runs exactly once per widget lifetime."""
|
|
313
|
+
|
|
314
|
+
async def _init_once(self, key: str, coro_func: Callable[..., Coroutine[Any, Any, None]], *args: Any) -> None:
|
|
315
|
+
"""Run one sub-step of _init() at most once across retries.
|
|
316
|
+
|
|
317
|
+
_showEvent retries the whole _init() after a partial failure (e.g. one of several
|
|
318
|
+
gathered comm.subscribe_state() calls raising while its siblings already subscribed).
|
|
319
|
+
comm.subscribe_state() unconditionally appends to comm's subscription list with no
|
|
320
|
+
deduplication, so a sub-step that already succeeded must not run again on retry --
|
|
321
|
+
override _init() and wrap each independent, subscribing sub-step in this instead of
|
|
322
|
+
calling it directly.
|
|
323
|
+
"""
|
|
324
|
+
if key in self._init_steps_done:
|
|
325
|
+
return
|
|
326
|
+
await coro_func(*args)
|
|
327
|
+
self._init_steps_done.add(key)
|
|
328
|
+
|
|
274
329
|
def hideEvent(self, event: QtGui.QHideEvent) -> None:
|
|
275
330
|
# run in loop
|
|
276
331
|
asyncio.create_task(self._hide_event(event))
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import logging
|
|
2
3
|
from typing import Any
|
|
3
4
|
from PySide6 import QtWidgets, QtCore # type: ignore
|
|
@@ -38,6 +39,13 @@ from .qt.camerawidget_ui import Ui_CameraWidget
|
|
|
38
39
|
log = logging.getLogger(__name__)
|
|
39
40
|
|
|
40
41
|
|
|
42
|
+
# how long to wait for a module's first state value when initializing each control. The comm
|
|
43
|
+
# default is 10 s per interface, which would let a slow-publishing camera hold the page blank
|
|
44
|
+
# for ~70 s in the worst case; with this shorter timeout the control just keeps its default
|
|
45
|
+
# value and the subscription callback corrects it as soon as state arrives.
|
|
46
|
+
_WAIT_FOR_STATE_TIMEOUT = 2.0
|
|
47
|
+
|
|
48
|
+
|
|
41
49
|
class CameraWidget(BaseWidget, Ui_CameraWidget):
|
|
42
50
|
signal_update_gui = QtCore.Signal()
|
|
43
51
|
signal_new_image = QtCore.Signal(NewImageEvent, str)
|
|
@@ -108,6 +116,26 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
|
|
|
108
116
|
await self.add_to_sidebar(self.create_widget(TemperaturesWidget, module=self.module))
|
|
109
117
|
|
|
110
118
|
async def _init(self) -> None:
|
|
119
|
+
# every interface is initialized independently (caps -> state -> subscribe, in that
|
|
120
|
+
# order per interface), so run them concurrently instead of serially: a slow-publishing
|
|
121
|
+
# camera used to hold the page blank for up to ~70 s (7 interfaces x the 10 s default
|
|
122
|
+
# wait_for_state timeout); now each interface waits at most _WAIT_FOR_STATE_TIMEOUT and
|
|
123
|
+
# the others fill in regardless
|
|
124
|
+
await asyncio.gather(
|
|
125
|
+
self._init_once("window", self._init_window),
|
|
126
|
+
self._init_once("binning", self._init_binning),
|
|
127
|
+
self._init_once("gain", self._init_gain),
|
|
128
|
+
self._init_once("image_format", self._init_image_format),
|
|
129
|
+
self._init_once("image_type", self._init_image_type),
|
|
130
|
+
self._init_once("exposure", self._init_exposure),
|
|
131
|
+
self._init_once("exposure_time", self._init_exposure_time),
|
|
132
|
+
self._init_once("data_sequence", self._init_data_sequence),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# update GUI
|
|
136
|
+
self.signal_update_gui.emit()
|
|
137
|
+
|
|
138
|
+
async def _init_window(self) -> None:
|
|
111
139
|
# window
|
|
112
140
|
window_caps = await self.comm.get_capabilities(self.module, IWindow)
|
|
113
141
|
if window_caps is not None:
|
|
@@ -117,7 +145,7 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
|
|
|
117
145
|
self.spinWindowHeight.setMaximum(int(window_caps.full_frame_height))
|
|
118
146
|
async with self.comm.safe_proxy(self.module, IWindow) as proxy:
|
|
119
147
|
if proxy is not None:
|
|
120
|
-
state = await proxy.wait_for_state(IWindow)
|
|
148
|
+
state = await proxy.wait_for_state(IWindow, timeout=_WAIT_FOR_STATE_TIMEOUT)
|
|
121
149
|
if state is not None:
|
|
122
150
|
self.spinWindowLeft.setValue(state.x)
|
|
123
151
|
self.spinWindowTop.setValue(state.y)
|
|
@@ -129,6 +157,7 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
|
|
|
129
157
|
self.spinWindowWidth.setValue(window_caps.full_frame_width)
|
|
130
158
|
self.spinWindowHeight.setValue(window_caps.full_frame_height)
|
|
131
159
|
|
|
160
|
+
async def _init_binning(self) -> None:
|
|
132
161
|
# binning
|
|
133
162
|
binning_caps = await self.comm.get_capabilities(self.module, IBinning)
|
|
134
163
|
if binning_caps is not None:
|
|
@@ -137,20 +166,22 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
|
|
|
137
166
|
self.comboBinning.addItems(binnings)
|
|
138
167
|
async with self.comm.safe_proxy(self.module, IBinning) as proxy:
|
|
139
168
|
if proxy is not None:
|
|
140
|
-
state = await proxy.wait_for_state(IBinning)
|
|
169
|
+
state = await proxy.wait_for_state(IBinning, timeout=_WAIT_FOR_STATE_TIMEOUT)
|
|
141
170
|
if state is not None:
|
|
142
171
|
self.comboBinning.setCurrentText(f"{state.x}x{state.y}")
|
|
143
172
|
await self.comm.subscribe_state(self.module, IBinning, self._update_binning)
|
|
144
173
|
|
|
174
|
+
async def _init_gain(self) -> None:
|
|
145
175
|
# gain
|
|
146
176
|
async with self.comm.safe_proxy(self.module, IGain) as proxy:
|
|
147
177
|
if proxy is not None:
|
|
148
|
-
state = await proxy.wait_for_state(IGain)
|
|
178
|
+
state = await proxy.wait_for_state(IGain, timeout=_WAIT_FOR_STATE_TIMEOUT)
|
|
149
179
|
if state is not None:
|
|
150
180
|
self.spinGain.setValue(state.gain)
|
|
151
181
|
self.spinGainOffset.setValue(state.offset)
|
|
152
182
|
await self.comm.subscribe_state(self.module, IGain, self._update_gain)
|
|
153
183
|
|
|
184
|
+
async def _init_image_format(self) -> None:
|
|
154
185
|
# image format
|
|
155
186
|
image_format_caps = await self.comm.get_capabilities(self.module, IImageFormat)
|
|
156
187
|
if image_format_caps is not None:
|
|
@@ -159,44 +190,45 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
|
|
|
159
190
|
self.comboImageFormat.addItems([f.name for f in image_formats])
|
|
160
191
|
async with self.comm.safe_proxy(self.module, IImageFormat) as proxy:
|
|
161
192
|
if proxy is not None:
|
|
162
|
-
state = await proxy.wait_for_state(IImageFormat)
|
|
193
|
+
state = await proxy.wait_for_state(IImageFormat, timeout=_WAIT_FOR_STATE_TIMEOUT)
|
|
163
194
|
if state is not None:
|
|
164
195
|
self.comboImageFormat.setCurrentText(state.image_format.name)
|
|
165
196
|
await self.comm.subscribe_state(self.module, IImageFormat, self._update_image_format)
|
|
166
197
|
|
|
198
|
+
async def _init_image_type(self) -> None:
|
|
167
199
|
# image type
|
|
168
200
|
async with self.comm.safe_proxy(self.module, IImageType) as proxy:
|
|
169
201
|
if proxy is not None:
|
|
170
|
-
state = await proxy.wait_for_state(IImageType)
|
|
202
|
+
state = await proxy.wait_for_state(IImageType, timeout=_WAIT_FOR_STATE_TIMEOUT)
|
|
171
203
|
if state is not None:
|
|
172
204
|
self.comboImageType.setCurrentText(state.image_type.name)
|
|
173
205
|
await self.comm.subscribe_state(self.module, IImageType, self._update_image_type)
|
|
174
206
|
|
|
207
|
+
async def _init_exposure(self) -> None:
|
|
175
208
|
# exposure (status, progress, time left)
|
|
176
209
|
async with self.comm.safe_proxy(self.module, IExposure) as proxy:
|
|
177
210
|
if proxy is not None:
|
|
178
|
-
state = await proxy.wait_for_state(IExposure)
|
|
211
|
+
state = await proxy.wait_for_state(IExposure, timeout=_WAIT_FOR_STATE_TIMEOUT)
|
|
179
212
|
if state is not None:
|
|
180
213
|
self.exposure_status = state.status
|
|
181
214
|
self.exposure_progress = state.progress
|
|
182
215
|
self.exposure_time_left = state.exposure_time_left
|
|
183
216
|
await self.comm.subscribe_state(self.module, IExposure, self._update_exposure)
|
|
184
217
|
|
|
218
|
+
async def _init_exposure_time(self) -> None:
|
|
185
219
|
# exposure time
|
|
186
220
|
async with self.comm.safe_proxy(self.module, IExposureTime) as proxy:
|
|
187
221
|
if proxy is not None:
|
|
188
|
-
state = await proxy.wait_for_state(IExposureTime)
|
|
222
|
+
state = await proxy.wait_for_state(IExposureTime, timeout=_WAIT_FOR_STATE_TIMEOUT)
|
|
189
223
|
if state is not None:
|
|
190
224
|
self.spinExpTime.setValue(state.exposure_time)
|
|
191
225
|
await self.comm.subscribe_state(self.module, IExposureTime, self._update_exposure_time)
|
|
192
226
|
|
|
227
|
+
async def _init_data_sequence(self) -> None:
|
|
193
228
|
# data sequence
|
|
194
229
|
if await self.comm.has_proxy(self.module, IDataSequence):
|
|
195
230
|
await self.comm.subscribe_state(self.module, IDataSequence, self._update_sequence)
|
|
196
231
|
|
|
197
|
-
# update GUI
|
|
198
|
-
self.signal_update_gui.emit()
|
|
199
|
-
|
|
200
232
|
def _update_binning(self, state: BinningState):
|
|
201
233
|
self.comboBinning.setCurrentText(f"{state.x}x{state.y}")
|
|
202
234
|
|