pyobs-aravis 2.0.0.dev5__tar.gz → 2.0.0.dev7__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.dev5 → pyobs_aravis-2.0.0.dev7}/PKG-INFO +1 -1
  2. {pyobs_aravis-2.0.0.dev5 → pyobs_aravis-2.0.0.dev7}/pyobs_aravis/araviscamera.py +49 -10
  3. {pyobs_aravis-2.0.0.dev5 → pyobs_aravis-2.0.0.dev7}/pyproject.toml +1 -1
  4. {pyobs_aravis-2.0.0.dev5 → pyobs_aravis-2.0.0.dev7}/uv.lock +1 -1
  5. {pyobs_aravis-2.0.0.dev5 → pyobs_aravis-2.0.0.dev7}/.github/workflows/pypi.yml +0 -0
  6. {pyobs_aravis-2.0.0.dev5 → pyobs_aravis-2.0.0.dev7}/.github/workflows/ruff.yml +0 -0
  7. {pyobs_aravis-2.0.0.dev5 → pyobs_aravis-2.0.0.dev7}/.pre-commit-config.yaml +0 -0
  8. {pyobs_aravis-2.0.0.dev5 → pyobs_aravis-2.0.0.dev7}/.readthedocs.yml +0 -0
  9. {pyobs_aravis-2.0.0.dev5 → pyobs_aravis-2.0.0.dev7}/CHANGELOG.rst +0 -0
  10. {pyobs_aravis-2.0.0.dev5 → pyobs_aravis-2.0.0.dev7}/LICENSE +0 -0
  11. {pyobs_aravis-2.0.0.dev5 → pyobs_aravis-2.0.0.dev7}/README.md +0 -0
  12. {pyobs_aravis-2.0.0.dev5 → pyobs_aravis-2.0.0.dev7}/docs/Makefile +0 -0
  13. {pyobs_aravis-2.0.0.dev5 → pyobs_aravis-2.0.0.dev7}/docs/make.bat +0 -0
  14. {pyobs_aravis-2.0.0.dev5 → pyobs_aravis-2.0.0.dev7}/docs/requirements.txt +0 -0
  15. {pyobs_aravis-2.0.0.dev5 → pyobs_aravis-2.0.0.dev7}/docs/source/_static/pyobs.gif +0 -0
  16. {pyobs_aravis-2.0.0.dev5 → pyobs_aravis-2.0.0.dev7}/docs/source/conf.py +0 -0
  17. {pyobs_aravis-2.0.0.dev5 → pyobs_aravis-2.0.0.dev7}/docs/source/index.rst +0 -0
  18. {pyobs_aravis-2.0.0.dev5 → pyobs_aravis-2.0.0.dev7}/pyobs_aravis/__init__.py +0 -0
  19. {pyobs_aravis-2.0.0.dev5 → pyobs_aravis-2.0.0.dev7}/pyobs_aravis/aravis.py +0 -0
  20. {pyobs_aravis-2.0.0.dev5 → pyobs_aravis-2.0.0.dev7}/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.dev5
3
+ Version: 2.0.0.dev7
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."""
@@ -32,7 +39,7 @@ class AravisCamera(BaseVideo, IExposureTime):
32
39
  BaseVideo.__init__(self, **kwargs)
33
40
  from . import aravis
34
41
 
35
- self._device_name = device
42
+ self._camera_device_name = device
36
43
  self._camera: aravis.Camera | None = None
37
44
  self._settings: dict[str, Any] = {} if settings is None else settings
38
45
  self._camera_lock = asyncio.Lock()
@@ -51,7 +58,7 @@ class AravisCamera(BaseVideo, IExposureTime):
51
58
  await BaseVideo.open(self)
52
59
 
53
60
  ids: list[str] = aravis.get_device_ids() # type: ignore[assignment]
54
- if self._device_name not in ids:
61
+ if self._camera_device_name not in ids:
55
62
  raise ValueError("Could not find given device name in list of available cameras.")
56
63
 
57
64
  await self.activate_camera()
@@ -59,15 +66,14 @@ class AravisCamera(BaseVideo, IExposureTime):
59
66
  async def close(self) -> None:
60
67
  """Close the module."""
61
68
  await BaseVideo.close(self)
62
- async with self._camera_lock:
63
- self._close_camera()
69
+ await self._deactivate_camera()
64
70
 
65
71
  def _open_camera(self) -> None:
66
72
  """Open camera."""
67
73
  from . import aravis
68
74
 
69
- log.info("Connecting to camera %s...", self._device_name)
70
- self._camera = aravis.Camera(self._device_name) # type: ignore[assignment]
75
+ log.info("Connecting to camera %s...", self._camera_device_name)
76
+ self._camera = aravis.Camera(self._camera_device_name) # type: ignore[assignment]
71
77
  log.info("Connected.")
72
78
 
73
79
  for key, value in self._settings.items():
@@ -80,19 +86,52 @@ class AravisCamera(BaseVideo, IExposureTime):
80
86
  """Close camera."""
81
87
  if self._camera is not None:
82
88
  log.info("Closing camera...")
83
- self._camera.stop_acquisition() # type: ignore[union-attr]
84
- self._camera.shutdown() # type: ignore[union-attr]
89
+ try:
90
+ self._camera.stop_acquisition() # type: ignore[union-attr]
91
+ self._camera.shutdown() # type: ignore[union-attr]
92
+ except Exception:
93
+ log.exception("Error closing camera.")
85
94
  self._camera = None
86
95
 
96
+ @staticmethod
97
+ async def _run_blocking(func: Callable[[], None], timeout: float = _SDK_CALL_TIMEOUT) -> bool:
98
+ """Run a blocking aravis/GLib call in a daemon thread, so a hung call can't freeze the module.
99
+
100
+ A plain executor isn't used here, since its worker threads are non-daemon and Python joins
101
+ them on interpreter shutdown -- a hung call would then just move the freeze to process exit.
102
+
103
+ Returns:
104
+ True if func completed within timeout, False if it's still running in the background.
105
+ """
106
+ loop = asyncio.get_running_loop()
107
+ future: asyncio.Future[None] = loop.create_future()
108
+
109
+ def _wrapper() -> None:
110
+ try:
111
+ func()
112
+ finally:
113
+ loop.call_soon_threadsafe(future.set_result, None)
114
+
115
+ threading.Thread(target=_wrapper, daemon=True).start()
116
+ try:
117
+ await asyncio.wait_for(future, timeout=timeout)
118
+ return True
119
+ except TimeoutError:
120
+ return False
121
+
87
122
  async def _activate_camera(self) -> None:
88
123
  """Open camera on activation."""
89
124
  async with self._camera_lock:
90
- self._open_camera()
125
+ if not await self._run_blocking(self._open_camera):
126
+ log.error("Timed out connecting to camera after %.1fs.", _SDK_CALL_TIMEOUT)
127
+ self._camera = None
91
128
 
92
129
  async def _deactivate_camera(self) -> None:
93
130
  """Close camera on deactivation."""
94
131
  async with self._camera_lock:
95
- self._close_camera()
132
+ if not await self._run_blocking(self._close_camera):
133
+ log.error("Timed out closing camera after %.1fs, abandoning cleanup.", _SDK_CALL_TIMEOUT)
134
+ self._camera = None
96
135
 
97
136
  async def _capture(self) -> None:
98
137
  """Take new images in loop."""
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyobs-aravis"
3
- version = "2.0.0.dev5"
3
+ version = "2.0.0.dev7"
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.dev5"
1773
+ version = "2.0.0.dev7"
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'" },