citrascope 0.6.1__py3-none-any.whl → 0.8.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 +97 -38
- citrascope/hardware/abstract_astro_hardware_adapter.py +144 -8
- citrascope/hardware/adapter_registry.py +10 -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 +102 -0
- citrascope/hardware/devices/camera/rpi_hq_camera.py +353 -0
- citrascope/hardware/devices/camera/usb_camera.py +402 -0
- citrascope/hardware/devices/camera/ximea_camera.py +744 -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 +787 -0
- citrascope/hardware/filter_sync.py +94 -0
- citrascope/hardware/indi_adapter.py +6 -2
- citrascope/hardware/kstars_dbus_adapter.py +67 -96
- citrascope/hardware/nina_adv_http_adapter.py +81 -64
- citrascope/hardware/nina_adv_http_survey_template.json +4 -4
- citrascope/settings/citrascope_settings.py +25 -0
- citrascope/tasks/runner.py +105 -0
- citrascope/tasks/scope/static_telescope_task.py +17 -12
- citrascope/tasks/task.py +3 -0
- citrascope/time/__init__.py +13 -0
- citrascope/time/time_health.py +96 -0
- citrascope/time/time_monitor.py +164 -0
- citrascope/time/time_sources.py +62 -0
- citrascope/web/app.py +274 -51
- citrascope/web/static/app.js +379 -36
- citrascope/web/static/config.js +448 -108
- citrascope/web/static/filters.js +55 -0
- citrascope/web/static/style.css +39 -0
- citrascope/web/templates/dashboard.html +176 -36
- {citrascope-0.6.1.dist-info → citrascope-0.8.0.dist-info}/METADATA +17 -1
- citrascope-0.8.0.dist-info/RECORD +62 -0
- citrascope-0.6.1.dist-info/RECORD +0 -41
- {citrascope-0.6.1.dist-info → citrascope-0.8.0.dist-info}/WHEEL +0 -0
- {citrascope-0.6.1.dist-info → citrascope-0.8.0.dist-info}/entry_points.txt +0 -0
- {citrascope-0.6.1.dist-info → citrascope-0.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Filter synchronization utilities for syncing hardware filters to backend API."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def extract_enabled_filter_names(filter_config: dict) -> list[str]:
|
|
5
|
+
"""Extract names of enabled filters from hardware configuration.
|
|
6
|
+
|
|
7
|
+
Args:
|
|
8
|
+
filter_config: Dict mapping filter IDs to config dicts with 'name' and 'enabled' keys
|
|
9
|
+
|
|
10
|
+
Returns:
|
|
11
|
+
List of filter names where enabled=True
|
|
12
|
+
"""
|
|
13
|
+
enabled_names = []
|
|
14
|
+
for filter_id, config in filter_config.items():
|
|
15
|
+
if config.get("enabled", False):
|
|
16
|
+
enabled_names.append(config["name"])
|
|
17
|
+
return enabled_names
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_spectral_config_from_expanded(expanded_filters: list[dict]) -> tuple[dict, list[str]]:
|
|
21
|
+
"""Build discrete spectral_config from API expanded filter response.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
expanded_filters: List of filter dicts from /filters/expand API response,
|
|
25
|
+
each with 'name', 'central_wavelength_nm', 'bandwidth_nm', 'is_known'
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Tuple of (spectral_config dict, list of unknown filter names)
|
|
29
|
+
"""
|
|
30
|
+
filter_specs = []
|
|
31
|
+
unknown_filters = []
|
|
32
|
+
|
|
33
|
+
for f in expanded_filters:
|
|
34
|
+
filter_specs.append(
|
|
35
|
+
{"name": f["name"], "central_wavelength_nm": f["central_wavelength_nm"], "bandwidth_nm": f["bandwidth_nm"]}
|
|
36
|
+
)
|
|
37
|
+
if not f.get("is_known", True):
|
|
38
|
+
unknown_filters.append(f["name"])
|
|
39
|
+
|
|
40
|
+
spectral_config = {"type": "discrete", "filters": filter_specs}
|
|
41
|
+
|
|
42
|
+
return spectral_config, unknown_filters
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def sync_filters_to_backend(api_client, telescope_id: str, filter_config: dict, logger) -> bool:
|
|
46
|
+
"""Sync enabled filters from hardware to backend API.
|
|
47
|
+
|
|
48
|
+
Extracts enabled filter names, expands them via filter library API,
|
|
49
|
+
builds spectral_config, and updates telescope record.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
api_client: CitraApiClient instance
|
|
53
|
+
telescope_id: UUID string of telescope to update
|
|
54
|
+
filter_config: Hardware filter configuration dict
|
|
55
|
+
logger: Logger instance for output
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
True if sync succeeded, False otherwise
|
|
59
|
+
"""
|
|
60
|
+
if not filter_config:
|
|
61
|
+
logger.debug("No filter configuration to sync")
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
# Extract enabled filter names
|
|
65
|
+
enabled_filter_names = extract_enabled_filter_names(filter_config)
|
|
66
|
+
|
|
67
|
+
if not enabled_filter_names:
|
|
68
|
+
logger.debug("No enabled filters to sync")
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
logger.info(f"Syncing {len(enabled_filter_names)} enabled filters to backend: {enabled_filter_names}")
|
|
72
|
+
|
|
73
|
+
# Expand filter names to full spectral specs via API
|
|
74
|
+
expand_response = api_client.expand_filters(enabled_filter_names)
|
|
75
|
+
if not expand_response or "filters" not in expand_response:
|
|
76
|
+
logger.warning("Failed to expand filter names - API returned no data")
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
# Build spectral_config from expanded filters
|
|
80
|
+
expanded_filters = expand_response["filters"]
|
|
81
|
+
spectral_config, unknown_filters = build_spectral_config_from_expanded(expanded_filters)
|
|
82
|
+
|
|
83
|
+
if unknown_filters:
|
|
84
|
+
logger.warning(f"Unknown filters (using defaults): {unknown_filters}")
|
|
85
|
+
|
|
86
|
+
# Update telescope spectral_config via PATCH
|
|
87
|
+
update_response = api_client.update_telescope_spectral_config(telescope_id, spectral_config)
|
|
88
|
+
|
|
89
|
+
if update_response:
|
|
90
|
+
logger.info(f"Successfully synced {len(spectral_config['filters'])} filters to backend")
|
|
91
|
+
return True
|
|
92
|
+
else:
|
|
93
|
+
logger.warning("Failed to update telescope spectral_config on backend")
|
|
94
|
+
return False
|
|
@@ -47,7 +47,7 @@ class IndiAdapter(PyIndi.BaseClient, AbstractAstroHardwareAdapter):
|
|
|
47
47
|
# TetraSolver.high_memory()
|
|
48
48
|
|
|
49
49
|
@classmethod
|
|
50
|
-
def get_settings_schema(cls) -> list[SettingSchemaEntry]:
|
|
50
|
+
def get_settings_schema(cls, **kwargs) -> list[SettingSchemaEntry]:
|
|
51
51
|
"""
|
|
52
52
|
Return a schema describing configurable settings for the INDI adapter.
|
|
53
53
|
"""
|
|
@@ -60,6 +60,7 @@ class IndiAdapter(PyIndi.BaseClient, AbstractAstroHardwareAdapter):
|
|
|
60
60
|
"description": "INDI server hostname or IP address",
|
|
61
61
|
"required": True,
|
|
62
62
|
"placeholder": "localhost or 192.168.1.100",
|
|
63
|
+
"group": "Connection",
|
|
63
64
|
},
|
|
64
65
|
{
|
|
65
66
|
"name": "port",
|
|
@@ -71,6 +72,7 @@ class IndiAdapter(PyIndi.BaseClient, AbstractAstroHardwareAdapter):
|
|
|
71
72
|
"placeholder": "7624",
|
|
72
73
|
"min": 1,
|
|
73
74
|
"max": 65535,
|
|
75
|
+
"group": "Connection",
|
|
74
76
|
},
|
|
75
77
|
{
|
|
76
78
|
"name": "telescope_name",
|
|
@@ -80,6 +82,7 @@ class IndiAdapter(PyIndi.BaseClient, AbstractAstroHardwareAdapter):
|
|
|
80
82
|
"description": "Name of the telescope device (leave empty to auto-detect)",
|
|
81
83
|
"required": False,
|
|
82
84
|
"placeholder": "Telescope Simulator",
|
|
85
|
+
"group": "Devices",
|
|
83
86
|
},
|
|
84
87
|
{
|
|
85
88
|
"name": "camera_name",
|
|
@@ -89,6 +92,7 @@ class IndiAdapter(PyIndi.BaseClient, AbstractAstroHardwareAdapter):
|
|
|
89
92
|
"description": "Name of the camera device (leave empty to auto-detect)",
|
|
90
93
|
"required": False,
|
|
91
94
|
"placeholder": "CCD Simulator",
|
|
95
|
+
"group": "Devices",
|
|
92
96
|
},
|
|
93
97
|
]
|
|
94
98
|
|
|
@@ -726,5 +730,5 @@ class IndiAdapter(PyIndi.BaseClient, AbstractAstroHardwareAdapter):
|
|
|
726
730
|
def get_observation_strategy(self) -> ObservationStrategy:
|
|
727
731
|
return ObservationStrategy.MANUAL
|
|
728
732
|
|
|
729
|
-
def perform_observation_sequence(self,
|
|
733
|
+
def perform_observation_sequence(self, task, satellite_data) -> str:
|
|
730
734
|
raise NotImplementedError
|
|
@@ -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
|
|
@@ -97,7 +84,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
97
84
|
self.scheduler: dbus.Interface | None = None
|
|
98
85
|
|
|
99
86
|
@classmethod
|
|
100
|
-
def get_settings_schema(cls) -> list[SettingSchemaEntry]:
|
|
87
|
+
def get_settings_schema(cls, **kwargs) -> list[SettingSchemaEntry]:
|
|
101
88
|
"""
|
|
102
89
|
Return a schema describing configurable settings for the KStars DBus adapter.
|
|
103
90
|
"""
|
|
@@ -110,6 +97,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
110
97
|
"description": "D-Bus service name for KStars (default: org.kde.kstars)",
|
|
111
98
|
"required": False,
|
|
112
99
|
"placeholder": "org.kde.kstars",
|
|
100
|
+
"group": "Connection",
|
|
113
101
|
},
|
|
114
102
|
{
|
|
115
103
|
"name": "ccd_name",
|
|
@@ -119,6 +107,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
119
107
|
"description": "Name of the camera device in your Ekos profile (check Ekos logs on connect for available devices)",
|
|
120
108
|
"required": False,
|
|
121
109
|
"placeholder": "CCD Simulator",
|
|
110
|
+
"group": "Devices",
|
|
122
111
|
},
|
|
123
112
|
{
|
|
124
113
|
"name": "filter_wheel_name",
|
|
@@ -128,6 +117,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
128
117
|
"description": "Name of the filter wheel device (leave empty if no filter wheel)",
|
|
129
118
|
"required": False,
|
|
130
119
|
"placeholder": "Filter Simulator",
|
|
120
|
+
"group": "Devices",
|
|
131
121
|
},
|
|
132
122
|
{
|
|
133
123
|
"name": "optical_train_name",
|
|
@@ -137,6 +127,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
137
127
|
"description": "Name of the optical train in your Ekos profile (check Ekos logs on connect for available trains)",
|
|
138
128
|
"required": False,
|
|
139
129
|
"placeholder": "Primary",
|
|
130
|
+
"group": "Devices",
|
|
140
131
|
},
|
|
141
132
|
{
|
|
142
133
|
"name": "exposure_time",
|
|
@@ -148,6 +139,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
148
139
|
"placeholder": "1.0",
|
|
149
140
|
"min": 0.001,
|
|
150
141
|
"max": 300.0,
|
|
142
|
+
"group": "Imaging",
|
|
151
143
|
},
|
|
152
144
|
{
|
|
153
145
|
"name": "frame_count",
|
|
@@ -159,6 +151,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
159
151
|
"placeholder": "1",
|
|
160
152
|
"min": 1,
|
|
161
153
|
"max": 100,
|
|
154
|
+
"group": "Imaging",
|
|
162
155
|
},
|
|
163
156
|
{
|
|
164
157
|
"name": "binning_x",
|
|
@@ -170,6 +163,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
170
163
|
"placeholder": "1",
|
|
171
164
|
"min": 1,
|
|
172
165
|
"max": 4,
|
|
166
|
+
"group": "Imaging",
|
|
173
167
|
},
|
|
174
168
|
{
|
|
175
169
|
"name": "binning_y",
|
|
@@ -181,6 +175,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
181
175
|
"placeholder": "1",
|
|
182
176
|
"min": 1,
|
|
183
177
|
"max": 4,
|
|
178
|
+
"group": "Imaging",
|
|
184
179
|
},
|
|
185
180
|
{
|
|
186
181
|
"name": "image_format",
|
|
@@ -191,6 +186,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
191
186
|
"required": False,
|
|
192
187
|
"placeholder": "Mono",
|
|
193
188
|
"options": ["Mono", "RGGB", "RGB"],
|
|
189
|
+
"group": "Imaging",
|
|
194
190
|
},
|
|
195
191
|
]
|
|
196
192
|
|
|
@@ -236,7 +232,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
236
232
|
raise FileNotFoundError(f"Template not found: {template_path}")
|
|
237
233
|
return template_path.read_text()
|
|
238
234
|
|
|
239
|
-
def _create_sequence_file(self, task_id: str, satellite_data: dict, output_dir: Path) -> Path:
|
|
235
|
+
def _create_sequence_file(self, task_id: str, satellite_data: dict, output_dir: Path, task=None) -> Path:
|
|
240
236
|
"""
|
|
241
237
|
Create an ESQ sequence file from template.
|
|
242
238
|
|
|
@@ -244,6 +240,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
244
240
|
task_id: Unique task identifier
|
|
245
241
|
satellite_data: Dictionary containing target information
|
|
246
242
|
output_dir: Base output directory for captures
|
|
243
|
+
task: Optional task object containing filter assignment
|
|
247
244
|
|
|
248
245
|
Returns:
|
|
249
246
|
Path to the created sequence file
|
|
@@ -254,7 +251,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
254
251
|
target_name = satellite_data.get("name", "Unknown").replace(" ", "_")
|
|
255
252
|
|
|
256
253
|
# Generate job blocks based on filter configuration
|
|
257
|
-
jobs_xml = self._generate_job_blocks(output_dir)
|
|
254
|
+
jobs_xml = self._generate_job_blocks(output_dir, task)
|
|
258
255
|
|
|
259
256
|
# Replace placeholders
|
|
260
257
|
sequence_content = template.replace("{{JOBS}}", jobs_xml)
|
|
@@ -274,13 +271,14 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
274
271
|
self.logger.info(f"Created sequence file: {sequence_file}")
|
|
275
272
|
return sequence_file
|
|
276
273
|
|
|
277
|
-
def _generate_job_blocks(self, output_dir: Path) -> str:
|
|
274
|
+
def _generate_job_blocks(self, output_dir: Path, task=None) -> str:
|
|
278
275
|
"""
|
|
279
276
|
Generate XML job blocks for each filter in filter_map.
|
|
280
277
|
If no filters discovered, generates single job with no filter.
|
|
281
278
|
|
|
282
279
|
Args:
|
|
283
280
|
output_dir: Base output directory for captures
|
|
281
|
+
task: Optional task object containing filter assignment
|
|
284
282
|
|
|
285
283
|
Returns:
|
|
286
284
|
XML string containing one or more <Job> blocks
|
|
@@ -324,30 +322,31 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
324
322
|
|
|
325
323
|
jobs = []
|
|
326
324
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
)
|
|
333
|
-
for filter_idx in sorted(self.filter_map.keys()):
|
|
334
|
-
filter_info = self.filter_map[filter_idx]
|
|
335
|
-
filter_name = filter_info["name"]
|
|
336
|
-
|
|
337
|
-
job_xml = job_template.format(
|
|
338
|
-
exposure=self.exposure_time,
|
|
339
|
-
format=self.image_format,
|
|
340
|
-
binning_x=self.binning_x,
|
|
341
|
-
binning_y=self.binning_y,
|
|
342
|
-
filter_name=filter_name,
|
|
343
|
-
count=self.frame_count,
|
|
344
|
-
output_dir=str(output_dir),
|
|
345
|
-
)
|
|
346
|
-
jobs.append(job_xml)
|
|
347
|
-
else:
|
|
348
|
-
# Single-filter mode: use '--' for no filter
|
|
325
|
+
# Select filters to use for this task (allow_no_filter for KStars '--' fallback)
|
|
326
|
+
filters_to_use = self.select_filters_for_task(task, allow_no_filter=True)
|
|
327
|
+
|
|
328
|
+
if filters_to_use is None:
|
|
329
|
+
# No filters available - use '--' for no filter wheel
|
|
349
330
|
filter_name = "--" if not self.filter_wheel_name else "Luminance"
|
|
350
|
-
|
|
331
|
+
task_id_str = task.id if task else "unknown"
|
|
332
|
+
self.logger.info(f"Using fallback filter '{filter_name}' for task {task_id_str}")
|
|
333
|
+
|
|
334
|
+
job_xml = job_template.format(
|
|
335
|
+
exposure=self.exposure_time,
|
|
336
|
+
format=self.image_format,
|
|
337
|
+
binning_x=self.binning_x,
|
|
338
|
+
binning_y=self.binning_y,
|
|
339
|
+
filter_name=filter_name,
|
|
340
|
+
count=self.frame_count,
|
|
341
|
+
output_dir=str(output_dir),
|
|
342
|
+
)
|
|
343
|
+
jobs.append(job_xml)
|
|
344
|
+
return "\n".join(jobs)
|
|
345
|
+
|
|
346
|
+
# Generate jobs for selected filters
|
|
347
|
+
for filter_idx in sorted(filters_to_use.keys()):
|
|
348
|
+
filter_info = filters_to_use[filter_idx]
|
|
349
|
+
filter_name = filter_info["name"]
|
|
351
350
|
|
|
352
351
|
job_xml = job_template.format(
|
|
353
352
|
exposure=self.exposure_time,
|
|
@@ -424,13 +423,15 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
424
423
|
|
|
425
424
|
assert self.bus is not None
|
|
426
425
|
|
|
427
|
-
# Calculate expected number of images based on filters
|
|
428
|
-
|
|
429
|
-
|
|
426
|
+
# Calculate expected number of images based on enabled filters
|
|
427
|
+
enabled_filter_count = (
|
|
428
|
+
sum(1 for f in self.filter_map.values() if f.get("enabled", True)) if self.filter_map else 1
|
|
429
|
+
)
|
|
430
|
+
expected_total_images = enabled_filter_count * self.frame_count
|
|
430
431
|
|
|
431
432
|
self.logger.info(
|
|
432
433
|
f"Waiting for scheduler job completion (timeout: {timeout}s, "
|
|
433
|
-
f"expecting {expected_total_images} images across {
|
|
434
|
+
f"expecting {expected_total_images} images across {enabled_filter_count} filters)..."
|
|
434
435
|
)
|
|
435
436
|
start_time = time.time()
|
|
436
437
|
|
|
@@ -529,12 +530,12 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
529
530
|
|
|
530
531
|
return matching_files
|
|
531
532
|
|
|
532
|
-
def perform_observation_sequence(self,
|
|
533
|
+
def perform_observation_sequence(self, task, satellite_data: dict) -> list[str]:
|
|
533
534
|
"""
|
|
534
535
|
Execute a complete observation sequence using Ekos Scheduler.
|
|
535
536
|
|
|
536
537
|
Args:
|
|
537
|
-
|
|
538
|
+
task: Task object containing id and filter assignment
|
|
538
539
|
satellite_data: Dictionary with keys: 'name', and either 'ra'/'dec' or TLE data
|
|
539
540
|
|
|
540
541
|
Returns:
|
|
@@ -558,7 +559,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
558
559
|
output_dir.mkdir(exist_ok=True, parents=True)
|
|
559
560
|
|
|
560
561
|
# Clear task-specific directory to prevent Ekos from thinking job is already done
|
|
561
|
-
task_output_dir = output_dir /
|
|
562
|
+
task_output_dir = output_dir / task.id
|
|
562
563
|
if task_output_dir.exists():
|
|
563
564
|
shutil.rmtree(task_output_dir)
|
|
564
565
|
self.logger.info(f"Cleared existing output directory: {task_output_dir}")
|
|
@@ -568,20 +569,20 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
568
569
|
self.logger.info(f"Output directory: {task_output_dir}")
|
|
569
570
|
|
|
570
571
|
# Create sequence and scheduler job files (use task-specific directory)
|
|
571
|
-
sequence_file = self._create_sequence_file(
|
|
572
|
-
job_file = self._create_scheduler_job(
|
|
572
|
+
sequence_file = self._create_sequence_file(task.id, satellite_data, task_output_dir, task)
|
|
573
|
+
job_file = self._create_scheduler_job(task.id, satellite_data, sequence_file)
|
|
573
574
|
|
|
574
575
|
# Ensure temp files are cleaned up even on failure
|
|
575
576
|
try:
|
|
576
|
-
self._execute_observation(
|
|
577
|
+
self._execute_observation(task.id, output_dir, sequence_file, job_file)
|
|
577
578
|
finally:
|
|
578
579
|
# Cleanup temp files
|
|
579
580
|
self._cleanup_temp_files(sequence_file, job_file)
|
|
580
581
|
|
|
581
582
|
# Retrieve and return captured images
|
|
582
|
-
image_paths = self._retrieve_captured_images(
|
|
583
|
+
image_paths = self._retrieve_captured_images(task.id, output_dir)
|
|
583
584
|
if not image_paths:
|
|
584
|
-
raise RuntimeError(f"No images captured for task {
|
|
585
|
+
raise RuntimeError(f"No images captured for task {task.id}")
|
|
585
586
|
|
|
586
587
|
self.logger.info(f"Observation sequence complete: {len(image_paths)} images captured")
|
|
587
588
|
return image_paths
|
|
@@ -858,14 +859,16 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
858
859
|
# Use 0-based indexing for filter_map (slot 1 -> index 0)
|
|
859
860
|
filter_idx = slot_num - 1
|
|
860
861
|
|
|
861
|
-
# If filter already in map (from saved settings), preserve focus position
|
|
862
|
+
# If filter already in map (from saved settings), preserve focus position and enabled state
|
|
862
863
|
if filter_idx in self.filter_map:
|
|
863
864
|
focus_position = self.filter_map[filter_idx].get("focus_position", 0)
|
|
865
|
+
enabled = self.filter_map[filter_idx].get("enabled", True)
|
|
864
866
|
self.logger.debug(
|
|
865
|
-
f"Filter slot {slot_num} ({filter_name}): using saved focus position {focus_position}"
|
|
867
|
+
f"Filter slot {slot_num} ({filter_name}): using saved focus position {focus_position}, enabled: {enabled}"
|
|
866
868
|
)
|
|
867
869
|
else:
|
|
868
870
|
focus_position = 0
|
|
871
|
+
enabled = True # Default new filters to enabled
|
|
869
872
|
self.logger.debug(
|
|
870
873
|
f"Filter slot {slot_num} ({filter_name}): new filter, using default focus position"
|
|
871
874
|
)
|
|
@@ -873,6 +876,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
873
876
|
self.filter_map[filter_idx] = {
|
|
874
877
|
"name": filter_name,
|
|
875
878
|
"focus_position": focus_position,
|
|
879
|
+
"enabled": enabled,
|
|
876
880
|
}
|
|
877
881
|
except Exception as e:
|
|
878
882
|
self.logger.warning(f"Could not read filter slot {slot_num}: {e}")
|
|
@@ -888,54 +892,21 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
|
|
|
888
892
|
self.logger.info(f"Filter discovery failed (non-fatal): {e}")
|
|
889
893
|
# Leave filter_map empty, use single-filter mode
|
|
890
894
|
|
|
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.
|
|
895
|
+
def supports_autofocus(self) -> bool:
|
|
896
|
+
"""Indicates that KStars adapter does not support autofocus yet.
|
|
901
897
|
|
|
902
898
|
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
|
-
}
|
|
899
|
+
bool: False (autofocus not implemented).
|
|
913
900
|
"""
|
|
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.
|
|
901
|
+
return False
|
|
919
902
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
focus_position: New focus position in steps
|
|
903
|
+
def supports_filter_management(self) -> bool:
|
|
904
|
+
"""Indicates whether this adapter supports filter/focus management.
|
|
923
905
|
|
|
924
906
|
Returns:
|
|
925
|
-
bool: True if
|
|
907
|
+
bool: True if filters were discovered, False otherwise.
|
|
926
908
|
"""
|
|
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
|
|
909
|
+
return bool(self.filter_map)
|
|
939
910
|
|
|
940
911
|
def disconnect(self):
|
|
941
912
|
raise NotImplementedError
|