pyobs-aravis 2.0.0.dev7__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 (20) hide show
  1. {pyobs_aravis-2.0.0.dev7 → pyobs_aravis-2.0.0.dev9}/PKG-INFO +1 -1
  2. {pyobs_aravis-2.0.0.dev7 → pyobs_aravis-2.0.0.dev9}/pyobs_aravis/araviscamera.py +53 -5
  3. {pyobs_aravis-2.0.0.dev7 → pyobs_aravis-2.0.0.dev9}/pyproject.toml +1 -1
  4. {pyobs_aravis-2.0.0.dev7 → pyobs_aravis-2.0.0.dev9}/uv.lock +1 -1
  5. {pyobs_aravis-2.0.0.dev7 → pyobs_aravis-2.0.0.dev9}/.github/workflows/pypi.yml +0 -0
  6. {pyobs_aravis-2.0.0.dev7 → pyobs_aravis-2.0.0.dev9}/.github/workflows/ruff.yml +0 -0
  7. {pyobs_aravis-2.0.0.dev7 → pyobs_aravis-2.0.0.dev9}/.pre-commit-config.yaml +0 -0
  8. {pyobs_aravis-2.0.0.dev7 → pyobs_aravis-2.0.0.dev9}/.readthedocs.yml +0 -0
  9. {pyobs_aravis-2.0.0.dev7 → pyobs_aravis-2.0.0.dev9}/CHANGELOG.rst +0 -0
  10. {pyobs_aravis-2.0.0.dev7 → pyobs_aravis-2.0.0.dev9}/LICENSE +0 -0
  11. {pyobs_aravis-2.0.0.dev7 → pyobs_aravis-2.0.0.dev9}/README.md +0 -0
  12. {pyobs_aravis-2.0.0.dev7 → pyobs_aravis-2.0.0.dev9}/docs/Makefile +0 -0
  13. {pyobs_aravis-2.0.0.dev7 → pyobs_aravis-2.0.0.dev9}/docs/make.bat +0 -0
  14. {pyobs_aravis-2.0.0.dev7 → pyobs_aravis-2.0.0.dev9}/docs/requirements.txt +0 -0
  15. {pyobs_aravis-2.0.0.dev7 → pyobs_aravis-2.0.0.dev9}/docs/source/_static/pyobs.gif +0 -0
  16. {pyobs_aravis-2.0.0.dev7 → pyobs_aravis-2.0.0.dev9}/docs/source/conf.py +0 -0
  17. {pyobs_aravis-2.0.0.dev7 → pyobs_aravis-2.0.0.dev9}/docs/source/index.rst +0 -0
  18. {pyobs_aravis-2.0.0.dev7 → pyobs_aravis-2.0.0.dev9}/pyobs_aravis/__init__.py +0 -0
  19. {pyobs_aravis-2.0.0.dev7 → pyobs_aravis-2.0.0.dev9}/pyobs_aravis/aravis.py +0 -0
  20. {pyobs_aravis-2.0.0.dev7 → pyobs_aravis-2.0.0.dev9}/pyobs_aravis/gui.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyobs-aravis
3
- Version: 2.0.0.dev7
3
+ Version: 2.0.0.dev9
4
4
  Summary: pyobs module for Aravis cameras
5
5
  Author-email: Tim-Oliver Husser <thusser@uni-goettingen.de>
6
6
  License-Expression: MIT
@@ -16,6 +16,11 @@ log = logging.getLogger(__name__)
16
16
  # rather than let a single dead camera freeze the whole module.
17
17
  _SDK_CALL_TIMEOUT = 5.0
18
18
 
19
+ # pop_frame() is polled from _capture() and is expected to legitimately take a while (up to the
20
+ # camera's own frame interval/exposure time), unlike the other SDK calls above -- a much more
21
+ # generous timeout than _SDK_CALL_TIMEOUT, so normal operation never trips it.
22
+ _FRAME_WAIT_TIMEOUT = 30.0
23
+
19
24
 
20
25
  class AravisCamera(BaseVideo, IExposureTime):
21
26
  """A pyobs module for Aravis cameras."""
@@ -57,7 +62,18 @@ class AravisCamera(BaseVideo, IExposureTime):
57
62
 
58
63
  await BaseVideo.open(self)
59
64
 
60
- ids: list[str] = aravis.get_device_ids() # type: ignore[assignment]
65
+ # device discovery is a blocking, network-based scan (GigE Vision/USB3 Vision devices
66
+ # reply to a broadcast query) that can take multiple seconds -- run it like the other
67
+ # aravis/GLib calls (see _run_blocking) instead of freezing the whole module's event
68
+ # loop, and with it, the ability to respond to any other module, for that long
69
+ ids: list[str] = []
70
+
71
+ def _list_device_ids() -> None:
72
+ ids.extend(aravis.get_device_ids()) # type: ignore[arg-type]
73
+
74
+ if not await self._run_blocking(_list_device_ids):
75
+ raise TimeoutError(f"Timed out listing available cameras after {_SDK_CALL_TIMEOUT}s.")
76
+
61
77
  if self._camera_device_name not in ids:
62
78
  raise ValueError("Could not find given device name in list of available cameras.")
63
79
 
@@ -142,10 +158,10 @@ class AravisCamera(BaseVideo, IExposureTime):
142
158
  await asyncio.sleep(0.1)
143
159
  continue
144
160
 
145
- frame: npt.NDArray[Any] = self._camera.pop_frame() # type: ignore[union-attr]
146
- while frame is None:
147
- await asyncio.sleep(0.01)
148
- frame = self._camera.pop_frame() # type: ignore[union-attr]
161
+ frame = await self._wait_for_frame()
162
+ if frame is None:
163
+ # camera went away, or the wait timed out -- back off and retry
164
+ continue
149
165
 
150
166
  if time.time() - last < self._interval:
151
167
  await asyncio.sleep(0.01)
@@ -157,6 +173,38 @@ class AravisCamera(BaseVideo, IExposureTime):
157
173
  except Exception:
158
174
  await asyncio.sleep(1)
159
175
 
176
+ async def _wait_for_frame(self, timeout: float = _FRAME_WAIT_TIMEOUT) -> npt.NDArray[Any] | None:
177
+ """Waits for the next frame without blocking the event loop.
178
+
179
+ Polls pop_frame() from a background thread (see _run_blocking) rather than polling it
180
+ directly from the async loop with a sleep in between each attempt -- pop_frame() is
181
+ assumed non-blocking in the common case, but if the underlying aravis/GLib call ever
182
+ doesn't honor that (camera hiccup, network stall for GigE Vision), polling it directly
183
+ would freeze the whole module for as long as that lasts, repeatedly, for the module's
184
+ entire runtime. Runs the whole "poll until ready" loop as a single blocking call instead,
185
+ so only one thread gets spawned per delivered frame rather than one per 10ms poll.
186
+
187
+ Returns:
188
+ The next frame, or None if the camera disappeared mid-wait or the wait timed out.
189
+ """
190
+ result: list[npt.NDArray[Any]] = []
191
+
192
+ def _poll() -> None:
193
+ camera = self._camera
194
+ while camera is not None:
195
+ frame = camera.pop_frame() # type: ignore[union-attr]
196
+ # pop_frame() can return a non-None array that's empty along axis 0 instead of
197
+ # None -- treat that the same as "not ready yet" rather than a real frame
198
+ if frame is not None and frame.size != 0: # type: ignore[union-attr]
199
+ result.append(frame) # type: ignore[arg-type]
200
+ return
201
+ time.sleep(0.01)
202
+
203
+ if not await self._run_blocking(_poll, timeout=timeout):
204
+ log.error("Timed out waiting for a frame after %.1fs.", timeout)
205
+ return None
206
+ return result[0] if result else None
207
+
160
208
  async def set_exposure_time(self, exposure_time: float, **kwargs: Any) -> None:
161
209
  """Set the exposure time in seconds.
162
210
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyobs-aravis"
3
- version = "2.0.0.dev7"
3
+ version = "2.0.0.dev9"
4
4
  description = "pyobs module for Aravis cameras"
5
5
  authors = [{ name = "Tim-Oliver Husser", email = "thusser@uni-goettingen.de" }]
6
6
  requires-python = ">=3.11,<3.14"
@@ -1770,7 +1770,7 @@ wheels = [
1770
1770
 
1771
1771
  [[package]]
1772
1772
  name = "pyobs-aravis"
1773
- version = "2.0.0.dev7"
1773
+ version = "2.0.0.dev9"
1774
1774
  source = { editable = "." }
1775
1775
  dependencies = [
1776
1776
  { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },