pyobs-fli 2.0.0.dev6__tar.gz → 2.0.0.dev9__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 (76) hide show
  1. pyobs_fli-2.0.0.dev9/.github/workflows/pypi.yml +65 -0
  2. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/PKG-INFO +2 -2
  3. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/pyobs_fli/flicamera.py +6 -6
  4. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/pyobs_fli/flidriver.pyx +122 -35
  5. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/pyobs_fli/libfli.pxd +1 -1
  6. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/pyproject.toml +11 -2
  7. pyobs_fli-2.0.0.dev9/specs/plans/2026-08-16-nogil-fli-driver-calls.md +106 -0
  8. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/uv.lock +568 -11
  9. pyobs_fli-2.0.0.dev6/.github/workflows/pypi.yml +0 -26
  10. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/.github/dependabot.yml +0 -0
  11. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/.github/workflows/ruff.yml +0 -0
  12. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/.pre-commit-config.yaml +0 -0
  13. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/.readthedocs.yml +0 -0
  14. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/CHANGELOG.rst +0 -0
  15. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/CMakeLists.txt +0 -0
  16. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/LICENSE +0 -0
  17. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/README.md +0 -0
  18. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/docs/Makefile +0 -0
  19. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/docs/make.bat +0 -0
  20. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/docs/requirements.txt +0 -0
  21. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/docs/source/_static/pyobs.gif +0 -0
  22. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/docs/source/conf.py +0 -0
  23. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/docs/source/index.rst +0 -0
  24. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/Makefile +0 -0
  25. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/docxx.sty +0 -0
  26. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli-camera-parport.c +0 -0
  27. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli-camera-parport.h +0 -0
  28. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli-camera-usb.c +0 -0
  29. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli-camera-usb.h +0 -0
  30. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli-camera.c +0 -0
  31. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli-camera.h +0 -0
  32. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli-debug.h +0 -0
  33. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli-filter-focuser.c +0 -0
  34. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli-filter-focuser.h +0 -0
  35. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli-libfli.h +0 -0
  36. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli-mem.c +0 -0
  37. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli-mem.h +0 -0
  38. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli-raw.c +0 -0
  39. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli-raw.h +0 -0
  40. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli.c +0 -0
  41. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli.dxx +0 -0
  42. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli.h +0 -0
  43. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/bsd/libfli-usb-sys.c +0 -0
  44. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/libfli-debug.c +0 -0
  45. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/libfli-parport.h +0 -0
  46. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/libfli-serial.c +0 -0
  47. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/libfli-serial.h +0 -0
  48. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/libfli-sys.c +0 -0
  49. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/libfli-sys.h +0 -0
  50. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/libfli-usb.c +0 -0
  51. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/libfli-usb.h +0 -0
  52. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/linux/fli_ioctl.h +0 -0
  53. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/linux/fliusb_ioctl.h +0 -0
  54. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/linux/libfli-parport.c +0 -0
  55. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/linux/libfli-usb-sys.c +0 -0
  56. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/osx/libfli-usb-sys.c +0 -0
  57. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/osx/libfli-usb-sys.h +0 -0
  58. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/libfli-debug.c +0 -0
  59. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/libfli-parport.h +0 -0
  60. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/libfli-serial.c +0 -0
  61. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/libfli-serial.h +0 -0
  62. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/libfli-sys.h +0 -0
  63. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/libfli-usb.c +0 -0
  64. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/libfli-usb.h +0 -0
  65. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/libfli-windows-parport.c +0 -0
  66. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/libfli-windows.c +0 -0
  67. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/libfli.def +0 -0
  68. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/libfli.rc +0 -0
  69. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/libfli.sln +0 -0
  70. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/libfli.vcproj +0 -0
  71. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/libfli64.def +0 -0
  72. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/resource.h +0 -0
  73. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/poetry.lock +0 -0
  74. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/pyobs_fli/__init__.py +0 -0
  75. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/pyobs_fli/flibase.py +0 -0
  76. {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/pyobs_fli/flifilterwheel.py +0 -0
@@ -0,0 +1,65 @@
1
+ name: Publish package to PyPI
2
+ on:
3
+ push:
4
+ tags:
5
+ - 'v*'
6
+ jobs:
7
+ build-wheels:
8
+ name: Build wheels
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - name: Check out repository code
12
+ uses: actions/checkout@v4
13
+
14
+ - name: Build wheels
15
+ uses: pypa/cibuildwheel@v4.2.0
16
+
17
+ - name: Upload wheels
18
+ uses: actions/upload-artifact@v4
19
+ with:
20
+ name: wheels
21
+ path: wheelhouse/*.whl
22
+
23
+ build-sdist:
24
+ name: Build sdist
25
+ runs-on: ubuntu-latest
26
+ steps:
27
+ - name: Check out repository code
28
+ uses: actions/checkout@v4
29
+
30
+ - name: Set up Python
31
+ uses: astral-sh/setup-uv@v5
32
+ with:
33
+ enable-cache: true
34
+ python-version: "3.13"
35
+
36
+ - name: Build sdist
37
+ run: uv build --sdist
38
+
39
+ - name: Upload sdist
40
+ uses: actions/upload-artifact@v4
41
+ with:
42
+ name: sdist
43
+ path: dist/*.tar.gz
44
+
45
+ publish:
46
+ name: Publish to PyPI
47
+ needs: [build-wheels, build-sdist]
48
+ runs-on: ubuntu-latest
49
+ steps:
50
+ - name: Download artifacts
51
+ uses: actions/download-artifact@v4
52
+ with:
53
+ path: dist
54
+ merge-multiple: true
55
+
56
+ - name: Set up Python
57
+ uses: astral-sh/setup-uv@v5
58
+ with:
59
+ enable-cache: true
60
+ python-version: "3.13"
61
+
62
+ - name: Publish
63
+ env:
64
+ PYPI_TOKEN: ${{ secrets.pypi_password }}
65
+ run: uv publish --token $PYPI_TOKEN dist/*
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyobs-fli
3
- Version: 2.0.0.dev6
3
+ Version: 2.0.0.dev9
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
- Requires-Python: <3.14,>=3.11
7
+ Requires-Python: >=3.11
8
8
  Requires-Dist: astropy<9,>=7.0.1
9
9
  Requires-Dist: numpy<3,>=2.2.5
10
10
  Requires-Dist: pyobs-core<3,>=2.0.0.dev48
@@ -8,7 +8,7 @@ from typing import Any
8
8
  import numpy as np
9
9
  from pyobs.images import Image
10
10
  from pyobs.interfaces import IAbortable, IBinning, ICamera, ICooling, ITemperatures, IWindow
11
- from pyobs.interfaces.IBinning import BinningCapabilities, BinningState
11
+ from pyobs.interfaces.IBinning import Binning, BinningCapabilities, BinningState
12
12
  from pyobs.interfaces.ICooling import CoolingState
13
13
  from pyobs.interfaces.ITemperatures import SensorReading, TemperaturesState
14
14
  from pyobs.interfaces.IWindow import WindowCapabilities, WindowState
@@ -84,10 +84,10 @@ class FliCamera(FliBaseMixin, BaseCamera, ICamera, IWindow, IBinning, ICooling,
84
84
  IBinning,
85
85
  BinningCapabilities(
86
86
  binnings=[
87
- BinningState(x=1, y=1),
88
- BinningState(x=2, y=2),
89
- BinningState(x=3, y=3),
90
- BinningState(x=4, y=4),
87
+ Binning(x=1, y=1),
88
+ Binning(x=2, y=2),
89
+ Binning(x=3, y=3),
90
+ Binning(x=4, y=4),
91
91
  ]
92
92
  ),
93
93
  )
@@ -196,7 +196,7 @@ class FliCamera(FliBaseMixin, BaseCamera, ICamera, IWindow, IBinning, ICooling,
196
196
 
197
197
  def _wait() -> bool:
198
198
  while True:
199
- if driver.is_exposing():
199
+ if driver.is_data_ready():
200
200
  return not abort_event.is_set()
201
201
  if abort_event.is_set():
202
202
  return False
@@ -42,26 +42,38 @@ cdef class FliDriver:
42
42
  cdef flidomain_t domain
43
43
  cdef char filename[1024]
44
44
  cdef char name[1024]
45
+ cdef flidomain_t list_domain = FLIDOMAIN_USB | device_type.value
46
+ cdef long res
47
+ cdef bint have_first
45
48
 
46
49
  # create list of USB camera
47
- if FLICreateList(FLIDOMAIN_USB | device_type.value) != 0:
50
+ with nogil:
51
+ res = FLICreateList(list_domain)
52
+ if res != 0:
48
53
  raise ValueError('Could not create list of FLI cameras.')
49
54
 
50
55
  # init list of devices
51
56
  devices = []
52
57
 
53
58
  # get first camera
54
- if FLIListFirst(&domain, <char*>filename, 1024, <char*>name, 1024) == 0:
59
+ with nogil:
60
+ have_first = FLIListFirst(&domain, <char*>filename, 1024, <char*>name, 1024) == 0
61
+ if have_first:
55
62
  # store first device
56
63
  devices.append(DeviceInfo(domain=domain, filename=filename, name=name))
57
64
 
58
65
  # loop other devices
59
- while FLIListNext(&domain, <char*>filename, 1024, <char*>name, 1024) == 0:
66
+ while True:
67
+ with nogil:
68
+ res = FLIListNext(&domain, <char*>filename, 1024, <char*>name, 1024)
69
+ if res != 0:
70
+ break
60
71
  # store device
61
72
  devices.append(DeviceInfo(domain=domain, filename=filename, name=name))
62
73
 
63
74
  # clean up and return
64
- FLIDeleteList()
75
+ with nogil:
76
+ FLIDeleteList()
65
77
  return devices
66
78
 
67
79
  """Storage for the device info."""
@@ -84,7 +96,12 @@ cdef class FliDriver:
84
96
  Raises:
85
97
  ValueError: If opening failed.
86
98
  """
87
- res = FLIOpen(&self._device, self._device_info.filename, self._device_info.domain)
99
+ cdef bytes filename_bytes = self._device_info.filename
100
+ cdef char* filename = filename_bytes
101
+ cdef flidomain_t domain = self._device_info.domain
102
+ cdef long res
103
+ with nogil:
104
+ res = FLIOpen(&self._device, filename, domain)
88
105
  if res != 0:
89
106
  raise ValueError('Could not open device.')
90
107
 
@@ -94,7 +111,9 @@ cdef class FliDriver:
94
111
  Raises:
95
112
  ValueError: If closing failed.
96
113
  """
97
- res = FLIClose(self._device)
114
+ cdef long res
115
+ with nogil:
116
+ res = FLIClose(self._device)
98
117
  if res != 0:
99
118
  raise ValueError('Could not open device.')
100
119
 
@@ -115,9 +134,11 @@ cdef class FliDriver:
115
134
 
116
135
  # variables
117
136
  cdef long width, hoffset, hbin, height, voffset, vbin
118
-
137
+ cdef long res
138
+
119
139
  # get dimensions
120
- res = FLIGetReadoutDimensions(self._device, &width, &hoffset, &hbin, &height, &voffset, &vbin)
140
+ with nogil:
141
+ res = FLIGetReadoutDimensions(self._device, &width, &hoffset, &hbin, &height, &voffset, &vbin)
121
142
  if res != 0:
122
143
  raise ValueError('Could not query readout dimensions.')
123
144
 
@@ -136,9 +157,11 @@ cdef class FliDriver:
136
157
 
137
158
  # variables
138
159
  cdef long ul_x, ul_y, lr_x, lr_y
160
+ cdef long res
139
161
 
140
162
  # get area
141
- res = FLIGetVisibleArea(self._device, &ul_x, &ul_y, &lr_x, &lr_y)
163
+ with nogil:
164
+ res = FLIGetVisibleArea(self._device, &ul_x, &ul_y, &lr_x, &lr_y)
142
165
  if res != 0:
143
166
  raise ValueError('Could not query visible area.')
144
167
 
@@ -157,9 +180,11 @@ cdef class FliDriver:
157
180
 
158
181
  # variables
159
182
  cdef long ul_x, ul_y, lr_x, lr_y
183
+ cdef long res
160
184
 
161
185
  # get area
162
- res = FLIGetArrayArea(self._device, &ul_x, &ul_y, &lr_x, &lr_y)
186
+ with nogil:
187
+ res = FLIGetArrayArea(self._device, &ul_x, &ul_y, &lr_x, &lr_y)
163
188
  if res != 0:
164
189
  raise ValueError('Could not query total area.')
165
190
 
@@ -177,13 +202,19 @@ cdef class FliDriver:
177
202
  ValueError: If setting binning failed.
178
203
  """
179
204
 
205
+ cdef long x_c = x
206
+ cdef long y_c = y
207
+ cdef long res
208
+
180
209
  # set x binning
181
- res = FLISetHBin(self._device, x)
210
+ with nogil:
211
+ res = FLISetHBin(self._device, x_c)
182
212
  if res != 0:
183
213
  raise ValueError('Could not set x binning.')
184
214
 
185
215
  # set y binning
186
- res = FLISetVBin(self._device, y)
216
+ with nogil:
217
+ res = FLISetVBin(self._device, y_c)
187
218
  if res != 0:
188
219
  raise ValueError('Could not set y binning.')
189
220
 
@@ -200,8 +231,15 @@ cdef class FliDriver:
200
231
  ValueError: If setting the window failed.
201
232
  """
202
233
 
234
+ cdef long left_c = left
235
+ cdef long top_c = top
236
+ cdef long right_c = left + width
237
+ cdef long bottom_c = top + height
238
+ cdef long res
239
+
203
240
  # set window
204
- res = FLISetImageArea(self._device, left, top, left + width, top + height)
241
+ with nogil:
242
+ res = FLISetImageArea(self._device, left_c, top_c, right_c, bottom_c)
205
243
  if res != 0:
206
244
  raise ValueError('Could not set window.')
207
245
 
@@ -215,13 +253,18 @@ cdef class FliDriver:
215
253
  ValueError: If initialization failed.
216
254
  """
217
255
 
256
+ cdef fliframe_t frame_type = FLI_FRAME_TYPE_NORMAL if open_shutter else FLI_FRAME_TYPE_DARK
257
+ cdef long res
258
+
218
259
  # set TDI
219
- res = FLISetTDI(self._device, 0, 0)
260
+ with nogil:
261
+ res = FLISetTDI(self._device, 0, 0)
220
262
  if res != 0:
221
263
  raise ValueError('Could not set TDI.')
222
264
 
223
265
  # set frame type
224
- res = FLISetFrameType(self._device, FLI_FRAME_TYPE_NORMAL if open_shutter else FLI_FRAME_TYPE_DARK)
266
+ with nogil:
267
+ res = FLISetFrameType(self._device, frame_type)
225
268
  if res != 0:
226
269
  raise ValueError('Could not set frame type.')
227
270
 
@@ -235,8 +278,12 @@ cdef class FliDriver:
235
278
  ValueError: If setting of exposure time failed.
236
279
  """
237
280
 
281
+ cdef long exptime_c = exptime
282
+ cdef long res
283
+
238
284
  # set exptime
239
- res = FLISetExposureTime(self._device, exptime)
285
+ with nogil:
286
+ res = FLISetExposureTime(self._device, exptime_c)
240
287
  if res != 0:
241
288
  raise ValueError('Could not set exposure time.')
242
289
 
@@ -247,16 +294,19 @@ cdef class FliDriver:
247
294
  ValueError: If starting the exposure failed.
248
295
  """
249
296
 
297
+ cdef long res
298
+
250
299
  # expose
251
- res = FLIExposeFrame(self._device)
300
+ with nogil:
301
+ res = FLIExposeFrame(self._device)
252
302
  if res != 0:
253
303
  raise ValueError('Could not start exposure.')
254
304
 
255
- def is_exposing(self) -> bool:
256
- """Checks, whether the camera is currently exposing
305
+ def is_data_ready(self) -> bool:
306
+ """Checks, whether the camera has finished exposing and the image data is ready to be read out.
257
307
 
258
308
  Returns:
259
- bool: Whether camera is currently exposing.
309
+ bool: Whether data is ready.
260
310
 
261
311
  Raises:
262
312
  ValueError: If fetching device or exposure status failed.
@@ -264,12 +314,15 @@ cdef class FliDriver:
264
314
 
265
315
  # variables
266
316
  cdef long status, timeleft
317
+ cdef long res
267
318
 
268
319
  # get status
269
- res = FLIGetDeviceStatus(self._device, &status)
320
+ with nogil:
321
+ res = FLIGetDeviceStatus(self._device, &status)
270
322
  if res != 0:
271
323
  raise ValueError('Could not fetch device status.')
272
- res = FLIGetExposureStatus(self._device, &timeleft)
324
+ with nogil:
325
+ res = FLIGetExposureStatus(self._device, &timeleft)
273
326
  if res != 0:
274
327
  raise ValueError('Could not fetch remaining exposure time.')
275
328
 
@@ -292,9 +345,12 @@ cdef class FliDriver:
292
345
 
293
346
  # variables
294
347
  cdef double temp
348
+ cdef flichannel_t channel_c = channel.value
349
+ cdef long res
295
350
 
296
351
  # get it
297
- res = FLIReadTemperature(self._device, channel.value, &temp)
352
+ with nogil:
353
+ res = FLIReadTemperature(self._device, channel_c, &temp)
298
354
  if res != 0:
299
355
  raise ValueError('Could not fetch temperature.')
300
356
 
@@ -313,9 +369,11 @@ cdef class FliDriver:
313
369
 
314
370
  # variables
315
371
  cdef double power
372
+ cdef long res
316
373
 
317
374
  # get it
318
- res = FLIGetCoolerPower(self._device, &power)
375
+ with nogil:
376
+ res = FLIGetCoolerPower(self._device, &power)
319
377
  if res != 0:
320
378
  raise ValueError('Could not fetch cooler power.')
321
379
 
@@ -340,9 +398,12 @@ cdef class FliDriver:
340
398
 
341
399
  # get pointer to data
342
400
  cdef void* row_data = <void*> row.data
401
+ cdef size_t width_c = width
402
+ cdef long res
343
403
 
344
404
  # call library
345
- res = FLIGrabRow(self._device, row_data, width)
405
+ with nogil:
406
+ res = FLIGrabRow(self._device, row_data, width_c)
346
407
  if res != 0:
347
408
  raise ValueError('Could not grab row from camera.')
348
409
 
@@ -356,7 +417,9 @@ cdef class FliDriver:
356
417
  ValueError: If canceling failed.
357
418
 
358
419
  """
359
- res = FLICancelExposure(self._device)
420
+ cdef long res
421
+ with nogil:
422
+ res = FLICancelExposure(self._device)
360
423
  if res != 0:
361
424
  raise ValueError('Could not cancel exposure.')
362
425
 
@@ -369,7 +432,10 @@ cdef class FliDriver:
369
432
  Raises:
370
433
  ValueError: If setting temperature failed.
371
434
  """
372
- res = FLISetTemperature(self._device, setpoint)
435
+ cdef double setpoint_c = setpoint
436
+ cdef long res
437
+ with nogil:
438
+ res = FLISetTemperature(self._device, setpoint_c)
373
439
  if res != 0:
374
440
  raise ValueError('Could not set temperature.')
375
441
 
@@ -383,9 +449,11 @@ cdef class FliDriver:
383
449
  # variables
384
450
  cdef char *model
385
451
  cdef size_t len
452
+ cdef long res
386
453
 
387
454
  # get it
388
- res = FLIGetModel(self._device, model, len)
455
+ with nogil:
456
+ res = FLIGetModel(self._device, model, len)
389
457
  if res != 0:
390
458
  raise ValueError('Could not fetch model.')
391
459
 
@@ -397,9 +465,11 @@ cdef class FliDriver:
397
465
 
398
466
  # variables
399
467
  cdef char serial[1024]
468
+ cdef long res
400
469
 
401
470
  # get it
402
- res = FLIGetSerialString(self._device, <char*>serial, 1024)
471
+ with nogil:
472
+ res = FLIGetSerialString(self._device, <char*>serial, 1024)
403
473
  if res != 0:
404
474
  raise ValueError('Could not fetch serial string.')
405
475
 
@@ -415,9 +485,11 @@ cdef class FliDriver:
415
485
 
416
486
  # variables
417
487
  cdef long pos
488
+ cdef long res
418
489
 
419
490
  # get it
420
- res = FLIGetFilterPos(self._device, &pos)
491
+ with nogil:
492
+ res = FLIGetFilterPos(self._device, &pos)
421
493
  if res != 0:
422
494
  raise ValueError('Could not fetch filter position.')
423
495
 
@@ -431,8 +503,12 @@ cdef class FliDriver:
431
503
  pos: New filter position.
432
504
  """
433
505
 
506
+ cdef long pos_c = pos
507
+ cdef long res
508
+
434
509
  # set filter pos
435
- res = FLISetFilterPos(self._device, pos)
510
+ with nogil:
511
+ res = FLISetFilterPos(self._device, pos_c)
436
512
  if res != 0:
437
513
  raise ValueError('Could not set filter position.')
438
514
 
@@ -443,8 +519,12 @@ cdef class FliDriver:
443
519
  wheel: Number of wheel to set active.
444
520
  """
445
521
 
522
+ cdef long wheel_c = wheel
523
+ cdef long res
524
+
446
525
  # set active filter wheel
447
- res = FLISetActiveWheel(self._device, wheel)
526
+ with nogil:
527
+ res = FLISetActiveWheel(self._device, wheel_c)
448
528
  if res != 0:
449
529
  raise ValueError('Could not set active filter wheel.')
450
530
 
@@ -457,9 +537,11 @@ cdef class FliDriver:
457
537
 
458
538
  # variables
459
539
  cdef long wheel
540
+ cdef long res
460
541
 
461
542
  # get active filter wheel
462
- res = FLIGetActiveWheel(self._device, &wheel)
543
+ with nogil:
544
+ res = FLIGetActiveWheel(self._device, &wheel)
463
545
  if res != 0:
464
546
  raise ValueError('Could not fetch active filter wheel.')
465
547
  return wheel
@@ -473,9 +555,11 @@ cdef class FliDriver:
473
555
 
474
556
  # variables
475
557
  cdef long count
558
+ cdef long res
476
559
 
477
560
  # get active filter wheel
478
- res = FLIGetFilterCount(self._device, &count)
561
+ with nogil:
562
+ res = FLIGetFilterCount(self._device, &count)
479
563
  if res != 0:
480
564
  raise ValueError('Could not fetch filter count.')
481
565
  return count
@@ -491,9 +575,12 @@ cdef class FliDriver:
491
575
 
492
576
  # variables
493
577
  cdef char name[100]
578
+ cdef long pos_c = pos
579
+ cdef long res
494
580
 
495
581
  # get it
496
- res = FLIGetFilterName(self._device, pos, <char*>name, 100)
582
+ with nogil:
583
+ res = FLIGetFilterName(self._device, pos_c, <char*>name, 100)
497
584
  if res != 0:
498
585
  raise ValueError('Could not fetch filter name.')
499
586
 
@@ -1,4 +1,4 @@
1
- cdef extern from "../lib/libfli.h":
1
+ cdef extern from "../lib/libfli.h" nogil:
2
2
  # An opaque handle used by library functions to refer to FLI
3
3
  # hardware.
4
4
  cdef int FLI_INVALID_DEVICE
@@ -1,9 +1,9 @@
1
1
  [project]
2
2
  name = "pyobs-fli"
3
- version = "2.0.0.dev6"
3
+ version = "2.0.0.dev9"
4
4
  description = "pyobs module for FLI cameras"
5
5
  authors = [{ name = "Tim-Oliver Husser", email = "thusser@uni-goettingen.de" }]
6
- requires-python = ">=3.11,<3.14"
6
+ requires-python = ">=3.11"
7
7
  license = "MIT"
8
8
  dependencies = [
9
9
  "astropy>=7.0.1,<9",
@@ -22,6 +22,15 @@ dev = [
22
22
  [tool.scikit-build]
23
23
  wheel.packages = ["pyobs_fli"]
24
24
 
25
+ [tool.cibuildwheel]
26
+ build = "cp311-* cp312-* cp313-* cp314-*"
27
+ skip = "*-musllinux_* *-manylinux_i686"
28
+ archs = ["x86_64"]
29
+
30
+ [tool.cibuildwheel.linux]
31
+ manylinux-x86_64-image = "manylinux_2_28"
32
+ before-all = "dnf install -y epel-release && dnf install -y cfitsio-devel"
33
+
25
34
  [build-system]
26
35
  requires = ["scikit_build_core>=0.12", "cython>=3.0", "cython-cmake", "numpy>=2.0"]
27
36
  build-backend = "scikit_build_core.build"
@@ -0,0 +1,106 @@
1
+ # Plan: Release the GIL around libfli calls
2
+
3
+ Status: implemented
4
+
5
+ Tracks: https://github.com/pyobs/pyobs-fli/issues/75
6
+
7
+ ## Problem
8
+
9
+ A hung FLI device freezes the whole module, including XMPP. `FliBaseMixin._run_blocking`
10
+ (`pyobs_fli/flibase.py`) already runs every libfli call in a daemon thread bounded by
11
+ `asyncio.wait_for(..., timeout=5.0)`, which is the fix that worked for the same blocking-call
12
+ shape in `pyobs-aravis`/`pyobs-asi` (see `pyobs-core/specs/steering/
13
+ blocking-sdk-calls-must-not-run-on-the-event-loop.md`). It doesn't work here, because
14
+ `libfli.pxd` declares `FLIOpen`, `FLIGetReadoutDimensions`, etc. without `nogil`. Cython calls
15
+ into a non-`nogil` C function while still holding the GIL, so when the call blocks (unresponsive
16
+ USB device), the daemon thread holds the GIL for the entire hang. The event loop thread cannot
17
+ run *anything* on that GIL, including the `asyncio.wait_for` timeout machinery meant to bound the
18
+ call. The timeout never fires; the module goes fully unresponsive (observed: `fli230` stopped
19
+ answering `disco#info`, every peer logged repeated capability-fetch timeouts).
20
+
21
+ ## Decision
22
+
23
+ Mark the libfli C functions `nogil` in `libfli.pxd`, and wrap each call site in
24
+ `flidriver.pyx` in `with nogil:` so the GIL is actually released while the call runs. This
25
+ restores the daemon-thread timeout's ability to fire, since the event loop thread can run while
26
+ the SDK call blocks.
27
+
28
+ Considered and rejected: running FLI calls in a subprocess (the issue's "more robust, heavier"
29
+ alternative). Rejected for this pass because it's a much larger change (IPC, serialization of
30
+ buffers for `grab_row`, process lifecycle management) for the same practical outcome. Worth
31
+ reconsidering only if a `with nogil:` call turns out to still wedge somehow (e.g. libfli itself
32
+ deadlocks internally in a way that isn't a simple blocking wait).
33
+
34
+ ## Design
35
+
36
+ ### `libfli.pxd`
37
+
38
+ Change the whole `cdef extern from "../lib/libfli.h":` block to
39
+ `cdef extern from "../lib/libfli.h" nogil:`. This marks every declared function `nogil` at once,
40
+ including ones not yet called from `flidriver.pyx`, so new call sites added later don't silently
41
+ regress. Safe for the whole block: libfli is a pure hardware-I/O C library, none of its functions
42
+ take a Python object or call back into the interpreter.
43
+
44
+ ### `flidriver.pyx`
45
+
46
+ `nogil` only changes what's *allowed* inside `with nogil:` — it does not by itself move existing
47
+ calls off the GIL. Every call site needs a `with nogil:` around the actual `FLI*(...)` call, and
48
+ anything inside that block that isn't a C primitive has to move outside it first:
49
+
50
+ - **Enum/typed-arg values** (`device_type.value`, `channel.value`, and `int`/`bool`-annotated
51
+ Python parameters like `pos`, `wheel`, `x`, `y`, `left`, `exptime`, `open_shutter`): read into a
52
+ local `cdef` C variable before the `with nogil:` block, since attribute access and Python->C
53
+ unboxing both touch the GIL.
54
+ - **`self._device_info.filename` / `.domain`** (used in `open()`): same issue — read into local
55
+ `cdef` variables (`bytes` for filename, kept alive as a local so the underlying buffer stays
56
+ valid for the duration of the block; `flidomain_t` for domain) before entering `with nogil:`.
57
+ - **`self._device`**: no change needed. It's a C-typed (`cdef flidev_t`) field of a `cdef class`,
58
+ so reading it inside `with nogil:` is a direct field access, not a Python attribute lookup.
59
+ - **`list_devices()`**: the `while` loop currently interleaves a C call (`FLIListNext`) with a
60
+ Python-level `devices.append(...)`. Put just the C call in its own `with nogil:` per iteration
61
+ (accepted GIL-flip overhead — this runs at most a few times per `open()`, not in a hot loop);
62
+ the list bookkeeping stays under the GIL.
63
+ - **`grab_row()`**: `row_data` is already computed (as a raw pointer) before the call — just wrap
64
+ the `FLIGrabRow` call itself; no reordering needed.
65
+
66
+ Cython raises a compile-time error for any Python-object touch inside a `with nogil:` block, so
67
+ this is self-checking: if an extraction is missed, `cython --cplus` on `flidriver.pyx` fails
68
+ before any build/link step.
69
+
70
+ ### Verification
71
+
72
+ No existing test suite exercises the Cython extension (`pyobs-fli` has no `tests/` at all — a
73
+ hardware driver with no way to run libfli calls without real hardware). Verification is:
74
+
75
+ 1. `cython --cplus` compiles `flidriver.pyx` clean (catches any missed nogil extraction as a hard
76
+ error, not just a lint).
77
+ 2. Full `cmake`/`scikit-build-core` build succeeds and the resulting `.so` imports.
78
+ 3. Manual read-through confirming every `FLI*` call site in the diff is inside a `with nogil:`
79
+ block, and that nothing added to `libfli.pxd`'s `nogil` block passes a Python object by
80
+ reference to a C function that could retain/mutate it — none of the current API does.
81
+
82
+ No hardware-in-the-loop test is possible from here; actual freeze recovery on a hung `fli230` can
83
+ only be confirmed on-site.
84
+
85
+ ## Out of scope (flagged, not fixed here)
86
+
87
+ - `get_model()` in `flidriver.pyx` (~line 320) passes `model` and `len` to `FLIGetModel`
88
+ uninitialized (`cdef char *model` / `cdef size_t len`, never assigned before the call) — this
89
+ writes through a garbage pointer with a garbage length. Pre-existing bug, unrelated to the GIL
90
+ issue. Should be filed and fixed separately (needs a real fixed-size buffer, same shape as
91
+ `get_serial_string()`/`get_filter_name()`).
92
+ - `pyobs-flipro` has the structurally identical bug (no `_run_blocking` wrapper at all yet, let
93
+ alone `nogil`) — separate issue/PR in that repo, not touched here.
94
+
95
+ ## Implementation checklist
96
+
97
+ - [x] `libfli.pxd`: add `nogil` to the `cdef extern from "../lib/libfli.h":` line.
98
+ - [x] `flidriver.pyx`: wrap every `FLI*` call site in `with nogil:`, hoisting Python-object/typed-arg
99
+ reads above the block as described in Design.
100
+ - [x] `cython --cplus pyobs_fli/flidriver.pyx -I pyobs_fli` compiles with no errors (only the
101
+ pre-existing, separately-flagged `get_model()` uninitialized-variable warning).
102
+ - [x] Full build (`uv sync`) succeeds; `import pyobs_fli.flidriver` works and
103
+ `FliDriver.list_devices()` runs cleanly through the new `nogil` code path.
104
+ - [x] Open a PR against `develop` (matches this repo's branch convention — dependabot/feature PRs
105
+ land on `develop`, which then gets version-bumped into `main`). Merged as #76.
106
+ - [x] Update this doc's `Status:` to `implemented` once merged.