citrascope 0.6.1__tar.gz → 0.7.0__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.
- {citrascope-0.6.1 → citrascope-0.7.0}/PKG-INFO +1 -1
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/citra_scope_daemon.py +22 -38
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/hardware/abstract_astro_hardware_adapter.py +64 -6
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/hardware/kstars_dbus_adapter.py +29 -67
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/hardware/nina_adv_http_adapter.py +74 -59
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/hardware/nina_adv_http_survey_template.json +4 -4
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/settings/citrascope_settings.py +19 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/tasks/runner.py +103 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/web/app.py +82 -37
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/web/static/app.js +83 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/web/static/config.js +244 -39
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/web/templates/dashboard.html +62 -27
- {citrascope-0.6.1 → citrascope-0.7.0}/pyproject.toml +2 -2
- {citrascope-0.6.1 → citrascope-0.7.0}/.devcontainer/devcontainer.json +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/.flake8 +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/.github/copilot-instructions.md +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/.github/dependabot.yml +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/.github/workflows/pypi-publish.yml +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/.github/workflows/pytest.yml +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/.gitignore +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/.pre-commit-config.yaml +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/.python-version +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/.vscode/launch.json +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/LICENSE +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/README.md +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/__init__.py +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/__main__.py +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/api/abstract_api_client.py +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/api/citra_api_client.py +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/constants.py +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/hardware/adapter_registry.py +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/hardware/indi_adapter.py +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/hardware/kstars_scheduler_template.esl +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/hardware/kstars_sequence_template.esq +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/logging/__init__.py +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/logging/_citrascope_logger.py +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/logging/web_log_handler.py +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/settings/__init__.py +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/settings/settings_file_manager.py +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/tasks/scope/base_telescope_task.py +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/tasks/scope/static_telescope_task.py +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/tasks/scope/tracking_telescope_task.py +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/tasks/task.py +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/web/__init__.py +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/web/server.py +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/web/static/api.js +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/web/static/img/citra.png +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/web/static/img/favicon.png +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/web/static/style.css +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/citrascope/web/static/websocket.js +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/tests/unit/test_api_client.py +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/tests/unit/test_hardware_adapter.py +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/tests/unit/test_task_manager.py +0 -0
- {citrascope-0.6.1 → citrascope-0.7.0}/tests/unit/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: citrascope
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Remotely control a telescope while it polls for tasks, collects and edge processes data, and delivers results and data for further processing.
|
|
5
5
|
Project-URL: Homepage, https://citra.space
|
|
6
6
|
Project-URL: Documentation, https://docs.citra.space/citrascope/
|
|
@@ -35,7 +35,6 @@ class CitraScopeDaemon:
|
|
|
35
35
|
self.ground_station = None
|
|
36
36
|
self.telescope_record = None
|
|
37
37
|
self.configuration_error: Optional[str] = None
|
|
38
|
-
self._autofocus_in_progress = False
|
|
39
38
|
|
|
40
39
|
# Create web server instance (always enabled)
|
|
41
40
|
self.web_server = CitraScopeWebServer(daemon=self, host="0.0.0.0", port=self.settings.web_port)
|
|
@@ -200,14 +199,6 @@ class CitraScopeDaemon:
|
|
|
200
199
|
CITRASCOPE_LOGGER.error(error_msg, exc_info=True)
|
|
201
200
|
return False, error_msg
|
|
202
201
|
|
|
203
|
-
def is_autofocus_in_progress(self) -> bool:
|
|
204
|
-
"""Check if autofocus routine is currently running.
|
|
205
|
-
|
|
206
|
-
Returns:
|
|
207
|
-
bool: True if autofocus is in progress, False otherwise
|
|
208
|
-
"""
|
|
209
|
-
return self._autofocus_in_progress
|
|
210
|
-
|
|
211
202
|
def _save_filter_config(self):
|
|
212
203
|
"""Save filter configuration from adapter to settings if supported.
|
|
213
204
|
|
|
@@ -232,12 +223,7 @@ class CitraScopeDaemon:
|
|
|
232
223
|
CITRASCOPE_LOGGER.warning(f"Failed to save filter configuration: {e}")
|
|
233
224
|
|
|
234
225
|
def trigger_autofocus(self) -> tuple[bool, Optional[str]]:
|
|
235
|
-
"""
|
|
236
|
-
|
|
237
|
-
Requires task processing to be manually paused before running.
|
|
238
|
-
Checks that both:
|
|
239
|
-
1. Task processing is paused
|
|
240
|
-
2. No task is currently in-flight
|
|
226
|
+
"""Request autofocus to run at next safe point between tasks.
|
|
241
227
|
|
|
242
228
|
Returns:
|
|
243
229
|
Tuple of (success, error_message)
|
|
@@ -248,34 +234,32 @@ class CitraScopeDaemon:
|
|
|
248
234
|
if not self.hardware_adapter.supports_filter_management():
|
|
249
235
|
return False, "Hardware adapter does not support filter management"
|
|
250
236
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
return False, "Autofocus already in progress"
|
|
237
|
+
if not self.task_manager:
|
|
238
|
+
return False, "Task manager not initialized"
|
|
254
239
|
|
|
255
|
-
#
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
return False, "Task processing must be paused before running autofocus"
|
|
240
|
+
# Request autofocus - will run between tasks
|
|
241
|
+
self.task_manager.request_autofocus()
|
|
242
|
+
return True, None
|
|
259
243
|
|
|
260
|
-
|
|
261
|
-
|
|
244
|
+
def cancel_autofocus(self) -> bool:
|
|
245
|
+
"""Cancel pending autofocus request if queued.
|
|
262
246
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
247
|
+
Returns:
|
|
248
|
+
bool: True if autofocus was cancelled, False if nothing to cancel.
|
|
249
|
+
"""
|
|
250
|
+
if not self.task_manager:
|
|
251
|
+
return False
|
|
252
|
+
return self.task_manager.cancel_autofocus()
|
|
267
253
|
|
|
268
|
-
|
|
269
|
-
|
|
254
|
+
def is_autofocus_requested(self) -> bool:
|
|
255
|
+
"""Check if autofocus is currently queued.
|
|
270
256
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
finally:
|
|
278
|
-
self._autofocus_in_progress = False
|
|
257
|
+
Returns:
|
|
258
|
+
bool: True if autofocus is queued, False otherwise.
|
|
259
|
+
"""
|
|
260
|
+
if not self.task_manager:
|
|
261
|
+
return False
|
|
262
|
+
return self.task_manager.is_autofocus_requested()
|
|
279
263
|
|
|
280
264
|
def run(self):
|
|
281
265
|
# Start web server FIRST, so users can monitor/configure
|
{citrascope-0.6.1 → citrascope-0.7.0}/citrascope/hardware/abstract_astro_hardware_adapter.py
RENAMED
|
@@ -26,10 +26,12 @@ class FilterConfig(TypedDict):
|
|
|
26
26
|
Attributes:
|
|
27
27
|
name: Human-readable filter name (e.g., 'Luminance', 'Red', 'Ha')
|
|
28
28
|
focus_position: Focuser position for this filter in steps
|
|
29
|
+
enabled: Whether this filter is enabled for observations (default: True)
|
|
29
30
|
"""
|
|
30
31
|
|
|
31
32
|
name: str
|
|
32
33
|
focus_position: int
|
|
34
|
+
enabled: bool
|
|
33
35
|
|
|
34
36
|
|
|
35
37
|
class ObservationStrategy(Enum):
|
|
@@ -43,14 +45,28 @@ class AbstractAstroHardwareAdapter(ABC):
|
|
|
43
45
|
|
|
44
46
|
_slew_min_distance_deg: float = 2.0
|
|
45
47
|
scope_slew_rate_degrees_per_second: float = 0.0
|
|
48
|
+
DEFAULT_FOCUS_POSITION: int = 0 # Default focus position, can be overridden by subclasses
|
|
46
49
|
|
|
47
|
-
def __init__(self, images_dir: Path):
|
|
48
|
-
"""Initialize the adapter with images directory.
|
|
50
|
+
def __init__(self, images_dir: Path, **kwargs):
|
|
51
|
+
"""Initialize the adapter with images directory and optional filter configuration.
|
|
49
52
|
|
|
50
53
|
Args:
|
|
51
54
|
images_dir: Path to the images directory
|
|
55
|
+
**kwargs: Additional configuration including 'filters' dict
|
|
52
56
|
"""
|
|
53
57
|
self.images_dir = images_dir
|
|
58
|
+
self.filter_map = {}
|
|
59
|
+
|
|
60
|
+
# Load filter configuration from settings if available
|
|
61
|
+
saved_filters = kwargs.get("filters", {})
|
|
62
|
+
for filter_id, filter_data in saved_filters.items():
|
|
63
|
+
try:
|
|
64
|
+
# Default enabled to True for backward compatibility
|
|
65
|
+
if "enabled" not in filter_data:
|
|
66
|
+
filter_data["enabled"] = True
|
|
67
|
+
self.filter_map[int(filter_id)] = filter_data
|
|
68
|
+
except (ValueError, TypeError):
|
|
69
|
+
pass # Skip invalid filter IDs
|
|
54
70
|
|
|
55
71
|
@classmethod
|
|
56
72
|
@abstractmethod
|
|
@@ -201,6 +217,14 @@ class AbstractAstroHardwareAdapter(ABC):
|
|
|
201
217
|
"""
|
|
202
218
|
raise NotImplementedError(f"{self.__class__.__name__} does not support autofocus")
|
|
203
219
|
|
|
220
|
+
def supports_autofocus(self) -> bool:
|
|
221
|
+
"""Indicates whether this adapter supports autofocus functionality.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
bool: True if the adapter can perform autofocus, False otherwise.
|
|
225
|
+
"""
|
|
226
|
+
return False
|
|
227
|
+
|
|
204
228
|
def supports_filter_management(self) -> bool:
|
|
205
229
|
"""Indicates whether this adapter supports filter/focus management.
|
|
206
230
|
|
|
@@ -217,14 +241,22 @@ class AbstractAstroHardwareAdapter(ABC):
|
|
|
217
241
|
Each FilterConfig contains:
|
|
218
242
|
- name (str): Filter name
|
|
219
243
|
- focus_position (int): Focuser position for this filter
|
|
244
|
+
- enabled (bool): Whether filter is enabled for observations
|
|
220
245
|
|
|
221
246
|
Example:
|
|
222
247
|
{
|
|
223
|
-
"1": {"name": "Luminance", "focus_position": 9000},
|
|
224
|
-
"2": {"name": "Red", "focus_position": 9050}
|
|
248
|
+
"1": {"name": "Luminance", "focus_position": 9000, "enabled": True},
|
|
249
|
+
"2": {"name": "Red", "focus_position": 9050, "enabled": False}
|
|
225
250
|
}
|
|
226
251
|
"""
|
|
227
|
-
return {
|
|
252
|
+
return {
|
|
253
|
+
str(filter_id): {
|
|
254
|
+
"name": filter_data["name"],
|
|
255
|
+
"focus_position": filter_data["focus_position"],
|
|
256
|
+
"enabled": filter_data.get("enabled", True),
|
|
257
|
+
}
|
|
258
|
+
for filter_id, filter_data in self.filter_map.items()
|
|
259
|
+
}
|
|
228
260
|
|
|
229
261
|
def update_filter_focus(self, filter_id: str, focus_position: int) -> bool:
|
|
230
262
|
"""Update the focus position for a specific filter.
|
|
@@ -236,4 +268,30 @@ class AbstractAstroHardwareAdapter(ABC):
|
|
|
236
268
|
Returns:
|
|
237
269
|
bool: True if update was successful, False otherwise
|
|
238
270
|
"""
|
|
239
|
-
|
|
271
|
+
try:
|
|
272
|
+
filter_id_int = int(filter_id)
|
|
273
|
+
if filter_id_int in self.filter_map:
|
|
274
|
+
self.filter_map[filter_id_int]["focus_position"] = focus_position
|
|
275
|
+
return True
|
|
276
|
+
return False
|
|
277
|
+
except (ValueError, KeyError):
|
|
278
|
+
return False
|
|
279
|
+
|
|
280
|
+
def update_filter_enabled(self, filter_id: str, enabled: bool) -> bool:
|
|
281
|
+
"""Update the enabled state for a specific filter.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
filter_id: Filter ID as string
|
|
285
|
+
enabled: New enabled state
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
bool: True if update was successful, False otherwise
|
|
289
|
+
"""
|
|
290
|
+
try:
|
|
291
|
+
filter_id_int = int(filter_id)
|
|
292
|
+
if filter_id_int in self.filter_map:
|
|
293
|
+
self.filter_map[filter_id_int]["enabled"] = enabled
|
|
294
|
+
return True
|
|
295
|
+
return False
|
|
296
|
+
except (ValueError, KeyError):
|
|
297
|
+
return False
|
|
@@ -62,7 +62,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
62
62
|
images_dir: Path to the images directory
|
|
63
63
|
**kwargs: Configuration including bus_name, ccd_name, filter_wheel_name
|
|
64
64
|
"""
|
|
65
|
-
super().__init__(images_dir=images_dir)
|
|
65
|
+
super().__init__(images_dir=images_dir, **kwargs)
|
|
66
66
|
self.logger: logging.Logger = logger
|
|
67
67
|
self.bus_name = kwargs.get("bus_name") or "org.kde.kstars"
|
|
68
68
|
self.ccd_name = kwargs.get("ccd_name") or "CCD Simulator"
|
|
@@ -76,19 +76,6 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
76
76
|
self.binning_y = kwargs.get("binning_y", 1)
|
|
77
77
|
self.image_format = kwargs.get("image_format", "Mono")
|
|
78
78
|
|
|
79
|
-
# Filter management
|
|
80
|
-
self.filter_map: Dict[int, Dict[str, Any]] = {}
|
|
81
|
-
|
|
82
|
-
# Pre-populate filter_map from saved settings (if any)
|
|
83
|
-
# This will be merged with discovered filters in discover_filters()
|
|
84
|
-
saved_filters = kwargs.get("filters", {})
|
|
85
|
-
for filter_id, filter_data in saved_filters.items():
|
|
86
|
-
# Convert string keys back to int for internal use
|
|
87
|
-
try:
|
|
88
|
-
self.filter_map[int(filter_id)] = filter_data
|
|
89
|
-
except (ValueError, TypeError) as e:
|
|
90
|
-
self.logger.warning(f"Invalid filter ID '{filter_id}' in settings, skipping: {e}")
|
|
91
|
-
|
|
92
79
|
self.bus: dbus.SessionBus | None = None
|
|
93
80
|
self.kstars: dbus.Interface | None = None
|
|
94
81
|
self.ekos: dbus.Interface | None = None
|
|
@@ -324,14 +311,17 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
324
311
|
|
|
325
312
|
jobs = []
|
|
326
313
|
|
|
327
|
-
|
|
328
|
-
|
|
314
|
+
# Filter to only enabled filters
|
|
315
|
+
enabled_filters = {fid: fdata for fid, fdata in self.filter_map.items() if fdata.get("enabled", True)}
|
|
316
|
+
|
|
317
|
+
if enabled_filters:
|
|
318
|
+
# Multi-filter mode: create one job per enabled filter
|
|
329
319
|
self.logger.info(
|
|
330
|
-
f"Generating {len(
|
|
331
|
-
f"{[f['name'] for f in
|
|
320
|
+
f"Generating {len(enabled_filters)} jobs for enabled filters: "
|
|
321
|
+
f"{[f['name'] for f in enabled_filters.values()]}"
|
|
332
322
|
)
|
|
333
|
-
for filter_idx in sorted(
|
|
334
|
-
filter_info =
|
|
323
|
+
for filter_idx in sorted(enabled_filters.keys()):
|
|
324
|
+
filter_info = enabled_filters[filter_idx]
|
|
335
325
|
filter_name = filter_info["name"]
|
|
336
326
|
|
|
337
327
|
job_xml = job_template.format(
|
|
@@ -424,13 +414,15 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
424
414
|
|
|
425
415
|
assert self.bus is not None
|
|
426
416
|
|
|
427
|
-
# Calculate expected number of images based on filters
|
|
428
|
-
|
|
429
|
-
|
|
417
|
+
# Calculate expected number of images based on enabled filters
|
|
418
|
+
enabled_filter_count = (
|
|
419
|
+
sum(1 for f in self.filter_map.values() if f.get("enabled", True)) if self.filter_map else 1
|
|
420
|
+
)
|
|
421
|
+
expected_total_images = enabled_filter_count * self.frame_count
|
|
430
422
|
|
|
431
423
|
self.logger.info(
|
|
432
424
|
f"Waiting for scheduler job completion (timeout: {timeout}s, "
|
|
433
|
-
f"expecting {expected_total_images} images across {
|
|
425
|
+
f"expecting {expected_total_images} images across {enabled_filter_count} filters)..."
|
|
434
426
|
)
|
|
435
427
|
start_time = time.time()
|
|
436
428
|
|
|
@@ -858,14 +850,16 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
858
850
|
# Use 0-based indexing for filter_map (slot 1 -> index 0)
|
|
859
851
|
filter_idx = slot_num - 1
|
|
860
852
|
|
|
861
|
-
# If filter already in map (from saved settings), preserve focus position
|
|
853
|
+
# If filter already in map (from saved settings), preserve focus position and enabled state
|
|
862
854
|
if filter_idx in self.filter_map:
|
|
863
855
|
focus_position = self.filter_map[filter_idx].get("focus_position", 0)
|
|
856
|
+
enabled = self.filter_map[filter_idx].get("enabled", True)
|
|
864
857
|
self.logger.debug(
|
|
865
|
-
f"Filter slot {slot_num} ({filter_name}): using saved focus position {focus_position}"
|
|
858
|
+
f"Filter slot {slot_num} ({filter_name}): using saved focus position {focus_position}, enabled: {enabled}"
|
|
866
859
|
)
|
|
867
860
|
else:
|
|
868
861
|
focus_position = 0
|
|
862
|
+
enabled = True # Default new filters to enabled
|
|
869
863
|
self.logger.debug(
|
|
870
864
|
f"Filter slot {slot_num} ({filter_name}): new filter, using default focus position"
|
|
871
865
|
)
|
|
@@ -873,6 +867,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
873
867
|
self.filter_map[filter_idx] = {
|
|
874
868
|
"name": filter_name,
|
|
875
869
|
"focus_position": focus_position,
|
|
870
|
+
"enabled": enabled,
|
|
876
871
|
}
|
|
877
872
|
except Exception as e:
|
|
878
873
|
self.logger.warning(f"Could not read filter slot {slot_num}: {e}")
|
|
@@ -888,54 +883,21 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
888
883
|
self.logger.info(f"Filter discovery failed (non-fatal): {e}")
|
|
889
884
|
# Leave filter_map empty, use single-filter mode
|
|
890
885
|
|
|
891
|
-
def
|
|
892
|
-
"""Indicates
|
|
893
|
-
|
|
894
|
-
Returns:
|
|
895
|
-
bool: True if filters were discovered, False otherwise.
|
|
896
|
-
"""
|
|
897
|
-
return bool(self.filter_map)
|
|
898
|
-
|
|
899
|
-
def get_filter_config(self) -> Dict[str, Dict[str, Any]]:
|
|
900
|
-
"""Get the current filter configuration including focus positions.
|
|
886
|
+
def supports_autofocus(self) -> bool:
|
|
887
|
+
"""Indicates that KStars adapter does not support autofocus yet.
|
|
901
888
|
|
|
902
889
|
Returns:
|
|
903
|
-
|
|
904
|
-
Each FilterConfig contains:
|
|
905
|
-
- name (str): Filter name
|
|
906
|
-
- focus_position (int): Focuser position for this filter
|
|
907
|
-
|
|
908
|
-
Example:
|
|
909
|
-
{
|
|
910
|
-
"0": {"name": "Red", "focus_position": 9000},
|
|
911
|
-
"1": {"name": "Green", "focus_position": 9050}
|
|
912
|
-
}
|
|
890
|
+
bool: False (autofocus not implemented).
|
|
913
891
|
"""
|
|
914
|
-
|
|
915
|
-
return {str(k): v for k, v in self.filter_map.items()}
|
|
916
|
-
|
|
917
|
-
def update_filter_focus(self, filter_id: str, focus_position: int) -> bool:
|
|
918
|
-
"""Update the focus position for a specific filter.
|
|
892
|
+
return False
|
|
919
893
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
focus_position: New focus position in steps
|
|
894
|
+
def supports_filter_management(self) -> bool:
|
|
895
|
+
"""Indicates whether this adapter supports filter/focus management.
|
|
923
896
|
|
|
924
897
|
Returns:
|
|
925
|
-
bool: True if
|
|
898
|
+
bool: True if filters were discovered, False otherwise.
|
|
926
899
|
"""
|
|
927
|
-
|
|
928
|
-
idx = int(filter_id)
|
|
929
|
-
if idx in self.filter_map:
|
|
930
|
-
self.filter_map[idx]["focus_position"] = focus_position
|
|
931
|
-
self.logger.info(f"Updated filter '{self.filter_map[idx]['name']}' focus position to {focus_position}")
|
|
932
|
-
return True
|
|
933
|
-
else:
|
|
934
|
-
self.logger.warning(f"Filter ID {filter_id} not found in filter_map")
|
|
935
|
-
return False
|
|
936
|
-
except (ValueError, KeyError) as e:
|
|
937
|
-
self.logger.error(f"Failed to update filter focus: {e}")
|
|
938
|
-
return False
|
|
900
|
+
return bool(self.filter_map)
|
|
939
901
|
|
|
940
902
|
def disconnect(self):
|
|
941
903
|
raise NotImplementedError
|
|
@@ -30,19 +30,13 @@ class NinaAdvancedHttpAdapter(AbstractAstroHardwareAdapter):
|
|
|
30
30
|
SEQUENCE_URL = "/sequence/"
|
|
31
31
|
|
|
32
32
|
def __init__(self, logger: logging.Logger, images_dir: Path, **kwargs):
|
|
33
|
-
super().__init__(images_dir=images_dir)
|
|
33
|
+
super().__init__(images_dir=images_dir, **kwargs)
|
|
34
34
|
self.logger: logging.Logger = logger
|
|
35
35
|
self.nina_api_path = kwargs.get("nina_api_path", "http://nina:1888/v2/api")
|
|
36
36
|
|
|
37
|
-
self.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
for filter_id, filter_data in saved_filters.items():
|
|
41
|
-
# Convert string keys back to int for internal use
|
|
42
|
-
try:
|
|
43
|
-
self.filter_map[int(filter_id)] = filter_data
|
|
44
|
-
except (ValueError, TypeError) as e:
|
|
45
|
-
self.logger.warning(f"Invalid filter ID '{filter_id}' in settings, skipping: {e}")
|
|
37
|
+
self.binning_x = kwargs.get("binning_x", 1)
|
|
38
|
+
self.binning_y = kwargs.get("binning_y", 1)
|
|
39
|
+
self.autofocus_binning = kwargs.get("autofocus_binning", 1)
|
|
46
40
|
|
|
47
41
|
@classmethod
|
|
48
42
|
def get_settings_schema(cls) -> list[SettingSchemaEntry]:
|
|
@@ -60,21 +54,59 @@ class NinaAdvancedHttpAdapter(AbstractAstroHardwareAdapter):
|
|
|
60
54
|
"placeholder": "http://localhost:1888/v2/api",
|
|
61
55
|
"pattern": r"^https?://.*",
|
|
62
56
|
},
|
|
57
|
+
{
|
|
58
|
+
"name": "autofocus_binning",
|
|
59
|
+
"friendly_name": "Autofocus Binning",
|
|
60
|
+
"type": "int",
|
|
61
|
+
"default": 1,
|
|
62
|
+
"description": "Pixel binning for autofocus (1=no binning, 2=2x2, etc.)",
|
|
63
|
+
"required": False,
|
|
64
|
+
"placeholder": "1",
|
|
65
|
+
"min": 1,
|
|
66
|
+
"max": 4,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"name": "binning_x",
|
|
70
|
+
"friendly_name": "Binning X",
|
|
71
|
+
"type": "int",
|
|
72
|
+
"default": 1,
|
|
73
|
+
"description": "Horizontal pixel binning for observations (1=no binning, 2=2x2, etc.)",
|
|
74
|
+
"required": False,
|
|
75
|
+
"placeholder": "1",
|
|
76
|
+
"min": 1,
|
|
77
|
+
"max": 4,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"name": "binning_y",
|
|
81
|
+
"friendly_name": "Binning Y",
|
|
82
|
+
"type": "int",
|
|
83
|
+
"default": 1,
|
|
84
|
+
"description": "Vertical pixel binning for observations (1=no binning, 2=2x2, etc.)",
|
|
85
|
+
"required": False,
|
|
86
|
+
"placeholder": "1",
|
|
87
|
+
"min": 1,
|
|
88
|
+
"max": 4,
|
|
89
|
+
},
|
|
63
90
|
]
|
|
64
91
|
|
|
65
92
|
def do_autofocus(self):
|
|
66
|
-
"""Perform autofocus routine for all filters.
|
|
93
|
+
"""Perform autofocus routine for all enabled filters.
|
|
67
94
|
|
|
68
95
|
Slews telescope to Mirach (bright reference star) and runs autofocus
|
|
69
|
-
for each filter in the filter map, updating focus positions.
|
|
96
|
+
for each enabled filter in the filter map, updating focus positions.
|
|
70
97
|
|
|
71
98
|
Raises:
|
|
72
|
-
RuntimeError: If no filters discovered or
|
|
99
|
+
RuntimeError: If no filters discovered or no enabled filters
|
|
73
100
|
"""
|
|
74
101
|
if not self.filter_map:
|
|
75
102
|
raise RuntimeError("No filters discovered. Cannot perform autofocus.")
|
|
76
103
|
|
|
77
|
-
|
|
104
|
+
# Filter to only enabled filters
|
|
105
|
+
enabled_filters = {fid: fdata for fid, fdata in self.filter_map.items() if fdata.get("enabled", True)}
|
|
106
|
+
if not enabled_filters:
|
|
107
|
+
raise RuntimeError("No enabled filters. Cannot perform autofocus.")
|
|
108
|
+
|
|
109
|
+
self.logger.info(f"Performing autofocus routine on {len(enabled_filters)} enabled filter(s) ...")
|
|
78
110
|
# move telescope to bright star and start autofocus
|
|
79
111
|
# Mirach ra=(1+9/60.+47.45/3600.)*15 dec=(35+37/60.+11.1/3600.)
|
|
80
112
|
ra = (1 + 9 / 60.0 + 47.45 / 3600.0) * 15
|
|
@@ -96,7 +128,7 @@ class NinaAdvancedHttpAdapter(AbstractAstroHardwareAdapter):
|
|
|
96
128
|
self.logger.info("Waiting for mount to finish slewing...")
|
|
97
129
|
time.sleep(5)
|
|
98
130
|
|
|
99
|
-
for id, filter in
|
|
131
|
+
for id, filter in enabled_filters.items():
|
|
100
132
|
self.logger.info(f"Focusing Filter ID: {id}, Name: {filter['name']}")
|
|
101
133
|
# Pass existing focus position to preserve it if autofocus fails
|
|
102
134
|
existing_focus = filter.get("focus_position", self.DEFAULT_FOCUS_POSITION)
|
|
@@ -190,42 +222,42 @@ class NinaAdvancedHttpAdapter(AbstractAstroHardwareAdapter):
|
|
|
190
222
|
try:
|
|
191
223
|
# start connection to all equipments
|
|
192
224
|
self.logger.info("Connecting camera ...")
|
|
193
|
-
cam_status = requests.get(self.nina_api_path + self.CAM_URL + "connect").json()
|
|
225
|
+
cam_status = requests.get(self.nina_api_path + self.CAM_URL + "connect", timeout=5).json()
|
|
194
226
|
if not cam_status["Success"]:
|
|
195
227
|
self.logger.error(f"Failed to connect camera: {cam_status.get('Error')}")
|
|
196
228
|
return False
|
|
197
229
|
self.logger.info(f"Camera Connected!")
|
|
198
230
|
|
|
199
231
|
self.logger.info("Starting camera cooling ...")
|
|
200
|
-
cool_status = requests.get(self.nina_api_path + self.CAM_URL + "cool").json()
|
|
232
|
+
cool_status = requests.get(self.nina_api_path + self.CAM_URL + "cool", timeout=5).json()
|
|
201
233
|
if not cool_status["Success"]:
|
|
202
234
|
self.logger.warning(f"Failed to start camera cooling: {cool_status.get('Error')}")
|
|
203
235
|
else:
|
|
204
236
|
self.logger.info("Cooler started!")
|
|
205
237
|
|
|
206
238
|
self.logger.info("Connecting filterwheel ...")
|
|
207
|
-
filterwheel_status = requests.get(self.nina_api_path + self.FILTERWHEEL_URL + "connect").json()
|
|
239
|
+
filterwheel_status = requests.get(self.nina_api_path + self.FILTERWHEEL_URL + "connect", timeout=5).json()
|
|
208
240
|
if not filterwheel_status["Success"]:
|
|
209
241
|
self.logger.warning(f"Failed to connect filterwheel: {filterwheel_status.get('Error')}")
|
|
210
242
|
else:
|
|
211
243
|
self.logger.info(f"Filterwheel Connected!")
|
|
212
244
|
|
|
213
245
|
self.logger.info("Connecting focuser ...")
|
|
214
|
-
focuser_status = requests.get(self.nina_api_path + self.FOCUSER_URL + "connect").json()
|
|
246
|
+
focuser_status = requests.get(self.nina_api_path + self.FOCUSER_URL + "connect", timeout=5).json()
|
|
215
247
|
if not focuser_status["Success"]:
|
|
216
248
|
self.logger.warning(f"Failed to connect focuser: {focuser_status.get('Error')}")
|
|
217
249
|
else:
|
|
218
250
|
self.logger.info(f"Focuser Connected!")
|
|
219
251
|
|
|
220
252
|
self.logger.info("Connecting mount ...")
|
|
221
|
-
mount_status = requests.get(self.nina_api_path + self.MOUNT_URL + "connect").json()
|
|
253
|
+
mount_status = requests.get(self.nina_api_path + self.MOUNT_URL + "connect", timeout=5).json()
|
|
222
254
|
if not mount_status["Success"]:
|
|
223
255
|
self.logger.error(f"Failed to connect mount: {mount_status.get('Error')}")
|
|
224
256
|
return False
|
|
225
257
|
self.logger.info(f"Mount Connected!")
|
|
226
258
|
|
|
227
259
|
self.logger.info("Unparking mount ...")
|
|
228
|
-
mount_status = requests.get(self.nina_api_path + self.MOUNT_URL + "unpark").json()
|
|
260
|
+
mount_status = requests.get(self.nina_api_path + self.MOUNT_URL + "unpark", timeout=5).json()
|
|
229
261
|
if not mount_status["Success"]:
|
|
230
262
|
self.logger.error(f"Failed to unpark mount: {mount_status.get('Error')}")
|
|
231
263
|
return False
|
|
@@ -241,7 +273,7 @@ class NinaAdvancedHttpAdapter(AbstractAstroHardwareAdapter):
|
|
|
241
273
|
|
|
242
274
|
def discover_filters(self):
|
|
243
275
|
self.logger.info("Discovering filters ...")
|
|
244
|
-
filterwheel_info = requests.get(self.nina_api_path + self.FILTERWHEEL_URL + "info").json()
|
|
276
|
+
filterwheel_info = requests.get(self.nina_api_path + self.FILTERWHEEL_URL + "info", timeout=5).json()
|
|
245
277
|
if not filterwheel_info.get("Success"):
|
|
246
278
|
self.logger.error(f"Failed to get filterwheel info: {filterwheel_info.get('Error')}")
|
|
247
279
|
raise RuntimeError("Failed to get filterwheel info")
|
|
@@ -250,58 +282,33 @@ class NinaAdvancedHttpAdapter(AbstractAstroHardwareAdapter):
|
|
|
250
282
|
for filter in filters:
|
|
251
283
|
filter_id = filter["Id"]
|
|
252
284
|
filter_name = filter["Name"]
|
|
253
|
-
# Use existing focus position if filter already in map
|
|
285
|
+
# Use existing focus position and enabled state if filter already in map
|
|
254
286
|
if filter_id in self.filter_map:
|
|
255
287
|
focus_position = self.filter_map[filter_id].get("focus_position", self.DEFAULT_FOCUS_POSITION)
|
|
288
|
+
enabled = self.filter_map[filter_id].get("enabled", True)
|
|
256
289
|
self.logger.info(
|
|
257
|
-
f"Discovered filter: {filter_name} with ID: {filter_id}, using saved focus position: {focus_position}"
|
|
290
|
+
f"Discovered filter: {filter_name} with ID: {filter_id}, using saved focus position: {focus_position}, enabled: {enabled}"
|
|
258
291
|
)
|
|
259
292
|
else:
|
|
260
293
|
focus_position = self.DEFAULT_FOCUS_POSITION
|
|
294
|
+
enabled = True # Default new filters to enabled
|
|
261
295
|
self.logger.info(
|
|
262
296
|
f"Discovered new filter: {filter_name} with ID: {filter_id}, using default focus position: {focus_position}"
|
|
263
297
|
)
|
|
264
298
|
|
|
265
|
-
self.filter_map[filter_id] = {"name": filter_name, "focus_position": focus_position}
|
|
299
|
+
self.filter_map[filter_id] = {"name": filter_name, "focus_position": focus_position, "enabled": enabled}
|
|
266
300
|
|
|
267
301
|
def disconnect(self):
|
|
268
302
|
pass
|
|
269
303
|
|
|
304
|
+
def supports_autofocus(self) -> bool:
|
|
305
|
+
"""Indicates that NINA adapter supports autofocus."""
|
|
306
|
+
return True
|
|
307
|
+
|
|
270
308
|
def supports_filter_management(self) -> bool:
|
|
271
309
|
"""Indicates that NINA adapter supports filter/focus management."""
|
|
272
310
|
return True
|
|
273
311
|
|
|
274
|
-
def get_filter_config(self) -> dict[str, FilterConfig]:
|
|
275
|
-
"""Get current filter configuration with focus positions.
|
|
276
|
-
|
|
277
|
-
Returns:
|
|
278
|
-
dict: Filter configuration mapping filter ID strings to FilterConfig
|
|
279
|
-
"""
|
|
280
|
-
return {
|
|
281
|
-
str(filter_id): {"name": filter_data["name"], "focus_position": filter_data["focus_position"]}
|
|
282
|
-
for filter_id, filter_data in self.filter_map.items()
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
def update_filter_focus(self, filter_id: str, focus_position: int) -> bool:
|
|
286
|
-
"""Update the focus position for a specific filter.
|
|
287
|
-
|
|
288
|
-
Args:
|
|
289
|
-
filter_id: Filter ID as string
|
|
290
|
-
focus_position: New focus position in steps
|
|
291
|
-
|
|
292
|
-
Returns:
|
|
293
|
-
bool: True if update was successful, False otherwise
|
|
294
|
-
"""
|
|
295
|
-
try:
|
|
296
|
-
filter_id_int = int(filter_id)
|
|
297
|
-
if filter_id_int in self.filter_map:
|
|
298
|
-
self.filter_map[filter_id_int]["focus_position"] = focus_position
|
|
299
|
-
self.logger.info(f"Updated filter {filter_id} focus position to {focus_position}")
|
|
300
|
-
return True
|
|
301
|
-
return False
|
|
302
|
-
except (ValueError, KeyError):
|
|
303
|
-
return False
|
|
304
|
-
|
|
305
312
|
def is_telescope_connected(self) -> bool:
|
|
306
313
|
"""Check if telescope is connected and responsive."""
|
|
307
314
|
try:
|
|
@@ -445,8 +452,11 @@ class NinaAdvancedHttpAdapter(AbstractAstroHardwareAdapter):
|
|
|
445
452
|
"""
|
|
446
453
|
elset = satellite_data["most_recent_elset"]
|
|
447
454
|
|
|
448
|
-
# Load template as JSON
|
|
455
|
+
# Load template as JSON and replace binning placeholders
|
|
449
456
|
template_str = self._get_sequence_template()
|
|
457
|
+
template_str = template_str.replace("{binning_x}", str(self.binning_x))
|
|
458
|
+
template_str = template_str.replace("{binning_y}", str(self.binning_y))
|
|
459
|
+
template_str = template_str.replace("{autofocus_binning}", str(self.autofocus_binning))
|
|
450
460
|
sequence_json = json.loads(template_str)
|
|
451
461
|
|
|
452
462
|
nina_sequence_name = f"Citra Target: {satellite_data['name']}, Task: {task_id}"
|
|
@@ -486,7 +496,12 @@ class NinaAdvancedHttpAdapter(AbstractAstroHardwareAdapter):
|
|
|
486
496
|
|
|
487
497
|
id_counter = [base_id] # Use list so it can be modified in nested function
|
|
488
498
|
|
|
489
|
-
|
|
499
|
+
# Filter to only enabled filters for observation
|
|
500
|
+
enabled_filters = {fid: fdata for fid, fdata in self.filter_map.items() if fdata.get("enabled", True)}
|
|
501
|
+
if not enabled_filters:
|
|
502
|
+
raise RuntimeError("No enabled filters available for observation sequence")
|
|
503
|
+
|
|
504
|
+
for filter_id, filter_info in enabled_filters.items():
|
|
490
505
|
filter_name = filter_info["name"]
|
|
491
506
|
focus_position = filter_info["focus_position"]
|
|
492
507
|
|
|
@@ -565,7 +580,7 @@ class NinaAdvancedHttpAdapter(AbstractAstroHardwareAdapter):
|
|
|
565
580
|
raise RuntimeError("Failed to get images list from NINA")
|
|
566
581
|
|
|
567
582
|
images_to_download = []
|
|
568
|
-
expected_image_count = len(
|
|
583
|
+
expected_image_count = len(enabled_filters) # One image per enabled filter
|
|
569
584
|
images_found = len(images_response["Response"])
|
|
570
585
|
self.logger.info(
|
|
571
586
|
f"Found {images_found} images in NINA image history, considering the last {expected_image_count}"
|
{citrascope-0.6.1 → citrascope-0.7.0}/citrascope/hardware/nina_adv_http_survey_template.json
RENAMED
|
@@ -174,8 +174,8 @@
|
|
|
174
174
|
"_autoFocusBinning": {
|
|
175
175
|
"$id": "31",
|
|
176
176
|
"$type": "NINA.Core.Model.Equipment.BinningMode, NINA.Core",
|
|
177
|
-
"X":
|
|
178
|
-
"Y":
|
|
177
|
+
"X": {autofocus_binning},
|
|
178
|
+
"Y": {autofocus_binning}
|
|
179
179
|
},
|
|
180
180
|
"_autoFocusGain": -1,
|
|
181
181
|
"_autoFocusOffset": -1
|
|
@@ -205,8 +205,8 @@
|
|
|
205
205
|
"Binning": {
|
|
206
206
|
"$id": "34",
|
|
207
207
|
"$type": "NINA.Core.Model.Equipment.BinningMode, NINA.Core",
|
|
208
|
-
"X":
|
|
209
|
-
"Y":
|
|
208
|
+
"X": {binning_x},
|
|
209
|
+
"Y": {binning_y}
|
|
210
210
|
},
|
|
211
211
|
"ImageType": "LIGHT",
|
|
212
212
|
"ExposureCount": 130,
|