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.
Files changed (75) hide show
  1. pyobs_fli-2.0.0.dev6/.github/dependabot.yml +10 -0
  2. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/PKG-INFO +3 -3
  3. pyobs_fli-2.0.0.dev6/pyobs_fli/flibase.py +172 -0
  4. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/pyobs_fli/flicamera.py +74 -27
  5. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/pyobs_fli/flifilterwheel.py +13 -6
  6. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/pyproject.toml +4 -4
  7. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/uv.lock +478 -511
  8. pyobs_fli-2.0.0.dev4/pyobs_fli/flibase.py +0 -107
  9. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/.github/workflows/pypi.yml +0 -0
  10. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/.github/workflows/ruff.yml +0 -0
  11. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/.pre-commit-config.yaml +0 -0
  12. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/.readthedocs.yml +0 -0
  13. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/CHANGELOG.rst +0 -0
  14. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/CMakeLists.txt +0 -0
  15. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/LICENSE +0 -0
  16. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/README.md +0 -0
  17. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/docs/Makefile +0 -0
  18. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/docs/make.bat +0 -0
  19. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/docs/requirements.txt +0 -0
  20. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/docs/source/_static/pyobs.gif +0 -0
  21. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/docs/source/conf.py +0 -0
  22. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/docs/source/index.rst +0 -0
  23. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/Makefile +0 -0
  24. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/docxx.sty +0 -0
  25. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli-camera-parport.c +0 -0
  26. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli-camera-parport.h +0 -0
  27. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli-camera-usb.c +0 -0
  28. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli-camera-usb.h +0 -0
  29. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli-camera.c +0 -0
  30. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli-camera.h +0 -0
  31. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli-debug.h +0 -0
  32. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli-filter-focuser.c +0 -0
  33. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli-filter-focuser.h +0 -0
  34. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli-libfli.h +0 -0
  35. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli-mem.c +0 -0
  36. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli-mem.h +0 -0
  37. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli-raw.c +0 -0
  38. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli-raw.h +0 -0
  39. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli.c +0 -0
  40. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli.dxx +0 -0
  41. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/libfli.h +0 -0
  42. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/bsd/libfli-usb-sys.c +0 -0
  43. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/libfli-debug.c +0 -0
  44. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/libfli-parport.h +0 -0
  45. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/libfli-serial.c +0 -0
  46. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/libfli-serial.h +0 -0
  47. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/libfli-sys.c +0 -0
  48. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/libfli-sys.h +0 -0
  49. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/libfli-usb.c +0 -0
  50. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/libfli-usb.h +0 -0
  51. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/linux/fli_ioctl.h +0 -0
  52. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/linux/fliusb_ioctl.h +0 -0
  53. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/linux/libfli-parport.c +0 -0
  54. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/linux/libfli-usb-sys.c +0 -0
  55. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/osx/libfli-usb-sys.c +0 -0
  56. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/unix/osx/libfli-usb-sys.h +0 -0
  57. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/libfli-debug.c +0 -0
  58. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/libfli-parport.h +0 -0
  59. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/libfli-serial.c +0 -0
  60. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/libfli-serial.h +0 -0
  61. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/libfli-sys.h +0 -0
  62. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/libfli-usb.c +0 -0
  63. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/libfli-usb.h +0 -0
  64. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/libfli-windows-parport.c +0 -0
  65. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/libfli-windows.c +0 -0
  66. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/libfli.def +0 -0
  67. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/libfli.rc +0 -0
  68. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/libfli.sln +0 -0
  69. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/libfli.vcproj +0 -0
  70. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/libfli64.def +0 -0
  71. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/lib/windows/resource.h +0 -0
  72. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/poetry.lock +0 -0
  73. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/pyobs_fli/__init__.py +0 -0
  74. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/pyobs_fli/flidriver.pyx +0 -0
  75. {pyobs_fli-2.0.0.dev4 → pyobs_fli-2.0.0.dev6}/pyobs_fli/libfli.pxd +0 -0
@@ -0,0 +1,10 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "uv"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
7
+ day: "tuesday"
8
+ target-branch: "develop"
9
+ labels:
10
+ - "dependencies"
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyobs-fli
3
- Version: 2.0.0.dev4
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,>=7.0.1
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.dev6
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
- serial = self._driver.get_serial_string()
54
- log.info("Connected to camera with serial number: %s", serial)
57
+ driver = self._driver
55
58
 
56
- self._window, self._binning = self._driver.get_window_binning()
57
- self._full_frame = self._driver.get_full_frame()
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
- self._driver.init_exposure(open_shutter)
127
- self._driver.set_exposure_time(int(exposure_time * 1000.0))
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._driver.start_exposure()
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
- img = np.zeros((height, width), dtype=np.uint16)
142
- for row in range(height):
143
- img[row, :] = self._driver.grab_row(width)
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"] = (self._driver.get_temp(FliTemperature.CCD), "CCD temperature [C]")
149
- image.header["DET-COOL"] = (self._driver.get_cooler_power(), "Cooler power [percent]")
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, *self._driver.get_visible_frame())
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
- while True:
167
- if abort_event.is_set():
168
- await self._change_exposure_status(ExposureStatus.IDLE)
169
- raise InterruptedError("Aborted exposure.")
170
- if self._driver.is_exposing():
171
- break
172
- await asyncio.sleep(0.01)
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.set_temperature(float(setpoint) if setpoint is not None else 20.0)
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
- t_ccd = self._driver.get_temp(FliTemperature.CCD)
204
- t_base = self._driver.get_temp(FliTemperature.BASE)
205
- power = self._driver.get_cooler_power()
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
- serial = self._driver.get_serial_string()
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
- self._current_filter = self._resolve_filter_name(self._driver.get_filter_pos())
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.set_filter_pos(pos)
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, tuple[Any, 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.dev4"
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,<8",
9
+ "astropy>=7.0.1,<9",
10
10
  "numpy>=2.2.5,<3",
11
- "pyobs-core>=2.0.0.dev6,<3",
11
+ "pyobs-core>=2.0.0.dev48,<3",
12
12
  ]
13
13
 
14
14
  [dependency-groups]
15
15
  dev = [
16
- "black>=25.1.0,<26",
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",