pyobs-aravis 2.0.0.dev6__tar.gz → 2.0.0.dev8__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.dev6 → pyobs_aravis-2.0.0.dev8}/PKG-INFO +1 -1
  2. {pyobs_aravis-2.0.0.dev6 → pyobs_aravis-2.0.0.dev8}/pyobs_aravis/araviscamera.py +52 -5
  3. {pyobs_aravis-2.0.0.dev6 → pyobs_aravis-2.0.0.dev8}/pyproject.toml +1 -1
  4. {pyobs_aravis-2.0.0.dev6 → pyobs_aravis-2.0.0.dev8}/uv.lock +1 -1
  5. {pyobs_aravis-2.0.0.dev6 → pyobs_aravis-2.0.0.dev8}/.github/workflows/pypi.yml +0 -0
  6. {pyobs_aravis-2.0.0.dev6 → pyobs_aravis-2.0.0.dev8}/.github/workflows/ruff.yml +0 -0
  7. {pyobs_aravis-2.0.0.dev6 → pyobs_aravis-2.0.0.dev8}/.pre-commit-config.yaml +0 -0
  8. {pyobs_aravis-2.0.0.dev6 → pyobs_aravis-2.0.0.dev8}/.readthedocs.yml +0 -0
  9. {pyobs_aravis-2.0.0.dev6 → pyobs_aravis-2.0.0.dev8}/CHANGELOG.rst +0 -0
  10. {pyobs_aravis-2.0.0.dev6 → pyobs_aravis-2.0.0.dev8}/LICENSE +0 -0
  11. {pyobs_aravis-2.0.0.dev6 → pyobs_aravis-2.0.0.dev8}/README.md +0 -0
  12. {pyobs_aravis-2.0.0.dev6 → pyobs_aravis-2.0.0.dev8}/docs/Makefile +0 -0
  13. {pyobs_aravis-2.0.0.dev6 → pyobs_aravis-2.0.0.dev8}/docs/make.bat +0 -0
  14. {pyobs_aravis-2.0.0.dev6 → pyobs_aravis-2.0.0.dev8}/docs/requirements.txt +0 -0
  15. {pyobs_aravis-2.0.0.dev6 → pyobs_aravis-2.0.0.dev8}/docs/source/_static/pyobs.gif +0 -0
  16. {pyobs_aravis-2.0.0.dev6 → pyobs_aravis-2.0.0.dev8}/docs/source/conf.py +0 -0
  17. {pyobs_aravis-2.0.0.dev6 → pyobs_aravis-2.0.0.dev8}/docs/source/index.rst +0 -0
  18. {pyobs_aravis-2.0.0.dev6 → pyobs_aravis-2.0.0.dev8}/pyobs_aravis/__init__.py +0 -0
  19. {pyobs_aravis-2.0.0.dev6 → pyobs_aravis-2.0.0.dev8}/pyobs_aravis/aravis.py +0 -0
  20. {pyobs_aravis-2.0.0.dev6 → pyobs_aravis-2.0.0.dev8}/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.dev6
3
+ Version: 2.0.0.dev8
4
4
  Summary: pyobs module for Aravis cameras
5
5
  Author-email: Tim-Oliver Husser <thusser@uni-goettingen.de>
6
6
  License-Expression: MIT
@@ -1,6 +1,8 @@
1
1
  import asyncio
2
2
  import logging
3
+ import threading
3
4
  import time
5
+ from collections.abc import Callable
4
6
  from typing import Any
5
7
 
6
8
  import numpy.typing as npt
@@ -9,6 +11,11 @@ from pyobs.modules.camera import BaseVideo
9
11
 
10
12
  log = logging.getLogger(__name__)
11
13
 
14
+ # aravis/GLib calls are blocking and are made directly on the event loop thread (see _run_blocking).
15
+ # If the camera has gone unresponsive, they can hang indefinitely, so we bound them with a timeout
16
+ # rather than let a single dead camera freeze the whole module.
17
+ _SDK_CALL_TIMEOUT = 5.0
18
+
12
19
 
13
20
  class AravisCamera(BaseVideo, IExposureTime):
14
21
  """A pyobs module for Aravis cameras."""
@@ -50,7 +57,18 @@ class AravisCamera(BaseVideo, IExposureTime):
50
57
 
51
58
  await BaseVideo.open(self)
52
59
 
53
- ids: list[str] = aravis.get_device_ids() # type: ignore[assignment]
60
+ # device discovery is a blocking, network-based scan (GigE Vision/USB3 Vision devices
61
+ # reply to a broadcast query) that can take multiple seconds -- run it like the other
62
+ # aravis/GLib calls (see _run_blocking) instead of freezing the whole module's event
63
+ # loop, and with it, the ability to respond to any other module, for that long
64
+ ids: list[str] = []
65
+
66
+ def _list_device_ids() -> None:
67
+ ids.extend(aravis.get_device_ids()) # type: ignore[arg-type]
68
+
69
+ if not await self._run_blocking(_list_device_ids):
70
+ raise TimeoutError(f"Timed out listing available cameras after {_SDK_CALL_TIMEOUT}s.")
71
+
54
72
  if self._camera_device_name not in ids:
55
73
  raise ValueError("Could not find given device name in list of available cameras.")
56
74
 
@@ -59,8 +77,7 @@ class AravisCamera(BaseVideo, IExposureTime):
59
77
  async def close(self) -> None:
60
78
  """Close the module."""
61
79
  await BaseVideo.close(self)
62
- async with self._camera_lock:
63
- self._close_camera()
80
+ await self._deactivate_camera()
64
81
 
65
82
  def _open_camera(self) -> None:
66
83
  """Open camera."""
@@ -87,15 +104,45 @@ class AravisCamera(BaseVideo, IExposureTime):
87
104
  log.exception("Error closing camera.")
88
105
  self._camera = None
89
106
 
107
+ @staticmethod
108
+ async def _run_blocking(func: Callable[[], None], timeout: float = _SDK_CALL_TIMEOUT) -> bool:
109
+ """Run a blocking aravis/GLib call in a daemon thread, so a hung call can't freeze the module.
110
+
111
+ A plain executor isn't used here, since its worker threads are non-daemon and Python joins
112
+ them on interpreter shutdown -- a hung call would then just move the freeze to process exit.
113
+
114
+ Returns:
115
+ True if func completed within timeout, False if it's still running in the background.
116
+ """
117
+ loop = asyncio.get_running_loop()
118
+ future: asyncio.Future[None] = loop.create_future()
119
+
120
+ def _wrapper() -> None:
121
+ try:
122
+ func()
123
+ finally:
124
+ loop.call_soon_threadsafe(future.set_result, None)
125
+
126
+ threading.Thread(target=_wrapper, daemon=True).start()
127
+ try:
128
+ await asyncio.wait_for(future, timeout=timeout)
129
+ return True
130
+ except TimeoutError:
131
+ return False
132
+
90
133
  async def _activate_camera(self) -> None:
91
134
  """Open camera on activation."""
92
135
  async with self._camera_lock:
93
- self._open_camera()
136
+ if not await self._run_blocking(self._open_camera):
137
+ log.error("Timed out connecting to camera after %.1fs.", _SDK_CALL_TIMEOUT)
138
+ self._camera = None
94
139
 
95
140
  async def _deactivate_camera(self) -> None:
96
141
  """Close camera on deactivation."""
97
142
  async with self._camera_lock:
98
- self._close_camera()
143
+ if not await self._run_blocking(self._close_camera):
144
+ log.error("Timed out closing camera after %.1fs, abandoning cleanup.", _SDK_CALL_TIMEOUT)
145
+ self._camera = None
99
146
 
100
147
  async def _capture(self) -> None:
101
148
  """Take new images in loop."""
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyobs-aravis"
3
- version = "2.0.0.dev6"
3
+ version = "2.0.0.dev8"
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.dev6"
1773
+ version = "2.0.0.dev8"
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'" },