pyobs-core 2.0.0.dev11__py3-none-any.whl → 2.0.0.dev13__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.
Files changed (37) hide show
  1. pyobs/comm/xmpp/xmppcomm.py +17 -13
  2. pyobs/interfaces/IAcquisition.py +28 -8
  3. pyobs/interfaces/IAutoFocus.py +2 -1
  4. pyobs/interfaces/IAutoGuiding.py +19 -1
  5. pyobs/interfaces/IMode.py +3 -3
  6. pyobs/interfaces/__init__.py +7 -2
  7. pyobs/modules/focus/__init__.py +2 -1
  8. pyobs/modules/focus/dummyautofocus.py +111 -0
  9. pyobs/modules/focus/focusseries.py +9 -0
  10. pyobs/modules/pointing/_baseguiding.py +38 -9
  11. pyobs/modules/pointing/acquisition.py +54 -16
  12. pyobs/modules/pointing/dummyacquisition.py +91 -17
  13. pyobs/modules/pointing/dummyguiding.py +58 -3
  14. pyobs/modules/telescope/basetelescope.py +3 -1
  15. pyobs/modules/utils/dummymode.py +9 -11
  16. pyobs/modules/utils/telegram.py +14 -5
  17. pyobs/modules/weather/__init__.py +2 -1
  18. pyobs/modules/weather/mockweather.py +144 -0
  19. pyobs/robotic/scheduler/targets/__init__.py +2 -1
  20. pyobs/robotic/scheduler/targets/heliocentricpolar.py +40 -0
  21. pyobs/robotic/storage/lco/_schedulereader.py +1 -1
  22. pyobs/robotic/storage/lco/observationarchive.py +14 -3
  23. pyobs/robotic/storage/lco/scripts/script.py +55 -2
  24. pyobs/robotic/storage/lco/task.py +21 -14
  25. pyobs/robotic/storage/lco/taskarchive.py +2 -2
  26. pyobs/robotic/storage/lco/taskrunner.py +1 -1
  27. pyobs/utils/enums.py +24 -1
  28. pyobs/utils/gui/camera/datadisplaywidget.py +1 -1
  29. pyobs/utils/offsets/__init__.py +2 -2
  30. pyobs/utils/offsets/applyaltazoffsets.py +9 -8
  31. pyobs/utils/offsets/applyoffsets.py +15 -4
  32. pyobs/utils/offsets/applyradecoffsets.py +9 -8
  33. {pyobs_core-2.0.0.dev11.dist-info → pyobs_core-2.0.0.dev13.dist-info}/METADATA +1 -1
  34. {pyobs_core-2.0.0.dev11.dist-info → pyobs_core-2.0.0.dev13.dist-info}/RECORD +37 -34
  35. {pyobs_core-2.0.0.dev11.dist-info → pyobs_core-2.0.0.dev13.dist-info}/WHEEL +1 -1
  36. {pyobs_core-2.0.0.dev11.dist-info → pyobs_core-2.0.0.dev13.dist-info}/entry_points.txt +0 -0
  37. {pyobs_core-2.0.0.dev11.dist-info → pyobs_core-2.0.0.dev13.dist-info}/licenses/LICENSE +0 -0
@@ -13,7 +13,7 @@ from typing import TYPE_CHECKING, Any
13
13
 
14
14
  import slixmpp
15
15
  import slixmpp.exceptions
16
- from slixmpp import ElementBase
16
+ from slixmpp import JID, ElementBase
17
17
  from slixmpp.xmlstream import ET
18
18
  from slixmpp.xmlstream.handler import Callback
19
19
  from slixmpp.xmlstream.matcher import MatchXMLMask
@@ -382,7 +382,7 @@ class XmppComm(Comm):
382
382
 
383
383
  # request features
384
384
  try:
385
- info = await self._safe_send(self.client["xep_0030"].get_info, jid=jid, cached=False)
385
+ info = await self._safe_send(self.client.plugin["xep_0030"].get_info, jid=jid, cached=False)
386
386
  except (slixmpp.exceptions.IqError, slixmpp.exceptions.IqTimeout):
387
387
  return []
388
388
 
@@ -679,7 +679,7 @@ class XmppComm(Comm):
679
679
 
680
680
  # send it
681
681
  await self._safe_send(
682
- self.client["xep_0163"].publish,
682
+ self.client.plugin["xep_0163"].publish,
683
683
  stanza,
684
684
  node=f"urn:pyobs:event:{event.__class__.__name__}:{event.version}",
685
685
  callback=functools.partial(self._send_event_callback, event=event),
@@ -705,15 +705,15 @@ class XmppComm(Comm):
705
705
  # loop events
706
706
  for ev in events:
707
707
  # register event at XMPP
708
- self.client["xep_0030"].add_feature(f"urn:pyobs:event:{ev.__name__}:{ev.version}")
708
+ self.client.plugin["xep_0030"].add_feature(f"urn:pyobs:event:{ev.__name__}:{ev.version}")
709
709
 
710
710
  # if we have a handler, we're also interested in receiving such events
711
711
  if handler:
712
712
  # add interest
713
- self.client["xep_0163"].add_interest(f"urn:pyobs:event:{ev.__name__}:{ev.version}")
713
+ self.client.plugin["xep_0163"].add_interest(f"urn:pyobs:event:{ev.__name__}:{ev.version}")
714
714
 
715
715
  # update caps and send presence
716
- await self._safe_send(self.client["xep_0115"].update_caps)
716
+ await self._safe_send(self.client.plugin["xep_0115"].update_caps)
717
717
  self.client.send_presence()
718
718
 
719
719
  def _handle_event_sync(self, msg: Any) -> None:
@@ -779,7 +779,7 @@ class XmppComm(Comm):
779
779
  # send it to module
780
780
  self._send_event_to_module(event, msg["from"].username)
781
781
 
782
- async def _safe_send(self, method: Callable[[Any], Coroutine[Any, Any, None]], *args: Any, **kwargs: Any) -> Any:
782
+ async def _safe_send(self, method: Callable[..., Coroutine[Any, Any, Any]], *args: Any, **kwargs: Any) -> Any:
783
783
  """Safely send an XMPP message.
784
784
 
785
785
  Args:
@@ -827,7 +827,9 @@ class XmppComm(Comm):
827
827
  retract stanza or a node whose deliver_payloads flag is off.
828
828
  """
829
829
  try:
830
- result = await self._safe_send(self.client["xep_0060"].get_items, self._pubsub_service, node, max_items=1)
830
+ result = await self._safe_send(
831
+ self.client.plugin["xep_0060"].get_items, self._pubsub_service, node, max_items=1
832
+ )
831
833
  # Use raw XML to avoid lazy-loading issues with plugin_multi_attrib
832
834
  pubsub_ns = "http://jabber.org/protocol/pubsub"
833
835
  pubsub_xml = result.xml.find(f"{{{pubsub_ns}}}pubsub")
@@ -847,7 +849,7 @@ class XmppComm(Comm):
847
849
  node = self._state_node(self._module.name, interface) # type: ignore[union-attr]
848
850
  stanza = StateStanza()
849
851
  stanza.xml = _dataclass_to_xml(state, self._state_namespace(interface))
850
- await self._safe_send(self.client["xep_0060"].publish, self._pubsub_service, node, payload=stanza)
852
+ await self._safe_send(self.client.plugin["xep_0060"].publish, self._pubsub_service, node, payload=stanza)
851
853
 
852
854
  async def _get_disco_info(self, jid, node, ifrom, data):
853
855
  """Custom disco#info handler that adds <capability> elements for static module values."""
@@ -906,7 +908,7 @@ class XmppComm(Comm):
906
908
  jid = full_jid
907
909
  ns = f"urn:pyobs:capabilities:{interface.__name__}:{interface.version}"
908
910
  try:
909
- result = await asyncio.wait_for(self.client["xep_0030"].get_info(jid=jid), timeout=10.0)
911
+ result = await asyncio.wait_for(self.client.plugin["xep_0030"].get_info(jid=JID(jid)), timeout=10.0)
910
912
  except (TimeoutError, Exception) as e:
911
913
  log.warning("Failed to get capabilities for %s from %s: %s", interface.__name__, module, e)
912
914
  return None
@@ -957,7 +959,7 @@ class XmppComm(Comm):
957
959
  """
958
960
  for _attempt in range(30):
959
961
  try:
960
- await self._safe_send(self.client["xep_0060"].subscribe, self._pubsub_service, node)
962
+ await self._safe_send(self.client.plugin["xep_0060"].subscribe, self._pubsub_service, node)
961
963
  break
962
964
  except slixmpp.exceptions.IqError:
963
965
  await asyncio.sleep(1)
@@ -967,7 +969,9 @@ class XmppComm(Comm):
967
969
 
968
970
  # Fetch current value immediately after subscribing
969
971
  try:
970
- result = await self._safe_send(self.client["xep_0060"].get_items, self._pubsub_service, node, max_items=1)
972
+ result = await self._safe_send(
973
+ self.client.plugin["xep_0060"].get_items, self._pubsub_service, node, max_items=1
974
+ )
971
975
  pubsub_ns = "http://jabber.org/protocol/pubsub"
972
976
  pubsub_xml = result.xml.find(f"{{{pubsub_ns}}}pubsub")
973
977
  items_xml = pubsub_xml.find(f"{{{pubsub_ns}}}items") if pubsub_xml is not None else None
@@ -1011,7 +1015,7 @@ class XmppComm(Comm):
1011
1015
  # Last subscriber — unsubscribe from ejabberd and remove handler
1012
1016
  del self._state_node_handlers[node]
1013
1017
  try:
1014
- await self._safe_send(self.client["xep_0060"].unsubscribe, self._pubsub_service, node)
1018
+ await self._safe_send(self.client.plugin["xep_0060"].unsubscribe, self._pubsub_service, node)
1015
1019
  except (slixmpp.exceptions.IqError, slixmpp.exceptions.IqTimeout):
1016
1020
  pass # already gone server-side
1017
1021
 
@@ -1,10 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from abc import ABCMeta, abstractmethod
4
- from dataclasses import dataclass
4
+ from dataclasses import dataclass, field
5
5
  from typing import Annotated, Any
6
6
 
7
- from ..utils.enums import Unit
7
+ from ..utils.enums import OffsetFrame, Unit
8
8
  from ..utils.time import Time
9
9
  from .IAbortable import IAbortable
10
10
  from .IRunning import IRunning
@@ -17,10 +17,28 @@ class AcquisitionResult:
17
17
  dec: Annotated[float, Unit.DEGREES]
18
18
  alt: Annotated[float, Unit.DEGREES]
19
19
  az: Annotated[float, Unit.DEGREES]
20
- off_ra: Annotated[float, Unit.DEGREES] | None = None
21
- off_dec: Annotated[float, Unit.DEGREES] | None = None
22
- off_alt: Annotated[float, Unit.DEGREES] | None = None
23
- off_az: Annotated[float, Unit.DEGREES] | None = None
20
+ offset_frame: OffsetFrame | None = None
21
+ # (ra, dec) if offset_frame is RA_DEC, (alt, az) if ALT_AZ
22
+ offset_lon: Annotated[float, Unit.DEGREES] | None = None
23
+ offset_lat: Annotated[float, Unit.DEGREES] | None = None
24
+
25
+
26
+ @dataclass
27
+ class AcquisitionAttempt: # AcquisitionState.attempts element
28
+ attempt: int
29
+ distance: Annotated[float, Unit.ARCSEC]
30
+ offset_applied: bool
31
+ # accumulated telescope offset after this attempt; (ra, dec) if offset_frame is RA_DEC, (alt, az) if ALT_AZ
32
+ offset_frame: OffsetFrame | None = None
33
+ offset_lon: Annotated[float, Unit.DEGREES] | None = None
34
+ offset_lat: Annotated[float, Unit.DEGREES] | None = None
35
+
36
+
37
+ @dataclass
38
+ class AcquisitionState: # growing log of attempts during an acquisition run
39
+ attempts: list[AcquisitionAttempt] = field(default_factory=list)
40
+ result: AcquisitionResult | None = None
41
+ time: Time = field(default_factory=Time.now)
24
42
 
25
43
 
26
44
  class IAcquisition(IRunning, IAbortable, metaclass=ABCMeta):
@@ -28,6 +46,8 @@ class IAcquisition(IRunning, IAbortable, metaclass=ABCMeta):
28
46
 
29
47
  __module__ = "pyobs.interfaces"
30
48
 
49
+ state = AcquisitionState
50
+
31
51
  @abstractmethod
32
52
  async def acquire_target(self, **kwargs: Any) -> AcquisitionResult:
33
53
  """Acquire target at given coordinates.
@@ -36,7 +56,7 @@ class IAcquisition(IRunning, IAbortable, metaclass=ABCMeta):
36
56
  coordinates.
37
57
 
38
58
  Returns:
39
- Result with time, ra, dec, alt, az, and either off_ra/off_dec or off_alt/off_az offsets.
59
+ Result with time, ra, dec, alt, az, and an offset in whichever frame the mount supports.
40
60
 
41
61
  Raises:
42
62
  ValueError: If target could not be acquired.
@@ -44,4 +64,4 @@ class IAcquisition(IRunning, IAbortable, metaclass=ABCMeta):
44
64
  ...
45
65
 
46
66
 
47
- __all__ = ["AcquisitionResult", "IAcquisition"]
67
+ __all__ = ["AcquisitionResult", "AcquisitionAttempt", "AcquisitionState", "OffsetFrame", "IAcquisition"]
@@ -7,6 +7,7 @@ from typing import Annotated, Any
7
7
  from ..utils.enums import Unit
8
8
  from ..utils.time import Time
9
9
  from .IAbortable import IAbortable
10
+ from .IRunning import IRunning
10
11
 
11
12
 
12
13
  @dataclass
@@ -27,7 +28,7 @@ class AutoFocusState: # growing curve during autofocus run
27
28
  time: Time = field(default_factory=Time.now)
28
29
 
29
30
 
30
- class IAutoFocus(IAbortable, metaclass=ABCMeta):
31
+ class IAutoFocus(IRunning, IAbortable, metaclass=ABCMeta):
31
32
  """The module can perform an autofocus."""
32
33
 
33
34
  __module__ = "pyobs.interfaces"
@@ -1,13 +1,31 @@
1
+ from __future__ import annotations
2
+
1
3
  from abc import ABCMeta
4
+ from dataclasses import dataclass, field
5
+ from typing import Annotated
2
6
 
7
+ from ..utils.enums import OffsetFrame, Unit
8
+ from ..utils.time import Time
3
9
  from .IExposureTime import IExposureTime
4
10
  from .IStartStop import IStartStop
5
11
 
6
12
 
13
+ @dataclass
14
+ class GuidingState: # live open/closed-loop status, separate from the per-session FITS-header RMS stats
15
+ loop_closed: bool = False
16
+ # the correction applied for the last processed image, in the mount's native frame
17
+ offset_frame: OffsetFrame | None = None
18
+ offset_lon: Annotated[float, Unit.DEGREES] | None = None
19
+ offset_lat: Annotated[float, Unit.DEGREES] | None = None
20
+ time: Time = field(default_factory=Time.now)
21
+
22
+
7
23
  class IAutoGuiding(IStartStop, IExposureTime, metaclass=ABCMeta):
8
24
  """The module can perform auto-guiding."""
9
25
 
10
26
  __module__ = "pyobs.interfaces"
11
27
 
28
+ state = GuidingState
29
+
12
30
 
13
- __all__ = ["IAutoGuiding"]
31
+ __all__ = ["IAutoGuiding", "GuidingState"]
pyobs/interfaces/IMode.py CHANGED
@@ -28,15 +28,15 @@ class IMode(Interface, metaclass=ABCMeta):
28
28
  capabilities = ModeCapabilities
29
29
 
30
30
  @abstractmethod
31
- async def set_mode(self, mode: str, group: int = 0, **kwargs: Any) -> None:
31
+ async def set_mode(self, mode: str, group: str = "", **kwargs: Any) -> None:
32
32
  """Set the current mode.
33
33
 
34
34
  Args:
35
35
  mode: Name of mode to set.
36
- group: Group number
36
+ group: Name of the group to set the mode for.
37
37
 
38
38
  Raises:
39
- ValueError: If an invalid mode was given.
39
+ ValueError: If an invalid mode or group was given.
40
40
  MoveError: If mode selector cannot be moved.
41
41
  """
42
42
  ...
@@ -11,10 +11,11 @@ implement :class:`~pyobs.interfaces.ICamera`.
11
11
 
12
12
  __title__ = "Interfaces"
13
13
 
14
+ from ..utils.enums import OffsetFrame
14
15
  from .IAbortable import IAbortable
15
- from .IAcquisition import AcquisitionResult, IAcquisition
16
+ from .IAcquisition import AcquisitionAttempt, AcquisitionResult, AcquisitionState, IAcquisition
16
17
  from .IAutoFocus import AutoFocusPoint, AutoFocusResult, AutoFocusState, IAutoFocus
17
- from .IAutoGuiding import IAutoGuiding
18
+ from .IAutoGuiding import GuidingState, IAutoGuiding
18
19
  from .IAutonomous import IAutonomous
19
20
  from .IBinning import Binning, BinningCapabilities, BinningState, IBinning
20
21
  from .ICalibrate import ICalibrate
@@ -64,12 +65,16 @@ from .IWindow import IWindow, WindowCapabilities, WindowState
64
65
  __all__ = [
65
66
  "IAbortable",
66
67
  "AcquisitionResult",
68
+ "AcquisitionAttempt",
69
+ "AcquisitionState",
70
+ "OffsetFrame",
67
71
  "IAcquisition",
68
72
  "IAutoFocus",
69
73
  "AutoFocusResult",
70
74
  "AutoFocusPoint",
71
75
  "AutoFocusState",
72
76
  "IAutoGuiding",
77
+ "GuidingState",
73
78
  "IAutonomous",
74
79
  "Binning",
75
80
  "IBinning",
@@ -5,7 +5,8 @@ TODO: write doc
5
5
 
6
6
  __title__ = "Focus"
7
7
 
8
+ from .dummyautofocus import DummyAutoFocus
8
9
  from .focusmodel import FocusModel
9
10
  from .focusseries import AutoFocusSeries
10
11
 
11
- __all__ = ["FocusModel", "AutoFocusSeries"]
12
+ __all__ = ["FocusModel", "AutoFocusSeries", "DummyAutoFocus"]
@@ -0,0 +1,111 @@
1
+ import asyncio
2
+ import logging
3
+ import math
4
+ import random
5
+ from typing import Any
6
+
7
+ from pyobs.events import FocusFoundEvent
8
+ from pyobs.interfaces import IAutoFocus, IRunning
9
+ from pyobs.interfaces.IAutoFocus import AutoFocusPoint, AutoFocusResult, AutoFocusState
10
+ from pyobs.interfaces.IRunning import RunningState
11
+ from pyobs.modules import Module
12
+ from pyobs.utils import exceptions as exc
13
+
14
+ log = logging.getLogger(__name__)
15
+
16
+
17
+ class DummyAutoFocus(Module, IAutoFocus):
18
+ """Dummy class for auto-focusing a telescope."""
19
+
20
+ __module__ = "pyobs.modules.focus"
21
+
22
+ def __init__(
23
+ self,
24
+ wait_time: float = 0.5,
25
+ best_focus: float = 10.0,
26
+ min_value: float = 3.0,
27
+ curve_width: float = 0.0253,
28
+ **kwargs: Any,
29
+ ):
30
+ """Create a new dummy auto-focus.
31
+
32
+ Args:
33
+ wait_time: Time to wait between focus steps, in seconds.
34
+ best_focus: Focus value the series converges to.
35
+ min_value: Value (e.g. HFD) at the focus curve's minimum.
36
+ curve_width: Width parameter of the focus curve; with the defaults, a
37
+ count=5/step=0.1 series (the typical case) spans from min_value at the
38
+ centre to about 20 at the outer points.
39
+ """
40
+ Module.__init__(self, **kwargs)
41
+
42
+ self._wait_time = wait_time
43
+ self._best_focus = best_focus
44
+ self._min_value = min_value
45
+ self._curve_width = curve_width
46
+ self._running = False
47
+ self._abort = asyncio.Event()
48
+
49
+ async def open(self) -> None:
50
+ """Open module."""
51
+ await Module.open(self)
52
+ await self.comm.register_event(FocusFoundEvent)
53
+ await self.comm.set_state(IAutoFocus, AutoFocusState())
54
+ await self.comm.set_state(IRunning, RunningState(running=False))
55
+
56
+ async def auto_focus(self, count: int, step: float, exposure_time: float, **kwargs: Any) -> AutoFocusResult:
57
+ """Perform an autofocus series.
58
+
59
+ Args:
60
+ count: Number of images to take on each side of the initial guess.
61
+ step: Step size.
62
+ exposure_time: Exposure time for images.
63
+
64
+ Returns:
65
+ Result of autofocus.
66
+
67
+ Raises:
68
+ ValueError: If focus could not be obtained.
69
+ """
70
+
71
+ try:
72
+ self._running = True
73
+ self._abort = asyncio.Event()
74
+ await self.comm.set_state(IRunning, RunningState(running=True))
75
+ return await self._auto_focus(count, step)
76
+ finally:
77
+ self._running = False
78
+ await self.comm.set_state(IRunning, RunningState(running=False))
79
+
80
+ async def _auto_focus(self, count: int, step: float) -> AutoFocusResult:
81
+ # a real focus curve (HFD/FWHM vs. focus position) is a hyperbola, not a parabola:
82
+ # value(x) = sqrt(min_value^2 + ((x - best_focus) / curve_width)^2)
83
+ points: list[AutoFocusPoint] = []
84
+ await self.comm.set_state(IAutoFocus, AutoFocusState(points=points))
85
+
86
+ for i in range(-count, count + 1):
87
+ if self._abort.is_set():
88
+ raise exc.AbortedError()
89
+
90
+ focus = self._best_focus + i * step
91
+ value = math.sqrt(self._min_value**2 + ((focus - self._best_focus) / self._curve_width) ** 2)
92
+ value += random.gauss(0.0, self._min_value * 0.05)
93
+ points = points + [AutoFocusPoint(focus=focus, value=value)]
94
+ await self.comm.set_state(IAutoFocus, AutoFocusState(points=points))
95
+
96
+ await asyncio.sleep(self._wait_time)
97
+
98
+ error = abs(random.gauss(0.0, step / 10))
99
+ await self.comm.send_event(FocusFoundEvent(self._best_focus, error))
100
+ return AutoFocusResult(focus=self._best_focus, focus_err=error)
101
+
102
+ async def is_running(self, **kwargs: Any) -> bool:
103
+ """Whether a service is running."""
104
+ return self._running
105
+
106
+ async def abort(self, **kwargs: Any) -> None:
107
+ """Abort current actions."""
108
+ self._abort.set()
109
+
110
+
111
+ __all__ = ["DummyAutoFocus"]
@@ -13,8 +13,10 @@ from pyobs.interfaces import (
13
13
  IFilters,
14
14
  IFocuser,
15
15
  IImageType,
16
+ IRunning,
16
17
  )
17
18
  from pyobs.interfaces.IAutoFocus import AutoFocusResult, AutoFocusState
19
+ from pyobs.interfaces.IRunning import RunningState
18
20
  from pyobs.mixins import CameraSettingsMixin
19
21
  from pyobs.modules import Module, raises, timeout
20
22
  from pyobs.object import get_object
@@ -99,6 +101,7 @@ class AutoFocusSeries(Module, CameraSettingsMixin, IAutoFocus):
99
101
 
100
102
  # publish initial states
101
103
  await self.comm.set_state(IAutoFocus, AutoFocusState())
104
+ await self.comm.set_state(IRunning, RunningState(running=False))
102
105
 
103
106
  @raises(exc.AbortedError, exc.FocusError)
104
107
  @timeout(600)
@@ -124,11 +127,13 @@ class AutoFocusSeries(Module, CameraSettingsMixin, IAutoFocus):
124
127
  try:
125
128
  log.info("Performing auto-focus...")
126
129
  self._running = True
130
+ await self.comm.set_state(IRunning, RunningState(running=True))
127
131
  focus, error = await self._auto_focus(count, step, exposure_time, **kwargs)
128
132
  return AutoFocusResult(focus=focus, focus_err=error)
129
133
 
130
134
  finally:
131
135
  self._running = False
136
+ await self.comm.set_state(IRunning, RunningState(running=False))
132
137
 
133
138
  async def _auto_focus(self, count: int, step: float, exposure_time: float, **kwargs: Any) -> tuple[float, float]:
134
139
  # do camera settings
@@ -290,6 +295,10 @@ class AutoFocusSeries(Module, CameraSettingsMixin, IAutoFocus):
290
295
  """Abort current actions."""
291
296
  self._abort.set()
292
297
 
298
+ async def is_running(self, **kwargs: Any) -> bool:
299
+ """Whether a service is running."""
300
+ return self._running
301
+
293
302
  async def _on_bad_weather(self, event: Event, sender: str) -> bool:
294
303
  """Abort series if a bad weather event occurs.
295
304
 
@@ -8,7 +8,9 @@ import astropy.units as u
8
8
  from astropy.coordinates import SkyCoord
9
9
 
10
10
  from pyobs.images import Image
11
- from pyobs.interfaces import FitsHeaderEntry, IAutoGuiding, IFitsHeaderAfter, IFitsHeaderBefore
11
+ from pyobs.interfaces import FitsHeaderEntry, GuidingState, IAutoGuiding, IFitsHeaderAfter, IFitsHeaderBefore, IRunning
12
+ from pyobs.interfaces.IRunning import RunningState
13
+ from pyobs.utils.enums import OffsetFrame
12
14
  from pyobs.utils.time import Time
13
15
 
14
16
  from ...interfaces import ITelescope
@@ -60,6 +62,9 @@ class BaseGuiding(BasePointing, IAutoGuiding, IFitsHeaderBefore, IFitsHeaderAfte
60
62
  self._reset_at_focus = reset_at_focus
61
63
  self._reset_at_filter = reset_at_filter
62
64
  self._loop_closed = False
65
+ self._last_offset_frame: OffsetFrame | None = None
66
+ self._last_offset_lon: float | None = None
67
+ self._last_offset_lat: float | None = None
63
68
 
64
69
  # headers of last and of reference image
65
70
  self._last_header = None
@@ -72,15 +77,23 @@ class BaseGuiding(BasePointing, IAutoGuiding, IFitsHeaderBefore, IFitsHeaderAfte
72
77
  self._statistics = get_object(guiding_statistic, GuidingStatistics)
73
78
  self._uptime = GuidingStatisticsUptime()
74
79
 
80
+ async def open(self) -> None:
81
+ """Open module."""
82
+ await BasePointing.open(self)
83
+ await self.comm.set_state(IRunning, RunningState(running=False))
84
+ await self.comm.set_state(IAutoGuiding, GuidingState())
85
+
75
86
  async def start(self, **kwargs: Any) -> None:
76
87
  """Starts/resets auto-guiding."""
77
88
  log.info("Start auto-guiding...")
78
89
  await self._reset_guiding(enabled=True)
90
+ await self.comm.set_state(IRunning, RunningState(running=True))
79
91
 
80
92
  async def stop(self, **kwargs: Any) -> None:
81
93
  """Stops auto-guiding."""
82
94
  log.info("Stopping auto-guiding...")
83
95
  await self._reset_guiding(enabled=False)
96
+ await self.comm.set_state(IRunning, RunningState(running=False))
84
97
 
85
98
  async def is_running(self, **kwargs: Any) -> bool:
86
99
  """Whether auto-guiding is running.
@@ -142,7 +155,7 @@ class BaseGuiding(BasePointing, IAutoGuiding, IFitsHeaderBefore, IFitsHeaderAfte
142
155
  image: If given, new reference image.
143
156
  """
144
157
  self._enabled = enabled
145
- self._set_loop_state(False)
158
+ await self._set_loop_state(False)
146
159
  self._ref_header = None if image is None else image.header
147
160
  self._last_header = None if image is None else image.header
148
161
 
@@ -152,9 +165,24 @@ class BaseGuiding(BasePointing, IAutoGuiding, IFitsHeaderBefore, IFitsHeaderAfte
152
165
  # if image is given, process it
153
166
  await self.run_pipeline(image)
154
167
 
155
- def _set_loop_state(self, state: bool) -> None:
168
+ async def _set_loop_state(
169
+ self, state: bool, frame: OffsetFrame | None = None, lon: float | None = None, lat: float | None = None
170
+ ) -> None:
156
171
  self._uptime.add_data(state)
157
172
  self._loop_closed = state
173
+ if frame is not None:
174
+ self._last_offset_frame = frame
175
+ self._last_offset_lon = lon
176
+ self._last_offset_lat = lat
177
+ await self.comm.set_state(
178
+ IAutoGuiding,
179
+ GuidingState(
180
+ loop_closed=state,
181
+ offset_frame=self._last_offset_frame,
182
+ offset_lon=self._last_offset_lon,
183
+ offset_lat=self._last_offset_lat,
184
+ ),
185
+ )
158
186
 
159
187
  async def _process_image(self, image: Image) -> Image | None:
160
188
  """Processes a single image and offsets telescope.
@@ -228,7 +256,7 @@ class BaseGuiding(BasePointing, IAutoGuiding, IFitsHeaderBefore, IFitsHeaderAfte
228
256
  # exposure time too large?
229
257
  if self._max_exposure_time is not None and image.header["EXPTIME"] > self._max_exposure_time:
230
258
  log.warning("Exposure time too large, skipping auto-guiding for now...")
231
- self._set_loop_state(False)
259
+ await self._set_loop_state(False)
232
260
  return None
233
261
 
234
262
  # remember header
@@ -243,21 +271,22 @@ class BaseGuiding(BasePointing, IAutoGuiding, IFitsHeaderBefore, IFitsHeaderAfte
243
271
  # get telescope
244
272
  if not await self.has_proxy(self._telescope, ITelescope):
245
273
  log.error("Given telescope does not exist or is not of correct type.")
246
- self._set_loop_state(False)
274
+ await self._set_loop_state(False)
247
275
  return image
248
276
 
249
277
  # apply offsets
250
278
  try:
251
279
  async with self.proxy(self._telescope, ITelescope) as telescope:
252
- if await self._apply(image, telescope, self._location):
253
- self._set_loop_state(True)
280
+ result = await self._apply(image, telescope, self._location)
281
+ if result.applied:
282
+ await self._set_loop_state(True, result.frame, result.lon, result.lat)
254
283
  log.info("Finished image.")
255
284
  else:
256
285
  log.info("Could not apply offsets.")
257
- self._set_loop_state(False)
286
+ await self._set_loop_state(False)
258
287
  except ValueError as e:
259
288
  log.info("Could not apply offsets: %s", e)
260
- self._set_loop_state(False)
289
+ await self._set_loop_state(False)
261
290
 
262
291
  # return image, in case we added important data
263
292
  return image