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.
- pyobs_fli-2.0.0.dev9/.github/workflows/pypi.yml +65 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/PKG-INFO +2 -2
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/pyobs_fli/flicamera.py +6 -6
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/pyobs_fli/flidriver.pyx +122 -35
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/pyobs_fli/libfli.pxd +1 -1
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/pyproject.toml +11 -2
- pyobs_fli-2.0.0.dev9/specs/plans/2026-08-16-nogil-fli-driver-calls.md +106 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/uv.lock +568 -11
- pyobs_fli-2.0.0.dev6/.github/workflows/pypi.yml +0 -26
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/.github/dependabot.yml +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/.github/workflows/ruff.yml +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/.pre-commit-config.yaml +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/.readthedocs.yml +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/CHANGELOG.rst +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/CMakeLists.txt +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/LICENSE +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/README.md +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/docs/Makefile +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/docs/make.bat +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/docs/requirements.txt +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/docs/source/_static/pyobs.gif +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/docs/source/conf.py +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/docs/source/index.rst +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/Makefile +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/docxx.sty +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli-camera-parport.c +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli-camera-parport.h +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli-camera-usb.c +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli-camera-usb.h +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli-camera.c +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli-camera.h +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli-debug.h +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli-filter-focuser.c +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli-filter-focuser.h +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli-libfli.h +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli-mem.c +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli-mem.h +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli-raw.c +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli-raw.h +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli.c +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli.dxx +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/libfli.h +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/bsd/libfli-usb-sys.c +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/libfli-debug.c +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/libfli-parport.h +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/libfli-serial.c +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/libfli-serial.h +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/libfli-sys.c +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/libfli-sys.h +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/libfli-usb.c +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/libfli-usb.h +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/linux/fli_ioctl.h +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/linux/fliusb_ioctl.h +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/linux/libfli-parport.c +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/linux/libfli-usb-sys.c +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/osx/libfli-usb-sys.c +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/unix/osx/libfli-usb-sys.h +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/libfli-debug.c +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/libfli-parport.h +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/libfli-serial.c +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/libfli-serial.h +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/libfli-sys.h +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/libfli-usb.c +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/libfli-usb.h +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/libfli-windows-parport.c +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/libfli-windows.c +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/libfli.def +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/libfli.rc +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/libfli.sln +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/libfli.vcproj +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/libfli64.def +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/lib/windows/resource.h +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/poetry.lock +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/pyobs_fli/__init__.py +0 -0
- {pyobs_fli-2.0.0.dev6 → pyobs_fli-2.0.0.dev9}/pyobs_fli/flibase.py +0 -0
- {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.
|
|
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:
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
256
|
-
"""Checks, whether the camera is
|
|
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
|
|
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
|
-
|
|
320
|
+
with nogil:
|
|
321
|
+
res = FLIGetDeviceStatus(self._device, &status)
|
|
270
322
|
if res != 0:
|
|
271
323
|
raise ValueError('Could not fetch device status.')
|
|
272
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,9 +1,9 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pyobs-fli"
|
|
3
|
-
version = "2.0.0.
|
|
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
|
|
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.
|