pyobs-fli 2.0.0.dev4__tar.gz → 2.0.0.dev6__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.dev6/.github/dependabot.yml +10 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/PKG-INFO +3 -3
- pyobs_fli-2.0.0.dev6/pyobs_fli/flibase.py +172 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/pyobs_fli/flicamera.py +74 -27
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/pyobs_fli/flifilterwheel.py +13 -6
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/pyproject.toml +4 -4
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/uv.lock +478 -511
- pyobs_fli-2.0.0.dev4/pyobs_fli/flibase.py +0 -107
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/.github/workflows/pypi.yml +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/.github/workflows/ruff.yml +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/.pre-commit-config.yaml +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/.readthedocs.yml +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/CHANGELOG.rst +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/CMakeLists.txt +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/LICENSE +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/README.md +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/docs/Makefile +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/docs/make.bat +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/docs/requirements.txt +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/docs/source/_static/pyobs.gif +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/docs/source/conf.py +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/docs/source/index.rst +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/Makefile +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/docxx.sty +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli-camera-parport.c +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli-camera-parport.h +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli-camera-usb.c +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli-camera-usb.h +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli-camera.c +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli-camera.h +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli-debug.h +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli-filter-focuser.c +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli-filter-focuser.h +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli-libfli.h +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli-mem.c +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli-mem.h +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli-raw.c +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli-raw.h +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli.c +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli.dxx +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli.h +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/bsd/libfli-usb-sys.c +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/libfli-debug.c +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/libfli-parport.h +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/libfli-serial.c +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/libfli-serial.h +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/libfli-sys.c +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/libfli-sys.h +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/libfli-usb.c +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/libfli-usb.h +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/linux/fli_ioctl.h +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/linux/fliusb_ioctl.h +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/linux/libfli-parport.c +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/linux/libfli-usb-sys.c +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/osx/libfli-usb-sys.c +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/osx/libfli-usb-sys.h +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/libfli-debug.c +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/libfli-parport.h +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/libfli-serial.c +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/libfli-serial.h +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/libfli-sys.h +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/libfli-usb.c +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/libfli-usb.h +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/libfli-windows-parport.c +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/libfli-windows.c +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/libfli.def +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/libfli.rc +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/libfli.sln +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/libfli.vcproj +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/libfli64.def +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/resource.h +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/poetry.lock +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/pyobs_fli/__init__.py +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/pyobs_fli/flidriver.pyx +0 -0
- {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/pyobs_fli/libfli.pxd +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyobs-fli
|
|
3
|
-
Version: 2.0.0.
|
|
3
|
+
Version: 2.0.0.dev6
|
|
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.14,>=3.11
|
|
8
|
-
Requires-Dist: astropy<
|
|
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.dev48
|
|
11
11
|
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import threading
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Any, TypeVar, cast
|
|
6
|
+
|
|
7
|
+
from pyobs_fli.flidriver import DeviceType
|
|
8
|
+
|
|
9
|
+
log = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
_T = TypeVar("_T")
|
|
12
|
+
|
|
13
|
+
# FLI SDK calls are blocking and are made directly on the event loop thread (see _run_blocking).
|
|
14
|
+
# If the device has gone unresponsive, they can hang indefinitely, so they're bounded with a
|
|
15
|
+
# timeout rather than let a single dead device freeze the whole module.
|
|
16
|
+
_SDK_CALL_TIMEOUT = 5.0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FliBaseMixin:
|
|
20
|
+
"""A base class for pyobs module for FLI devices."""
|
|
21
|
+
|
|
22
|
+
__module__ = "pyobs_fli"
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
dev_type: DeviceType,
|
|
27
|
+
dev_name: str | None = None,
|
|
28
|
+
dev_path: str | None = None,
|
|
29
|
+
keep_alive_ping: int = 10,
|
|
30
|
+
**kwargs: Any,
|
|
31
|
+
):
|
|
32
|
+
"""Initializes a new FliCamera.
|
|
33
|
+
|
|
34
|
+
If neither dev_name nor dev_path are given, the first found device is used.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
dev_type: Device type.
|
|
38
|
+
dev_name: Optional name for device.
|
|
39
|
+
dev_path: Optional path to device.
|
|
40
|
+
keep_alive_ping: Interval for keep alive ping.
|
|
41
|
+
"""
|
|
42
|
+
from .flidriver import FliDriver # type: ignore
|
|
43
|
+
|
|
44
|
+
# variables
|
|
45
|
+
self._dev_type = dev_type
|
|
46
|
+
self._dev_name = dev_name
|
|
47
|
+
self._dev_path = dev_path
|
|
48
|
+
self._keep_alive_ping = keep_alive_ping
|
|
49
|
+
self._driver: FliDriver | None = None
|
|
50
|
+
self._device: Any | None = None
|
|
51
|
+
|
|
52
|
+
# keep alive
|
|
53
|
+
self.add_background_task(self._keep_alive) # type: ignore[attr-defined]
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
async def _run_blocking(func: Callable[[], None], timeout: float = _SDK_CALL_TIMEOUT) -> bool:
|
|
57
|
+
"""Run a blocking FLI SDK call in a daemon thread, so a hung call can't freeze the module.
|
|
58
|
+
|
|
59
|
+
A plain executor isn't used here, since its worker threads are non-daemon and Python joins
|
|
60
|
+
them on interpreter shutdown -- a hung call would then just move the freeze to process exit.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
True if func completed within timeout, False if it's still running in the background.
|
|
64
|
+
"""
|
|
65
|
+
loop = asyncio.get_running_loop()
|
|
66
|
+
future: asyncio.Future[None] = loop.create_future()
|
|
67
|
+
|
|
68
|
+
def _wrapper() -> None:
|
|
69
|
+
try:
|
|
70
|
+
func()
|
|
71
|
+
finally:
|
|
72
|
+
loop.call_soon_threadsafe(future.set_result, None)
|
|
73
|
+
|
|
74
|
+
threading.Thread(target=_wrapper, daemon=True).start()
|
|
75
|
+
try:
|
|
76
|
+
await asyncio.wait_for(future, timeout=timeout)
|
|
77
|
+
return True
|
|
78
|
+
except TimeoutError:
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
async def _run_blocking_or_raise(self, func: Callable[[], _T], timeout: float = _SDK_CALL_TIMEOUT) -> _T:
|
|
82
|
+
"""Run a blocking FLI SDK call in a thread, returning its result or re-raising what it raised.
|
|
83
|
+
|
|
84
|
+
Unlike _run_blocking(), this also carries the callable's return value/exception back to the
|
|
85
|
+
caller -- several FLI calls drive control flow via their return value or a raised ValueError
|
|
86
|
+
(e.g. device enumeration, readout), which a bare fire-and-forget thread call would otherwise
|
|
87
|
+
silently lose.
|
|
88
|
+
"""
|
|
89
|
+
outcome: list[Any] = []
|
|
90
|
+
|
|
91
|
+
def _wrapper() -> None:
|
|
92
|
+
try:
|
|
93
|
+
outcome.append(func())
|
|
94
|
+
except BaseException as e:
|
|
95
|
+
outcome.append(e)
|
|
96
|
+
|
|
97
|
+
if not await self._run_blocking(_wrapper, timeout=timeout):
|
|
98
|
+
raise TimeoutError(f"Timed out waiting for FLI SDK call after {timeout}s.")
|
|
99
|
+
value = outcome[0]
|
|
100
|
+
if isinstance(value, BaseException):
|
|
101
|
+
raise value
|
|
102
|
+
return cast(_T, value)
|
|
103
|
+
|
|
104
|
+
async def open(self) -> None:
|
|
105
|
+
"""Open module."""
|
|
106
|
+
from .flidriver import FliDriver
|
|
107
|
+
|
|
108
|
+
def _open() -> None:
|
|
109
|
+
# list devices
|
|
110
|
+
devices = FliDriver.list_devices(self._dev_type)
|
|
111
|
+
if len(devices) == 0:
|
|
112
|
+
raise ValueError("No FLI device found.")
|
|
113
|
+
|
|
114
|
+
# do we have a device name or path given?
|
|
115
|
+
if self._dev_name is not None or self._dev_path is not None:
|
|
116
|
+
# yes, find matching device
|
|
117
|
+
for dev in devices:
|
|
118
|
+
if dev.name.decode("utf-8") == self._dev_name or dev.filename.decode("utf-8") == self._dev_path:
|
|
119
|
+
self._device = dev
|
|
120
|
+
break
|
|
121
|
+
else:
|
|
122
|
+
raise ValueError("No matching device found, check dev_name/dev_path.")
|
|
123
|
+
|
|
124
|
+
else:
|
|
125
|
+
# no, just pick first one
|
|
126
|
+
self._device = devices[0]
|
|
127
|
+
|
|
128
|
+
# open driver
|
|
129
|
+
log.info(
|
|
130
|
+
'Opening connection to "%s" at %s...',
|
|
131
|
+
self._device.name.decode("utf-8"),
|
|
132
|
+
self._device.filename.decode("utf-8"),
|
|
133
|
+
)
|
|
134
|
+
self._driver = FliDriver(self._device)
|
|
135
|
+
self._driver.open()
|
|
136
|
+
|
|
137
|
+
await self._run_blocking_or_raise(_open)
|
|
138
|
+
|
|
139
|
+
async def close(self) -> None:
|
|
140
|
+
# not open?
|
|
141
|
+
if self._driver is not None:
|
|
142
|
+
# close connection
|
|
143
|
+
driver = self._driver
|
|
144
|
+
if not await self._run_blocking(driver.close):
|
|
145
|
+
log.error("Timed out closing FLI device after %.1fs.", _SDK_CALL_TIMEOUT)
|
|
146
|
+
self._driver = None
|
|
147
|
+
|
|
148
|
+
async def _keep_alive(self) -> None:
|
|
149
|
+
"""Keep connection to camera alive."""
|
|
150
|
+
from .flidriver import FliDriver
|
|
151
|
+
|
|
152
|
+
while True:
|
|
153
|
+
# is there a valid driver?
|
|
154
|
+
if self._driver is not None:
|
|
155
|
+
# then we should be able to call it
|
|
156
|
+
driver = self._driver
|
|
157
|
+
try:
|
|
158
|
+
await self._run_blocking_or_raise(driver.get_serial_string)
|
|
159
|
+
except ValueError:
|
|
160
|
+
# no? then reopen driver
|
|
161
|
+
log.warning("Lost connection to camera, reopening it.")
|
|
162
|
+
|
|
163
|
+
def _reopen() -> None:
|
|
164
|
+
driver.close()
|
|
165
|
+
self._driver = FliDriver(self._device)
|
|
166
|
+
|
|
167
|
+
await self._run_blocking_or_raise(_reopen)
|
|
168
|
+
|
|
169
|
+
await asyncio.sleep(self._keep_alive_ping)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
__all__ = ["FliBaseMixin"]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
3
|
import math
|
|
4
|
+
import time
|
|
4
5
|
from datetime import UTC, datetime
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
@@ -12,6 +13,7 @@ from pyobs.interfaces.ICooling import CoolingState
|
|
|
12
13
|
from pyobs.interfaces.ITemperatures import SensorReading, TemperaturesState
|
|
13
14
|
from pyobs.interfaces.IWindow import WindowCapabilities, WindowState
|
|
14
15
|
from pyobs.modules.camera.basecamera import BaseCamera
|
|
16
|
+
from pyobs.utils import exceptions as exc
|
|
15
17
|
from pyobs.utils.enums import ExposureStatus
|
|
16
18
|
|
|
17
19
|
from .flibase import FliBaseMixin
|
|
@@ -19,6 +21,8 @@ from .flidriver import DeviceType
|
|
|
19
21
|
|
|
20
22
|
log = logging.getLogger(__name__)
|
|
21
23
|
|
|
24
|
+
_READOUT_TIMEOUT = 60.0
|
|
25
|
+
|
|
22
26
|
|
|
23
27
|
class FliCamera(FliBaseMixin, BaseCamera, ICamera, IWindow, IBinning, ICooling, ITemperatures, IAbortable):
|
|
24
28
|
"""A pyobs module for FLI cameras."""
|
|
@@ -50,11 +54,16 @@ class FliCamera(FliBaseMixin, BaseCamera, ICamera, IWindow, IBinning, ICooling,
|
|
|
50
54
|
if self._driver is None:
|
|
51
55
|
raise ValueError("No driver found.")
|
|
52
56
|
|
|
53
|
-
|
|
54
|
-
log.info("Connected to camera with serial number: %s", serial)
|
|
57
|
+
driver = self._driver
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
def _get_info() -> tuple[str, tuple[int, int, int, int], tuple[int, int], tuple[int, int, int, int]]:
|
|
60
|
+
serial = driver.get_serial_string()
|
|
61
|
+
window, binning = driver.get_window_binning()
|
|
62
|
+
full_frame = driver.get_full_frame()
|
|
63
|
+
return serial, window, binning, full_frame
|
|
64
|
+
|
|
65
|
+
serial, self._window, self._binning, self._full_frame = await self._run_blocking_or_raise(_get_info)
|
|
66
|
+
log.info("Connected to camera with serial number: %s", serial)
|
|
58
67
|
|
|
59
68
|
if self._temp_setpoint is not None:
|
|
60
69
|
await self.set_cooling(True, self._temp_setpoint)
|
|
@@ -106,9 +115,9 @@ class FliCamera(FliBaseMixin, BaseCamera, ICamera, IWindow, IBinning, ICooling,
|
|
|
106
115
|
|
|
107
116
|
if self._driver is None:
|
|
108
117
|
raise ValueError("No camera driver.")
|
|
118
|
+
driver = self._driver
|
|
109
119
|
|
|
110
120
|
log.info("Set binning to %dx%d.", self._binning[0], self._binning[1])
|
|
111
|
-
self._driver.set_binning(*self._binning)
|
|
112
121
|
|
|
113
122
|
width = int(math.floor(self._window[2]) / self._binning[0])
|
|
114
123
|
height = int(math.floor(self._window[3]) / self._binning[1])
|
|
@@ -121,32 +130,50 @@ class FliCamera(FliBaseMixin, BaseCamera, ICamera, IWindow, IBinning, ICooling,
|
|
|
121
130
|
self._window[0],
|
|
122
131
|
self._window[1],
|
|
123
132
|
)
|
|
124
|
-
self._driver.set_window(self._window[0], self._window[1], width, height)
|
|
125
133
|
|
|
126
|
-
|
|
127
|
-
|
|
134
|
+
def _prepare() -> None:
|
|
135
|
+
driver.set_binning(*self._binning)
|
|
136
|
+
driver.set_window(self._window[0], self._window[1], width, height)
|
|
137
|
+
driver.init_exposure(open_shutter)
|
|
138
|
+
driver.set_exposure_time(int(exposure_time * 1000.0))
|
|
139
|
+
|
|
140
|
+
await self._run_blocking_or_raise(_prepare)
|
|
128
141
|
|
|
129
142
|
log.info(
|
|
130
143
|
"Starting exposure with %s shutter for %.2f seconds...", "open" if open_shutter else "closed", exposure_time
|
|
131
144
|
)
|
|
132
145
|
date_obs = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S.%f")
|
|
133
146
|
|
|
134
|
-
self.
|
|
147
|
+
await self._run_blocking_or_raise(driver.start_exposure)
|
|
135
148
|
await self._wait_exposure(abort_event, exposure_time, open_shutter)
|
|
136
149
|
|
|
137
150
|
log.info("Exposure finished, reading out...")
|
|
138
151
|
await self._change_exposure_status(ExposureStatus.READOUT)
|
|
139
152
|
width = int(math.floor(self._window[2] / self._binning[0]))
|
|
140
153
|
height = int(math.floor(self._window[3] / self._binning[1]))
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
img
|
|
154
|
+
|
|
155
|
+
def _readout() -> np.ndarray:
|
|
156
|
+
img = np.zeros((height, width), dtype=np.uint16)
|
|
157
|
+
for row in range(height):
|
|
158
|
+
img[row, :] = driver.grab_row(width)
|
|
159
|
+
return img
|
|
160
|
+
|
|
161
|
+
img = await self._run_blocking_or_raise(_readout, timeout=_READOUT_TIMEOUT)
|
|
162
|
+
|
|
163
|
+
def _get_headers() -> tuple[float, float, tuple[int, int, int, int]]:
|
|
164
|
+
return (
|
|
165
|
+
driver.get_temp(FliTemperature.CCD),
|
|
166
|
+
driver.get_cooler_power(),
|
|
167
|
+
driver.get_visible_frame(),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
ccd_temp, cooler_power, visible_frame = await self._run_blocking_or_raise(_get_headers)
|
|
144
171
|
|
|
145
172
|
image = Image(img) # type: ignore[arg-type]
|
|
146
173
|
image.header["DATE-OBS"] = (date_obs, "Date and time of start of exposure")
|
|
147
174
|
image.header["EXPTIME"] = (exposure_time, "Exposure time [s]")
|
|
148
|
-
image.header["DET-TEMP"] = (
|
|
149
|
-
image.header["DET-COOL"] = (
|
|
175
|
+
image.header["DET-TEMP"] = (ccd_temp, "CCD temperature [C]")
|
|
176
|
+
image.header["DET-COOL"] = (cooler_power, "Cooler power [percent]")
|
|
150
177
|
image.header["DET-TSET"] = (self._temp_setpoint, "Cooler setpoint [C]")
|
|
151
178
|
image.header["INSTRUME"] = (self._driver.name, "Name of instrument")
|
|
152
179
|
image.header["XBINNING"] = image.header["DET-BIN1"] = (self._binning[0], "Binning factor used on X axis")
|
|
@@ -157,24 +184,32 @@ class FliCamera(FliBaseMixin, BaseCamera, ICamera, IWindow, IBinning, ICooling,
|
|
|
157
184
|
image.header["DATAMAX"] = (float(np.max(img)), "Maximum data value")
|
|
158
185
|
image.header["DATAMEAN"] = (float(np.mean(img)), "Mean data value")
|
|
159
186
|
|
|
160
|
-
self.set_biassec_trimsec(image.header, *
|
|
187
|
+
self.set_biassec_trimsec(image.header, *visible_frame)
|
|
161
188
|
|
|
162
189
|
log.info("Readout finished.")
|
|
163
190
|
return image
|
|
164
191
|
|
|
165
192
|
async def _wait_exposure(self, abort_event: asyncio.Event, exposure_time: float, open_shutter: bool) -> None:
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
193
|
+
if self._driver is None:
|
|
194
|
+
raise ValueError("No camera driver.")
|
|
195
|
+
driver = self._driver
|
|
196
|
+
|
|
197
|
+
def _wait() -> bool:
|
|
198
|
+
while True:
|
|
199
|
+
if driver.is_exposing():
|
|
200
|
+
return not abort_event.is_set()
|
|
201
|
+
if abort_event.is_set():
|
|
202
|
+
return False
|
|
203
|
+
time.sleep(0.01)
|
|
204
|
+
|
|
205
|
+
if not await self._run_blocking_or_raise(_wait, timeout=exposure_time + 30):
|
|
206
|
+
await self._change_exposure_status(ExposureStatus.IDLE)
|
|
207
|
+
raise exc.AbortedError("Aborted exposure.")
|
|
173
208
|
|
|
174
209
|
async def _abort_exposure(self) -> None:
|
|
175
210
|
if self._driver is None:
|
|
176
211
|
raise ValueError("No camera driver.")
|
|
177
|
-
self._driver.cancel_exposure
|
|
212
|
+
await self._run_blocking_or_raise(self._driver.cancel_exposure)
|
|
178
213
|
|
|
179
214
|
async def set_cooling(self, enabled: bool, setpoint: float, **kwargs: Any) -> None:
|
|
180
215
|
"""Enables/disables cooling and sets setpoint."""
|
|
@@ -188,7 +223,12 @@ class FliCamera(FliBaseMixin, BaseCamera, ICamera, IWindow, IBinning, ICooling,
|
|
|
188
223
|
|
|
189
224
|
self._temp_setpoint = setpoint if enabled else None
|
|
190
225
|
self._cooling_enabled = enabled
|
|
191
|
-
self._driver
|
|
226
|
+
driver = self._driver
|
|
227
|
+
|
|
228
|
+
def _set() -> None:
|
|
229
|
+
driver.set_temperature(float(setpoint) if setpoint is not None else 20.0)
|
|
230
|
+
|
|
231
|
+
await self._run_blocking_or_raise(_set)
|
|
192
232
|
await self.comm.set_state(
|
|
193
233
|
ICooling, CoolingState(setpoint=setpoint if setpoint is not None else 20.0, power=None, enabled=enabled)
|
|
194
234
|
)
|
|
@@ -200,9 +240,16 @@ class FliCamera(FliBaseMixin, BaseCamera, ICamera, IWindow, IBinning, ICooling,
|
|
|
200
240
|
while True:
|
|
201
241
|
try:
|
|
202
242
|
if self._driver is not None:
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
243
|
+
driver = self._driver
|
|
244
|
+
|
|
245
|
+
def _poll() -> tuple[float, float, float]:
|
|
246
|
+
return (
|
|
247
|
+
driver.get_temp(FliTemperature.CCD),
|
|
248
|
+
driver.get_temp(FliTemperature.BASE),
|
|
249
|
+
driver.get_cooler_power(),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
t_ccd, t_base, power = await self._run_blocking_or_raise(_poll)
|
|
206
253
|
setpoint = self._temp_setpoint if self._temp_setpoint is not None else 20.0
|
|
207
254
|
await self.comm.set_state(
|
|
208
255
|
ICooling, CoolingState(setpoint=setpoint, power=round(power), enabled=self._cooling_enabled)
|
|
@@ -4,7 +4,7 @@ from typing import Any
|
|
|
4
4
|
|
|
5
5
|
import pyobs.utils.exceptions as exc
|
|
6
6
|
from pyobs.events import FilterChangedEvent
|
|
7
|
-
from pyobs.interfaces import IFilters, IFitsHeaderBefore, IReady
|
|
7
|
+
from pyobs.interfaces import FitsHeaderEntry, IFilters, IFitsHeaderBefore, IReady
|
|
8
8
|
from pyobs.interfaces.IFilters import FiltersCapabilities, FilterState
|
|
9
9
|
from pyobs.interfaces.IReady import ReadyState
|
|
10
10
|
from pyobs.mixins import MotionStatusMixin
|
|
@@ -47,7 +47,8 @@ class FliFilterWheel(FliBaseMixin, Module, MotionStatusMixin, IFilters, IFitsHea
|
|
|
47
47
|
if self._driver is None:
|
|
48
48
|
raise ValueError("No driver found.")
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
driver = self._driver
|
|
51
|
+
serial = await self._run_blocking_or_raise(driver.get_serial_string)
|
|
51
52
|
log.info("Connected to filter wheel with serial number: %s", serial)
|
|
52
53
|
|
|
53
54
|
await self._change_motion_status(MotionStatus.IDLE)
|
|
@@ -58,7 +59,8 @@ class FliFilterWheel(FliBaseMixin, Module, MotionStatusMixin, IFilters, IFitsHea
|
|
|
58
59
|
all_filters = list(chain.from_iterable(self._filter_names))
|
|
59
60
|
await self.comm.set_capabilities(IFilters, FiltersCapabilities(filters=all_filters))
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
pos = await self._run_blocking_or_raise(driver.get_filter_pos)
|
|
63
|
+
self._current_filter = self._resolve_filter_name(pos)
|
|
62
64
|
await self.comm.set_state(IFilters, FilterState(filter=self._current_filter))
|
|
63
65
|
await self.comm.set_state(IReady, ReadyState(ready=True))
|
|
64
66
|
|
|
@@ -90,7 +92,12 @@ class FliFilterWheel(FliBaseMixin, Module, MotionStatusMixin, IFilters, IFitsHea
|
|
|
90
92
|
|
|
91
93
|
log.info("Setting filter to %s at position %d...", filter_name, pos)
|
|
92
94
|
await self._change_motion_status(MotionStatus.SLEWING)
|
|
93
|
-
self._driver
|
|
95
|
+
driver = self._driver
|
|
96
|
+
|
|
97
|
+
def _set() -> None:
|
|
98
|
+
driver.set_filter_pos(pos)
|
|
99
|
+
|
|
100
|
+
await self._run_blocking_or_raise(_set)
|
|
94
101
|
self._current_filter = filter_name
|
|
95
102
|
await self._change_motion_status(MotionStatus.POSITIONED)
|
|
96
103
|
await self.comm.send_event(FilterChangedEvent(filter_name))
|
|
@@ -107,9 +114,9 @@ class FliFilterWheel(FliBaseMixin, Module, MotionStatusMixin, IFilters, IFitsHea
|
|
|
107
114
|
|
|
108
115
|
async def get_fits_header_before(
|
|
109
116
|
self, namespaces: list[str] | None = None, **kwargs: Any
|
|
110
|
-
) -> dict[str,
|
|
117
|
+
) -> dict[str, FitsHeaderEntry]:
|
|
111
118
|
"""Returns FITS header for the current status of this module."""
|
|
112
|
-
return {"FILTER": (self._current_filter, "Current filter")}
|
|
119
|
+
return {"FILTER": FitsHeaderEntry(self._current_filter, "Current filter")}
|
|
113
120
|
|
|
114
121
|
|
|
115
122
|
__all__ = ["FliFilterWheel"]
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pyobs-fli"
|
|
3
|
-
version = "2.0.0.
|
|
3
|
+
version = "2.0.0.dev6"
|
|
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,<3.14"
|
|
7
7
|
license = "MIT"
|
|
8
8
|
dependencies = [
|
|
9
|
-
"astropy>=7.0.1,<
|
|
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.dev48,<3",
|
|
12
12
|
]
|
|
13
13
|
|
|
14
14
|
[dependency-groups]
|
|
15
15
|
dev = [
|
|
16
|
-
"black>=25.1.0,<
|
|
16
|
+
"black>=25.1.0,<27",
|
|
17
17
|
"pre-commit>=4.2.0,<5",
|
|
18
18
|
"ruff>=0.9.0",
|
|
19
19
|
"pyrefly>=1.1.1",
|