pyobs-zwoeaf 2.0.0.dev8__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 (42) hide show
  1. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/PKG-INFO +1 -1
  2. pyobs_zwoeaf-2.0.0.dev9/lib/lib/mac/libEAFFocuser.dylib.1.6 +0 -0
  3. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/pyproject.toml +1 -1
  4. pyobs_zwoeaf-2.0.0.dev9/src/pyobs_zwoeaf/eaffocuser.py +246 -0
  5. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/uv.lock +1 -1
  6. pyobs_zwoeaf-2.0.0.dev8/lib/lib/mac/libEAFFocuser.dylib +0 -1
  7. pyobs_zwoeaf-2.0.0.dev8/src/pyobs_zwoeaf/eaffocuser.py +0 -148
  8. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/.github/workflows/pypi.yml +0 -0
  9. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/.github/workflows/ruff.yml +0 -0
  10. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/.gitignore +0 -0
  11. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/.pre-commit-config.yaml +0 -0
  12. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/.readthedocs.yml +0 -0
  13. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/CMakeLists.txt +0 -0
  14. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/LICENSE +0 -0
  15. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/README.md +0 -0
  16. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/docs/Makefile +0 -0
  17. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/docs/make.bat +0 -0
  18. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/docs/requirements.txt +0 -0
  19. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/docs/source/_static/pyobs.gif +0 -0
  20. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/docs/source/conf.py +0 -0
  21. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/docs/source/index.rst +0 -0
  22. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/lib/demo/c/Makefile +0 -0
  23. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/lib/demo/c/bin/x86/test_console +0 -0
  24. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/lib/demo/c/main.cpp +0 -0
  25. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/lib/include/EAF_focuser.h +0 -0
  26. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/lib/lib/armv6/libEAFFocuser.a +0 -0
  27. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/lib/lib/armv6/libEAFFocuser.so.1.6 +0 -0
  28. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/lib/lib/armv7/libEAFFocuser.a +0 -0
  29. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/lib/lib/armv7/libEAFFocuser.so.1.6 +0 -0
  30. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/lib/lib/armv8/libEAFFocuser.a +0 -0
  31. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/lib/lib/armv8/libEAFFocuser.so.1.6 +0 -0
  32. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/lib/lib/eaf.rules +0 -0
  33. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/lib/lib/mac/libEAFFocuser.a +0 -0
  34. /pyobs_zwoeaf-2.0.0.dev8/lib/lib/mac/libEAFFocuser.dylib.1.6 → /pyobs_zwoeaf-2.0.0.dev9/lib/lib/mac/libEAFFocuser.dylib +0 -0
  35. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/lib/lib/x64/libEAFFocuser.a +0 -0
  36. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/lib/lib/x64/libEAFFocuser.so.1.6 +0 -0
  37. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/lib/lib/x86/libEAFFocuser.a +0 -0
  38. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/lib/lib/x86/libEAFFocuser.so.1.6 +0 -0
  39. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/lib/license.txt +0 -0
  40. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/src/pyobs_zwoeaf/EAF_focuser.cpp +0 -0
  41. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/src/pyobs_zwoeaf/__init__.py +0 -0
  42. {pyobs_zwoeaf-2.0.0.dev8 → pyobs_zwoeaf-2.0.0.dev9}/src/pyobs_zwoeaf/cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyobs-zwoeaf
3
- Version: 2.0.0.dev8
3
+ Version: 2.0.0.dev9
4
4
  Summary: pyobs model for ZWO EAF focus motors
5
5
  Author-Email: Lennart Jonas Buhrke <l.buhrke@stud.uni-goettingen.de>, Tim-Oliver Husser <thusser@uni-goettingen.de>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyobs-zwoeaf"
3
- version = "2.0.0.dev8"
3
+ version = "2.0.0.dev9"
4
4
  description = "pyobs model for ZWO EAF focus motors"
5
5
  authors = [
6
6
  { name = "Lennart Jonas Buhrke", email = "l.buhrke@stud.uni-goettingen.de" },
@@ -0,0 +1,246 @@
1
+ import asyncio
2
+ import logging
3
+ import threading
4
+ import time
5
+ from collections.abc import Callable
6
+ from typing import Any
7
+
8
+ import pyobs.utils.exceptions as exc
9
+ from pyobs.interfaces import IFocuser, IReady, ITemperatures
10
+ from pyobs.interfaces.IFocuser import FocuserState
11
+ from pyobs.interfaces.IReady import ReadyState
12
+ from pyobs.interfaces.ITemperatures import SensorReading, TemperaturesState
13
+ from pyobs.mixins import MotionStatusMixin
14
+ from pyobs.modules import Module
15
+ from pyobs.utils.enums import MotionStatus
16
+
17
+ log = logging.getLogger(__name__)
18
+
19
+ _STEP_TO_MM = 0.00242105263
20
+
21
+ # the ZWO EAF SDK's calls are blocking and are made directly on the event loop thread (see
22
+ # _run_blocking). If the focuser has gone unresponsive, they can hang indefinitely, so they're
23
+ # bounded with a timeout rather than let a single dead connection freeze the whole module.
24
+ _SDK_CALL_TIMEOUT = 5.0
25
+
26
+ # set_focus()'s move-and-wait-until-done sequence legitimately takes longer than the other SDK
27
+ # calls above, so it gets its own, more generous timeout.
28
+ _MOVE_TIMEOUT = 60.0
29
+
30
+
31
+ class EAFFocuser(Module, MotionStatusMixin, IFocuser, ITemperatures):
32
+ """A pyobs module for the ZWO EAF electronic auto focuser."""
33
+
34
+ __module__ = "pyobs_zwoeaf"
35
+
36
+ def __init__(
37
+ self,
38
+ device_number: int = 0,
39
+ max_steps: int = 60000,
40
+ backlash: int = 0,
41
+ direction: bool = True,
42
+ sound: bool = True,
43
+ **kwargs: Any,
44
+ ) -> None:
45
+ Module.__init__(self, **kwargs)
46
+ MotionStatusMixin.__init__(self)
47
+
48
+ self._device_number = device_number
49
+ self._max_steps = max_steps
50
+ self._backlash = backlash
51
+ self._direction = direction
52
+ self._sound = sound
53
+ self._eaf: Any | None = None
54
+ self._focus_setpoint = 0.0
55
+ self._focus_offset = 0.0
56
+
57
+ self.add_background_task(self._poll_temperature)
58
+
59
+ @staticmethod
60
+ async def _run_blocking(
61
+ func: Callable[[], None], timeout: float = _SDK_CALL_TIMEOUT
62
+ ) -> bool:
63
+ """Run a blocking EAF SDK call in a daemon thread, so a hung call can't freeze the module.
64
+
65
+ A plain executor isn't used here, since its worker threads are non-daemon and Python joins
66
+ them on interpreter shutdown -- a hung call would then just move the freeze to process exit.
67
+
68
+ Returns:
69
+ True if func completed within timeout, False if it's still running in the background.
70
+ """
71
+ loop = asyncio.get_running_loop()
72
+ future: asyncio.Future[None] = loop.create_future()
73
+
74
+ def _wrapper() -> None:
75
+ try:
76
+ func()
77
+ finally:
78
+ loop.call_soon_threadsafe(future.set_result, None)
79
+
80
+ threading.Thread(target=_wrapper, daemon=True).start()
81
+ try:
82
+ await asyncio.wait_for(future, timeout=timeout)
83
+ return True
84
+ except TimeoutError:
85
+ return False
86
+
87
+ async def open(self) -> None:
88
+ """Open module."""
89
+ await Module.open(self)
90
+
91
+ from .EAF_focuser import EAF # type: ignore[import-untyped]
92
+
93
+ self._eaf = EAF()
94
+ eaf = self._eaf
95
+
96
+ result: list[tuple[bool, float, float]] = []
97
+
98
+ def _connect() -> None:
99
+ if not eaf.connect(self._device_number):
100
+ result.append((False, 0.0, 0.0))
101
+ return
102
+ eaf.setMaximalStep(self._max_steps)
103
+ eaf.setBacklash(self._backlash)
104
+ eaf.setDirection(self._direction)
105
+ eaf.setSound(self._sound)
106
+ result.append((True, eaf.getTemperature(), eaf.getPosition()))
107
+
108
+ if not await self._run_blocking(_connect):
109
+ raise TimeoutError(
110
+ f"Timed out connecting to EAF focuser after {_SDK_CALL_TIMEOUT}s."
111
+ )
112
+ connected, temperature, position = result[0]
113
+ if not connected:
114
+ raise ValueError(
115
+ "EAF focuser failed to connect. Is the device connected via USB? Correct device_number?"
116
+ )
117
+
118
+ log.info("Connected to EAF focuser, temperature: %.2f°C", temperature)
119
+
120
+ await MotionStatusMixin.open(self)
121
+ await self._change_motion_status(MotionStatus.IDLE)
122
+
123
+ self._focus_setpoint = position * _STEP_TO_MM
124
+ await self.comm.set_state(
125
+ IFocuser,
126
+ FocuserState(focus=self._focus_setpoint, focus_offset=self._focus_offset),
127
+ )
128
+ await self.comm.set_state(IReady, ReadyState(ready=True))
129
+
130
+ async def close(self) -> None:
131
+ """Close module."""
132
+ if self._eaf is not None:
133
+ eaf = self._eaf
134
+ self._eaf = None
135
+ if not await self._run_blocking(eaf.disconnect):
136
+ log.error(
137
+ "Timed out disconnecting EAF focuser after %.1fs.",
138
+ _SDK_CALL_TIMEOUT,
139
+ )
140
+ await Module.close(self)
141
+
142
+ async def init(self, **kwargs: Any) -> None:
143
+ pass
144
+
145
+ async def park(self, **kwargs: Any) -> None:
146
+ """Park focuser at position zero."""
147
+ await self.stop_motion()
148
+ await self.set_focus(0.0)
149
+
150
+ async def stop_motion(self, device: str | None = None, **kwargs: Any) -> None:
151
+ """Stop focuser motion."""
152
+ if self._eaf is not None:
153
+ if not await self._run_blocking(self._eaf.stop):
154
+ log.error(
155
+ "Timed out stopping EAF focuser motion after %.1fs.",
156
+ _SDK_CALL_TIMEOUT,
157
+ )
158
+ await self._change_motion_status(MotionStatus.IDLE)
159
+
160
+ async def set_focus(self, focus: float, **kwargs: Any) -> None:
161
+ """Move focuser to given position.
162
+
163
+ Args:
164
+ focus: New focus position in mm.
165
+
166
+ Raises:
167
+ MoveError: If focuser cannot be moved.
168
+ """
169
+ if self._eaf is None:
170
+ raise ValueError("Not connected.")
171
+ eaf = self._eaf
172
+
173
+ total_mm = focus + self._focus_offset
174
+ step = int(total_mm / _STEP_TO_MM)
175
+ log.info(
176
+ "Moving EAF to %.4f mm (offset %.4f mm, step %d)...",
177
+ focus,
178
+ self._focus_offset,
179
+ step,
180
+ )
181
+
182
+ await self._change_motion_status(MotionStatus.SLEWING)
183
+
184
+ result: list[bool] = []
185
+
186
+ def _move() -> None:
187
+ if not eaf.move(step):
188
+ result.append(False)
189
+ return
190
+ # run the whole "move, then poll until done" sequence as a single blocking call, so
191
+ # only one thread gets spawned per move rather than one per 0.5s poll (see _run_blocking)
192
+ while eaf.isMoving():
193
+ time.sleep(0.1)
194
+ result.append(True)
195
+
196
+ if not await self._run_blocking(_move, timeout=_MOVE_TIMEOUT):
197
+ await self._change_motion_status(MotionStatus.ERROR)
198
+ raise exc.MoveError(f"Timed out moving EAF motor after {_MOVE_TIMEOUT}s.")
199
+ if not result[0]:
200
+ await self._change_motion_status(MotionStatus.ERROR)
201
+ raise exc.MoveError("Could not move EAF motor.")
202
+
203
+ self._focus_setpoint = focus
204
+ await self._change_motion_status(MotionStatus.POSITIONED)
205
+ await self.comm.set_state(
206
+ IFocuser,
207
+ FocuserState(focus=self._focus_setpoint, focus_offset=self._focus_offset),
208
+ )
209
+
210
+ async def set_focus_offset(self, offset: float, **kwargs: Any) -> None:
211
+ """Set focus offset and re-apply.
212
+
213
+ Args:
214
+ offset: New focus offset in mm.
215
+
216
+ Raises:
217
+ MoveError: If focuser cannot be moved.
218
+ """
219
+ log.info("Setting focus offset to %.4f mm.", offset)
220
+ self._focus_offset = offset
221
+ await self.set_focus(self._focus_setpoint)
222
+
223
+ async def _poll_temperature(self) -> None:
224
+ """Background task: periodically reads EAF temperature."""
225
+ while True:
226
+ try:
227
+ if self._eaf is not None:
228
+ eaf = self._eaf
229
+ result: list[float] = []
230
+
231
+ def _get_temp() -> None:
232
+ result.append(eaf.getTemperature())
233
+
234
+ if await self._run_blocking(_get_temp):
235
+ await self.comm.set_state(
236
+ ITemperatures,
237
+ TemperaturesState(
238
+ readings=[SensorReading(name="EAF", value=result[0])]
239
+ ),
240
+ )
241
+ except Exception:
242
+ pass
243
+ await asyncio.sleep(10)
244
+
245
+
246
+ __all__ = ["EAFFocuser"]
@@ -1357,7 +1357,7 @@ wheels = [
1357
1357
 
1358
1358
  [[package]]
1359
1359
  name = "pyobs-zwoeaf"
1360
- version = "2.0.0.dev8"
1360
+ version = "2.0.0.dev9"
1361
1361
  source = { editable = "." }
1362
1362
  dependencies = [
1363
1363
  { name = "pyobs-core" },
@@ -1 +0,0 @@
1
- libEAFFocuser.dylib.1.6
@@ -1,148 +0,0 @@
1
- import asyncio
2
- import logging
3
- from typing import Any
4
-
5
- import pyobs.utils.exceptions as exc
6
- from pyobs.interfaces import IFocuser, IReady, ITemperatures
7
- from pyobs.interfaces.IFocuser import FocuserState
8
- from pyobs.interfaces.IReady import ReadyState
9
- from pyobs.interfaces.ITemperatures import SensorReading, TemperaturesState
10
- from pyobs.mixins import MotionStatusMixin
11
- from pyobs.modules import Module
12
- from pyobs.utils.enums import MotionStatus
13
-
14
- log = logging.getLogger(__name__)
15
-
16
- _STEP_TO_MM = 0.00242105263
17
-
18
-
19
- class EAFFocuser(Module, MotionStatusMixin, IFocuser, ITemperatures):
20
- """A pyobs module for the ZWO EAF electronic auto focuser."""
21
-
22
- __module__ = "pyobs_zwoeaf"
23
-
24
- def __init__(
25
- self,
26
- device_number: int = 0,
27
- max_steps: int = 60000,
28
- backlash: int = 0,
29
- direction: bool = True,
30
- sound: bool = True,
31
- **kwargs: Any,
32
- ) -> None:
33
- Module.__init__(self, **kwargs)
34
- MotionStatusMixin.__init__(self)
35
-
36
- self._device_number = device_number
37
- self._max_steps = max_steps
38
- self._backlash = backlash
39
- self._direction = direction
40
- self._sound = sound
41
- self._eaf: Any | None = None
42
- self._focus_setpoint = 0.0
43
- self._focus_offset = 0.0
44
-
45
- self.add_background_task(self._poll_temperature)
46
-
47
- async def open(self) -> None:
48
- """Open module."""
49
- await Module.open(self)
50
-
51
- from .EAF_focuser import EAF # type: ignore[import-untyped]
52
-
53
- self._eaf = EAF()
54
- if not self._eaf.connect(self._device_number):
55
- raise ValueError("EAF focuser failed to connect. Is the device connected via USB? Correct device_number?")
56
-
57
- self._eaf.setMaximalStep(self._max_steps)
58
- self._eaf.setBacklash(self._backlash)
59
- self._eaf.setDirection(self._direction)
60
- self._eaf.setSound(self._sound)
61
-
62
- log.info("Connected to EAF focuser, temperature: %.2f°C", self._eaf.getTemperature())
63
-
64
- await MotionStatusMixin.open(self)
65
- await self._change_motion_status(MotionStatus.IDLE)
66
-
67
- self._focus_setpoint = self._eaf.getPosition() * _STEP_TO_MM
68
- await self.comm.set_state(IFocuser, FocuserState(focus=self._focus_setpoint, focus_offset=self._focus_offset))
69
- await self.comm.set_state(IReady, ReadyState(ready=True))
70
-
71
- async def close(self) -> None:
72
- """Close module."""
73
- if self._eaf is not None:
74
- self._eaf.disconnect()
75
- self._eaf = None
76
- await Module.close(self)
77
-
78
- async def init(self, **kwargs: Any) -> None:
79
- pass
80
-
81
- async def park(self, **kwargs: Any) -> None:
82
- """Park focuser at position zero."""
83
- await self.stop_motion()
84
- await self.set_focus(0.0)
85
-
86
- async def stop_motion(self, device: str | None = None, **kwargs: Any) -> None:
87
- """Stop focuser motion."""
88
- if self._eaf is not None:
89
- self._eaf.stop()
90
- await self._change_motion_status(MotionStatus.IDLE)
91
-
92
- async def set_focus(self, focus: float, **kwargs: Any) -> None:
93
- """Move focuser to given position.
94
-
95
- Args:
96
- focus: New focus position in mm.
97
-
98
- Raises:
99
- MoveError: If focuser cannot be moved.
100
- """
101
- if self._eaf is None:
102
- raise ValueError("Not connected.")
103
-
104
- total_mm = focus + self._focus_offset
105
- step = int(total_mm / _STEP_TO_MM)
106
- log.info("Moving EAF to %.4f mm (offset %.4f mm, step %d)...", focus, self._focus_offset, step)
107
-
108
- await self._change_motion_status(MotionStatus.SLEWING)
109
- if not self._eaf.move(step):
110
- await self._change_motion_status(MotionStatus.ERROR)
111
- raise exc.MoveError("Could not move EAF motor.")
112
-
113
- while self._eaf.isMoving():
114
- await asyncio.sleep(0.5)
115
-
116
- self._focus_setpoint = focus
117
- await self._change_motion_status(MotionStatus.POSITIONED)
118
- await self.comm.set_state(IFocuser, FocuserState(focus=self._focus_setpoint, focus_offset=self._focus_offset))
119
-
120
- async def set_focus_offset(self, offset: float, **kwargs: Any) -> None:
121
- """Set focus offset and re-apply.
122
-
123
- Args:
124
- offset: New focus offset in mm.
125
-
126
- Raises:
127
- MoveError: If focuser cannot be moved.
128
- """
129
- log.info("Setting focus offset to %.4f mm.", offset)
130
- self._focus_offset = offset
131
- await self.set_focus(self._focus_setpoint)
132
-
133
- async def _poll_temperature(self) -> None:
134
- """Background task: periodically reads EAF temperature."""
135
- while True:
136
- try:
137
- if self._eaf is not None:
138
- temp = self._eaf.getTemperature()
139
- await self.comm.set_state(
140
- ITemperatures,
141
- TemperaturesState(readings=[SensorReading(name="EAF", value=temp)]),
142
- )
143
- except Exception:
144
- pass
145
- await asyncio.sleep(10)
146
-
147
-
148
- __all__ = ["EAFFocuser"]