citrascope 0.7.0__py3-none-any.whl → 0.9.0__py3-none-any.whl
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.
- citrascope/api/abstract_api_client.py +14 -0
- citrascope/api/citra_api_client.py +41 -0
- citrascope/citra_scope_daemon.py +75 -0
- citrascope/hardware/abstract_astro_hardware_adapter.py +97 -2
- citrascope/hardware/adapter_registry.py +15 -3
- citrascope/hardware/devices/__init__.py +17 -0
- citrascope/hardware/devices/abstract_hardware_device.py +79 -0
- citrascope/hardware/devices/camera/__init__.py +13 -0
- citrascope/hardware/devices/camera/abstract_camera.py +114 -0
- citrascope/hardware/devices/camera/rpi_hq_camera.py +353 -0
- citrascope/hardware/devices/camera/usb_camera.py +407 -0
- citrascope/hardware/devices/camera/ximea_camera.py +756 -0
- citrascope/hardware/devices/device_registry.py +273 -0
- citrascope/hardware/devices/filter_wheel/__init__.py +7 -0
- citrascope/hardware/devices/filter_wheel/abstract_filter_wheel.py +73 -0
- citrascope/hardware/devices/focuser/__init__.py +7 -0
- citrascope/hardware/devices/focuser/abstract_focuser.py +78 -0
- citrascope/hardware/devices/mount/__init__.py +7 -0
- citrascope/hardware/devices/mount/abstract_mount.py +115 -0
- citrascope/hardware/direct_hardware_adapter.py +805 -0
- citrascope/hardware/dummy_adapter.py +202 -0
- citrascope/hardware/filter_sync.py +94 -0
- citrascope/hardware/indi_adapter.py +6 -2
- citrascope/hardware/kstars_dbus_adapter.py +46 -37
- citrascope/hardware/nina_adv_http_adapter.py +13 -11
- citrascope/settings/citrascope_settings.py +6 -0
- citrascope/tasks/runner.py +2 -0
- citrascope/tasks/scope/static_telescope_task.py +17 -12
- citrascope/tasks/task.py +3 -0
- citrascope/time/__init__.py +14 -0
- citrascope/time/time_health.py +103 -0
- citrascope/time/time_monitor.py +186 -0
- citrascope/time/time_sources.py +261 -0
- citrascope/web/app.py +260 -60
- citrascope/web/static/app.js +121 -731
- citrascope/web/static/components.js +136 -0
- citrascope/web/static/config.js +259 -420
- citrascope/web/static/filters.js +55 -0
- citrascope/web/static/formatters.js +129 -0
- citrascope/web/static/store-init.js +204 -0
- citrascope/web/static/style.css +44 -0
- citrascope/web/templates/_config.html +175 -0
- citrascope/web/templates/_config_hardware.html +208 -0
- citrascope/web/templates/_monitoring.html +242 -0
- citrascope/web/templates/dashboard.html +109 -377
- {citrascope-0.7.0.dist-info → citrascope-0.9.0.dist-info}/METADATA +18 -1
- citrascope-0.9.0.dist-info/RECORD +69 -0
- citrascope-0.7.0.dist-info/RECORD +0 -41
- {citrascope-0.7.0.dist-info → citrascope-0.9.0.dist-info}/WHEEL +0 -0
- {citrascope-0.7.0.dist-info → citrascope-0.9.0.dist-info}/entry_points.txt +0 -0
- {citrascope-0.7.0.dist-info → citrascope-0.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""Device adapter registry.
|
|
2
|
+
|
|
3
|
+
This module provides a centralized registry for all device adapters.
|
|
4
|
+
Similar to the hardware adapter registry, but for individual device types.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import importlib
|
|
8
|
+
from typing import Any, Dict, Type
|
|
9
|
+
|
|
10
|
+
from citrascope.hardware.devices.camera import AbstractCamera
|
|
11
|
+
from citrascope.hardware.devices.filter_wheel import AbstractFilterWheel
|
|
12
|
+
from citrascope.hardware.devices.focuser import AbstractFocuser
|
|
13
|
+
from citrascope.hardware.devices.mount import AbstractMount
|
|
14
|
+
|
|
15
|
+
# Registry of available camera devices
|
|
16
|
+
CAMERA_DEVICES: Dict[str, Dict[str, str]] = {
|
|
17
|
+
"ximea": {
|
|
18
|
+
"module": "citrascope.hardware.devices.camera.ximea_camera",
|
|
19
|
+
"class_name": "XimeaHyperspectralCamera",
|
|
20
|
+
"description": "Ximea Hyperspectral Camera (MQ series)",
|
|
21
|
+
},
|
|
22
|
+
"rpi_hq": {
|
|
23
|
+
"module": "citrascope.hardware.devices.camera.rpi_hq_camera",
|
|
24
|
+
"class_name": "RaspberryPiHQCamera",
|
|
25
|
+
"description": "Raspberry Pi High Quality Camera (IMX477)",
|
|
26
|
+
},
|
|
27
|
+
"usb_camera": {
|
|
28
|
+
"module": "citrascope.hardware.devices.camera.usb_camera",
|
|
29
|
+
"class_name": "UsbCamera",
|
|
30
|
+
"description": "USB Camera via OpenCV (guide cameras, planetary cameras, etc.)",
|
|
31
|
+
},
|
|
32
|
+
# Future cameras:
|
|
33
|
+
# "zwo": {...},
|
|
34
|
+
# "ascom": {...},
|
|
35
|
+
# "qhy": {...},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Registry of available mount devices
|
|
39
|
+
MOUNT_DEVICES: Dict[str, Dict[str, str]] = {
|
|
40
|
+
# Future mounts:
|
|
41
|
+
# "celestron": {...},
|
|
42
|
+
# "skywatcher": {...},
|
|
43
|
+
# "ascom": {...},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Registry of available filter wheel devices
|
|
47
|
+
FILTER_WHEEL_DEVICES: Dict[str, Dict[str, str]] = {
|
|
48
|
+
# Future filter wheels:
|
|
49
|
+
# "zwo": {...},
|
|
50
|
+
# "ascom": {...},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Registry of available focuser devices
|
|
54
|
+
FOCUSER_DEVICES: Dict[str, Dict[str, str]] = {
|
|
55
|
+
# Future focusers:
|
|
56
|
+
# "moonlite": {...},
|
|
57
|
+
# "ascom": {...},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_camera_class(camera_type: str) -> Type[AbstractCamera]:
|
|
62
|
+
"""Get the camera class for the given camera type.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
camera_type: The type of camera (e.g., "ximea", "zwo")
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The camera adapter class
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ValueError: If the camera type is not registered
|
|
72
|
+
ImportError: If the camera module cannot be imported
|
|
73
|
+
"""
|
|
74
|
+
if camera_type not in CAMERA_DEVICES:
|
|
75
|
+
available = ", ".join(f"'{name}'" for name in CAMERA_DEVICES.keys())
|
|
76
|
+
raise ValueError(f"Unknown camera type: '{camera_type}'. Valid options are: {available}")
|
|
77
|
+
|
|
78
|
+
device_info = CAMERA_DEVICES[camera_type]
|
|
79
|
+
module = importlib.import_module(device_info["module"])
|
|
80
|
+
device_class = getattr(module, device_info["class_name"])
|
|
81
|
+
|
|
82
|
+
return device_class
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_mount_class(mount_type: str) -> Type[AbstractMount]:
|
|
86
|
+
"""Get the mount class for the given mount type.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
mount_type: The type of mount
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
The mount adapter class
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
ValueError: If the mount type is not registered
|
|
96
|
+
ImportError: If the mount module cannot be imported
|
|
97
|
+
"""
|
|
98
|
+
if mount_type not in MOUNT_DEVICES:
|
|
99
|
+
available = ", ".join(f"'{name}'" for name in MOUNT_DEVICES.keys())
|
|
100
|
+
raise ValueError(f"Unknown mount type: '{mount_type}'. Valid options are: {available}")
|
|
101
|
+
|
|
102
|
+
device_info = MOUNT_DEVICES[mount_type]
|
|
103
|
+
module = importlib.import_module(device_info["module"])
|
|
104
|
+
device_class = getattr(module, device_info["class_name"])
|
|
105
|
+
|
|
106
|
+
return device_class
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def get_filter_wheel_class(filter_wheel_type: str) -> Type[AbstractFilterWheel]:
|
|
110
|
+
"""Get the filter wheel class for the given filter wheel type.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
filter_wheel_type: The type of filter wheel
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
The filter wheel adapter class
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
ValueError: If the filter wheel type is not registered
|
|
120
|
+
ImportError: If the filter wheel module cannot be imported
|
|
121
|
+
"""
|
|
122
|
+
if filter_wheel_type not in FILTER_WHEEL_DEVICES:
|
|
123
|
+
available = ", ".join(f"'{name}'" for name in FILTER_WHEEL_DEVICES.keys())
|
|
124
|
+
raise ValueError(f"Unknown filter wheel type: '{filter_wheel_type}'. Valid options are: {available}")
|
|
125
|
+
|
|
126
|
+
device_info = FILTER_WHEEL_DEVICES[filter_wheel_type]
|
|
127
|
+
module = importlib.import_module(device_info["module"])
|
|
128
|
+
device_class = getattr(module, device_info["class_name"])
|
|
129
|
+
|
|
130
|
+
return device_class
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def get_focuser_class(focuser_type: str) -> Type[AbstractFocuser]:
|
|
134
|
+
"""Get the focuser class for the given focuser type.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
focuser_type: The type of focuser
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
The focuser adapter class
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
ValueError: If the focuser type is not registered
|
|
144
|
+
ImportError: If the focuser module cannot be imported
|
|
145
|
+
"""
|
|
146
|
+
if focuser_type not in FOCUSER_DEVICES:
|
|
147
|
+
available = ", ".join(f"'{name}'" for name in FOCUSER_DEVICES.keys())
|
|
148
|
+
raise ValueError(f"Unknown focuser type: '{focuser_type}'. Valid options are: {available}")
|
|
149
|
+
|
|
150
|
+
device_info = FOCUSER_DEVICES[focuser_type]
|
|
151
|
+
module = importlib.import_module(device_info["module"])
|
|
152
|
+
device_class = getattr(module, device_info["class_name"])
|
|
153
|
+
|
|
154
|
+
return device_class
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def list_devices(device_type: str) -> Dict[str, Dict[str, str]]:
|
|
158
|
+
"""Get a dictionary of all registered devices of a specific type.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
device_type: Type of device ("camera", "mount", "filter_wheel", "focuser")
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Dict mapping device names to their info including friendly_name
|
|
165
|
+
"""
|
|
166
|
+
registries = {
|
|
167
|
+
"camera": CAMERA_DEVICES,
|
|
168
|
+
"mount": MOUNT_DEVICES,
|
|
169
|
+
"filter_wheel": FILTER_WHEEL_DEVICES,
|
|
170
|
+
"focuser": FOCUSER_DEVICES,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
registry = registries.get(device_type, {})
|
|
174
|
+
result = {}
|
|
175
|
+
|
|
176
|
+
for name, info in registry.items():
|
|
177
|
+
# Try to get friendly name from device class
|
|
178
|
+
try:
|
|
179
|
+
if device_type == "camera":
|
|
180
|
+
device_class = get_camera_class(name)
|
|
181
|
+
elif device_type == "mount":
|
|
182
|
+
device_class = get_mount_class(name)
|
|
183
|
+
elif device_type == "filter_wheel":
|
|
184
|
+
device_class = get_filter_wheel_class(name)
|
|
185
|
+
elif device_type == "focuser":
|
|
186
|
+
device_class = get_focuser_class(name)
|
|
187
|
+
else:
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
friendly_name = device_class.get_friendly_name()
|
|
191
|
+
except Exception:
|
|
192
|
+
# Fallback to description if friendly_name not available
|
|
193
|
+
friendly_name = info["description"]
|
|
194
|
+
|
|
195
|
+
result[name] = {
|
|
196
|
+
"friendly_name": friendly_name,
|
|
197
|
+
"description": info["description"],
|
|
198
|
+
"module": info["module"],
|
|
199
|
+
"class_name": info["class_name"],
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return result
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def get_device_schema(device_type: str, device_name: str) -> list:
|
|
206
|
+
"""Get the configuration schema for a specific device.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
device_type: Type of device ("camera", "mount", "filter_wheel", "focuser")
|
|
210
|
+
device_name: The name of the device (e.g., "ximea", "celestron")
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
The device's settings schema
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
ValueError: If the device type or name is not registered
|
|
217
|
+
ImportError: If the device module cannot be imported
|
|
218
|
+
"""
|
|
219
|
+
if device_type == "camera":
|
|
220
|
+
device_class = get_camera_class(device_name)
|
|
221
|
+
elif device_type == "mount":
|
|
222
|
+
device_class = get_mount_class(device_name)
|
|
223
|
+
elif device_type == "filter_wheel":
|
|
224
|
+
device_class = get_filter_wheel_class(device_name)
|
|
225
|
+
elif device_type == "focuser":
|
|
226
|
+
device_class = get_focuser_class(device_name)
|
|
227
|
+
else:
|
|
228
|
+
raise ValueError(f"Unknown device type: '{device_type}'")
|
|
229
|
+
|
|
230
|
+
return device_class.get_settings_schema()
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def check_dependencies(device_class: Type[Any]) -> dict[str, Any]:
|
|
234
|
+
"""Check if dependencies for a device are available.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
device_class: Device class to check
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Dict with keys:
|
|
241
|
+
- available (bool): True if all dependencies installed
|
|
242
|
+
- missing (list[str]): List of missing package names
|
|
243
|
+
- install_cmd (str): Command to install missing packages
|
|
244
|
+
"""
|
|
245
|
+
import time
|
|
246
|
+
|
|
247
|
+
start_time = time.time()
|
|
248
|
+
|
|
249
|
+
deps = device_class.get_dependencies()
|
|
250
|
+
packages = deps.get("packages", [])
|
|
251
|
+
install_extra = deps.get("install_extra", "")
|
|
252
|
+
|
|
253
|
+
missing = []
|
|
254
|
+
for package in packages:
|
|
255
|
+
try:
|
|
256
|
+
importlib.import_module(package)
|
|
257
|
+
except ImportError:
|
|
258
|
+
missing.append(package)
|
|
259
|
+
|
|
260
|
+
available = len(missing) == 0
|
|
261
|
+
install_cmd = f"pip install citrascope[{install_extra}]" if install_extra else f"pip install {' '.join(missing)}"
|
|
262
|
+
|
|
263
|
+
elapsed = time.time() - start_time
|
|
264
|
+
if elapsed > 0.05: # Log if takes more than 50ms
|
|
265
|
+
from citrascope.logging import CITRASCOPE_LOGGER
|
|
266
|
+
|
|
267
|
+
CITRASCOPE_LOGGER.info(f"Dependency check for {device_class.__name__} took {elapsed:.3f}s")
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
"available": available,
|
|
271
|
+
"missing": missing,
|
|
272
|
+
"install_cmd": install_cmd,
|
|
273
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Abstract filter wheel device interface."""
|
|
2
|
+
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from citrascope.hardware.devices.abstract_hardware_device import AbstractHardwareDevice
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AbstractFilterWheel(AbstractHardwareDevice):
|
|
10
|
+
"""Abstract base class for filter wheel devices.
|
|
11
|
+
|
|
12
|
+
Provides a common interface for controlling motorized filter wheels.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def set_filter_position(self, position: int) -> bool:
|
|
17
|
+
"""Move to specified filter position.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
position: Filter position (0-indexed)
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
True if move initiated successfully, False otherwise
|
|
24
|
+
"""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def get_filter_position(self) -> Optional[int]:
|
|
29
|
+
"""Get current filter position.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Current filter position (0-indexed), or None if unavailable
|
|
33
|
+
"""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def is_moving(self) -> bool:
|
|
38
|
+
"""Check if filter wheel is currently moving.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
True if moving, False if stationary
|
|
42
|
+
"""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def get_filter_count(self) -> int:
|
|
47
|
+
"""Get the number of filter positions.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Number of available filter positions
|
|
51
|
+
"""
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def get_filter_names(self) -> list[str]:
|
|
56
|
+
"""Get the names of all filters.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
List of filter names for each position
|
|
60
|
+
"""
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def set_filter_names(self, names: list[str]) -> bool:
|
|
65
|
+
"""Set the names for all filter positions.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
names: List of filter names (must match filter count)
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
True if names set successfully, False otherwise
|
|
72
|
+
"""
|
|
73
|
+
pass
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Abstract focuser device interface."""
|
|
2
|
+
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from citrascope.hardware.devices.abstract_hardware_device import AbstractHardwareDevice
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AbstractFocuser(AbstractHardwareDevice):
|
|
10
|
+
"""Abstract base class for focuser devices.
|
|
11
|
+
|
|
12
|
+
Provides a common interface for controlling motorized focusers.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def move_absolute(self, position: int) -> bool:
|
|
17
|
+
"""Move to absolute focuser position.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
position: Target position in steps
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
True if move initiated successfully, False otherwise
|
|
24
|
+
"""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def move_relative(self, steps: int) -> bool:
|
|
29
|
+
"""Move focuser by relative number of steps.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
steps: Number of steps to move (positive=outward, negative=inward)
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
True if move initiated successfully, False otherwise
|
|
36
|
+
"""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def get_position(self) -> Optional[int]:
|
|
41
|
+
"""Get current focuser position.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Current position in steps, or None if unavailable
|
|
45
|
+
"""
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def is_moving(self) -> bool:
|
|
50
|
+
"""Check if focuser is currently moving.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
True if moving, False if stationary
|
|
54
|
+
"""
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
def abort_move(self):
|
|
59
|
+
"""Stop the current focuser movement."""
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def get_max_position(self) -> Optional[int]:
|
|
64
|
+
"""Get the maximum focuser position.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Maximum position in steps, or None if unlimited/unknown
|
|
68
|
+
"""
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
@abstractmethod
|
|
72
|
+
def get_temperature(self) -> Optional[float]:
|
|
73
|
+
"""Get focuser temperature reading if available.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Temperature in degrees Celsius, or None if not available
|
|
77
|
+
"""
|
|
78
|
+
pass
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Abstract mount device interface."""
|
|
2
|
+
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
from typing import Optional, Tuple
|
|
5
|
+
|
|
6
|
+
from citrascope.hardware.devices.abstract_hardware_device import AbstractHardwareDevice
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AbstractMount(AbstractHardwareDevice):
|
|
10
|
+
"""Abstract base class for telescope mount devices.
|
|
11
|
+
|
|
12
|
+
Provides a common interface for controlling equatorial and alt-az mounts.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def slew_to_radec(self, ra: float, dec: float) -> bool:
|
|
17
|
+
"""Slew the mount to specified RA/Dec coordinates.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
ra: Right Ascension in degrees
|
|
21
|
+
dec: Declination in degrees
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
True if slew initiated successfully, False otherwise
|
|
25
|
+
"""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def is_slewing(self) -> bool:
|
|
30
|
+
"""Check if mount is currently slewing.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
True if slewing, False if stationary or tracking
|
|
34
|
+
"""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def abort_slew(self):
|
|
39
|
+
"""Stop the current slew operation."""
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def get_radec(self) -> Tuple[float, float]:
|
|
44
|
+
"""Get current mount RA/Dec position.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Tuple of (RA in degrees, Dec in degrees)
|
|
48
|
+
"""
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def start_tracking(self, rate: Optional[str] = "sidereal") -> bool:
|
|
53
|
+
"""Start tracking at specified rate.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
rate: Tracking rate - "sidereal", "lunar", "solar", or device-specific
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
True if tracking started successfully, False otherwise
|
|
60
|
+
"""
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def stop_tracking(self) -> bool:
|
|
65
|
+
"""Stop tracking.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
True if tracking stopped successfully, False otherwise
|
|
69
|
+
"""
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
def is_tracking(self) -> bool:
|
|
74
|
+
"""Check if mount is currently tracking.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
True if tracking, False otherwise
|
|
78
|
+
"""
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
@abstractmethod
|
|
82
|
+
def park(self) -> bool:
|
|
83
|
+
"""Park the mount to its home position.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
True if park initiated successfully, False otherwise
|
|
87
|
+
"""
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
@abstractmethod
|
|
91
|
+
def unpark(self) -> bool:
|
|
92
|
+
"""Unpark the mount from its home position.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
True if unpark successful, False otherwise
|
|
96
|
+
"""
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
@abstractmethod
|
|
100
|
+
def is_parked(self) -> bool:
|
|
101
|
+
"""Check if mount is parked.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
True if parked, False otherwise
|
|
105
|
+
"""
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
@abstractmethod
|
|
109
|
+
def get_mount_info(self) -> dict:
|
|
110
|
+
"""Get mount capabilities and information.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Dictionary containing mount specs and capabilities
|
|
114
|
+
"""
|
|
115
|
+
pass
|