pyBROTlib 0.1.4__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.
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyBROTlib
3
+ Version: 0.1.4
4
+ Summary: MQTT interface to BROT telescopes
5
+ Author: Tim-Oliver Husser, Lukas Melzig
6
+ Author-email: Tim-Oliver Husser <thusser@uni-goettingen.de>, Lukas Melzig <lukas.melzig@stud.uni-goettingen.de>
7
+ License-Expression: MIT
8
+ Requires-Dist: aiomqtt>=2.4.0
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+
File without changes
@@ -0,0 +1,26 @@
1
+ [project]
2
+ name = "pyBROTlib"
3
+ version = "0.1.4"
4
+ description = "MQTT interface to BROT telescopes"
5
+ readme = "README.md"
6
+ authors = [{ name = "Tim-Oliver Husser", email = "thusser@uni-goettingen.de" }, { name = "Lukas Melzig", email = "lukas.melzig@stud.uni-goettingen.de" }]
7
+ requires-python = ">=3.11"
8
+ license = "MIT"
9
+ dependencies = [
10
+ "aiomqtt>=2.4.0",
11
+ ]
12
+
13
+ [dependency-groups]
14
+ dev = [
15
+ "mypy>=1.15.0,<2",
16
+ "black>=25.1.0,<26",
17
+ "pre-commit>=4.2.0,<5",
18
+ "flake8>=7.3.0",
19
+ ]
20
+
21
+ [build-system]
22
+ requires = ["uv_build>=0.9.8,<0.10.0"]
23
+ build-backend = "uv_build"
24
+
25
+ [tool.setuptools.package-data]
26
+ "pyobs" = ["py.typed"]
File without changes
@@ -0,0 +1,11 @@
1
+ from .transport import Transport
2
+
3
+
4
+ class BROTBase:
5
+ def __init__(self, transport: Transport, telescope_name: str):
6
+ self._transport = transport
7
+ self._telemetry = self._transport.telemetry
8
+ self._telescope_name = telescope_name
9
+
10
+
11
+ __all__ = ["BROTBase"]
@@ -0,0 +1,16 @@
1
+ from .dome import BROTDome
2
+ from .focus import BROTFocus
3
+ from .mirrorcovers import BROTMirrorCovers
4
+ from .telescope import BROTTelescope
5
+ from .transport import Transport
6
+
7
+
8
+ class BROT:
9
+ def __init__(self, transport: Transport, telescope_name: str):
10
+ self.dome = BROTDome(transport, telescope_name)
11
+ self.focus = BROTFocus(transport, telescope_name)
12
+ self.mirrorcovers = BROTMirrorCovers(transport, telescope_name)
13
+ self.telescope = BROTTelescope(transport, telescope_name)
14
+
15
+
16
+ __all__ = ["BROT"]
@@ -0,0 +1,87 @@
1
+ from enum import Enum
2
+
3
+ from .base import BROTBase
4
+
5
+
6
+ class DomeShutterStatus(Enum):
7
+ CLOSED = "closed"
8
+ MOVING = "moving"
9
+ OPEN = "open"
10
+ UNKNOWN = "unknown"
11
+
12
+
13
+ class DomeStatus(Enum):
14
+ PARKED = "parked"
15
+ ERROR = "error"
16
+ TRACKING = "tracking"
17
+ UNKNOWN = "unknown"
18
+
19
+
20
+ class BROTDome(BROTBase):
21
+ @property
22
+ def shutter(self) -> DomeShutterStatus:
23
+ match self._telemetry.AUXILIARY.DOME.REALPOS:
24
+ case 0.0:
25
+ return DomeShutterStatus.CLOSED
26
+ case 0.5:
27
+ return DomeShutterStatus.MOVING
28
+ case 1.0:
29
+ return DomeShutterStatus.OPEN
30
+ case _:
31
+ return DomeShutterStatus.UNKNOWN
32
+
33
+ @property
34
+ def in_motion(self) -> bool:
35
+ return (self._telemetry.AUXILIARY.DOME.REALPOS == 0.5) or (self._telemetry.AUXILIARY.DOME.MOTION_STATE == 1.0)
36
+ @property
37
+ def azimuth(self) -> float:
38
+ return self._telemetry.AUXILIARY.DOME.AZ
39
+
40
+ @property
41
+ def status(self) -> DomeStatus:
42
+ match self._telemetry.AUXILIARY.DOME.READY_STATE:
43
+ case 0.0:
44
+ return DomeStatus.PARKED
45
+ case -1.0:
46
+ return DomeStatus.ERROR
47
+ case 8.0:
48
+ return DomeStatus.TRACKING
49
+ case _:
50
+ return DomeStatus.UNKNOWN
51
+
52
+ @property
53
+ def error_state(self) -> bool:
54
+ return self._telemetry.AUXILIARY.DOME.ERROR_STATE != 0
55
+
56
+ async def open(self) -> None:
57
+ await self._transport.publish(
58
+ f"{self._telescope_name}/Telescope/SET", "command dome_open=1"
59
+ )
60
+
61
+ async def close(self) -> None:
62
+ await self._transport.publish(
63
+ f"{self._telescope_name}/Telescope/SET", "command dome_close=1"
64
+ )
65
+
66
+ async def start_tracking(self) -> None:
67
+ await self._transport.publish(
68
+ f"{self._telescope_name}/Telescope/SET", "command dome_track=1"
69
+ )
70
+
71
+ async def stop_tracking(self) -> None:
72
+ await self._transport.publish(
73
+ f"{self._telescope_name}/Telescope/SET", "command dome_track=0"
74
+ )
75
+
76
+ async def park(self) -> None:
77
+ await self._transport.publish(
78
+ f"{self._telescope_name}/Telescope/SET", "command dome_park=1"
79
+ )
80
+
81
+ async def reset(self) -> None:
82
+ await self._transport.publish(
83
+ f"{self._telescope_name}/Telescope/SET", "command dome_reset=1"
84
+ )
85
+
86
+
87
+ __all__ = ["BROTDome", "DomeStatus", "DomeShutterStatus"]
@@ -0,0 +1,21 @@
1
+ from .base import BROTBase
2
+
3
+
4
+ class BROTFocus(BROTBase):
5
+ @property
6
+ def position(self) -> float:
7
+ return self._telemetry.POSITION.INSTRUMENTAL.FOCUS.CURRPOS
8
+
9
+ async def set(self, focus: float) -> None:
10
+ await self._transport.publish(f"{self._telescope_name}/Telescope/SET", f"command focus={focus}")
11
+
12
+ @property
13
+ def powered(self) -> bool:
14
+ return self._telemetry.POSITION.INSTRUMENTAL.FOCUS.POWER_STATE == 1.0
15
+
16
+ @property
17
+ def referenced(self) -> bool:
18
+ return self._telemetry.POSITION.INSTRUMENTAL.FOCUS.REFERENCED == 1.0
19
+
20
+
21
+ __all__ = ["BROTFocus"]
@@ -0,0 +1,24 @@
1
+ from enum import Enum
2
+
3
+ from .base import BROTBase
4
+
5
+
6
+ class MirrorCoverStatus(Enum):
7
+ CLOSED = "closed"
8
+ MOVING = "moving"
9
+ OPEN = "open"
10
+ UNKNOWN = "unknown"
11
+
12
+
13
+ class BROTMirrorCovers(BROTBase):
14
+ @property
15
+ def status(self) -> MirrorCoverStatus:
16
+ match self._telemetry.AUXILIARY.COVER.REALPOS:
17
+ case 0.0:
18
+ return MirrorCoverStatus.CLOSED
19
+ case 0.5:
20
+ return MirrorCoverStatus.MOVING
21
+ case 1.0:
22
+ return MirrorCoverStatus.OPEN
23
+ case _:
24
+ return MirrorCoverStatus.UNKNOWN
@@ -0,0 +1,70 @@
1
+ from typing import get_type_hints
2
+ from aiomqtt import Client, Message # type: ignore
3
+
4
+ from .transport import Transport
5
+
6
+
7
+ class MQTTTransport(Transport):
8
+ def __init__(self, host: str, port: int):
9
+ super().__init__()
10
+
11
+ self.host = host
12
+ self.port = port
13
+
14
+ def __str__(self) -> str:
15
+ return f"MQTT(host={self.host}, port={self.port})"
16
+
17
+ async def publish(self, topic: str, message: str) -> None:
18
+ async with Client(self.host, self.port) as client:
19
+ await client.publish(topic, payload=message.encode("utf-8"))
20
+
21
+ async def run(self) -> None:
22
+ async with Client(self.host, self.port) as client:
23
+ self._connected = True
24
+ await client.subscribe("#")
25
+ async for message in client.messages:
26
+ await self._process_message(message)
27
+
28
+ async def _process_message(self, msg: Message) -> None:
29
+ # Telemetry handling
30
+ if "Telemetry" in msg.topic.value:
31
+ # we only want bytes...
32
+ if not isinstance(msg.payload, bytes):
33
+ return
34
+
35
+ # analyse message
36
+ key, value = msg.payload.decode("utf-8").split(" ")[1].split("=")
37
+ s = key.upper().split(".")
38
+ obj = self.telemetry
39
+
40
+ # dict with ALL telemetry
41
+ self.data[key] = str(value)
42
+
43
+ # find object in telemetry tree
44
+ for token in s[:-1]:
45
+ if hasattr(obj, token):
46
+ obj = getattr(obj, token)
47
+ else:
48
+ print("Unknown variable:", key)
49
+ return
50
+
51
+ # does it exist?
52
+ val: bool | int | float | str
53
+ if hasattr(obj, s[-1]):
54
+ typ = get_type_hints(obj)[s[-1]]
55
+ if typ == bool:
56
+ val = value.lower() == "true"
57
+ elif typ == int:
58
+ val = int(value)
59
+ elif typ == float:
60
+ val = float(value)
61
+ else:
62
+ val = value
63
+ setattr(obj, s[-1], val)
64
+
65
+ if "Log" in msg.topic.value:
66
+ pass
67
+ # payload = str(msg.payload)[2:-2].split(' message="')
68
+ # log_message = payload[1]
69
+ # log_level = payload[0].split("level=")[1]
70
+ # self.logMessageReceived.emit(log_level, log_message)
File without changes
@@ -0,0 +1,161 @@
1
+ from dataclasses import dataclass, field
2
+
3
+
4
+ @dataclass
5
+ class TelescopeInfo:
6
+ NAME: str = "Unknown"
7
+ DIAMETER: float = 0.0
8
+ CABINET: str = "Unknown"
9
+ MANUFACTURER: str = "Unknown"
10
+
11
+
12
+ @dataclass
13
+ class TelescopeConfig:
14
+ CAPABILITIES: int = 0
15
+ MOUNTOPTIONS: str = "Unknown"
16
+ MOUNT: str = "Unknown"
17
+
18
+
19
+ @dataclass
20
+ class TelescopeStatus:
21
+ GLOBAL: int = 0
22
+
23
+
24
+ @dataclass
25
+ class Telescope:
26
+ READY_STATE: float = 0.0
27
+ MOTION_STATE: float = 0.0
28
+ INFO: TelescopeInfo = field(default_factory=TelescopeInfo)
29
+ CONFIG: TelescopeConfig = field(default_factory=TelescopeConfig)
30
+ STATUS: TelescopeStatus = field(default_factory=TelescopeStatus)
31
+
32
+
33
+ @dataclass
34
+ class ObjectInstrumental:
35
+ RA: float = 0.0
36
+ DEC: float = 0.0
37
+ HA: float = 0.0
38
+
39
+
40
+ @dataclass
41
+ class ObjectHorizontal:
42
+ ALT: float = 0.0
43
+ AZ: float = 0.0
44
+
45
+
46
+ @dataclass
47
+ class ObjectEquatorial:
48
+ EPOCH: str = "Unknown"
49
+ EQUINOX: str = "Unknown"
50
+ RA_PM: float = 0.0
51
+ DEC_PM: float = 0.0
52
+ RA_RATE: float = 0.0
53
+ DEC_RATE: float = 0.0
54
+ RA: float = 0.0
55
+ DEC: float = 0.0
56
+ HA: float = 0.0
57
+
58
+
59
+ @dataclass
60
+ class Object:
61
+ INSTRUMENTAL: ObjectInstrumental = field(default_factory=ObjectInstrumental)
62
+ HORIZONTAL: ObjectHorizontal = field(default_factory=ObjectHorizontal)
63
+ EQUATORIAL: ObjectEquatorial = field(default_factory=ObjectEquatorial)
64
+
65
+
66
+ @dataclass
67
+ class PointingOffsets:
68
+ HA: float = 0.0
69
+ DEC: float = 0.0
70
+ ALT: float = 0.0
71
+ AZ: float = 0.0
72
+
73
+
74
+ @dataclass
75
+ class Pointing:
76
+ OFFSETS: PointingOffsets = field(default_factory=PointingOffsets)
77
+ SLEWTIME: float = 0.0
78
+
79
+
80
+ @dataclass
81
+ class PositionLocal:
82
+ SIDEREAL_TIME: float = 0.0
83
+ JD: float = 0.0
84
+ LATITUDE: float = 0.0
85
+ LONGITUDE: float = 0.0
86
+ HEIGHT: float = 0.0
87
+
88
+
89
+ @dataclass
90
+ class PositionAxis:
91
+ POWER_STATE: float = 0.0
92
+ ERROR_STATE: str = ""
93
+ MOTION_STATE: int = 0
94
+ REFERENCED: float = 0.0
95
+ REALPOS: float = 0.0
96
+ CURRPOS: float = 0.0
97
+ TARGETPOS: float = 0.0
98
+ CURRSPEED: float = 0.0
99
+ TARGETDISTANCE: float = 0.0
100
+ OFFSET: float = 0.0
101
+
102
+
103
+ @dataclass
104
+ class PositionInstrumental:
105
+ HA: PositionAxis = field(default_factory=PositionAxis)
106
+ DEC: PositionAxis = field(default_factory=PositionAxis)
107
+ ALT: PositionAxis = field(default_factory=PositionAxis)
108
+ AZ: PositionAxis = field(default_factory=PositionAxis)
109
+ FOCUS: PositionAxis = field(default_factory=PositionAxis)
110
+
111
+
112
+ @dataclass
113
+ class PositionHorizontal:
114
+ ALT: float = 0.0
115
+ AZ: float = 0.0
116
+ DOME: float = 0.0
117
+
118
+
119
+ @dataclass
120
+ class PositionEquatorial:
121
+ RA_J2000: float = 0.0
122
+ DEC_J2000: float = 0.0
123
+ HA_J2000: float = 0.0
124
+
125
+
126
+ @dataclass
127
+ class Position:
128
+ LOCAL: PositionLocal = field(default_factory=PositionLocal)
129
+ INSTRUMENTAL: PositionInstrumental = field(default_factory=PositionInstrumental)
130
+ HORIZONTAL: PositionHorizontal = field(default_factory=PositionHorizontal)
131
+ EQUATORIAL: PositionEquatorial = field(default_factory=PositionEquatorial)
132
+
133
+
134
+ @dataclass
135
+ class Dome:
136
+ REALPOS: float = 0.0
137
+ TARGETPOS: float = 0.0
138
+ ERROR_STATE: int = 0
139
+ READY_STATE: float = 0.0
140
+ MOTION_STATE: float = 0.0
141
+ AZ: float = 0.0
142
+
143
+
144
+ @dataclass
145
+ class Cover:
146
+ REALPOS: float = 0.0
147
+
148
+
149
+ @dataclass
150
+ class Auxiliary:
151
+ DOME: Dome = field(default_factory=Dome)
152
+ COVER: Cover = field(default_factory=Cover)
153
+
154
+
155
+ @dataclass
156
+ class Telemetry:
157
+ TELESCOPE: Telescope = field(default_factory=Telescope)
158
+ OBJECT: Object = field(default_factory=Object)
159
+ POINTING: Pointing = field(default_factory=Pointing)
160
+ POSITION: Position = field(default_factory=Position)
161
+ AUXILIARY: Auxiliary = field(default_factory=Auxiliary)
@@ -0,0 +1,186 @@
1
+ from enum import Enum
2
+ from typing import Any
3
+
4
+ from .base import BROTBase
5
+
6
+
7
+ class TelescopeStatus(Enum):
8
+ PARKED = "parked"
9
+ ONLINE = "online"
10
+ ERROR = "error"
11
+ INITPARK = "initpark"
12
+
13
+
14
+ class GlobalTelescopeStatus(Enum):
15
+ NOTELESCOPE = "notelescope"
16
+ OPERATIONAL = "operational"
17
+ PANIC = "panic"
18
+ ERROR = "error"
19
+ WARNING = "warning"
20
+ INFO = "info"
21
+ UNKNOWN = "unknown"
22
+
23
+
24
+ class BROTAxis(BROTBase):
25
+ def __init__(self, name: str, *args: Any, **kwargs: Any):
26
+ super().__init__(*args, **kwargs)
27
+ self._axis_name = name
28
+
29
+ @property
30
+ def error_state(self) -> int:
31
+ return int(
32
+ getattr(self._telemetry.POSITION.INSTRUMENTAL, self._axis_name).ERROR_STATE
33
+ )
34
+
35
+
36
+ class BROTTelescope(BROTBase):
37
+ def __init__(self, *args: Any, **kwargs: Any):
38
+ super().__init__(*args, **kwargs)
39
+ self.alt = BROTAxis("ALT", *args, **kwargs)
40
+ self.az = BROTAxis("AZ", *args, **kwargs)
41
+ self.ha = BROTAxis("HA", *args, **kwargs)
42
+ self.dec = BROTAxis("DEC", *args, **kwargs)
43
+ self.focus = BROTAxis("FOCUS", *args, **kwargs)
44
+
45
+ @property
46
+ def name(self) -> str:
47
+ return self._telemetry.TELESCOPE.INFO.NAME
48
+
49
+ @property
50
+ def diameter(self) -> float:
51
+ return self._telemetry.TELESCOPE.INFO.DIAMETER
52
+
53
+ @property
54
+ def cabinet(self) -> str:
55
+ return self._telemetry.TELESCOPE.INFO.CABINET
56
+
57
+ @property
58
+ def manufacturer(self) -> str:
59
+ return self._telemetry.TELESCOPE.INFO.MANUFACTURER
60
+
61
+ @property
62
+ def mount_options(self) -> str:
63
+ return self._telemetry.TELESCOPE.CONFIG.MOUNTOPTIONS
64
+
65
+ @property
66
+ def mount(self) -> str:
67
+ return self._telemetry.TELESCOPE.CONFIG.MOUNT
68
+
69
+ async def track(self, ra: float, dec: float) -> None:
70
+ await self._transport.publish(
71
+ f"{self._telescope_name}/Telescope/SET", f"command rightascension={ra}"
72
+ )
73
+ await self._transport.publish(
74
+ f"{self._telescope_name}/Telescope/SET", f"command declination={dec}"
75
+ )
76
+ await self._transport.publish(
77
+ f"{self._telescope_name}/Telescope/SET", "command track=1"
78
+ )
79
+
80
+ async def move(self, alt: float, az: float) -> None:
81
+ await self._transport.publish(
82
+ f"{self._telescope_name}/Telescope/SET", f"command elevation={alt}"
83
+ )
84
+ await self._transport.publish(
85
+ f"{self._telescope_name}/Telescope/SET", f"command azimuth={az}"
86
+ )
87
+ await self._transport.publish(
88
+ f"{self._telescope_name}/Telescope/SET", "command slew=1"
89
+ )
90
+
91
+ @property
92
+ def offset_ha(self) -> float:
93
+ return self._telemetry.POSITION.INSTRUMENTAL.HA.OFFSET * 3600.0
94
+
95
+ async def set_offset_ha(self, offset: float) -> None:
96
+ await self._transport.publish(
97
+ f"{self._telescope_name}/Telescope/SET",
98
+ f"command hourangleoffset={offset/3600.}",
99
+ )
100
+
101
+ @property
102
+ def offset_dec(self) -> float:
103
+ return self._telemetry.POSITION.INSTRUMENTAL.DEC.OFFSET * 3600.0
104
+
105
+ async def set_offset_dec(self, offset: float) -> None:
106
+ await self._transport.publish(
107
+ f"{self._telescope_name}/Telescope/SET",
108
+ f"command declinationoffset={offset/3600.}",
109
+ )
110
+
111
+ @property
112
+ def offset_alt(self) -> float:
113
+ return self._telemetry.POSITION.INSTRUMENTAL.ALT.OFFSET * 3600.0
114
+
115
+ async def set_offset_alt(self, offset: float) -> None:
116
+ await self._transport.publish(
117
+ f"{self._telescope_name}/Telescope/SET",
118
+ f"command elevationoffset={offset/3600.}",
119
+ )
120
+
121
+ @property
122
+ def offset_az(self) -> float:
123
+ return self._telemetry.POSITION.INSTRUMENTAL.AZ.OFFSET * 3600.0
124
+
125
+ async def set_offset_az(self, offset: float) -> None:
126
+ await self._transport.publish(
127
+ f"{self._telescope_name}/Telescope/SET",
128
+ f"command azimuthoffset={offset/3600.}",
129
+ )
130
+
131
+ @property
132
+ def status(self) -> TelescopeStatus:
133
+ match self._telemetry.TELESCOPE.READY_STATE:
134
+ case 0.0:
135
+ return TelescopeStatus.PARKED
136
+ case 1.0:
137
+ return TelescopeStatus.ONLINE
138
+ case -1.0:
139
+ return TelescopeStatus.ERROR
140
+ case _:
141
+ return TelescopeStatus.INITPARK
142
+
143
+ @property
144
+ def global_status(self) -> GlobalTelescopeStatus:
145
+ match self._telemetry.TELESCOPE.STATUS.GLOBAL:
146
+ case -1.0:
147
+ return GlobalTelescopeStatus.NOTELESCOPE
148
+ case 0.0:
149
+ return GlobalTelescopeStatus.OPERATIONAL
150
+ case 1.0:
151
+ return GlobalTelescopeStatus.PANIC
152
+ case 2.0:
153
+ return GlobalTelescopeStatus.ERROR
154
+ case 4.0:
155
+ return GlobalTelescopeStatus.WARNING
156
+ case 8.0:
157
+ return GlobalTelescopeStatus.INFO
158
+ case _:
159
+ return GlobalTelescopeStatus.UNKNOWN
160
+
161
+ @property
162
+ def initpark(self) -> float:
163
+ return self._telemetry.TELESCOPE.READY_STATE * 100.0
164
+
165
+ async def power_on(self) -> None:
166
+ await self._transport.publish(
167
+ f"{self._telescope_name}/Telescope/SET", "command power=true"
168
+ )
169
+
170
+ async def stop(self) -> None:
171
+ await self._transport.publish(
172
+ f"{self._telescope_name}/Telescope/SET", "command stop=TRUE"
173
+ )
174
+
175
+ async def park(self) -> None:
176
+ await self._transport.publish(
177
+ f"{self._telescope_name}/Telescope/SET", "command park=true"
178
+ )
179
+
180
+ async def reset(self) -> None:
181
+ await self._transport.publish(
182
+ f"{self._telescope_name}/Telescope/SET", "command reset=1"
183
+ )
184
+
185
+
186
+ __all__ = ["BROTTelescope", "TelescopeStatus", "GlobalTelescopeStatus"]
@@ -0,0 +1,24 @@
1
+ from abc import ABCMeta, abstractmethod
2
+ from typing import Any
3
+
4
+ from .telemetry import Telemetry
5
+
6
+
7
+ class Transport(metaclass=ABCMeta):
8
+ def __init__(self) -> None:
9
+ super().__init__()
10
+ self.data: dict[str, Any] = {}
11
+ self.telemetry = Telemetry()
12
+ self._connected = False
13
+
14
+ @abstractmethod
15
+ async def run(self) -> None:
16
+ pass
17
+
18
+ @abstractmethod
19
+ async def publish(self, topic: str, message: str) -> None:
20
+ pass
21
+
22
+ @property
23
+ def connected(self) -> bool:
24
+ return self._connected