ariel-tcu 0.17.3__py3-none-any.whl → 0.18.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.
- ariel_tcu/settings.yaml +4 -0
- ariel_tcu-0.18.0.dist-info/METADATA +40 -0
- ariel_tcu-0.18.0.dist-info/RECORD +16 -0
- {ariel_tcu-0.17.3.dist-info → ariel_tcu-0.18.0.dist-info}/entry_points.txt +3 -0
- egse/ariel/tcu/__init__.py +12 -7
- egse/ariel/tcu/tcu.py +108 -16
- egse/ariel/tcu/tcu_cmd_utils.py +65 -8
- egse/ariel/tcu/tcu_cs.py +62 -32
- egse/ariel/tcu/tcu_devif.py +27 -0
- egse/ariel/tcu/tcu_protocol.py +2 -2
- egse/ariel/tcu/tcu_ui.py +693 -0
- ariel_tcu-0.17.3.dist-info/METADATA +0 -14
- ariel_tcu-0.17.3.dist-info/RECORD +0 -15
- {ariel_tcu-0.17.3.dist-info → ariel_tcu-0.18.0.dist-info}/WHEEL +0 -0
egse/ariel/tcu/tcu_ui.py
ADDED
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import multiprocessing
|
|
3
|
+
import threading
|
|
4
|
+
import types
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Union, Any, Callable
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import typer
|
|
10
|
+
from PyQt5.QtCore import QLockFile
|
|
11
|
+
from PyQt5.QtWidgets import (
|
|
12
|
+
QApplication,
|
|
13
|
+
QMainWindow,
|
|
14
|
+
QTabWidget,
|
|
15
|
+
QWidget,
|
|
16
|
+
QVBoxLayout,
|
|
17
|
+
QComboBox,
|
|
18
|
+
QLineEdit,
|
|
19
|
+
QHBoxLayout,
|
|
20
|
+
QLabel,
|
|
21
|
+
QPushButton,
|
|
22
|
+
QMessageBox,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from egse.ariel.tcu import (
|
|
26
|
+
TcuMode,
|
|
27
|
+
NUM_M2MD_AXES,
|
|
28
|
+
NUM_M2MD_POSITIONS,
|
|
29
|
+
NUM_TSM_PROBES_PER_FRAME,
|
|
30
|
+
NUM_TSM_FRAMES,
|
|
31
|
+
AXIS_VELOCITY,
|
|
32
|
+
)
|
|
33
|
+
from egse.ariel.tcu.tcu import TcuInterface, TcuHex, TcuProxy
|
|
34
|
+
from egse.ariel.tcu.tcu_cs import is_tcu_cs_active
|
|
35
|
+
from egse.gui import QHLine
|
|
36
|
+
from egse.log import logging
|
|
37
|
+
from egse.observer import Observable, Observer
|
|
38
|
+
from egse.process import ProcessStatus
|
|
39
|
+
from egse.resource import get_resource
|
|
40
|
+
from egse.response import Failure
|
|
41
|
+
from egse.settings import Settings
|
|
42
|
+
from egse.system import do_every
|
|
43
|
+
|
|
44
|
+
MODULE_LOGGER = logging.getLogger(__name__)
|
|
45
|
+
TCU_COMPONENTS = ["GENERAL", "M2MD", "TSM", "HK"]
|
|
46
|
+
CTRL_SETTINGS = Settings.load("Ariel TCU Controller")
|
|
47
|
+
|
|
48
|
+
app = typer.Typer()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class TcuUIModel:
|
|
52
|
+
"""Model in the MVC pattern that makes the TCU UI application."""
|
|
53
|
+
|
|
54
|
+
def __init__(self):
|
|
55
|
+
"""Initialisation of the TCU UI Model."""
|
|
56
|
+
|
|
57
|
+
# This is used to generate the hex string that is sent to the Arduino, so it can be shown in the TCU UI
|
|
58
|
+
# (Might be discontinued in the future)
|
|
59
|
+
|
|
60
|
+
self.tcu_hex = TcuHex()
|
|
61
|
+
|
|
62
|
+
# This is used to establish a connection to the TCU and actually send commands to it
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
self.tcu_proxy: TcuProxy[TcuHex, None] = TcuProxy()
|
|
66
|
+
except RuntimeError:
|
|
67
|
+
MODULE_LOGGER.error("Could not connect to Ariel TCU Control Server")
|
|
68
|
+
self.tcu_proxy: Union[TcuProxy, None] = None
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def build_dyn_cmds_list() -> dict:
|
|
72
|
+
"""Keep track of which dynamic commands have been defined.
|
|
73
|
+
|
|
74
|
+
Per component of the TCU, we store a dictionary of the dynamic commands that have been defined for that
|
|
75
|
+
component. The keys in these dictionaries are the names of the dynamic commands, and the values are the
|
|
76
|
+
functions that implement them.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Dictionary that stores - per component of the TCU - a dictionary of the dynamic commands that have been
|
|
80
|
+
defined for that component.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
dyn_cmds = {}
|
|
84
|
+
for component in TCU_COMPONENTS:
|
|
85
|
+
dyn_cmds[component] = {}
|
|
86
|
+
|
|
87
|
+
for cmd_name, func in inspect.getmembers(TcuInterface, inspect.isfunction):
|
|
88
|
+
if hasattr(func, "target_comp"):
|
|
89
|
+
target_comp = getattr(func, "target_comp")
|
|
90
|
+
dyn_cmds[target_comp][cmd_name] = func
|
|
91
|
+
|
|
92
|
+
return dyn_cmds
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class TcuUiView(QMainWindow, Observable):
|
|
96
|
+
"""View in the MVC pattern that makes the TCU UI application."""
|
|
97
|
+
|
|
98
|
+
def __init__(self):
|
|
99
|
+
"""Initialisation of the TCU UI View."""
|
|
100
|
+
|
|
101
|
+
super(TcuUiView, self).__init__()
|
|
102
|
+
Observable.__init__(self)
|
|
103
|
+
|
|
104
|
+
self.setGeometry(300, 300, 500, 300)
|
|
105
|
+
self.setWindowTitle("Telescope Control Unit")
|
|
106
|
+
|
|
107
|
+
# Central widget = tabs per component
|
|
108
|
+
|
|
109
|
+
self.tabs = QTabWidget()
|
|
110
|
+
self.setCentralWidget(self.tabs)
|
|
111
|
+
|
|
112
|
+
def build_tabs(self, dyn_cmds: dict, observer):
|
|
113
|
+
"""Build the tabs of the TCU UI view.
|
|
114
|
+
|
|
115
|
+
For each component of the TCU commanding, a tab is created. In this tab, you can find:
|
|
116
|
+
|
|
117
|
+
- Drop-down menu with the list of available dynamic commands for the component,
|
|
118
|
+
- If applicable: entry for cargo1,
|
|
119
|
+
- If applicable: entry for cargo2,
|
|
120
|
+
- "Run" button,
|
|
121
|
+
- Command string (read-only),
|
|
122
|
+
- Response (read-only).
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
dyn_cmds (dict): Dictionary that stores - per component of the TCU - a dictionary of the dynamic commands
|
|
126
|
+
that have been defined for that component.
|
|
127
|
+
observer: Observer that will be notified when a command is run (i.e. when the "Run" button is clicked).
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
for tab_name, dyn_cmd_for_tab in dyn_cmds.items():
|
|
131
|
+
tab = TabWidget(dyn_cmd_for_tab)
|
|
132
|
+
tab.add_observer(observer)
|
|
133
|
+
|
|
134
|
+
self.tabs.addTab(tab, tab_name)
|
|
135
|
+
|
|
136
|
+
# When all tabs have been populated, select the first dynamic command in the drop-down menu in the first tab,
|
|
137
|
+
# to make sure that the correct input parameters are requested (if applicable)
|
|
138
|
+
|
|
139
|
+
self.tabs.currentChanged.connect(self.on_tab_changed)
|
|
140
|
+
self.on_tab_changed(0)
|
|
141
|
+
|
|
142
|
+
def on_tab_changed(self, _):
|
|
143
|
+
"""Takes action when another tab is selected.
|
|
144
|
+
|
|
145
|
+
When a new tab has been selected, select the current dynamic command in the drop-down menu in the new tab,
|
|
146
|
+
to make sure that the correct input parameters are requested (if applicable).
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
self.tabs.currentWidget().dyn_cmd_drop_down_menu_changed(
|
|
150
|
+
self.tabs.currentWidget().dyn_cmd_drop_down_menu.currentText()
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class TabWidget(QWidget, Observable):
|
|
155
|
+
"""Tab in the TCU UI View."""
|
|
156
|
+
|
|
157
|
+
def __init__(self, dyn_cmds: dict):
|
|
158
|
+
"""Initialisation of a tab in the TCU UI View.
|
|
159
|
+
|
|
160
|
+
In this tab, you can find:
|
|
161
|
+
|
|
162
|
+
- Drop-down menu with the list of available dynamic commands for the component,
|
|
163
|
+
- If applicable: entry for cargo1,
|
|
164
|
+
- If applicable: entry for cargo2,
|
|
165
|
+
- "Run" button,
|
|
166
|
+
- Command string (read-only),
|
|
167
|
+
- Response (read-only).
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
dyn_cmds (dict): Dictionary that stores for one TCU component a dictionary of the dynamic commands
|
|
171
|
+
that have been defined for that component.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
super().__init__()
|
|
175
|
+
layout = QVBoxLayout(self)
|
|
176
|
+
|
|
177
|
+
# Dynamic commands that are available for this component
|
|
178
|
+
# (i.e. the dictionary of dynamic commands that have been defined for this component)
|
|
179
|
+
|
|
180
|
+
self.dyn_cmds = dyn_cmds
|
|
181
|
+
|
|
182
|
+
# Entries for cargo1 and/or cargo2 (if applicable)
|
|
183
|
+
# We have to define these before we create the drop-down menu with the dynamic commands
|
|
184
|
+
|
|
185
|
+
self.cargo1_layout = CargoLayout(1)
|
|
186
|
+
self.cargo2_layout = CargoLayout(2)
|
|
187
|
+
|
|
188
|
+
# Drop-down menu with the list of dynamic commands that are available for this component
|
|
189
|
+
|
|
190
|
+
self.dyn_cmd_drop_down_menu = QComboBox()
|
|
191
|
+
|
|
192
|
+
for cmd_name in self.dyn_cmds:
|
|
193
|
+
self.dyn_cmd_drop_down_menu.addItem(cmd_name)
|
|
194
|
+
self.dyn_cmd_drop_down_menu.currentTextChanged.connect(self.dyn_cmd_drop_down_menu_changed)
|
|
195
|
+
|
|
196
|
+
# Button to send the command to the Arduino
|
|
197
|
+
|
|
198
|
+
button = QPushButton("Run")
|
|
199
|
+
button.clicked.connect(self.button_clicked)
|
|
200
|
+
|
|
201
|
+
# Display the hex string tha is sent to the Arduino
|
|
202
|
+
|
|
203
|
+
cmd_string_layout = QHBoxLayout()
|
|
204
|
+
self.cmd_string = QLineEdit()
|
|
205
|
+
self.cmd_string.setReadOnly(True)
|
|
206
|
+
cmd_string_layout.addWidget(QLabel("Command string"))
|
|
207
|
+
cmd_string_layout.addWidget(self.cmd_string)
|
|
208
|
+
|
|
209
|
+
# Display the response that is received from the Arduino
|
|
210
|
+
# (after the command has been sent by clicking the "Run" button)
|
|
211
|
+
|
|
212
|
+
response_layout = QHBoxLayout()
|
|
213
|
+
self.response = QLineEdit()
|
|
214
|
+
self.response.setReadOnly(True)
|
|
215
|
+
response_layout.addWidget(QLabel("Response"))
|
|
216
|
+
response_layout.addWidget(self.response)
|
|
217
|
+
|
|
218
|
+
# Assembly of all components in the tab
|
|
219
|
+
|
|
220
|
+
layout.addWidget(self.dyn_cmd_drop_down_menu) # Drop-down menu with available dynamic commands
|
|
221
|
+
layout.addLayout(self.cargo1_layout) # Cargo1
|
|
222
|
+
layout.addLayout(self.cargo2_layout) # Cargo2
|
|
223
|
+
layout.addWidget(button) # "Run" button
|
|
224
|
+
layout.addWidget(QHLine()) # Horizontal separator
|
|
225
|
+
layout.addLayout(cmd_string_layout) # Hex string that is sent to the Arduino
|
|
226
|
+
layout.addWidget(QHLine()) # Horizontal separator
|
|
227
|
+
layout.addLayout(response_layout) # Response that is received from the Arduino
|
|
228
|
+
|
|
229
|
+
def dyn_cmd_drop_down_menu_changed(self, dyn_cmd_name):
|
|
230
|
+
"""Takes action when another dynamic command has been selected in the drop-down menu.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
dyn_cmd_name (str): Name of the dynamic command that has been selected in the drop-down menu.
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
# Extract the signature of the selected function (1st item is always `self`)
|
|
237
|
+
|
|
238
|
+
signature = inspect.signature(self.dyn_cmds[dyn_cmd_name])
|
|
239
|
+
parameters = signature.parameters
|
|
240
|
+
|
|
241
|
+
# `self` + cargo1 + cargo2 (the latter two may have a different name -> extract from signature)
|
|
242
|
+
|
|
243
|
+
if len(parameters) == 3:
|
|
244
|
+
self.cargo1_layout.set_visible(True)
|
|
245
|
+
self.cargo1_layout.update_entry(list(parameters.keys())[-2])
|
|
246
|
+
self.cargo2_layout.set_visible(True)
|
|
247
|
+
self.cargo2_layout.update_entry(list(parameters.keys())[-1])
|
|
248
|
+
|
|
249
|
+
# `self` + cargo2 (the latter may have a different name -> extract from signature)
|
|
250
|
+
|
|
251
|
+
elif len(parameters) == 2:
|
|
252
|
+
self.cargo1_layout.set_visible(False)
|
|
253
|
+
self.cargo2_layout.set_visible(True)
|
|
254
|
+
self.cargo2_layout.update_entry(list(parameters.keys())[-1])
|
|
255
|
+
|
|
256
|
+
# `self` (no cargo)
|
|
257
|
+
|
|
258
|
+
else:
|
|
259
|
+
self.cargo1_layout.set_visible(False)
|
|
260
|
+
self.cargo2_layout.set_visible(False)
|
|
261
|
+
|
|
262
|
+
self.repaint()
|
|
263
|
+
|
|
264
|
+
def button_clicked(self):
|
|
265
|
+
"""Takes action when the "Run" button is clicked.
|
|
266
|
+
|
|
267
|
+
When the "Run" button is clicked, the observer (i.c. TCU UI Controller) is notified. It will receive a tuple
|
|
268
|
+
with the following information:
|
|
269
|
+
|
|
270
|
+
- The tab widget in which the "Run" button has been clicked (needed to be able to fill out the response),
|
|
271
|
+
- The dynamic command that has been selected in the drop-down menu (function),
|
|
272
|
+
- The cargo1 value (if applicable),
|
|
273
|
+
- The cargo2 value (if applicable).
|
|
274
|
+
|
|
275
|
+
The TCU UI Controller will make sure that the hex string that is sent to the Arduino and the response that is
|
|
276
|
+
received from the Arduino are displayed in the UI. It will receive this information from the TCU UI Model.
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
self.notify_observers(
|
|
280
|
+
(
|
|
281
|
+
self, # Tab widget in which the "Run" button has been clicked
|
|
282
|
+
self.dyn_cmds[self.dyn_cmd_drop_down_menu.currentText()], # Selected dynamic command (as a function)
|
|
283
|
+
self.cargo1_layout.entry.text(), # Cargo1 entry
|
|
284
|
+
self.cargo2_layout.entry.text(), # Cargo2 entry
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class CargoLayout(QHBoxLayout):
|
|
290
|
+
"""Layout for the input argument(s) of the dynamic commands."""
|
|
291
|
+
|
|
292
|
+
def __init__(self, cargo: int):
|
|
293
|
+
"""Initialisation of a layout for the given cargo argument (1 or 2).
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
cargo (int): Number of the cargo argument (1 or 2).
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
super().__init__()
|
|
300
|
+
|
|
301
|
+
# Label for the argument ("cargo1" / "cargo2", depending on the input, as default)
|
|
302
|
+
|
|
303
|
+
self.label = QLabel(f"cargo{cargo}")
|
|
304
|
+
|
|
305
|
+
# By default, the entry is a QLineEdit
|
|
306
|
+
|
|
307
|
+
self.entry = QLineEdit()
|
|
308
|
+
|
|
309
|
+
self.addWidget(self.label)
|
|
310
|
+
self.addWidget(self.entry)
|
|
311
|
+
|
|
312
|
+
def update_entry(self, cargo_name: str):
|
|
313
|
+
"""Update the entry for the input argument with the given name.
|
|
314
|
+
|
|
315
|
+
Updating the entry entails:
|
|
316
|
+
|
|
317
|
+
- Updating the label with the name of the input argument,
|
|
318
|
+
- Replacing the entry with the appropriate widget (this can be a line edit, a combo box, etc.).
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
cargo_name (str): Name of the input argument.
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
# Update the name of the input argument
|
|
325
|
+
|
|
326
|
+
self.label.setText(cargo_name)
|
|
327
|
+
|
|
328
|
+
# The `axis` argument is only used for M2MD commands, to denote the axis that should be commanded
|
|
329
|
+
# -> Use a drop-down menu instead of the default QLineEdit
|
|
330
|
+
|
|
331
|
+
if cargo_name == "axis":
|
|
332
|
+
self.replace_entry_with_axis()
|
|
333
|
+
|
|
334
|
+
# The `tcu_mode` argument is only used to change the TCU mode
|
|
335
|
+
# -> Use a drop-down menu instead of the default QLineEdit
|
|
336
|
+
|
|
337
|
+
elif cargo_name == "tcu_mode":
|
|
338
|
+
self.replace_with_tcu_mode()
|
|
339
|
+
|
|
340
|
+
# The `position` argument is only used for `sw_rs_xx_sw_rise` and `sw_rs_xx_sw_fall`, to denote the position
|
|
341
|
+
# that should be commanded
|
|
342
|
+
# -> Use a drop-down menu instead of the default QLineEdit (read the number of positions from the settings)
|
|
343
|
+
|
|
344
|
+
elif cargo_name == "position":
|
|
345
|
+
self.replace_with_int_combo_box(1, NUM_M2MD_POSITIONS)
|
|
346
|
+
|
|
347
|
+
# The `probe` argument is only used for `tsm_adc_value_xx_currentn`, `tsm_adc_value_xx_biasn`,
|
|
348
|
+
# `tsm_adc_value_xx_currentp`, and `tsm_adc_value_xx_biasp`
|
|
349
|
+
# -> Use a drop-down menu instead of the default QLineEdit (read the number of probes from the settings)
|
|
350
|
+
|
|
351
|
+
elif cargo_name == "probe":
|
|
352
|
+
self.replace_with_int_combo_box(1, NUM_TSM_FRAMES * NUM_TSM_PROBES_PER_FRAME)
|
|
353
|
+
|
|
354
|
+
elif cargo_name == "speed":
|
|
355
|
+
self.replace_with_axis_speed_combo_box()
|
|
356
|
+
|
|
357
|
+
# By default, the entry is a QLineEdit
|
|
358
|
+
|
|
359
|
+
else:
|
|
360
|
+
self.replace_with_line_edit()
|
|
361
|
+
|
|
362
|
+
def replace_with_line_edit(self):
|
|
363
|
+
"""Replaces the entry with a QLineEdit (if it's not a QLineEdit already)."""
|
|
364
|
+
|
|
365
|
+
if not isinstance(self.entry, QLineEdit):
|
|
366
|
+
# Remove the current entry
|
|
367
|
+
|
|
368
|
+
self.removeWidget(self.entry)
|
|
369
|
+
self.entry.hide()
|
|
370
|
+
self.entry.deleteLater()
|
|
371
|
+
|
|
372
|
+
# Replace it with a QLineEdit
|
|
373
|
+
|
|
374
|
+
self.entry = QLineEdit()
|
|
375
|
+
self.insertWidget(1, self.entry)
|
|
376
|
+
|
|
377
|
+
def replace_entry_with_axis(self):
|
|
378
|
+
"""Replaces the entry with an AxisComboBox (if it's not an AxisComboBox already)."""
|
|
379
|
+
|
|
380
|
+
if not isinstance(self.entry, AxisComboBox):
|
|
381
|
+
# Remove the current entry
|
|
382
|
+
|
|
383
|
+
self.removeWidget(self.entry)
|
|
384
|
+
self.entry.hide()
|
|
385
|
+
self.entry.deleteLater()
|
|
386
|
+
|
|
387
|
+
# Replace it with an AxisComboBox
|
|
388
|
+
|
|
389
|
+
self.entry = AxisComboBox()
|
|
390
|
+
self.insertWidget(1, self.entry)
|
|
391
|
+
|
|
392
|
+
def replace_with_tcu_mode(self):
|
|
393
|
+
"""Replaces the entry with a TcuModeComboBox (if it's not a TcuModeComboBox already)."""
|
|
394
|
+
|
|
395
|
+
if not isinstance(self.entry, TcuModeComboBox):
|
|
396
|
+
# Remove the current entry
|
|
397
|
+
|
|
398
|
+
self.removeWidget(self.entry)
|
|
399
|
+
self.entry.hide()
|
|
400
|
+
self.entry.deleteLater()
|
|
401
|
+
|
|
402
|
+
# Replace it with a TcuModeComboBox
|
|
403
|
+
|
|
404
|
+
self.entry = TcuModeComboBox()
|
|
405
|
+
self.insertWidget(1, self.entry)
|
|
406
|
+
|
|
407
|
+
def replace_with_int_combo_box(self, min_value: int, max_value: int):
|
|
408
|
+
"""Replaces the entry with an IntComboBox (if it's not an IntComboBox already).
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
min_value (int): Minimum value of the IntComboBox.
|
|
412
|
+
max_value (int): Maximum value of the IntComboBox.
|
|
413
|
+
"""
|
|
414
|
+
|
|
415
|
+
# Remove the current entry
|
|
416
|
+
|
|
417
|
+
self.removeWidget(self.entry)
|
|
418
|
+
self.entry.hide()
|
|
419
|
+
self.entry.deleteLater()
|
|
420
|
+
|
|
421
|
+
# Replace it with an IntComboBox
|
|
422
|
+
|
|
423
|
+
self.entry = IntComboBox(min_value, max_value)
|
|
424
|
+
self.insertWidget(1, self.entry)
|
|
425
|
+
|
|
426
|
+
def replace_with_axis_speed_combo_box(self):
|
|
427
|
+
"""Replaces the entry with an AxisSpeedComboBox (if it's not an AxisSpeedComboBox already)."""
|
|
428
|
+
|
|
429
|
+
if not isinstance(self.entry, AxisSpeedComboBox):
|
|
430
|
+
# Remove the current entry
|
|
431
|
+
|
|
432
|
+
self.removeWidget(self.entry)
|
|
433
|
+
self.entry.hide()
|
|
434
|
+
self.entry.deleteLater()
|
|
435
|
+
|
|
436
|
+
# Replace it with an AxisSpeedComboBox
|
|
437
|
+
|
|
438
|
+
self.entry = AxisSpeedComboBox()
|
|
439
|
+
self.insertWidget(1, self.entry)
|
|
440
|
+
|
|
441
|
+
def set_visible(self, visible: bool):
|
|
442
|
+
"""Changes the visibility of the label and the entry.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
visible (bool): Indicates whether the label and the entry should be visible or not.
|
|
446
|
+
"""
|
|
447
|
+
|
|
448
|
+
self.label.setVisible(visible)
|
|
449
|
+
self.entry.setVisible(visible)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
class AxisComboBox(QComboBox):
|
|
453
|
+
def __init__(self):
|
|
454
|
+
"""Initialisation of a drop-down menu for the M2MD axes."""
|
|
455
|
+
|
|
456
|
+
super().__init__()
|
|
457
|
+
|
|
458
|
+
for axis in range(1, NUM_M2MD_AXES + 1):
|
|
459
|
+
self.addItem(f"M2MD axis {axis}")
|
|
460
|
+
self.setEditable(False)
|
|
461
|
+
|
|
462
|
+
def text(self):
|
|
463
|
+
"""Converts the selected text to a valid input argument for the dynamic command."""
|
|
464
|
+
|
|
465
|
+
return self.currentText()[-1]
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
class TcuModeComboBox(QComboBox):
|
|
469
|
+
def __init__(self):
|
|
470
|
+
"""Initialisation of a drop-down menu for the TCU mode."""
|
|
471
|
+
|
|
472
|
+
super().__init__()
|
|
473
|
+
|
|
474
|
+
for tcu_mode in TcuMode:
|
|
475
|
+
self.addItem(tcu_mode.name)
|
|
476
|
+
self.setEditable(False)
|
|
477
|
+
|
|
478
|
+
def text(self):
|
|
479
|
+
"""Converts the selected text to a valid input argument for the dynamic command."""
|
|
480
|
+
|
|
481
|
+
return TcuMode[self.currentText()].value
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
class IntComboBox(QComboBox):
|
|
485
|
+
def __init__(self, min_value: int, max_value: int):
|
|
486
|
+
"""Initialisation of a drop-down menu for a range of integers.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
min_value (int): Minimum value in the drop-down menu.
|
|
490
|
+
max_value (int): Maximum value in the drop-down menu.
|
|
491
|
+
"""
|
|
492
|
+
|
|
493
|
+
super().__init__()
|
|
494
|
+
|
|
495
|
+
self.addItems([str(x) for x in range(min_value, max_value + 1)])
|
|
496
|
+
self.setEditable(False)
|
|
497
|
+
|
|
498
|
+
def text(self):
|
|
499
|
+
"""Converts the selected text to a valid input argument for the dynamic command."""
|
|
500
|
+
|
|
501
|
+
return int(self.currentText())
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
class AxisSpeedComboBox(QComboBox):
|
|
505
|
+
def __init__(self):
|
|
506
|
+
"""Initialisation of a drop-down menu for the axis speed."""
|
|
507
|
+
|
|
508
|
+
super().__init__()
|
|
509
|
+
|
|
510
|
+
for speed in AXIS_VELOCITY:
|
|
511
|
+
self.addItem(f"{speed}Hz")
|
|
512
|
+
|
|
513
|
+
self.setEditable(False)
|
|
514
|
+
|
|
515
|
+
def text(self):
|
|
516
|
+
"""Converts the selected text to a valid input argument for the dynamic command.
|
|
517
|
+
|
|
518
|
+
The selected text is of the form "XHz", where X is the axis speed in Hz. This has to be converted into a hex
|
|
519
|
+
string that represents this axis speed.
|
|
520
|
+
|
|
521
|
+
Returns:
|
|
522
|
+
Axis speed in hex string format.
|
|
523
|
+
"""
|
|
524
|
+
|
|
525
|
+
return AXIS_VELOCITY[int(self.currentText()[:-2])]
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
class TcuUiController(Observer):
|
|
529
|
+
"""Controller in the MVC pattern that makes the TCU UI application."""
|
|
530
|
+
|
|
531
|
+
def __init__(self, model: TcuUIModel, view: TcuUiView):
|
|
532
|
+
"""Initialisation of the TCU UI Controller."""
|
|
533
|
+
|
|
534
|
+
super().__init__()
|
|
535
|
+
|
|
536
|
+
self.model = model
|
|
537
|
+
self.view = view
|
|
538
|
+
|
|
539
|
+
self.view.build_tabs(self.model.build_dyn_cmds_list(), self)
|
|
540
|
+
|
|
541
|
+
def do(self, actions):
|
|
542
|
+
# Abstract method from the Observer class, which we do not need here
|
|
543
|
+
pass
|
|
544
|
+
|
|
545
|
+
def update(self, changed_object):
|
|
546
|
+
"""Updates the TCU UI when the "Run" button is clicked in one of the tabs.
|
|
547
|
+
|
|
548
|
+
In this context, updating means:
|
|
549
|
+
|
|
550
|
+
- Generating the command string that will be sent to the Arduino and display it in the TCU UI View,
|
|
551
|
+
- Sending the command to the Arduino and display the response in the TCU UI View.
|
|
552
|
+
"""
|
|
553
|
+
|
|
554
|
+
origin_tab, func, cargo1, cargo2 = changed_object
|
|
555
|
+
|
|
556
|
+
signature = inspect.signature(func)
|
|
557
|
+
args = ()
|
|
558
|
+
kwargs = {}
|
|
559
|
+
|
|
560
|
+
parameters = signature.parameters
|
|
561
|
+
|
|
562
|
+
if len(parameters) == 3:
|
|
563
|
+
cargo1_name = list(parameters.keys())[-2]
|
|
564
|
+
kwargs[cargo1_name] = cargo1
|
|
565
|
+
|
|
566
|
+
if len(parameters) >= 2:
|
|
567
|
+
cargo2_name = list(parameters.keys())[-1]
|
|
568
|
+
kwargs[cargo2_name] = cargo2
|
|
569
|
+
|
|
570
|
+
# Generate the command string + display
|
|
571
|
+
|
|
572
|
+
cmd_string = call_unbound(func, self.model.tcu_hex, *args, **kwargs)
|
|
573
|
+
origin_tab.cmd_string.setText(cmd_string.decode())
|
|
574
|
+
|
|
575
|
+
# Execute the command + display the response
|
|
576
|
+
|
|
577
|
+
if self.model.tcu_proxy:
|
|
578
|
+
response = call_unbound(func, self.model.tcu_proxy, *args, **kwargs)
|
|
579
|
+
|
|
580
|
+
if isinstance(response, Failure):
|
|
581
|
+
origin_tab.response.setStyleSheet("background-color: red;")
|
|
582
|
+
origin_tab.response.setText(str(response.cause))
|
|
583
|
+
|
|
584
|
+
else:
|
|
585
|
+
if hasattr(response, "decode") and callable(getattr(response, "decode")):
|
|
586
|
+
response = response.decode()
|
|
587
|
+
|
|
588
|
+
origin_tab.response.setText(str(response))
|
|
589
|
+
|
|
590
|
+
elif is_tcu_cs_active():
|
|
591
|
+
self.model.tcu_proxy = TcuProxy()
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def call_unbound(func: Callable, instance: object, *args: Any, **kwargs: Any) -> Any:
|
|
595
|
+
"""Executes the given function on the given instance with the given arguments.
|
|
596
|
+
|
|
597
|
+
This helper handles three cases:
|
|
598
|
+
|
|
599
|
+
1. `func` is already a bound method (has `__self__`) -> Call it directly.
|
|
600
|
+
2. Prefer looking up the attribute on `instance` with `getattr(instance, func.__name__)`.
|
|
601
|
+
This lets Python perform normal Method Resolution Order (MRO) so overrides on the instance's class or proxy
|
|
602
|
+
wrappers are returned as bound methods.
|
|
603
|
+
3. If look-up fails (no such attribute on the instance), fall back to binding the original function to `instance`
|
|
604
|
+
using `types.MethodType` (this may call the base/class implementation).
|
|
605
|
+
|
|
606
|
+
Args:
|
|
607
|
+
func (Callable): Function (dynamic command) to execute.
|
|
608
|
+
instance (object): Instance on which to execute the function.
|
|
609
|
+
*args: Optional positional arguments for the function.
|
|
610
|
+
**kwargs: Optional keyword arguments for the function.
|
|
611
|
+
|
|
612
|
+
Returns:
|
|
613
|
+
Any: Result of the function execution.
|
|
614
|
+
"""
|
|
615
|
+
|
|
616
|
+
# Case 1: already bound -> call directly
|
|
617
|
+
|
|
618
|
+
if getattr(func, "__self__", None) is not None:
|
|
619
|
+
return func(*args, **kwargs)
|
|
620
|
+
|
|
621
|
+
# Case 2: preferred lookup on the instance so Python performs normal MRO (Method Resolution Order) and returns
|
|
622
|
+
# the appropriate bound method (honours overrides / proxy behaviour).
|
|
623
|
+
|
|
624
|
+
try:
|
|
625
|
+
bound = getattr(instance, func.__name__)
|
|
626
|
+
return bound(*args, **kwargs)
|
|
627
|
+
except AttributeError:
|
|
628
|
+
# Case 3: Fall-back to binding the original function to the instance. This will create a bound method that
|
|
629
|
+
# directly calls the function as if it were defined on the instance.
|
|
630
|
+
|
|
631
|
+
bound = types.MethodType(func, instance)
|
|
632
|
+
return bound(*args, **kwargs)
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
# def call_method_hex(instance: TcuHex, func: Callable, *args, **kwargs) -> Any:
|
|
636
|
+
# """Executes the given dynamic command for the given TCU interface.
|
|
637
|
+
#
|
|
638
|
+
# Args:
|
|
639
|
+
# instance (TcuHex): TCU interface for which to execute the given dynamic command.
|
|
640
|
+
# func (Callable): Dynamic command to execute.
|
|
641
|
+
# *args: Optional positional arguments for the dynamic command.
|
|
642
|
+
# **kwargs: Optional keyword arguments for the dynamic command.
|
|
643
|
+
#
|
|
644
|
+
# Returns:
|
|
645
|
+
# Response received from the TCU interface.
|
|
646
|
+
# """
|
|
647
|
+
#
|
|
648
|
+
# wrapper = instance.handle_dynamic_command(func)
|
|
649
|
+
# result = wrapper(*args, **kwargs)
|
|
650
|
+
#
|
|
651
|
+
# return result
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
@app.command()
|
|
655
|
+
def main():
|
|
656
|
+
multiprocessing.current_process().name = "tcu_ui"
|
|
657
|
+
lock_file = QLockFile(str(Path("~/tcu_ui.app.lock").expanduser()))
|
|
658
|
+
|
|
659
|
+
tcu_app = QApplication(["-stylesheet", str(get_resource(":/styles/default.qss"))])
|
|
660
|
+
# app_logo = get_resource(":/icons/logo-tcu.svg")
|
|
661
|
+
# app.setWindowIcon(QIcon(str(app_logo)))
|
|
662
|
+
|
|
663
|
+
if lock_file.tryLock(100):
|
|
664
|
+
process_status = ProcessStatus()
|
|
665
|
+
|
|
666
|
+
timer_thread = threading.Thread(target=do_every, args=(10, process_status.update))
|
|
667
|
+
timer_thread.daemon = True
|
|
668
|
+
timer_thread.start()
|
|
669
|
+
|
|
670
|
+
# Create the TCU UI, following the MVC-model
|
|
671
|
+
|
|
672
|
+
model = TcuUIModel()
|
|
673
|
+
view = TcuUiView()
|
|
674
|
+
controller = TcuUiController(model, view)
|
|
675
|
+
view.add_observer(controller)
|
|
676
|
+
|
|
677
|
+
view.show()
|
|
678
|
+
|
|
679
|
+
return tcu_app.exec_()
|
|
680
|
+
else:
|
|
681
|
+
error_message = QMessageBox()
|
|
682
|
+
error_message.setIcon(QMessageBox.Warning)
|
|
683
|
+
error_message.setWindowTitle("Error")
|
|
684
|
+
error_message.setText("The TCU GUI application is already running!")
|
|
685
|
+
error_message.setStandardButtons(QMessageBox.Ok)
|
|
686
|
+
|
|
687
|
+
return error_message.exec()
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
if __name__ == "__main__":
|
|
691
|
+
logging.basicConfig(level=logging.DEBUG, format=Settings.LOG_FORMAT_FULL)
|
|
692
|
+
|
|
693
|
+
sys.exit(app())
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: ariel-tcu
|
|
3
|
-
Version: 0.17.3
|
|
4
|
-
Summary: Telescope Control Unit (TCU) for Ariel
|
|
5
|
-
Author: IVS KU Leuven
|
|
6
|
-
Maintainer-email: Rik Huygen <rik.huygen@kuleuven.be>, Sara Regibo <sara.regibo@kuleuven.be>
|
|
7
|
-
License-Expression: MIT
|
|
8
|
-
Keywords: Ariel,hardware testing,software framework,telescope control
|
|
9
|
-
Requires-Python: >=3.10
|
|
10
|
-
Requires-Dist: cgse-common
|
|
11
|
-
Requires-Dist: cgse-core
|
|
12
|
-
Requires-Dist: cgse-gui
|
|
13
|
-
Requires-Dist: crcmod>=1.7
|
|
14
|
-
Requires-Dist: pyserial>=3.5
|