ariel-tcu 0.17.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,246 @@
1
+ """Control Server for the Ariel Telescope Control Unit (TCU)."""
2
+
3
+ import multiprocessing
4
+ import logging
5
+ import sys
6
+ from typing import Annotated
7
+
8
+ import zmq
9
+ import typer
10
+ import rich
11
+
12
+ from egse.ariel.tcu import (
13
+ COMMANDING_PORT,
14
+ HOSTNAME,
15
+ PROTOCOL,
16
+ SERVICE_PORT,
17
+ MONITORING_PORT,
18
+ STORAGE_MNEMONIC,
19
+ PROCESS_NAME,
20
+ SERVICE_TYPE,
21
+ )
22
+ from egse.control import is_control_server_active, ControlServer
23
+ from egse.registry.client import RegistryClient
24
+ from egse.services import ServiceProxy
25
+ from egse.storage import store_housekeeping_information
26
+ from egse.zmq_ser import connect_address, get_port_number
27
+ from egse.ariel.tcu.tcu_protocol import TcuProtocol
28
+ from egse.ariel.tcu.tcu import TcuProxy
29
+
30
+ logger = logging.getLogger("egse.ariel.tcu")
31
+
32
+
33
+ def is_tcu_cs_active(timeout: float = 0.5) -> bool:
34
+ """Checks whether the Ariel TCU Control Server is running.
35
+
36
+ Args:
37
+ timeout (float): Timeout when waiting for a reply [s].
38
+
39
+ Returns:
40
+ True if the Ariel TCU Control Server is running and replied with the expected answer; False otherwise.
41
+ """
42
+
43
+ endpoint = connect_address(PROTOCOL, HOSTNAME, COMMANDING_PORT)
44
+
45
+ return is_control_server_active(endpoint, timeout)
46
+
47
+
48
+ class TcuControlServer(ControlServer):
49
+ def __init__(self, simulator: bool = False):
50
+ super().__init__()
51
+
52
+ multiprocessing.current_process().name = PROCESS_NAME
53
+
54
+ self.logger = logger
55
+ self.service_name = PROCESS_NAME
56
+ self.service_type = SERVICE_TYPE
57
+
58
+ self.device_protocol = TcuProtocol(self, simulator=simulator)
59
+
60
+ self.logger.info(f"Binding ZeroMQ socket to {self.device_protocol.get_bind_address()}")
61
+
62
+ self.device_protocol.bind(self.dev_ctrl_cmd_sock)
63
+
64
+ self.poller.register(self.dev_ctrl_cmd_sock, zmq.POLLIN)
65
+
66
+ self.register_service(SERVICE_TYPE)
67
+
68
+ def get_communication_protocol(self) -> str:
69
+ """Returns the communication protocol used by the Ariel TCU Control Server.
70
+
71
+ Returns:
72
+ Communication protocol used by the Ariel TCU Control Server, as specified in the settings.
73
+ """
74
+
75
+ return PROTOCOL
76
+
77
+ def get_commanding_port(self) -> int:
78
+ """Returns the commanding port used by the Ariel TCU Control Server.
79
+
80
+ Returns:
81
+ Commanding port used by the Ariel TCU Control Server, as specified in the settings.
82
+ """
83
+
84
+ return get_port_number(self.dev_ctrl_cmd_sock) or COMMANDING_PORT
85
+
86
+ def get_service_port(self) -> int:
87
+ """Returns the service port used by the Ariel TCU Control Server.
88
+
89
+ Returns:
90
+ Service port used by the Ariel TCU Control Server, as specified in the settings.
91
+ """
92
+
93
+ return get_port_number(self.dev_ctrl_service_sock) or SERVICE_PORT
94
+
95
+ def get_monitoring_port(self) -> int:
96
+ """Returns the monitoring port used by the Ariel TCU Control Server.
97
+
98
+ Returns:
99
+ Monitoring port used by the Ariel TCU Control Server, as specified in the settings.
100
+ """
101
+
102
+ return get_port_number(self.dev_ctrl_mon_sock) or MONITORING_PORT
103
+
104
+ def get_storage_mnemonic(self) -> str:
105
+ """Returns the storage mnemonic used by the Ariel TCU Control Server.
106
+
107
+ Returns:
108
+ Storage mnemonic used by the Ariel TCU Control Server, as specified in the settings.
109
+ """
110
+
111
+ return STORAGE_MNEMONIC
112
+
113
+ def is_storage_manager_active(self):
114
+ """Checks whether the Storage Manager is active."""
115
+ from egse.storage import is_storage_manager_active
116
+
117
+ return is_storage_manager_active()
118
+
119
+ def store_housekeeping_information(self, data):
120
+ """Sends housekeeping information of the Ariel TCU to the Storage Manager."""
121
+
122
+ origin = self.get_storage_mnemonic()
123
+ store_housekeeping_information(origin, data)
124
+
125
+ def register_to_storage_manager(self):
126
+ from egse.storage import register_to_storage_manager
127
+ from egse.storage.persistence import TYPES
128
+
129
+ register_to_storage_manager(
130
+ origin=self.get_storage_mnemonic(),
131
+ persistence_class=TYPES["CSV"],
132
+ prep={
133
+ "column_names": list(self.device_protocol.get_housekeeping().keys()),
134
+ "mode": "a",
135
+ },
136
+ )
137
+
138
+ def unregister_from_storage_manager(self):
139
+ from egse.storage import unregister_from_storage_manager
140
+
141
+ unregister_from_storage_manager(origin=self.get_storage_mnemonic())
142
+
143
+ # def before_serve(self):
144
+ # start_http_server(CTRL_SETTINGS.METRICS_PORT)
145
+
146
+ def after_serve(self):
147
+ self.deregister_service()
148
+
149
+
150
+ app = typer.Typer()
151
+
152
+
153
+ @app.command()
154
+ def start(
155
+ simulator: Annotated[
156
+ bool, typer.Option("--simulator", "--sim", help="start the Ariel TCU Control Server in simulator mode")
157
+ ] = False,
158
+ ):
159
+ """Starts the Ariel TCU Control Server."""
160
+
161
+ try:
162
+ controller = TcuControlServer(simulator)
163
+ controller.serve()
164
+ except KeyboardInterrupt:
165
+ print("Shutdown requested...exiting")
166
+ except SystemExit as exc:
167
+ exit_code = exc.code if hasattr(exc, "code") else 0
168
+ print(f"System Exit with code {exc.code}")
169
+ sys.exit(exit_code)
170
+ except Exception:
171
+ logger.exception("Cannot start the Ariel TCU Control Server")
172
+ # The above line does exactly the same as the traceback, but on the logger
173
+ # import traceback
174
+ # traceback.print_exc(file=sys.stdout)
175
+
176
+ return 0
177
+
178
+
179
+ @app.command()
180
+ def stop():
181
+ """Sends a `quit_server` command to the Ariel TCU Control Server."""
182
+
183
+ with RegistryClient() as reg:
184
+ service = reg.discover_service(SERVICE_TYPE)
185
+ rich.print("service = ", service)
186
+
187
+ if service:
188
+ proxy = ServiceProxy(protocol="tcp", hostname=service["host"], port=service["metadata"]["service_port"])
189
+ proxy.quit_server()
190
+ else:
191
+ # *_, device_type, controller_type = get_hexapod_controller_pars(device_id)
192
+
193
+ try:
194
+ with TcuProxy() as tcu_proxy:
195
+ with tcu_proxy.get_service_proxy() as sp:
196
+ sp.quit_server()
197
+ except ConnectionError:
198
+ rich.print("[red]Couldn't connect to 'tcu_cs', process probably not running. ")
199
+
200
+
201
+ @app.command()
202
+ def status():
203
+ """Requests the status information from the Ariel TCU Control Server."""
204
+
205
+ with RegistryClient() as reg:
206
+ service = reg.discover_service(SERVICE_TYPE)
207
+
208
+ if service:
209
+ protocol = service.get("protocol", "tcp")
210
+ hostname = service["host"]
211
+ port = service["port"]
212
+ service_port = service["metadata"]["service_port"]
213
+ monitoring_port = service["metadata"]["monitoring_port"]
214
+ endpoint = connect_address(protocol, hostname, port)
215
+ else:
216
+ rich.print(
217
+ f"[red]The Ariel TCU Control Server isn't registered as a service. The Control Server cannot be "
218
+ f"contacted without the required information from the service registry.[/]"
219
+ )
220
+ rich.print("Ariel TCU: [red]not active")
221
+ return
222
+
223
+ if is_control_server_active(endpoint):
224
+ rich.print("Ariel TCU: [green]active")
225
+
226
+ with TcuProxy() as tcu:
227
+ sim = tcu.is_simulator()
228
+ connected = tcu.is_connected()
229
+ ip = tcu.get_ip_address()
230
+ rich.print(f"mode: {'simulator' if sim else 'device'}{'' if connected else ' not'} connected")
231
+ rich.print(f"hostname: {ip}")
232
+ rich.print(f"commanding port: {port}")
233
+ rich.print(f"service port: {service_port}")
234
+ rich.print(f"monitoring port: {monitoring_port}")
235
+ else:
236
+ rich.print("Ariel TCU: [red]not active")
237
+
238
+
239
+ if __name__ == "__main__":
240
+ import logging
241
+
242
+ from egse.logger import set_all_logger_levels
243
+
244
+ set_all_logger_levels(logging.DEBUG)
245
+
246
+ sys.exit(app())
@@ -0,0 +1,104 @@
1
+ """Device connection interface for the Ariel Telescope Control Unit (TCU)."""
2
+
3
+ import logging
4
+ import time
5
+
6
+ import serial
7
+
8
+ from egse.device import DeviceConnectionInterface, DeviceTransport
9
+ from egse.settings import Settings
10
+
11
+ logger = logging.getLogger(__name__)
12
+ DEVICE_SETTINGS = Settings.load("Ariel TCU Controller")
13
+
14
+
15
+ class TcuError(Exception):
16
+ """Generic TCU error for low-level classes."""
17
+
18
+ pass
19
+
20
+
21
+ class TcuDeviceInterface(DeviceConnectionInterface, DeviceTransport):
22
+ """Defines the low-level interface to the Ariel TCU (Arduino)."""
23
+
24
+ def __init__(self, port: int = None):
25
+ """Initialisation of a serial interface to the TCU Arduino.
26
+
27
+ Args:
28
+ port (int): Serial port to which to connect to the TCU Arduino.
29
+ """
30
+
31
+ super().__init__()
32
+
33
+ # self.hostname = hostname or DEVICE_SETTINGS["HOSTNAME"]
34
+ self.port = port or DEVICE_SETTINGS["COM_PORT"]
35
+
36
+ self.arduino = serial.Serial()
37
+ self.arduino.port = self.port
38
+ self.arduino.baudrate = DEVICE_SETTINGS["BAUD_RATE"]
39
+ self.arduino.bytesize = DEVICE_SETTINGS["NUM_DATA_BITS"]
40
+ self.arduino.parity = DEVICE_SETTINGS["PARITY"]
41
+ self.arduino.stopbits = DEVICE_SETTINGS["NUM_STOP_BITS"]
42
+
43
+ def connect(self) -> None:
44
+ """Opens the serial port to the TCU Arduino.
45
+
46
+ Raises:
47
+ TcuError: When the serial port could not be opened.
48
+ """
49
+
50
+ if self.is_connected():
51
+ raise TcuError("TCU already connected.")
52
+ # if self.hostname in (None, ""):
53
+ # raise TcuError("TCU hostname is not initialised.")
54
+ if self.port in (None, 0):
55
+ raise TcuError("TCU serial port is not initialised.")
56
+
57
+ try:
58
+ self.arduino.open()
59
+ except Exception as exc:
60
+ raise TcuError(f"Failed to open TCU serial port {self.port}") from exc
61
+
62
+ def disconnect(self) -> None:
63
+ """Closes the serial port to the TCU Arduino.
64
+
65
+ Raises:
66
+ TcuError: When the serial port could not be closed.
67
+ """
68
+
69
+ try:
70
+ self.arduino.close()
71
+ except Exception as exc:
72
+ raise TcuError(f"Failed to close TCU serial port {self.port}") from exc
73
+
74
+ def reconnect(self) -> None:
75
+ """Re-connects to the Ariel TCU Arduino."""
76
+
77
+ if self.is_connected():
78
+ self.disconnect()
79
+ self.connect()
80
+
81
+ def is_connected(self) -> bool:
82
+ """Checks whether the serial port to the TCU Arduino is open.
83
+
84
+ Returns:
85
+ True if the serial port to the TCU Arduino is open; False otherwise.
86
+ """
87
+
88
+ return self.arduino.is_open
89
+
90
+ def trans(self, command: str) -> bytes:
91
+ """Sends the given command to the TCU Arduino and returns the response.
92
+
93
+ This is seen as a transaction.
94
+
95
+ Args:
96
+ command: Command string to send to the TCU Arduino.
97
+
98
+ Returns:
99
+ Response string from the TCU Arduino.
100
+ """
101
+
102
+ self.arduino.write(command.encode("utf-8"))
103
+ time.sleep(0.05)
104
+ return self.arduino.readline()
@@ -0,0 +1,147 @@
1
+ """Command protocol for the Ariel Telescope Control Unit (TCU)."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+
6
+ from egse.ariel.tcu import NUM_TSM_PROBES_PER_FRAME
7
+ from egse.ariel.tcu.tcu import TcuController, TcuSimulator, TcuInterface
8
+ from egse.command import ClientServerCommand
9
+ from egse.control import ControlServer
10
+ from egse.device import DeviceConnectionState
11
+ from egse.protocol import DynamicCommandProtocol
12
+ from egse.settings import Settings
13
+ from egse.system import format_datetime
14
+ from egse.zmq_ser import bind_address
15
+
16
+ _HERE = Path(__file__).parent
17
+ DEVICE_SETTINGS = Settings.load(filename="tcu.yaml", location=_HERE)
18
+ logger = logging.getLogger("egse.ariel.tcu")
19
+
20
+
21
+ class TcuCommand(ClientServerCommand):
22
+ """Command class for the Ariel TCU Control Server."""
23
+
24
+ pass
25
+
26
+
27
+ class TcuProtocol(DynamicCommandProtocol):
28
+ """Command protocol for the Ariel TCU Control Server."""
29
+
30
+ def __init__(self, control_server: ControlServer, simulator: bool = False):
31
+ """Initialisation of an Ariel TCU protocol.
32
+
33
+ Args:
34
+ control_server (ControlServer): Ariel TCU Control Server.
35
+ simulator (bool): Whether to use a simulator as the backend.
36
+ """
37
+
38
+ super().__init__(control_server)
39
+
40
+ self.simulator = simulator
41
+
42
+ if self.simulator:
43
+ self.tcu = TcuSimulator()
44
+ else:
45
+ self.tcu = TcuController()
46
+
47
+ try:
48
+ self.tcu.connect()
49
+ except ConnectionError:
50
+ logger.warning("Couldn't establish connection to the Ariel TCU, check the log messages.")
51
+
52
+ # self.metrics = define_metrics("TCU")
53
+
54
+ def get_bind_address(self) -> str:
55
+ """Returns the bind address for the Ariel TCU Control Server.
56
+
57
+ Returns:
58
+ Bind address for the Ariel TCU Control Server.
59
+ """
60
+
61
+ return bind_address(self.control_server.get_communication_protocol(), self.control_server.get_commanding_port())
62
+
63
+ def get_device(self) -> TcuInterface:
64
+ """Returns the Ariel TCU interface.
65
+
66
+ Returns:
67
+ Ariel TCU interface.
68
+ """
69
+ return self.tcu
70
+
71
+ def get_status(self) -> dict:
72
+ """Returns the status information for the Ariel TCU Control Server.
73
+
74
+ Returns:
75
+ Status information for the Ariel TCU Control Server.
76
+ """
77
+
78
+ status = super().get_status()
79
+
80
+ if self.state == DeviceConnectionState.DEVICE_NOT_CONNECTED and not self.simulator:
81
+ return status
82
+
83
+ # TODO Add device-specific status information
84
+
85
+ return status
86
+
87
+ def get_housekeeping(self) -> dict:
88
+ """Returns the housekeeping information for the Ariel TCU Control Server.
89
+
90
+ Returns:
91
+ Housekeeping information for the Ariel TCU Control Server.
92
+ """
93
+
94
+ result = dict()
95
+ result["timestamp"] = format_datetime()
96
+
97
+ if self.state == DeviceConnectionState.DEVICE_NOT_CONNECTED and not self.simulator:
98
+ return result
99
+
100
+ result["TCU_MODE"] = self.tcu.get_tcu_mode()
101
+ result["TCU_VHK_PSU_VMOTOR"] = self.tcu.vhk_psu_vmotor()
102
+ result["TCU_VHK_PSU_VHI"] = self.tcu.vhk_psu_vhi()
103
+ result["TCU_VHK_PSU_VLOW"] = self.tcu.vhk_psu_vlow()
104
+ result["TCU_VHK_PSU_VMEDP"] = self.tcu.vhk_psu_vmedp()
105
+ result["TCU_VHK_PSU_VMEDN"] = self.tcu.vhk_psu_vmedn()
106
+ result["TCU_IHK_PSU_VMEDN"] = self.tcu.ihk_psu_vmedn()
107
+ result["TCU_IHK_PSU_VMEDP"] = self.tcu.ihk_psu_vmedp()
108
+ result["TCU_IHK_PSU_VLOW"] = self.tcu.ihk_psu_vlow()
109
+ result["TCU_IHK_PSU_VHI"] = self.tcu.ihk_psu_vhi()
110
+ result["TCU_IHK_PSU_VMOTOR"] = self.tcu.ihk_psu_vmotor()
111
+ result["TCU_THK_PSU_FIRST"] = self.tcu.thk_psu_first()
112
+ result["TCU_THK_M2MD_FIRST"] = self.tcu.thk_m2md_first()
113
+ result["TCU_THK_PSU_SECOND"] = self.tcu.thk_psu_second()
114
+ result["TCU_THK_M2MD_SECOND"] = self.tcu.thk_m2md_second()
115
+ result["TCU_THK_CTS_Q1"] = self.tcu.thk_cts_q1()
116
+ result["TCU_THK_CTS_Q2"] = self.tcu.thk_cts_q2()
117
+ result["TCU_THK_CTS_Q3"] = self.tcu.thk_cts_q3()
118
+ result["TCU_THK_CTS_Q4"] = self.tcu.thk_cts_q4()
119
+ result["TCU_THK_CTS_FPGA"] = self.tcu.thk_cts_fpga()
120
+ result["TCU_THK_CTS_ADS1282"] = self.tcu.thk_cts_ads1282()
121
+ result["TCU_VHK_THS_RET"] = self.tcu.vhk_ths_ret()
122
+ result["TCU_HK_ACQ_COUNTER"] = self.tcu.hk_acq_counter()
123
+
124
+ for probe in range(1, NUM_TSM_PROBES_PER_FRAME + 1):
125
+ bias_pos = self.tcu.tsm_adc_value_xx_biasp(probe=probe)
126
+ result[f"TCU_BIAS_POS_PROBE_{probe}"] = bias_pos
127
+ bias_neg = self.tcu.tsm_adc_value_xx_biasn(probe=probe)
128
+ result[f"TCU_BIAS_NEG_PROBE_{probe}"] = bias_neg
129
+ current_pos = self.tcu.tsm_adc_value_xx_currentp(probe=probe)
130
+ result[f"TCU_CURRENT_POS_PROBE_{probe}"] = current_pos
131
+ current_neg = self.tcu.tsm_adc_value_xx_currentn(probe=probe)
132
+ result[f"TCU_CURRENT_NEG_PROBE_{probe}"] = current_neg
133
+
134
+ # alpha = (bias_pos - bias_neg) / (current_pos - current_neg)
135
+ # (c0, c1) ??
136
+ # resistance = alpha / (alpha * c1 + c0)
137
+
138
+ return result
139
+
140
+ def is_device_connected(self) -> bool:
141
+ """Checks whether the Ariel TCU is connected.
142
+
143
+ Returns:
144
+ True if the Ariel TCU is connected; False otherwise.
145
+ """
146
+
147
+ return self.tcu.is_connected()