pyobs-alpaca 1.1.2__tar.gz → 2.0.0.dev2__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_alpaca-2.0.0.dev2/.github/workflows/ruff.yml +23 -0
- pyobs_alpaca-2.0.0.dev2/.pre-commit-config.yaml +10 -0
- {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/PKG-INFO +2 -2
- pyobs_alpaca-2.0.0.dev2/pyobs_alpaca/__init__.py +3 -0
- {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/pyobs_alpaca/device.py +9 -16
- {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/pyobs_alpaca/dome.py +39 -127
- {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/pyobs_alpaca/focuser.py +31 -114
- {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/pyobs_alpaca/telescope.py +92 -204
- {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/pyproject.toml +16 -6
- pyobs_alpaca-2.0.0.dev2/uv.lock +2107 -0
- pyobs_alpaca-1.1.2/.pre-commit-config.yaml +0 -15
- pyobs_alpaca-1.1.2/pyobs_alpaca/__init__.py +0 -8
- pyobs_alpaca-1.1.2/uv.lock +0 -1508
- {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/.github/workflows/pypi.yml +0 -0
- {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/.readthedocs.yml +0 -0
- {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/CHANGELOG.rst +0 -0
- {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/LICENSE +0 -0
- {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/README.md +0 -0
- {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/docs/Makefile +0 -0
- {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/docs/make.bat +0 -0
- {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/docs/source/_static/pyobs.gif +0 -0
- {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/docs/source/conf.py +0 -0
- {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/docs/source/index.rst +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: ruff
|
|
2
|
+
on: push
|
|
3
|
+
|
|
4
|
+
jobs:
|
|
5
|
+
ruff:
|
|
6
|
+
runs-on: ubuntu-latest
|
|
7
|
+
timeout-minutes: 10
|
|
8
|
+
|
|
9
|
+
steps:
|
|
10
|
+
- name: Check out repository code
|
|
11
|
+
uses: actions/checkout@v4
|
|
12
|
+
|
|
13
|
+
- name: Set up Python
|
|
14
|
+
uses: astral-sh/setup-uv@v5
|
|
15
|
+
with:
|
|
16
|
+
enable-cache: true
|
|
17
|
+
python-version: "3.13"
|
|
18
|
+
|
|
19
|
+
- name: Install packages
|
|
20
|
+
run: uv sync --all-extras --dev
|
|
21
|
+
|
|
22
|
+
- name: Run ruff
|
|
23
|
+
run: uv run --no-sync ruff check pyobs_alpaca/
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyobs-alpaca
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0.dev2
|
|
4
4
|
Summary: pyobs module for ASCOM Alpaca
|
|
5
5
|
Author-email: Tim-Oliver Husser <thusser@uni-goettingen.de>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
License-File: LICENSE
|
|
8
8
|
Requires-Python: >=3.11
|
|
9
9
|
Requires-Dist: numpy<3,>=2.2.5
|
|
10
|
-
Requires-Dist: pyobs-core<2
|
|
10
|
+
Requires-Dist: pyobs-core<3,>=2.0.0.dev1
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
3
|
from typing import Any, NamedTuple
|
|
4
|
-
import aiohttp
|
|
5
4
|
|
|
5
|
+
import aiohttp
|
|
6
6
|
from pyobs.object import Object
|
|
7
7
|
|
|
8
8
|
log = logging.getLogger("pyobs")
|
|
@@ -112,14 +112,7 @@ class AlpacaDevice(Object):
|
|
|
112
112
|
Returns:
|
|
113
113
|
Full Alpaca URL
|
|
114
114
|
"""
|
|
115
|
-
return "http
|
|
116
|
-
self._server,
|
|
117
|
-
self._port,
|
|
118
|
-
self._version,
|
|
119
|
-
self._type,
|
|
120
|
-
self._device,
|
|
121
|
-
name.lower(),
|
|
122
|
-
)
|
|
115
|
+
return f"http://{self._server}:{self._port}/api/{self._version}/{self._type}/{self._device}/{name.lower()}"
|
|
123
116
|
|
|
124
117
|
async def _get(self, name: str) -> Any:
|
|
125
118
|
"""Calls GET on Alpaca server, which returns value for variable with given name.
|
|
@@ -140,7 +133,7 @@ class AlpacaDevice(Object):
|
|
|
140
133
|
# request it
|
|
141
134
|
try:
|
|
142
135
|
async with aiohttp.ClientSession() as session:
|
|
143
|
-
async with session.get(url, timeout=5) as response:
|
|
136
|
+
async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as response:
|
|
144
137
|
if response.status != 200:
|
|
145
138
|
raise ConnectionError(
|
|
146
139
|
f"ALPACA server responded with error {response.status}: {await response.text()}."
|
|
@@ -148,13 +141,13 @@ class AlpacaDevice(Object):
|
|
|
148
141
|
json = await response.json()
|
|
149
142
|
resp = ServerGetResponse(**json)
|
|
150
143
|
|
|
151
|
-
except
|
|
144
|
+
except TimeoutError:
|
|
152
145
|
# raise a ConnectionError instead
|
|
153
146
|
raise ConnectionError("Connection to ALPACA server timed out.")
|
|
154
147
|
|
|
155
148
|
# check error
|
|
156
149
|
if resp.ErrorNumber != 0:
|
|
157
|
-
raise ConnectionError("Server error:
|
|
150
|
+
raise ConnectionError(f"Server error: {resp.ErrorMessage}")
|
|
158
151
|
|
|
159
152
|
# return value
|
|
160
153
|
return resp.Value
|
|
@@ -177,7 +170,7 @@ class AlpacaDevice(Object):
|
|
|
177
170
|
raise ConnectionError("Not connected to ASCOM.")
|
|
178
171
|
return await self._get(name)
|
|
179
172
|
|
|
180
|
-
async def put(self, name: str, timeout: float = 5, **values: Any) -> None:
|
|
173
|
+
async def put(self, name: str, timeout: float = 5.0, **values: Any) -> None:
|
|
181
174
|
"""Calls PUT on Alpaca server with given variable, which might set a variable or call a method.
|
|
182
175
|
|
|
183
176
|
Args:
|
|
@@ -199,7 +192,7 @@ class AlpacaDevice(Object):
|
|
|
199
192
|
# request it
|
|
200
193
|
try:
|
|
201
194
|
async with aiohttp.ClientSession() as session:
|
|
202
|
-
async with session.put(url, data=values, timeout=timeout) as response:
|
|
195
|
+
async with session.put(url, data=values, timeout=aiohttp.ClientTimeout(total=timeout)) as response:
|
|
203
196
|
if response.status != 200:
|
|
204
197
|
raise ConnectionError(
|
|
205
198
|
f"ALPACA server responded with error {response.status}: {await response.text()}."
|
|
@@ -207,13 +200,13 @@ class AlpacaDevice(Object):
|
|
|
207
200
|
json = await response.json()
|
|
208
201
|
resp = ServerPutResponse(**json)
|
|
209
202
|
|
|
210
|
-
except
|
|
203
|
+
except TimeoutError:
|
|
211
204
|
# raise a ConnectionError instead
|
|
212
205
|
raise ConnectionError("Connection to ALPACA server timed out.")
|
|
213
206
|
|
|
214
207
|
# check error
|
|
215
208
|
if resp.ErrorNumber != 0:
|
|
216
|
-
raise ConnectionError("Server error:
|
|
209
|
+
raise ConnectionError(f"Server error: {resp.ErrorMessage}")
|
|
217
210
|
|
|
218
211
|
|
|
219
212
|
__all__ = ["AlpacaDevice"]
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
3
|
import time
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Any
|
|
5
5
|
|
|
6
|
-
from pyobs.events import
|
|
6
|
+
from pyobs.events import RoofClosingEvent, RoofOpenedEvent
|
|
7
|
+
from pyobs.interfaces import AltAzState, IPointingAltAz, IReady, ReadyState
|
|
7
8
|
from pyobs.mixins import FollowMixin
|
|
8
|
-
from pyobs.interfaces import IPointingAltAz
|
|
9
9
|
from pyobs.modules import timeout
|
|
10
10
|
from pyobs.modules.roof import BaseDome
|
|
11
|
+
from pyobs.utils import exceptions as exc
|
|
11
12
|
from pyobs.utils.enums import MotionStatus
|
|
12
13
|
from pyobs.utils.parallel import event_wait
|
|
13
14
|
from pyobs.utils.threads import LockWithAbort
|
|
14
|
-
|
|
15
|
+
|
|
15
16
|
from .device import AlpacaDevice
|
|
16
17
|
|
|
17
18
|
log = logging.getLogger("pyobs")
|
|
@@ -20,8 +21,8 @@ log = logging.getLogger("pyobs")
|
|
|
20
21
|
class AlpacaDome(FollowMixin, BaseDome):
|
|
21
22
|
__module__ = "pyobs_alpaca"
|
|
22
23
|
|
|
23
|
-
def __init__(self, tolerance: float = 3, park_az: float = 180, follow:
|
|
24
|
-
"""Initializes a new ASCOM Alpaca
|
|
24
|
+
def __init__(self, tolerance: float = 3, park_az: float = 180, follow: str | None = None, **kwargs: Any):
|
|
25
|
+
"""Initializes a new ASCOM Alpaca dome.
|
|
25
26
|
|
|
26
27
|
Args:
|
|
27
28
|
tolerance: Tolerance for azimuth.
|
|
@@ -60,9 +61,8 @@ class AlpacaDome(FollowMixin, BaseDome):
|
|
|
60
61
|
async def open(self) -> None:
|
|
61
62
|
"""Open module."""
|
|
62
63
|
await BaseDome.open(self)
|
|
63
|
-
|
|
64
|
-
# init status to IDLE
|
|
65
64
|
await self._change_motion_status(MotionStatus.IDLE)
|
|
65
|
+
await self.comm.set_state(IReady, ReadyState(ready=self._device.connected))
|
|
66
66
|
|
|
67
67
|
async def _send_open_dome(self) -> None:
|
|
68
68
|
"""Send command to open dome."""
|
|
@@ -74,40 +74,28 @@ class AlpacaDome(FollowMixin, BaseDome):
|
|
|
74
74
|
|
|
75
75
|
@timeout(1200000)
|
|
76
76
|
async def init(self, **kwargs: Any) -> None:
|
|
77
|
-
"""Open dome.
|
|
77
|
+
"""Open dome."""
|
|
78
78
|
|
|
79
|
-
Raises:
|
|
80
|
-
pyobs.utils.exceptions.InitError: If dome cannot be opened.
|
|
81
|
-
"""
|
|
82
|
-
|
|
83
|
-
# weather?
|
|
84
79
|
if not self.is_weather_good():
|
|
85
80
|
raise exc.InitError("Weather seems to be bad.")
|
|
86
81
|
|
|
87
|
-
|
|
88
|
-
if await self.get_motion_status() == MotionStatus.INITIALIZING:
|
|
82
|
+
if self.motion_status() == MotionStatus.INITIALIZING:
|
|
89
83
|
return
|
|
90
84
|
|
|
91
|
-
# acquire lock
|
|
92
85
|
async with LockWithAbort(self._lock_shutter, self._abort_shutter):
|
|
93
|
-
# log
|
|
94
86
|
log.info("Opening dome...")
|
|
95
87
|
await self._change_motion_status(MotionStatus.INITIALIZING)
|
|
96
88
|
|
|
97
|
-
# execute command
|
|
98
89
|
await self._send_open_dome()
|
|
99
90
|
time_attempt = time.time()
|
|
100
91
|
|
|
101
|
-
# wait for it
|
|
102
92
|
status = None
|
|
103
93
|
while status != 0:
|
|
104
|
-
# error?
|
|
105
94
|
if status == 4:
|
|
106
95
|
log.error("Could not open dome.")
|
|
107
96
|
await self._change_motion_status(MotionStatus.UNKNOWN)
|
|
108
97
|
return
|
|
109
98
|
|
|
110
|
-
# wait a little and update
|
|
111
99
|
await event_wait(self._abort_shutter, 1)
|
|
112
100
|
try:
|
|
113
101
|
status = await self._device.get("ShutterStatus")
|
|
@@ -115,18 +103,16 @@ class AlpacaDome(FollowMixin, BaseDome):
|
|
|
115
103
|
await self._change_motion_status(MotionStatus.UNKNOWN)
|
|
116
104
|
raise exc.InitError("Could not open dome.")
|
|
117
105
|
|
|
118
|
-
# waited >10s?
|
|
119
106
|
if time.time() - time_attempt > 10:
|
|
120
107
|
await self._send_open_dome()
|
|
121
108
|
time_attempt = time.time()
|
|
122
109
|
|
|
123
|
-
# set new status
|
|
124
110
|
log.info("Dome opened.")
|
|
125
111
|
await self._change_motion_status(MotionStatus.POSITIONED)
|
|
126
112
|
await self.comm.send_event(RoofOpenedEvent())
|
|
127
113
|
|
|
128
114
|
async def _send_close_dome(self) -> None:
|
|
129
|
-
"""Send command to
|
|
115
|
+
"""Send command to close dome."""
|
|
130
116
|
try:
|
|
131
117
|
await self._device.put("CloseShutter")
|
|
132
118
|
await self._device.put("SlewToAzimuth", Azimuth=0)
|
|
@@ -136,37 +122,26 @@ class AlpacaDome(FollowMixin, BaseDome):
|
|
|
136
122
|
|
|
137
123
|
@timeout(1200000)
|
|
138
124
|
async def park(self, **kwargs: Any) -> None:
|
|
139
|
-
"""Close dome.
|
|
125
|
+
"""Close dome."""
|
|
140
126
|
|
|
141
|
-
|
|
142
|
-
pyobs.utils.exceptions.ParkError: If dome cannot be opened.
|
|
143
|
-
"""
|
|
144
|
-
|
|
145
|
-
# if already closing, ignore
|
|
146
|
-
if await self.get_motion_status() == MotionStatus.PARKING:
|
|
127
|
+
if self.motion_status() == MotionStatus.PARKING:
|
|
147
128
|
return
|
|
148
129
|
|
|
149
|
-
# acquire lock
|
|
150
130
|
async with LockWithAbort(self._lock_shutter, self._abort_shutter):
|
|
151
|
-
# log
|
|
152
131
|
log.info("Closing dome...")
|
|
153
132
|
await self._change_motion_status(MotionStatus.PARKING)
|
|
154
133
|
await self.comm.send_event(RoofClosingEvent())
|
|
155
134
|
|
|
156
|
-
# send command for closing shutter and rotate to South
|
|
157
135
|
await self._send_close_dome()
|
|
158
136
|
time_attempt = time.time()
|
|
159
137
|
|
|
160
|
-
# wait for it
|
|
161
138
|
status = None
|
|
162
139
|
while status != 1:
|
|
163
|
-
# error?
|
|
164
140
|
if status == 4:
|
|
165
141
|
log.error("Could not close dome.")
|
|
166
142
|
await self._change_motion_status(MotionStatus.UNKNOWN)
|
|
167
143
|
raise exc.ParkError("Could not close dome.")
|
|
168
144
|
|
|
169
|
-
# wait a little and update
|
|
170
145
|
await event_wait(self._abort_shutter, 1)
|
|
171
146
|
try:
|
|
172
147
|
status = await self._device.get("ShutterStatus")
|
|
@@ -174,41 +149,27 @@ class AlpacaDome(FollowMixin, BaseDome):
|
|
|
174
149
|
await self._change_motion_status(MotionStatus.UNKNOWN)
|
|
175
150
|
raise exc.ParkError("Could not close dome.")
|
|
176
151
|
|
|
177
|
-
# waited >10s?
|
|
178
152
|
if time.time() - time_attempt > 10:
|
|
179
153
|
await self._send_close_dome()
|
|
180
154
|
time_attempt = time.time()
|
|
181
155
|
|
|
182
|
-
# set new status
|
|
183
156
|
log.info("Dome closed.")
|
|
184
157
|
await self._change_motion_status(MotionStatus.PARKED)
|
|
185
158
|
|
|
186
159
|
async def _move(self, az: float, abort: asyncio.Event) -> None:
|
|
187
|
-
"""Move the
|
|
188
|
-
|
|
189
|
-
Args:
|
|
190
|
-
az: Azimuth to move to.
|
|
191
|
-
abort: Abort event.
|
|
192
|
-
|
|
193
|
-
Raises:
|
|
194
|
-
pyobs.utils.exceptions.MoveError: If dome cannot be moved.
|
|
195
|
-
"""
|
|
160
|
+
"""Move the dome to the given azimuth."""
|
|
196
161
|
|
|
197
|
-
# execute command
|
|
198
162
|
try:
|
|
199
163
|
await self._device.put("SlewToAzimuth", Azimuth=self._adjust_azimuth(az))
|
|
200
164
|
except ConnectionError:
|
|
201
165
|
await self._change_motion_status(MotionStatus.UNKNOWN)
|
|
202
166
|
raise exc.MoveError("Could not move dome.")
|
|
203
167
|
|
|
204
|
-
# wait for it
|
|
205
168
|
log_timer = 0
|
|
206
169
|
while 180 - abs(abs(az - self._azimuth) - 180) > self._tolerance:
|
|
207
|
-
# abort?
|
|
208
170
|
if abort.is_set():
|
|
209
171
|
raise InterruptedError("Moving dome aborted.")
|
|
210
172
|
|
|
211
|
-
# log?
|
|
212
173
|
if log_timer == 0:
|
|
213
174
|
log.info(
|
|
214
175
|
"Moving dome from current az=%.2f° to %.2f° (%.2f° left)...",
|
|
@@ -216,117 +177,68 @@ class AlpacaDome(FollowMixin, BaseDome):
|
|
|
216
177
|
az,
|
|
217
178
|
180 - abs(abs(az - self._azimuth) - 180),
|
|
218
179
|
)
|
|
219
|
-
log_timer
|
|
220
|
-
if log_timer == 10:
|
|
221
|
-
log_timer = 0
|
|
180
|
+
log_timer = (log_timer + 1) % 10
|
|
222
181
|
|
|
223
|
-
# wait a little
|
|
224
182
|
await event_wait(abort, 1)
|
|
225
183
|
|
|
226
|
-
# finished
|
|
227
184
|
log.info("Moved to az=%.2f.", az)
|
|
228
185
|
|
|
229
186
|
@timeout(1200000)
|
|
230
187
|
async def move_altaz(self, alt: float, az: float, **kwargs: Any) -> None:
|
|
231
|
-
"""Moves to given coordinates.
|
|
188
|
+
"""Moves to given coordinates."""
|
|
232
189
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if not await self.is_ready():
|
|
190
|
+
# do nothing if not ready
|
|
191
|
+
bad_states = [
|
|
192
|
+
MotionStatus.PARKED,
|
|
193
|
+
MotionStatus.INITIALIZING,
|
|
194
|
+
MotionStatus.PARKING,
|
|
195
|
+
MotionStatus.ERROR,
|
|
196
|
+
MotionStatus.UNKNOWN,
|
|
197
|
+
]
|
|
198
|
+
if not self._device.connected or self.motion_status() in bad_states:
|
|
243
199
|
return
|
|
244
200
|
|
|
245
|
-
# destination az already set?
|
|
246
201
|
if az == self._set_az:
|
|
247
202
|
return
|
|
248
203
|
self._set_az = az
|
|
249
204
|
|
|
250
|
-
# is this a larger move?
|
|
251
205
|
large_move = abs(az - self._azimuth) > 2.0 * self._tolerance
|
|
252
|
-
|
|
253
|
-
# decide, whether we're tracking or just slewing
|
|
254
206
|
tracking = self.is_following and not large_move
|
|
255
207
|
|
|
256
|
-
# acquire lock
|
|
257
208
|
async with LockWithAbort(self._lock_move, self._abort_move):
|
|
258
|
-
# store altitude
|
|
259
209
|
self._altitude = alt
|
|
260
|
-
|
|
261
|
-
# change status to TRACKING or SLEWING, depending on whether we're tracking
|
|
262
210
|
await self._change_motion_status(MotionStatus.TRACKING if tracking else MotionStatus.SLEWING)
|
|
263
|
-
|
|
264
|
-
# move dome
|
|
265
211
|
await self._move(az, self._abort_move)
|
|
266
|
-
|
|
267
|
-
# change status to TRACKING or POSITIONED, depending on whether we're tracking
|
|
268
212
|
await self._change_motion_status(MotionStatus.TRACKING if self.is_following else MotionStatus.POSITIONED)
|
|
269
213
|
|
|
270
|
-
async def
|
|
271
|
-
"""
|
|
272
|
-
|
|
273
|
-
Returns:
|
|
274
|
-
Tuple of current Alt and Az in degrees.
|
|
275
|
-
"""
|
|
276
|
-
return self._altitude, self._azimuth
|
|
277
|
-
|
|
278
|
-
async def stop_motion(self, device: Optional[str] = None, **kwargs: Any) -> None:
|
|
279
|
-
"""Stop the motion.
|
|
280
|
-
|
|
281
|
-
Args:
|
|
282
|
-
device: Name of device to stop, or None for all.
|
|
283
|
-
"""
|
|
284
|
-
|
|
285
|
-
# not supported, but don't want to raise an exception
|
|
214
|
+
async def stop_motion(self, device: str | None = None, **kwargs: Any) -> None:
|
|
215
|
+
"""Stop the motion."""
|
|
286
216
|
pass
|
|
287
217
|
|
|
288
|
-
async def is_ready(self, **kwargs: Any) -> bool:
|
|
289
|
-
"""Returns the device is "ready", whatever that means for the specific device.
|
|
290
|
-
|
|
291
|
-
Returns:
|
|
292
|
-
Whether device is ready
|
|
293
|
-
"""
|
|
294
|
-
|
|
295
|
-
# check that motion is not in one of the states listed below
|
|
296
|
-
states = [
|
|
297
|
-
MotionStatus.PARKED,
|
|
298
|
-
MotionStatus.INITIALIZING,
|
|
299
|
-
MotionStatus.PARKING,
|
|
300
|
-
MotionStatus.ERROR,
|
|
301
|
-
MotionStatus.UNKNOWN,
|
|
302
|
-
]
|
|
303
|
-
return self._device.connected and await self.get_motion_status() not in states
|
|
304
|
-
|
|
305
218
|
async def _update_status(self) -> None:
|
|
306
219
|
"""Update status from dome."""
|
|
307
|
-
|
|
308
|
-
# loop forever
|
|
309
220
|
while True:
|
|
310
|
-
# get azimuth
|
|
311
221
|
try:
|
|
312
222
|
self._azimuth = self._adjust_azimuth(await self._device.get("Azimuth"))
|
|
223
|
+
await self.comm.set_state(IPointingAltAz, AltAzState(alt=self._altitude, az=self._azimuth))
|
|
313
224
|
except ConnectionError:
|
|
314
|
-
# ignore it
|
|
315
225
|
pass
|
|
316
226
|
|
|
317
|
-
|
|
227
|
+
bad_states = [
|
|
228
|
+
MotionStatus.PARKED,
|
|
229
|
+
MotionStatus.INITIALIZING,
|
|
230
|
+
MotionStatus.PARKING,
|
|
231
|
+
MotionStatus.ERROR,
|
|
232
|
+
MotionStatus.UNKNOWN,
|
|
233
|
+
]
|
|
234
|
+
ready = self._device.connected and self.motion_status() not in bad_states
|
|
235
|
+
await self.comm.set_state(IReady, ReadyState(ready=ready))
|
|
236
|
+
|
|
318
237
|
await asyncio.sleep(2)
|
|
319
238
|
|
|
320
239
|
@staticmethod
|
|
321
240
|
def _adjust_azimuth(az: float) -> float:
|
|
322
|
-
"""Baader measures azimuth as West of South
|
|
323
|
-
|
|
324
|
-
Args:
|
|
325
|
-
az: Azimuth.
|
|
326
|
-
|
|
327
|
-
Returns:
|
|
328
|
-
Converted azimuth.
|
|
329
|
-
"""
|
|
241
|
+
"""Baader measures azimuth as West of South — convert both ways."""
|
|
330
242
|
az += 180
|
|
331
243
|
if az >= 360:
|
|
332
244
|
az -= 360
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Any
|
|
4
4
|
|
|
5
|
-
from pyobs.
|
|
6
|
-
from pyobs.interfaces import IFocuser, IFitsHeaderBefore
|
|
5
|
+
from pyobs.interfaces import FocuserState, IFitsHeaderBefore, IFocuser, IReady, ReadyState
|
|
7
6
|
from pyobs.mixins import MotionStatusMixin
|
|
8
|
-
from pyobs.modules import timeout
|
|
7
|
+
from pyobs.modules import Module, timeout
|
|
8
|
+
from pyobs.utils import exceptions as exc
|
|
9
9
|
from pyobs.utils.enums import MotionStatus
|
|
10
10
|
from pyobs.utils.threads import LockWithAbort
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
from .device import AlpacaDevice
|
|
13
13
|
|
|
14
14
|
log = logging.getLogger(__name__)
|
|
@@ -45,166 +45,83 @@ class AlpacaFocuser(MotionStatusMixin, IFocuser, IFitsHeaderBefore, Module):
|
|
|
45
45
|
|
|
46
46
|
# init status
|
|
47
47
|
await self._change_motion_status(MotionStatus.IDLE, interface="IFocuser")
|
|
48
|
+
await self.comm.set_state(IReady, ReadyState(ready=self._device.connected))
|
|
48
49
|
|
|
49
50
|
async def init(self, **kwargs: Any) -> None:
|
|
50
|
-
"""Initialize device.
|
|
51
|
-
|
|
52
|
-
Raises:
|
|
53
|
-
pyobs.utils.exceptions.InitError: If device could not be initialized.
|
|
54
|
-
"""
|
|
51
|
+
"""Initialize device."""
|
|
55
52
|
pass
|
|
56
53
|
|
|
57
54
|
async def park(self, **kwargs: Any) -> None:
|
|
58
|
-
"""Park device.
|
|
59
|
-
|
|
60
|
-
Raises:
|
|
61
|
-
pyobs.utils.exceptions.ParkError: If device could not be parked.
|
|
62
|
-
"""
|
|
55
|
+
"""Park device."""
|
|
63
56
|
pass
|
|
64
57
|
|
|
65
58
|
async def get_fits_header_before(
|
|
66
|
-
self, namespaces:
|
|
67
|
-
) ->
|
|
68
|
-
"""Returns FITS header for the current status of this module.
|
|
69
|
-
|
|
70
|
-
Args:
|
|
71
|
-
namespaces: If given, only return FITS headers for the given namespaces.
|
|
59
|
+
self, namespaces: list[str] | None = None, **kwargs: Any
|
|
60
|
+
) -> dict[str, tuple[Any, str]]:
|
|
61
|
+
"""Returns FITS header for the current status of this module."""
|
|
72
62
|
|
|
73
|
-
Returns:
|
|
74
|
-
Dictionary containing FITS headers.
|
|
75
|
-
"""
|
|
76
|
-
|
|
77
|
-
# get pos and step size
|
|
78
|
-
# StepSize is in microns, so multiply with 1000
|
|
79
63
|
try:
|
|
80
64
|
pos = await self._device.get("Position")
|
|
81
65
|
step = await self._device.get("StepSize") * 1000.0
|
|
82
|
-
|
|
83
|
-
# return header
|
|
84
66
|
return {"TEL-FOCU": (pos / step, "Focus of telescope [mm]")}
|
|
85
|
-
|
|
86
67
|
except ConnectionError as e:
|
|
87
68
|
log.warning("Could not determine focus position: %s", e)
|
|
88
69
|
return {}
|
|
89
70
|
|
|
90
71
|
@timeout(60000)
|
|
91
72
|
async def set_focus(self, focus: float, **kwargs: Any) -> None:
|
|
92
|
-
"""Sets new focus.
|
|
93
|
-
|
|
94
|
-
Raises:
|
|
95
|
-
pyobs.utils.exceptions.MoveError: If telescope cannot be moved.
|
|
96
|
-
"""
|
|
97
|
-
|
|
98
|
-
# set focus + offset
|
|
99
|
-
|
|
73
|
+
"""Sets new focus."""
|
|
100
74
|
await self._set_focus(focus + self._focus_offset)
|
|
101
75
|
|
|
102
76
|
async def set_focus_offset(self, offset: float, **kwargs: Any) -> None:
|
|
103
|
-
"""Sets focus offset.
|
|
77
|
+
"""Sets focus offset."""
|
|
104
78
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
# get current focus (without offset)
|
|
113
|
-
focus = await self.get_focus()
|
|
79
|
+
# get current focus (without offset) directly from device
|
|
80
|
+
try:
|
|
81
|
+
pos = float(await self._device.get("Position"))
|
|
82
|
+
step = float(await self._device.get("StepSize")) * 1000.0
|
|
83
|
+
current_focus = pos / step - self._focus_offset
|
|
84
|
+
except ConnectionError:
|
|
85
|
+
raise exc.MoveError("Could not read focus position.")
|
|
114
86
|
|
|
115
|
-
# set offset
|
|
116
87
|
self._focus_offset = offset
|
|
117
|
-
|
|
118
|
-
# go to focus
|
|
119
|
-
await self._set_focus(focus + self._focus_offset)
|
|
88
|
+
await self._set_focus(current_focus + self._focus_offset)
|
|
120
89
|
|
|
121
90
|
async def _set_focus(self, focus: float) -> None:
|
|
122
|
-
"""Actually sets new focus.
|
|
91
|
+
"""Actually sets new focus."""
|
|
123
92
|
|
|
124
|
-
Args:
|
|
125
|
-
focus: New focus value.
|
|
126
|
-
"""
|
|
127
|
-
|
|
128
|
-
# acquire lock
|
|
129
93
|
async with LockWithAbort(self._lock_motion, self._abort_motion):
|
|
130
94
|
try:
|
|
131
|
-
# get step size
|
|
132
95
|
step = await self._device.get("StepSize")
|
|
133
96
|
|
|
134
|
-
# calculating new focus and move it
|
|
135
97
|
log.info("Moving focus to %.2fmm...", focus)
|
|
136
98
|
await self._change_motion_status(MotionStatus.SLEWING, interface="IFocuser")
|
|
137
99
|
foc = int(focus * step * 1000.0)
|
|
138
100
|
await self._device.put("Move", Position=foc)
|
|
139
101
|
|
|
140
|
-
# wait for it
|
|
141
102
|
while abs(await self._device.get("Position") - foc) > 10:
|
|
142
|
-
# abort?
|
|
143
103
|
if self._abort_motion.is_set():
|
|
144
104
|
await self._device.put("Halt")
|
|
145
105
|
await self._change_motion_status(MotionStatus.POSITIONED, interface="IFocuser")
|
|
146
106
|
raise InterruptedError("Setting focus aborted.")
|
|
147
|
-
|
|
148
|
-
# sleep a little
|
|
149
107
|
await asyncio.sleep(0.1)
|
|
150
108
|
|
|
151
|
-
|
|
152
|
-
log.info("Reached new focus of %.2fmm.",
|
|
109
|
+
pos = await self._device.get("Position")
|
|
110
|
+
log.info("Reached new focus of %.2fmm.", pos / step / 1000.0)
|
|
153
111
|
await self._change_motion_status(MotionStatus.POSITIONED, interface="IFocuser")
|
|
154
112
|
|
|
113
|
+
# publish new state
|
|
114
|
+
await self.comm.set_state(
|
|
115
|
+
IFocuser, FocuserState(focus=pos / step / 1000.0, focus_offset=self._focus_offset)
|
|
116
|
+
)
|
|
117
|
+
|
|
155
118
|
except ConnectionError:
|
|
156
119
|
await self._change_motion_status(MotionStatus.ERROR, interface="IFocuser")
|
|
157
120
|
raise exc.MoveError("Could not move focus.")
|
|
158
121
|
|
|
159
|
-
async def
|
|
160
|
-
"""
|
|
161
|
-
|
|
162
|
-
Returns:
|
|
163
|
-
Current focus.
|
|
164
|
-
"""
|
|
165
|
-
|
|
166
|
-
# get pos and step size
|
|
167
|
-
# StepSize is in microns, so multiply with 1000
|
|
168
|
-
pos = float(await self._device.get("Position"))
|
|
169
|
-
step = float(await self._device.get("StepSize")) * 1000.0
|
|
170
|
-
|
|
171
|
-
# return current focus - offset
|
|
172
|
-
return pos / step - self._focus_offset
|
|
173
|
-
|
|
174
|
-
async def get_focus_offset(self, **kwargs: Any) -> float:
|
|
175
|
-
"""Return current focus offset.
|
|
176
|
-
|
|
177
|
-
Returns:
|
|
178
|
-
Current focus offset.
|
|
179
|
-
"""
|
|
180
|
-
return self._focus_offset
|
|
181
|
-
|
|
182
|
-
async def stop_motion(self, device: Optional[str] = None, **kwargs: Any) -> None:
|
|
183
|
-
"""Stop the motion.
|
|
184
|
-
|
|
185
|
-
Args:
|
|
186
|
-
device: Name of device to stop, or None for all.
|
|
187
|
-
"""
|
|
188
|
-
|
|
189
|
-
# stop motion
|
|
122
|
+
async def stop_motion(self, device: str | None = None, **kwargs: Any) -> None:
|
|
123
|
+
"""Stop the motion."""
|
|
190
124
|
await self._device.put("Halt")
|
|
191
125
|
|
|
192
|
-
async def is_ready(self, **kwargs: Any) -> bool:
|
|
193
|
-
"""Returns the device is "ready", whatever that means for the specific device.
|
|
194
|
-
|
|
195
|
-
Returns:
|
|
196
|
-
True, if telescope is initialized and not in an error state.
|
|
197
|
-
"""
|
|
198
|
-
|
|
199
|
-
# check that motion is not in one of the states listed below
|
|
200
|
-
states = [
|
|
201
|
-
MotionStatus.PARKED,
|
|
202
|
-
MotionStatus.INITIALIZING,
|
|
203
|
-
MotionStatus.PARKING,
|
|
204
|
-
MotionStatus.ERROR,
|
|
205
|
-
MotionStatus.UNKNOWN,
|
|
206
|
-
]
|
|
207
|
-
return self._device.connected and await self.get_motion_status() not in states
|
|
208
|
-
|
|
209
126
|
|
|
210
127
|
__all__ = ["AlpacaFocuser"]
|