citrascope 0.5.2__py3-none-any.whl → 0.7.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.
@@ -0,0 +1,30 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <SchedulerList version="2.6">
3
+ <Job>
4
+ <Name>{{JOB_NAME}}</Name>
5
+ <Group></Group>
6
+ <Coordinates>
7
+ <J2000RA>{{TARGET_RA}}</J2000RA>
8
+ <J2000DE>{{TARGET_DEC}}</J2000DE>
9
+ </Coordinates>
10
+ <PositionAngle>-1</PositionAngle>
11
+ <Sequence>{{SEQUENCE_FILE}}</Sequence>
12
+ <FITSFILE></FITSFILE>
13
+ <StartupCondition>
14
+ <Condition>ASAP</Condition>
15
+ <CulminationOffset>0</CulminationOffset>
16
+ </StartupCondition>
17
+ <Constraints>
18
+ <Constraint value="{{MIN_ALTITUDE}}">MinimumAltitude</Constraint>
19
+ </Constraints>
20
+ <CompletionCondition>
21
+ <Condition>Repeat</Condition>
22
+ <Repeat>10</Repeat>
23
+ </CompletionCondition>
24
+ <Steps>
25
+ <Step>Track</Step>
26
+ </Steps>
27
+ <Priority>10</Priority>
28
+ <Enforced>true</Enforced>
29
+ </Job>
30
+ </SchedulerList>
@@ -0,0 +1,16 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <SequenceQueue version='2.6'>
3
+ <Observer>CitraScope</Observer>
4
+ <GuideDeviation enabled='false'>2</GuideDeviation>
5
+ <GuideStartDeviation enabled='false'>2</GuideStartDeviation>
6
+ <HFRCheck enabled='false'>
7
+ <HFRDeviation>0.5</HFRDeviation>
8
+ <HFRCheckAlgorithm>0</HFRCheckAlgorithm>
9
+ <HFRCheckThreshold>10</HFRCheckThreshold>
10
+ <HFRCheckFrames>1</HFRCheckFrames>
11
+ </HFRCheck>
12
+ <RefocusOnTemperatureDelta enabled='false'>1</RefocusOnTemperatureDelta>
13
+ <RefocusEveryN enabled='false'>60</RefocusEveryN>
14
+ <RefocusOnMeridianFlip enabled='false' />
15
+ {{JOBS}}
16
+ </SequenceQueue>
@@ -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.filter_map = {}
38
- # Load filter configuration from settings if available
39
- saved_filters = kwargs.get("filters", {})
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 network requests fail
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
- self.logger.info("Performing autofocus routine ...")
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 self.filter_map.items():
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, otherwise use default
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
- for filter_id, filter_info in self.filter_map.items():
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(self.filter_map) # One image per filter
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}"
@@ -174,8 +174,8 @@
174
174
  "_autoFocusBinning": {
175
175
  "$id": "31",
176
176
  "$type": "NINA.Core.Model.Equipment.BinningMode, NINA.Core",
177
- "X": 1,
178
- "Y": 1
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": 4,
209
- "Y": 4
208
+ "X": {binning_x},
209
+ "Y": {binning_y}
210
210
  },
211
211
  "ImageType": "LIGHT",
212
212
  "ExposureCount": 130,
@@ -53,8 +53,8 @@ class CitraScopeSettings:
53
53
  self.log_level: str = config.get("log_level", "INFO")
54
54
  self.keep_images: bool = config.get("keep_images", False)
55
55
 
56
- # Web port: CLI override if non-default, otherwise use config file
57
- self.web_port: int = web_port if web_port != DEFAULT_WEB_PORT else config.get("web_port", DEFAULT_WEB_PORT)
56
+ # Web port: CLI-only, never loaded from or saved to config file
57
+ self.web_port: int = web_port
58
58
 
59
59
  # Task retry configuration
60
60
  self.max_task_retries: int = config.get("max_task_retries", 3)
@@ -65,6 +65,22 @@ class CitraScopeSettings:
65
65
  self.file_logging_enabled: bool = config.get("file_logging_enabled", True)
66
66
  self.log_retention_days: int = config.get("log_retention_days", 30)
67
67
 
68
+ # Autofocus configuration (top-level/global settings)
69
+ self.scheduled_autofocus_enabled: bool = config.get("scheduled_autofocus_enabled", False)
70
+ self.autofocus_interval_minutes: int = config.get("autofocus_interval_minutes", 60)
71
+ self.last_autofocus_timestamp: Optional[int] = config.get("last_autofocus_timestamp")
72
+
73
+ # Validate autofocus interval
74
+ if (
75
+ not isinstance(self.autofocus_interval_minutes, int)
76
+ or self.autofocus_interval_minutes < 1
77
+ or self.autofocus_interval_minutes > 1439
78
+ ):
79
+ CITRASCOPE_LOGGER.warning(
80
+ f"Invalid autofocus_interval_minutes ({self.autofocus_interval_minutes}). Setting to default 60 minutes."
81
+ )
82
+ self.autofocus_interval_minutes = 60
83
+
68
84
  def get_images_dir(self) -> Path:
69
85
  """Get the path to the images directory.
70
86
 
@@ -90,7 +106,7 @@ class CitraScopeSettings:
90
106
  """Convert settings to dictionary for serialization.
91
107
 
92
108
  Returns:
93
- Dictionary of all settings.
109
+ Dictionary of all settings (excluding runtime-only settings like web_port).
94
110
  """
95
111
  return {
96
112
  "host": self.host,
@@ -102,12 +118,14 @@ class CitraScopeSettings:
102
118
  "adapter_settings": self._all_adapter_settings,
103
119
  "log_level": self.log_level,
104
120
  "keep_images": self.keep_images,
105
- "web_port": self.web_port,
106
121
  "max_task_retries": self.max_task_retries,
107
122
  "initial_retry_delay_seconds": self.initial_retry_delay_seconds,
108
123
  "max_retry_delay_seconds": self.max_retry_delay_seconds,
109
124
  "file_logging_enabled": self.file_logging_enabled,
110
125
  "log_retention_days": self.log_retention_days,
126
+ "scheduled_autofocus_enabled": self.scheduled_autofocus_enabled,
127
+ "autofocus_interval_minutes": self.autofocus_interval_minutes,
128
+ "last_autofocus_timestamp": self.last_autofocus_timestamp,
111
129
  }
112
130
 
113
131
  def save(self) -> None:
@@ -125,6 +143,9 @@ class CitraScopeSettings:
125
143
  Args:
126
144
  config: Configuration dict with flat adapter_settings for current adapter.
127
145
  """
146
+ # Remove runtime-only settings that should never be persisted
147
+ config.pop("web_port", None)
148
+
128
149
  # Nest incoming adapter_settings under hardware_adapter key
129
150
  adapter = config.get("hardware_adapter", self.hardware_adapter)
130
151
  if adapter:
@@ -43,6 +43,9 @@ class TaskManager:
43
43
  # Task processing control (always starts active)
44
44
  self._processing_active = True
45
45
  self._processing_lock = threading.Lock()
46
+ # Autofocus request flag (set by manual or scheduled triggers)
47
+ self._autofocus_requested = False
48
+ self._autofocus_lock = threading.Lock()
46
49
 
47
50
  def poll_tasks(self):
48
51
  while not self._stop_event.is_set():
@@ -219,6 +222,20 @@ class TaskManager:
219
222
  except Exception as e:
220
223
  self.logger.error(f"Exception in task_runner loop: {e}", exc_info=True)
221
224
  time.sleep(5) # avoid tight error loop
225
+
226
+ # Check for autofocus requests between tasks
227
+ with self._autofocus_lock:
228
+ should_autofocus = self._autofocus_requested
229
+ if should_autofocus:
230
+ self._autofocus_requested = False # Clear flag before execution
231
+ # Also check if scheduled autofocus should run (inside lock to prevent race condition)
232
+ elif self._should_run_scheduled_autofocus():
233
+ should_autofocus = True
234
+ self._autofocus_requested = False # Ensure flag is clear
235
+
236
+ if should_autofocus:
237
+ self._execute_autofocus()
238
+
222
239
  self._stop_event.wait(1)
223
240
 
224
241
  def _observe_satellite(self, task: Task):
@@ -278,6 +295,92 @@ class TaskManager:
278
295
  with self._processing_lock:
279
296
  return self._processing_active
280
297
 
298
+ def request_autofocus(self) -> bool:
299
+ """Request autofocus to run at next safe point between tasks.
300
+
301
+ Returns:
302
+ bool: True indicating request was queued.
303
+ """
304
+ with self._autofocus_lock:
305
+ self._autofocus_requested = True
306
+ self.logger.info("Autofocus requested - will run between tasks")
307
+ return True
308
+
309
+ def cancel_autofocus(self) -> bool:
310
+ """Cancel pending autofocus request if still queued.
311
+
312
+ Returns:
313
+ bool: True if autofocus was cancelled, False if nothing to cancel.
314
+ """
315
+ with self._autofocus_lock:
316
+ was_requested = self._autofocus_requested
317
+ self._autofocus_requested = False
318
+ if was_requested:
319
+ self.logger.info("Autofocus request cancelled")
320
+ return was_requested
321
+
322
+ def is_autofocus_requested(self) -> bool:
323
+ """Check if autofocus is currently requested/queued.
324
+
325
+ Returns:
326
+ bool: True if autofocus is queued, False otherwise.
327
+ """
328
+ with self._autofocus_lock:
329
+ return self._autofocus_requested
330
+
331
+ def _should_run_scheduled_autofocus(self) -> bool:
332
+ """Check if scheduled autofocus should run based on settings.
333
+
334
+ Returns:
335
+ bool: True if autofocus is enabled and interval has elapsed.
336
+ """
337
+ if not self.settings:
338
+ return False
339
+
340
+ # Check if scheduled autofocus is enabled (top-level setting)
341
+ if not self.settings.scheduled_autofocus_enabled:
342
+ return False
343
+
344
+ # Check if adapter supports autofocus
345
+ if not self.hardware_adapter.supports_autofocus():
346
+ return False
347
+
348
+ interval_minutes = self.settings.autofocus_interval_minutes
349
+ last_timestamp = self.settings.last_autofocus_timestamp
350
+
351
+ # If never run (None), treat as overdue and run immediately
352
+ if last_timestamp is None:
353
+ return True
354
+
355
+ # Check if interval has elapsed
356
+ elapsed_minutes = (int(time.time()) - last_timestamp) / 60
357
+ return elapsed_minutes >= interval_minutes
358
+
359
+ def _execute_autofocus(self) -> None:
360
+ """Execute autofocus routine and update timestamp on both success and failure."""
361
+ try:
362
+ self.logger.info("Starting autofocus routine...")
363
+ self.hardware_adapter.do_autofocus()
364
+
365
+ # Save updated filter configuration after autofocus
366
+ if self.hardware_adapter.supports_filter_management():
367
+ try:
368
+ filter_config = self.hardware_adapter.get_filter_config()
369
+ if filter_config and self.settings:
370
+ self.settings.adapter_settings["filters"] = filter_config
371
+ self.logger.info(f"Saved filter configuration with {len(filter_config)} filters")
372
+ except Exception as e:
373
+ self.logger.warning(f"Failed to save filter configuration after autofocus: {e}")
374
+
375
+ self.logger.info("Autofocus routine completed successfully")
376
+ except Exception as e:
377
+ self.logger.error(f"Autofocus failed: {str(e)}", exc_info=True)
378
+ finally:
379
+ # Always update timestamp to prevent retry spam
380
+ if self.settings:
381
+ self.settings.last_autofocus_timestamp = int(time.time())
382
+ self.settings.save()
383
+
281
384
  def start(self):
282
385
  self._stop_event.clear()
283
386
  self.poll_thread = threading.Thread(target=self.poll_tasks, daemon=True)
@@ -17,7 +17,12 @@ class StaticTelescopeTask(AbstractBaseTelescopeTask):
17
17
  filepaths = self.hardware_adapter.take_image(self.task.id, 2.0) # 2 second exposure
18
18
 
19
19
  if self.hardware_adapter.get_observation_strategy() == ObservationStrategy.SEQUENCE_TO_CONTROLLER:
20
- # Assume the hardware adapter has already pointed the telescope and started tracking
20
+ # Calculate current satellite position and add to satellite_data
21
+ target_ra, target_dec, _, _ = self.get_target_radec_and_rates(satellite_data)
22
+ satellite_data["ra"] = target_ra.degrees
23
+ satellite_data["dec"] = target_dec.degrees
24
+
25
+ # Sequence-based adapters handle pointing and tracking themselves
21
26
  filepaths = self.hardware_adapter.perform_observation_sequence(self.task.id, satellite_data)
22
27
 
23
28
  # Take the image