pyobs-fli 2.0.0.dev10__tar.gz → 2.0.0.dev11__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_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/PKG-INFO +5 -2
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/pyobs_fli/flibase.py +7 -1
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/pyobs_fli/flicamera.py +8 -4
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/pyobs_fli/flidriver.pyx +14 -5
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/pyobs_fli/flifilterwheel.py +2 -4
- pyobs_fli-2.0.0.dev11/pyobs_fli/gui.py +229 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/pyproject.toml +12 -2
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/tests/test_flicamera.py +15 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/tests/test_flifilterwheel.py +16 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/uv.lock +557 -15
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/.github/dependabot.yml +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/.github/workflows/pypi.yml +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/.github/workflows/pyrefly.yml +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/.github/workflows/pytest.yml +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/.github/workflows/ruff.yml +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/.pre-commit-config.yaml +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/.readthedocs.yml +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/CHANGELOG.rst +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/CMakeLists.txt +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/LICENSE +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/README.md +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/docs/Makefile +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/docs/make.bat +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/docs/requirements.txt +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/docs/source/_static/pyobs.gif +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/docs/source/conf.py +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/docs/source/index.rst +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/Makefile +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/docxx.sty +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/libfli-camera-parport.c +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/libfli-camera-parport.h +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/libfli-camera-usb.c +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/libfli-camera-usb.h +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/libfli-camera.c +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/libfli-camera.h +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/libfli-debug.h +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/libfli-filter-focuser.c +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/libfli-filter-focuser.h +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/libfli-libfli.h +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/libfli-mem.c +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/libfli-mem.h +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/libfli-raw.c +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/libfli-raw.h +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/libfli.c +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/libfli.dxx +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/libfli.h +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/unix/bsd/libfli-usb-sys.c +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/unix/libfli-debug.c +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/unix/libfli-parport.h +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/unix/libfli-serial.c +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/unix/libfli-serial.h +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/unix/libfli-sys.c +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/unix/libfli-sys.h +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/unix/libfli-usb.c +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/unix/libfli-usb.h +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/unix/linux/fli_ioctl.h +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/unix/linux/fliusb_ioctl.h +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/unix/linux/libfli-parport.c +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/unix/linux/libfli-usb-sys.c +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/unix/osx/libfli-usb-sys.c +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/unix/osx/libfli-usb-sys.h +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/windows/libfli-debug.c +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/windows/libfli-parport.h +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/windows/libfli-serial.c +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/windows/libfli-serial.h +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/windows/libfli-sys.h +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/windows/libfli-usb.c +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/windows/libfli-usb.h +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/windows/libfli-windows-parport.c +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/windows/libfli-windows.c +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/windows/libfli.def +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/windows/libfli.rc +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/windows/libfli.sln +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/windows/libfli.vcproj +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/windows/libfli64.def +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/lib/windows/resource.h +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/poetry.lock +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/pyobs_fli/__init__.py +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/pyobs_fli/libfli.pxd +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/specs/plans/2026-08-16-nogil-fli-driver-calls.md +0 -0
- {pyobs_fli-2.0.0.dev10 → pyobs_fli-2.0.0.dev11}/tests/test_import.py +0 -0
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyobs-fli
|
|
3
|
-
Version: 2.0.0.
|
|
3
|
+
Version: 2.0.0.dev11
|
|
4
4
|
Summary: pyobs module for FLI cameras
|
|
5
5
|
Author-Email: Tim-Oliver Husser <thusser@uni-goettingen.de>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Requires-Python: >=3.11
|
|
8
8
|
Requires-Dist: astropy<9,>=7.0.1
|
|
9
9
|
Requires-Dist: numpy<3,>=2.2.5
|
|
10
|
-
Requires-Dist: pyobs-core<3,>=2.0.0.
|
|
10
|
+
Requires-Dist: pyobs-core<3,>=2.0.0.dev82
|
|
11
|
+
Provides-Extra: gui
|
|
12
|
+
Requires-Dist: pyobs-core[gui]<3,>=2.0.0.dev48; extra == "gui"
|
|
13
|
+
Requires-Dist: qasync>=0.27; extra == "gui"
|
|
11
14
|
|
|
@@ -52,6 +52,11 @@ class FliBaseMixin:
|
|
|
52
52
|
# keep alive
|
|
53
53
|
self.add_background_task(self._keep_alive) # type: ignore[attr-defined]
|
|
54
54
|
|
|
55
|
+
# forward anything left to the next class in the MRO -- lets a class combining this mixin
|
|
56
|
+
# with Module/BaseCamera (which must run first, since add_background_task above needs it)
|
|
57
|
+
# claim its own kwargs cooperatively instead of this mixin silently absorbing them
|
|
58
|
+
super().__init__(**kwargs) # type: ignore[call-arg]
|
|
59
|
+
|
|
55
60
|
@staticmethod
|
|
56
61
|
async def _run_blocking(func: Callable[[], None], timeout: float = _SDK_CALL_TIMEOUT) -> bool:
|
|
57
62
|
"""Run a blocking FLI SDK call in a daemon thread, so a hung call can't freeze the module.
|
|
@@ -156,13 +161,14 @@ class FliBaseMixin:
|
|
|
156
161
|
driver = self._driver
|
|
157
162
|
try:
|
|
158
163
|
await self._run_blocking_or_raise(driver.get_serial_string)
|
|
159
|
-
except ValueError:
|
|
164
|
+
except (ValueError, OSError):
|
|
160
165
|
# no? then reopen driver
|
|
161
166
|
log.warning("Lost connection to camera, reopening it.")
|
|
162
167
|
|
|
163
168
|
def _reopen() -> None:
|
|
164
169
|
driver.close()
|
|
165
170
|
self._driver = FliDriver(self._device)
|
|
171
|
+
self._driver.open()
|
|
166
172
|
|
|
167
173
|
await self._run_blocking_or_raise(_reopen)
|
|
168
174
|
|
|
@@ -24,7 +24,7 @@ log = logging.getLogger(__name__)
|
|
|
24
24
|
_READOUT_TIMEOUT = 60.0
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
class FliCamera(
|
|
27
|
+
class FliCamera(BaseCamera, FliBaseMixin, ICamera, IWindow, IBinning, ICooling, ITemperatures, IAbortable):
|
|
28
28
|
"""A pyobs module for FLI cameras."""
|
|
29
29
|
|
|
30
30
|
__module__ = "pyobs_fli"
|
|
@@ -35,8 +35,7 @@ class FliCamera(FliBaseMixin, BaseCamera, ICamera, IWindow, IBinning, ICooling,
|
|
|
35
35
|
Args:
|
|
36
36
|
setpoint: Cooling temperature setpoint.
|
|
37
37
|
"""
|
|
38
|
-
|
|
39
|
-
FliBaseMixin.__init__(self, dev_type=DeviceType.CAMERA, **kwargs)
|
|
38
|
+
super().__init__(dev_type=DeviceType.CAMERA, **kwargs)
|
|
40
39
|
|
|
41
40
|
self._temp_setpoint: float | None = setpoint
|
|
42
41
|
self._cooling_enabled = False
|
|
@@ -159,7 +158,12 @@ class FliCamera(FliBaseMixin, BaseCamera, ICamera, IWindow, IBinning, ICooling,
|
|
|
159
158
|
img[row, :] = driver.grab_row(width)
|
|
160
159
|
return img
|
|
161
160
|
|
|
162
|
-
|
|
161
|
+
try:
|
|
162
|
+
img = await self._run_blocking_or_raise(_readout, timeout=_READOUT_TIMEOUT)
|
|
163
|
+
except Exception:
|
|
164
|
+
log.error("Readout failed, cancelling exposure.")
|
|
165
|
+
await self._abort_exposure()
|
|
166
|
+
raise
|
|
163
167
|
|
|
164
168
|
def _get_headers() -> tuple[float, float, tuple[int, int, int, int]]:
|
|
165
169
|
return (
|
|
@@ -8,6 +8,8 @@ import numpy as np
|
|
|
8
8
|
cimport numpy as np
|
|
9
9
|
np.import_array()
|
|
10
10
|
|
|
11
|
+
from libc.string cimport memset
|
|
12
|
+
|
|
11
13
|
from .libfli cimport *
|
|
12
14
|
|
|
13
15
|
|
|
@@ -447,18 +449,22 @@ cdef class FliDriver:
|
|
|
447
449
|
"""
|
|
448
450
|
|
|
449
451
|
# variables
|
|
450
|
-
cdef char
|
|
451
|
-
cdef size_t len
|
|
452
|
+
cdef char model[1024]
|
|
452
453
|
cdef long res
|
|
453
454
|
|
|
455
|
+
# zero the buffer so FLIGetModel's NUL-terminated string is followed by known NULs, and
|
|
456
|
+
# bytes(model) (which uses the full 1024-byte static length, not the NUL) can be truncated
|
|
457
|
+
# safely below
|
|
458
|
+
memset(model, 0, sizeof(model))
|
|
459
|
+
|
|
454
460
|
# get it
|
|
455
461
|
with nogil:
|
|
456
|
-
res = FLIGetModel(self._device, model,
|
|
462
|
+
res = FLIGetModel(self._device, <char*>model, 1024)
|
|
457
463
|
if res != 0:
|
|
458
464
|
raise ValueError('Could not fetch model.')
|
|
459
465
|
|
|
460
466
|
# return it
|
|
461
|
-
return
|
|
467
|
+
return bytes(model).split(b'\x00')[0].decode('utf-8')
|
|
462
468
|
|
|
463
469
|
def get_serial_string(self) -> str:
|
|
464
470
|
"""Returns serial string for camera."""
|
|
@@ -467,6 +473,9 @@ cdef class FliDriver:
|
|
|
467
473
|
cdef char serial[1024]
|
|
468
474
|
cdef long res
|
|
469
475
|
|
|
476
|
+
# zero the buffer (same NUL-truncation reasoning as get_model above)
|
|
477
|
+
memset(serial, 0, sizeof(serial))
|
|
478
|
+
|
|
470
479
|
# get it
|
|
471
480
|
with nogil:
|
|
472
481
|
res = FLIGetSerialString(self._device, <char*>serial, 1024)
|
|
@@ -474,7 +483,7 @@ cdef class FliDriver:
|
|
|
474
483
|
raise ValueError('Could not fetch serial string.')
|
|
475
484
|
|
|
476
485
|
# return it
|
|
477
|
-
return bytes(serial).decode('utf-8')
|
|
486
|
+
return bytes(serial).split(b'\x00')[0].decode('utf-8')
|
|
478
487
|
|
|
479
488
|
def get_filter_pos(self) -> int:
|
|
480
489
|
"""Returns current filter position.
|
|
@@ -17,7 +17,7 @@ from pyobs_fli.flidriver import DeviceType
|
|
|
17
17
|
log = logging.getLogger(__name__)
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
class FliFilterWheel(
|
|
20
|
+
class FliFilterWheel(Module, FliBaseMixin, MotionStatusMixin, IFilters, IFitsHeaderBefore):
|
|
21
21
|
"""A pyobs module for FLI filter wheels."""
|
|
22
22
|
|
|
23
23
|
__module__ = "pyobs_fli"
|
|
@@ -28,9 +28,7 @@ class FliFilterWheel(FliBaseMixin, Module, MotionStatusMixin, IFilters, IFitsHea
|
|
|
28
28
|
Args:
|
|
29
29
|
filter_names: Names of filters.
|
|
30
30
|
"""
|
|
31
|
-
|
|
32
|
-
FliBaseMixin.__init__(self, dev_type=DeviceType.FILTERWHEEL, **kwargs)
|
|
33
|
-
MotionStatusMixin.__init__(self, motion_status_interfaces=["IFilters"])
|
|
31
|
+
super().__init__(dev_type=DeviceType.FILTERWHEEL, motion_status_interfaces=["IFilters"], **kwargs)
|
|
34
32
|
|
|
35
33
|
self._filter_names: list[list[str]] = (
|
|
36
34
|
[filter_names] if isinstance(filter_names[0], str) else filter_names # type: ignore[arg-type,list-item]
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""Standalone PySide6 GUI driving FliDriver directly, for testing a FLI camera without a full
|
|
2
|
+
pyobs module.
|
|
3
|
+
|
|
4
|
+
Unverified against real FLI hardware.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import sys
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import qasync # type: ignore[import-untyped]
|
|
14
|
+
from astropy.io import fits
|
|
15
|
+
from pyobs.utils.enums import ImageFormat
|
|
16
|
+
from pyobs.utils.gui.camera import (
|
|
17
|
+
BinningWidget,
|
|
18
|
+
DataDisplayWidget,
|
|
19
|
+
ExposeWidget,
|
|
20
|
+
ExposureTimeWidget,
|
|
21
|
+
ImageFormatWidget,
|
|
22
|
+
ListPickerDialog,
|
|
23
|
+
)
|
|
24
|
+
from pyobs.utils.gui.camera.windowingwidget import WindowingWidget
|
|
25
|
+
from PySide6 import QtCore, QtWidgets # type: ignore[import-untyped]
|
|
26
|
+
|
|
27
|
+
from .flidriver import FliDriver, FliTemperature # type: ignore[import-untyped]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class MainWindow(QtWidgets.QMainWindow):
|
|
31
|
+
def __init__(self, driver: FliDriver, binning, full_frame) -> None:
|
|
32
|
+
super().__init__()
|
|
33
|
+
self.setWindowTitle("FLI Camera")
|
|
34
|
+
|
|
35
|
+
# the driver is opened by async_main() before this window is built, so __init__ stays
|
|
36
|
+
# free of blocking SDK calls
|
|
37
|
+
self._driver = driver
|
|
38
|
+
self._sdk_lock = threading.Lock()
|
|
39
|
+
self._abort_event = asyncio.Event()
|
|
40
|
+
self._exposing = False
|
|
41
|
+
self._closing = False
|
|
42
|
+
|
|
43
|
+
central = QtWidgets.QWidget()
|
|
44
|
+
self.setCentralWidget(central)
|
|
45
|
+
global_layout = QtWidgets.QHBoxLayout(central)
|
|
46
|
+
|
|
47
|
+
controls = QtWidgets.QGroupBox("Controls")
|
|
48
|
+
global_layout.addWidget(controls)
|
|
49
|
+
layout = QtWidgets.QVBoxLayout(controls)
|
|
50
|
+
|
|
51
|
+
self._window_widget = WindowingWidget(full_frame[2], full_frame[3])
|
|
52
|
+
layout.addWidget(self._window_widget)
|
|
53
|
+
|
|
54
|
+
binnings = [(1, 1), (2, 2), (3, 3), (4, 4)]
|
|
55
|
+
self._binning_widget = BinningWidget(binnings)
|
|
56
|
+
self._binning_widget.binning_changed.connect(self._window_widget.set_binning)
|
|
57
|
+
if binning in binnings:
|
|
58
|
+
self._binning_widget.combo_binnings.setCurrentIndex(binnings.index(binning))
|
|
59
|
+
layout.addWidget(self._binning_widget)
|
|
60
|
+
|
|
61
|
+
self._format_widget = ImageFormatWidget([ImageFormat.INT16])
|
|
62
|
+
layout.addWidget(self._format_widget)
|
|
63
|
+
|
|
64
|
+
self._exposure_time = ExposureTimeWidget()
|
|
65
|
+
layout.addWidget(self._exposure_time)
|
|
66
|
+
|
|
67
|
+
self._expose_widget = ExposeWidget()
|
|
68
|
+
self._expose_widget.expose_clicked.connect(self._expose_clicked)
|
|
69
|
+
self._expose_widget.abort_clicked.connect(self._abort_clicked)
|
|
70
|
+
layout.addWidget(self._expose_widget)
|
|
71
|
+
|
|
72
|
+
temp_group = QtWidgets.QGroupBox("Temperature")
|
|
73
|
+
temp_layout = QtWidgets.QFormLayout(temp_group)
|
|
74
|
+
self._label_ccd = QtWidgets.QLabel("—")
|
|
75
|
+
self._label_base = QtWidgets.QLabel("—")
|
|
76
|
+
self._label_power = QtWidgets.QLabel("—")
|
|
77
|
+
temp_layout.addRow("CCD:", self._label_ccd)
|
|
78
|
+
temp_layout.addRow("Base:", self._label_base)
|
|
79
|
+
temp_layout.addRow("Cooler:", self._label_power)
|
|
80
|
+
layout.addWidget(temp_group)
|
|
81
|
+
layout.addStretch()
|
|
82
|
+
|
|
83
|
+
self._data_display = DataDisplayWidget()
|
|
84
|
+
global_layout.addWidget(self._data_display)
|
|
85
|
+
|
|
86
|
+
self._temp_timer = QtCore.QTimer()
|
|
87
|
+
self._temp_timer.timeout.connect(self._refresh_temp)
|
|
88
|
+
self._temp_timer.start(5000)
|
|
89
|
+
asyncio.ensure_future(self._refresh_temp())
|
|
90
|
+
|
|
91
|
+
def _run_blocking(self, func):
|
|
92
|
+
"""Run a blocking driver call on the default executor, serialized behind self._sdk_lock."""
|
|
93
|
+
loop = asyncio.get_running_loop()
|
|
94
|
+
|
|
95
|
+
def _wrapper():
|
|
96
|
+
with self._sdk_lock:
|
|
97
|
+
return func()
|
|
98
|
+
|
|
99
|
+
return loop.run_in_executor(None, _wrapper)
|
|
100
|
+
|
|
101
|
+
@qasync.asyncSlot() # type: ignore[misc]
|
|
102
|
+
async def _refresh_temp(self) -> None:
|
|
103
|
+
if self._exposing or self._closing:
|
|
104
|
+
return
|
|
105
|
+
try:
|
|
106
|
+
ccd, base, power = await self._run_blocking(
|
|
107
|
+
lambda: (
|
|
108
|
+
self._driver.get_temp(FliTemperature.CCD),
|
|
109
|
+
self._driver.get_temp(FliTemperature.BASE),
|
|
110
|
+
self._driver.get_cooler_power(),
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
self._label_ccd.setText(f"{ccd:.1f} °C")
|
|
114
|
+
self._label_base.setText(f"{base:.1f} °C")
|
|
115
|
+
self._label_power.setText(f"{power:.0f} %")
|
|
116
|
+
except Exception:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
@qasync.asyncSlot(int) # type: ignore[misc]
|
|
120
|
+
async def _expose_clicked(self, count: int) -> None:
|
|
121
|
+
left, top, width, height = self._window_widget.values
|
|
122
|
+
idx = self._binning_widget.combo_binnings.currentIndex()
|
|
123
|
+
xbin, ybin = self._binning_widget._binnings[idx] # noqa: SLF001
|
|
124
|
+
exposure_time = self._exposure_time.value
|
|
125
|
+
|
|
126
|
+
def _prepare() -> None:
|
|
127
|
+
self._driver.set_binning(xbin, ybin)
|
|
128
|
+
self._driver.set_window(left * xbin, top * ybin, width, height)
|
|
129
|
+
self._driver.init_exposure(True)
|
|
130
|
+
self._driver.set_exposure_time(int(exposure_time * 1000.0))
|
|
131
|
+
|
|
132
|
+
def _readout() -> np.ndarray:
|
|
133
|
+
img = np.zeros((height, width), dtype=np.uint16)
|
|
134
|
+
for row in range(height):
|
|
135
|
+
img[row, :] = self._driver.grab_row(width)
|
|
136
|
+
return img
|
|
137
|
+
|
|
138
|
+
self._exposing = True
|
|
139
|
+
try:
|
|
140
|
+
for i in range(count):
|
|
141
|
+
if self._abort_event.is_set():
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
self._expose_widget.start_exposure(exposure_time)
|
|
145
|
+
await self._run_blocking(_prepare)
|
|
146
|
+
await self._run_blocking(self._driver.start_exposure)
|
|
147
|
+
|
|
148
|
+
if await self._wait_exposure():
|
|
149
|
+
await self._run_blocking(self._driver.cancel_exposure)
|
|
150
|
+
break
|
|
151
|
+
|
|
152
|
+
data = await self._run_blocking(_readout)
|
|
153
|
+
|
|
154
|
+
image = fits.PrimaryHDU(data)
|
|
155
|
+
image.header["EXPTIME"] = (exposure_time, "Exposure time [s]")
|
|
156
|
+
image.header["XBINNING"] = (xbin, "Binning factor used on X axis")
|
|
157
|
+
image.header["YBINNING"] = (ybin, "Binning factor used on Y axis")
|
|
158
|
+
self._data_display.set_data(image)
|
|
159
|
+
self._expose_widget.set_exposures_left(count - i - 1)
|
|
160
|
+
|
|
161
|
+
self._expose_widget.set_exposures_left(0)
|
|
162
|
+
self._abort_event.clear()
|
|
163
|
+
finally:
|
|
164
|
+
self._exposing = False
|
|
165
|
+
|
|
166
|
+
async def _wait_exposure(self) -> bool:
|
|
167
|
+
"""Return True if the exposure was aborted, False if data is ready."""
|
|
168
|
+
|
|
169
|
+
def _wait() -> bool:
|
|
170
|
+
while True:
|
|
171
|
+
if self._abort_event.is_set():
|
|
172
|
+
return True
|
|
173
|
+
if self._driver.is_data_ready():
|
|
174
|
+
return False
|
|
175
|
+
time.sleep(0.01)
|
|
176
|
+
|
|
177
|
+
return await self._run_blocking(_wait)
|
|
178
|
+
|
|
179
|
+
def _abort_clicked(self) -> None:
|
|
180
|
+
self._abort_event.set()
|
|
181
|
+
|
|
182
|
+
def closeEvent(self, event) -> None: # type: ignore[override]
|
|
183
|
+
self._temp_timer.stop()
|
|
184
|
+
# signal an in-flight exposure to stop, then wait for any SDK call in progress and close
|
|
185
|
+
self._closing = True
|
|
186
|
+
self._abort_event.set()
|
|
187
|
+
with self._sdk_lock:
|
|
188
|
+
self._driver.close()
|
|
189
|
+
super().closeEvent(event)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
async def async_main(app: QtWidgets.QApplication) -> None:
|
|
193
|
+
devices = FliDriver.list_devices()
|
|
194
|
+
if not devices:
|
|
195
|
+
QtWidgets.QMessageBox.critical(None, "Error", "No FLI camera found.")
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
if len(devices) > 1:
|
|
199
|
+
picker = ListPickerDialog([d.name.decode("utf-8") for d in devices])
|
|
200
|
+
if picker.exec() != QtWidgets.QDialog.DialogCode.Accepted:
|
|
201
|
+
return
|
|
202
|
+
device = devices[picker.comboBox().currentIndex()]
|
|
203
|
+
else:
|
|
204
|
+
device = devices[0]
|
|
205
|
+
|
|
206
|
+
# open the driver off the event loop before building the window, so MainWindow.__init__ stays
|
|
207
|
+
# free of blocking SDK calls
|
|
208
|
+
loop = asyncio.get_running_loop()
|
|
209
|
+
driver = await loop.run_in_executor(None, FliDriver, device)
|
|
210
|
+
await loop.run_in_executor(None, driver.open)
|
|
211
|
+
_, binning = await loop.run_in_executor(None, driver.get_window_binning)
|
|
212
|
+
full_frame = await loop.run_in_executor(None, driver.get_full_frame)
|
|
213
|
+
|
|
214
|
+
app_close_event = asyncio.Event()
|
|
215
|
+
app.aboutToQuit.connect(app_close_event.set)
|
|
216
|
+
|
|
217
|
+
window = MainWindow(driver, binning, full_frame)
|
|
218
|
+
window.show()
|
|
219
|
+
|
|
220
|
+
await app_close_event.wait()
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def main() -> None:
|
|
224
|
+
app = QtWidgets.QApplication(sys.argv)
|
|
225
|
+
asyncio.run(async_main(app), loop_factory=qasync.QEventLoop) # type: ignore[arg-type]
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
if __name__ == "__main__":
|
|
229
|
+
main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pyobs-fli"
|
|
3
|
-
version = "2.0.0.
|
|
3
|
+
version = "2.0.0.dev11"
|
|
4
4
|
description = "pyobs module for FLI cameras"
|
|
5
5
|
authors = [{ name = "Tim-Oliver Husser", email = "thusser@uni-goettingen.de" }]
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -8,7 +8,13 @@ license = "MIT"
|
|
|
8
8
|
dependencies = [
|
|
9
9
|
"astropy>=7.0.1,<9",
|
|
10
10
|
"numpy>=2.2.5,<3",
|
|
11
|
-
"pyobs-core>=2.0.0.
|
|
11
|
+
"pyobs-core>=2.0.0.dev82,<3",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.optional-dependencies]
|
|
15
|
+
gui = [
|
|
16
|
+
"pyobs-core[gui]>=2.0.0.dev48,<3",
|
|
17
|
+
"qasync>=0.27",
|
|
12
18
|
]
|
|
13
19
|
|
|
14
20
|
[dependency-groups]
|
|
@@ -21,6 +27,9 @@ dev = [
|
|
|
21
27
|
"pytest-asyncio>=0.24.0",
|
|
22
28
|
]
|
|
23
29
|
|
|
30
|
+
[project.scripts]
|
|
31
|
+
fli-gui = "pyobs_fli.gui:main"
|
|
32
|
+
|
|
24
33
|
[tool.scikit-build]
|
|
25
34
|
wheel.packages = ["pyobs_fli"]
|
|
26
35
|
|
|
@@ -51,6 +60,7 @@ select = ["E", "F", "W", "I", "UP", "G"]
|
|
|
51
60
|
[tool.pyrefly]
|
|
52
61
|
python-version = "3.11"
|
|
53
62
|
ignore-missing-imports = []
|
|
63
|
+
project-excludes = ["pyobs_fli/gui.py"]
|
|
54
64
|
|
|
55
65
|
[tool.pytest.ini_options]
|
|
56
66
|
asyncio_mode = "strict"
|
|
@@ -23,6 +23,21 @@ def test_constructor_defaults() -> None:
|
|
|
23
23
|
assert camera._driver is None
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def test_constructor_threads_fli_kwargs_cooperatively() -> None:
|
|
27
|
+
"""Regression test for the cooperative-super()-chain fix: FliCamera(BaseCamera, FliBaseMixin,
|
|
28
|
+
...) used to call BaseCamera.__init__ and FliBaseMixin.__init__ explicitly with the same
|
|
29
|
+
unfiltered kwargs -- FliBaseMixin-only kwargs like dev_path must still reach FliBaseMixin
|
|
30
|
+
through the single super().__init__() call, not get lost or leak to object.__init__()."""
|
|
31
|
+
from pyobs_fli.flidriver import DeviceType
|
|
32
|
+
|
|
33
|
+
camera = FliCamera(dev_name="cam1", dev_path="/dev/fliusb0", keep_alive_ping=5)
|
|
34
|
+
assert camera._dev_type == DeviceType.CAMERA
|
|
35
|
+
assert camera._dev_name == "cam1"
|
|
36
|
+
assert camera._dev_path == "/dev/fliusb0"
|
|
37
|
+
assert camera._keep_alive_ping == 5
|
|
38
|
+
assert camera._driver is None
|
|
39
|
+
|
|
40
|
+
|
|
26
41
|
@pytest.mark.asyncio
|
|
27
42
|
async def test_set_window() -> None:
|
|
28
43
|
camera = FliCamera()
|
|
@@ -19,6 +19,22 @@ def test_constructor_single_wheel() -> None:
|
|
|
19
19
|
assert wheel._filter_names == [["A", "B", "C"]]
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
def test_constructor_threads_fli_kwargs_cooperatively() -> None:
|
|
23
|
+
"""Regression test for the cooperative-super()-chain fix: FliFilterWheel(Module, FliBaseMixin,
|
|
24
|
+
MotionStatusMixin, ...) used to call Module.__init__, FliBaseMixin.__init__ and
|
|
25
|
+
MotionStatusMixin.__init__ explicitly with the same unfiltered kwargs -- live pyobs-monet
|
|
26
|
+
configs set dev_path explicitly, which must still reach FliBaseMixin through the single
|
|
27
|
+
super().__init__() call, not get lost or leak to object.__init__()."""
|
|
28
|
+
from pyobs_fli.flidriver import DeviceType
|
|
29
|
+
|
|
30
|
+
wheel = FliFilterWheel(filter_names=["A", "B", "C"], dev_path="/dev/fliusb0", keep_alive_ping=5)
|
|
31
|
+
assert wheel._dev_type == DeviceType.FILTERWHEEL
|
|
32
|
+
assert wheel._dev_path == "/dev/fliusb0"
|
|
33
|
+
assert wheel._keep_alive_ping == 5
|
|
34
|
+
assert wheel._driver is None
|
|
35
|
+
assert getattr(wheel, "_MotionStatusMixin__motion_status_interfaces") == ["IFilters"]
|
|
36
|
+
|
|
37
|
+
|
|
22
38
|
def test_constructor_two_wheels() -> None:
|
|
23
39
|
wheel = FliFilterWheel(filter_names=_TWO_WHEELS)
|
|
24
40
|
assert wheel._filter_names == _TWO_WHEELS
|