Coverage for / opt / hostedtoolcache / Python / 3.11.14 / x64 / lib / python3.11 / site-packages / rattlesnake / components / user_interface.py: 7%
1092 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-27 18:22 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-27 18:22 +0000
1# -*- coding: utf-8 -*-
2"""
3Controller subsystem to handle the user interface, including callback
4assignment and displaying results.
6Rattlesnake Vibration Control Software
7Copyright (C) 2021 National Technology & Engineering Solutions of Sandia, LLC
8(NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S.
9Government retains certain rights in this software.
11This program is free software: you can redistribute it and/or modify
12it under the terms of the GNU General Public License as published by
13the Free Software Foundation, either version 3 of the License, or
14(at your option) any later version.
16This program is distributed in the hope that it will be useful,
17but WITHOUT ANY WARRANTY; without even the implied warranty of
18MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19GNU General Public License for more details.
21You should have received a copy of the GNU General Public License
22along with this program. If not, see <https://www.gnu.org/licenses/>.
23"""
25import copy
26import datetime
27import multiprocessing as mp
29# pyqtgraph.setConfigOption('leftButtonPan',False)
30import os
31import re
32import time
33import traceback
35import netCDF4
36import numpy as np
37import openpyxl
38import pyqtgraph
39from qtpy import QtGui, QtWidgets, uic
40from qtpy.QtCore import ( # pylint: disable=no-name-in-module
41 QDir,
42 QEvent,
43 QObject,
44 QRunnable,
45 QThreadPool,
46 QTimer,
47 Signal,
48 Slot,
49)
51from .environments import environment_UIs as all_environment_UIs
52from .environments import ui_path
53from .ui_utilities import (
54 ChannelMonitor,
55 IPAddress,
56 IPAddressManager,
57 ProfileTimer,
58 get_table_bools,
59)
60from .utilities import (
61 Channel,
62 DataAcquisitionParameters,
63 GlobalCommands,
64 QueueContainer,
65 VerboseMessageQueue,
66 error_message_qt,
67)
69pyqtgraph.setConfigOption("background", "w")
70pyqtgraph.setConfigOption("foreground", "k")
72directory = os.path.split(__file__)[0]
73QDir.addSearchPath("images", os.path.join(directory, "themes", "images"))
75TASK_NAME = "UI"
78class UpdaterSignals(QObject):
79 """Defines the signals that will be sent from the GUI Updater to the GUI
81 Supported signals are:
83 finished
84 empty
86 update
87 `tuple` (widget_id,data)
88 """
90 finished = Signal()
91 update = Signal(tuple)
94class Updater(QRunnable):
95 """Updater thread to collect results from the subsystems and reflect the
96 changes in the GUI
97 """
99 def __init__(self, update_queue):
100 """
101 Initializes the updater with the queue and signals that will be emitted
102 when the queue has data in it.
104 Parameters
105 ----------
106 update_queue : mp.queues.Queue
107 Queue from which events will be captured.
109 """
110 super(Updater, self).__init__()
111 self.update_queue = update_queue
112 self.signals = UpdaterSignals()
113 self.verbose_queue = isinstance(self.update_queue, VerboseMessageQueue)
115 @Slot()
116 def run(self):
117 """Continually capture update events from the queue"""
118 while True:
119 if self.verbose_queue:
120 queue_data = self.update_queue.get(TASK_NAME)
121 else:
122 queue_data = self.update_queue.get()
123 if queue_data[0] == GlobalCommands.QUIT:
124 break
125 self.signals.update.emit(queue_data)
126 self.signals.finished.emit()
127 time.sleep(1)
130class Ui(QtWidgets.QMainWindow):
131 """Main user interface from which the controller will be controlled."""
133 def __init__(self, environments, queue_container: QueueContainer, profile_file=None):
134 """
135 Create the user interface with the specified parameters and queues
137 Parameters
138 ----------
139 environments : iterable
140 Iterable of control_type,control_name values to use to set up the
141 environments.
142 queue_container : QueueContainer
143 Namespace containing the queues that are used by the controller.
144 profile_file : str, optional
145 File path to an optional profile file that will be loaded to set
146 up the controller. The default is None.
148 """
149 try:
150 # Store input data
151 self._updated_size = False
152 self.queue_container = queue_container
153 self.environment_types = {name: control_type for control_type, name in environments}
154 self.environments = [name for control_type, name in environments]
155 self.environment_metadata = {name: None for name in self.environments}
156 self.profile_events = None
157 self.profile_timers = None
158 self.profile_list_update_timer = None
159 self.channel_monitor_window = None
160 self.lanxi_ip_addresses = []
162 # Create the user interface
163 super(Ui, self).__init__()
164 uic.loadUi(ui_path, self)
166 # Add tabs to the empty widgets based on the environments
167 self.environment_uis = {}
168 for environment_name, environment_type in self.environment_types.items():
169 environment_ui = all_environment_UIs[environment_type]
170 self.environment_uis[environment_name] = environment_ui(
171 environment_name,
172 self.environment_definition_environment_tabs,
173 self.system_id_environment_tabs,
174 self.test_prediction_environment_tabs,
175 self.run_environment_tabs,
176 self.queue_container.environment_command_queues[environment_name],
177 self.queue_container.controller_communication_queue,
178 self.queue_container.log_file_queue,
179 )
181 # Remove the system ID and test prediction tab if not used.
182 if self.system_id_environment_tabs.count() == 0:
183 self.rattlesnake_tabs.removeTab(self.rattlesnake_tabs.indexOf(self.system_id_tab))
184 self.has_system_id = False
185 self.complete_system_ids = None
186 else:
187 self.has_system_id = True
188 self.complete_system_ids = {
189 self.system_id_environment_tabs.tabText(i): False
190 for i in range(self.system_id_environment_tabs.count())
191 }
192 if self.test_prediction_environment_tabs.count() == 0:
193 self.rattlesnake_tabs.removeTab(
194 self.rattlesnake_tabs.indexOf(self.test_prediction_tab)
195 )
196 self.has_test_predictions = False
197 else:
198 self.has_test_predictions = True
200 # I might add this back in later, but for now we will just always show
201 # this tab.
202 # # If there is only one environment, remove the test profile tab
203 # if len(self.environments) == 1:
204 # self.rattlesnake_tabs.removeTab(self.rattlesnake_tabs.indexOf(self.profile_tab))
205 # # Also remove profile information from the run test page
206 # self.run_profile_widget.hide()
207 # self.has_test_profile = False
208 # else:
209 # self.has_test_profile = True
211 self.streaming_environment_select_combobox.addItems(self.environments)
213 self.manual_streaming_trigger_button.setVisible(False)
215 for i in range(self.run_environment_tabs.count()):
216 self.run_environment_tabs.widget(i).setEnabled(False)
218 self.threadpool = QThreadPool()
219 self.gui_updater = Updater(self.queue_container.gui_update_queue)
220 # Create a side thread to collect global messages
221 self.controller_instructions_collector = Updater(
222 self.queue_container.controller_communication_queue
223 )
225 # Start Workers
226 self.threadpool.start(self.gui_updater)
227 self.threadpool.start(self.controller_instructions_collector)
229 # Complete the remaining user interface
230 self.complete_ui()
231 self.connect_callbacks()
233 # Create the command map for profile instructions
234 self.command_map = {
235 "Start Streaming": self.start_streaming,
236 "Stop Streaming": self.stop_streaming,
237 "Disarm DAQ": self.disarm_test,
238 }
240 # Create a field to hold the loaded hardware file
241 self.hardware_file = None
242 self.setWindowIcon(QtGui.QIcon("logo/Rattlesnake_Icon.png"))
243 self.setWindowTitle("Rattlesnake Vibration Controller")
244 self.show()
246 # Hide the task trigger fields if necessary
247 self.task_trigger_update()
249 # If there is a loaded profile file, we need to handle it
250 # print('Loading Profile')
251 if profile_file is not None:
252 # Channel Table
253 # print('Loading Channel Table')
254 self.load_channel_table(None, profile_file)
255 # print('Loading Workbook')
256 workbook = openpyxl.load_workbook(profile_file, data_only=True)
257 # Hardware
258 # print('Setting Hardware')
259 hardware_sheet = workbook["Hardware"]
260 for i, row in enumerate(hardware_sheet.rows):
261 if i == 0:
262 hardware_index = int(row[1].value)
263 self.hardware_selector.blockSignals(True)
264 self.hardware_selector.setCurrentIndex(hardware_index)
265 self.hardware_selector.blockSignals(False)
266 self.hardware_update(select_file=False)
267 elif i == 1:
268 self.hardware_file = row[1].value
269 elif i == 2:
270 sample_rate = int(row[1].value)
271 self.lanxi_sample_rate_selector.setCurrentIndex(
272 round(np.log2(sample_rate / 4096))
273 )
274 self.sample_rate_selector.setValue(sample_rate)
275 elif i == 3:
276 self.time_per_read_selector.setValue(row[1].value)
277 elif i == 4:
278 self.time_per_write_selector.setValue(row[1].value)
279 # The rest of these are named variables
280 else:
281 name = str(row[0].value).lower().strip().replace(" ", "_")
282 if name == "":
283 continue
284 value = row[1].value
285 if name == "integration_oversampling":
286 self.integration_oversample_selector.setValue(int(value))
287 elif name == "task_trigger":
288 self.task_trigger_selector.setCurrentIndex(int(value))
289 elif name == "task_trigger_output_channel":
290 self.task_trigger_output_selector.setText(str(value))
291 elif name == "maximum_acquisition_processes":
292 self.lanxi_maximum_acquisition_processes_selector.setValue(int(value))
293 else:
294 print(f"Hardware sheet entry {row[0].value} not recognized")
295 # print('Initializing Data Acquisition')
296 self.initialize_data_acquisition()
297 # Now go through and do the environments
298 for environment_name, environment_ui in self.environment_uis.items():
299 # print('Setting Environment {:}'.format(environment_name))
300 environment_ui.set_parameters_from_template(workbook[environment_name])
301 # TODO: maybe uncomment this later (auto-load FRF matrix to system id
302 # tab if using frf virtual hardware)
303 # NOTE: would need to fix an order of operations bug in the transient module
304 # if hardware_index == 7 and isinstance(environment_ui, AbstractSysIdUI):
305 # try:
306 # environment_ui.load_sysid_matrix_file(self.hardware_file, popup=False)
307 # except KeyError:
308 # pass
309 # print('Initializing Environments')
310 self.initialize_environment_parameters()
311 # Now the profile
312 # print('Setting Test Profile')
313 profile_sheet = workbook["Test Profile"]
314 index = 2
315 profile_timestamps = []
316 profile_environment_names = []
317 profile_operation_names = []
318 profile_data_names = []
319 while True:
320 timestamp = profile_sheet.cell(index, 1).value
321 environment = profile_sheet.cell(index, 2).value
322 operation = profile_sheet.cell(index, 3).value
323 data = profile_sheet.cell(index, 4).value
324 if timestamp is None or (
325 isinstance(timestamp, str) and timestamp.strip() == ""
326 ):
327 break
328 # print('Adding Profile Event {:}, {:}, {:}, {:}'.format(
329 # timestamp,environment,operation,data))
330 # self.add_profile_event(None,timestamp,environment,operation,data)
331 profile_timestamps.append(timestamp)
332 profile_environment_names.append(environment)
333 profile_operation_names.append(operation)
334 profile_data_names.append(data)
335 index += 1
336 # print('Closing Workbook')
337 workbook.close()
338 # start_time = time.time()
339 self.profile_table.setRowCount(len(profile_timestamps))
340 # insert_row_time = time.time()
341 # print('Time to Insert Row: {:}'.format(insert_row_time-start_time))
342 for selected_row, (
343 timestamp,
344 environment,
345 operation,
346 data,
347 ) in enumerate(
348 zip(
349 profile_timestamps,
350 profile_environment_names,
351 profile_operation_names,
352 profile_data_names,
353 )
354 ):
355 timestamp_spinbox = QtWidgets.QDoubleSpinBox()
356 timestamp_spinbox.setMaximum(1e6)
357 timestamp_spinbox.setValue(float(timestamp))
358 self.profile_table.setCellWidget(selected_row, 0, timestamp_spinbox)
359 # create_spinbox_time = time.time()
360 # print('Time to Create Spinbox: {:}'.format(
361 # create_spinbox_time-insert_row_time))
362 # Next a combobox sets the environment
363 environment_combobox = QtWidgets.QComboBox()
364 environment_combobox.addItem("Global")
365 for environment_name in self.environments:
366 environment_combobox.addItem(environment_name)
367 environment_combobox.setCurrentIndex(environment_combobox.findText(environment))
368 self.profile_table.setCellWidget(selected_row, 1, environment_combobox)
369 # create_environment_combobox_time = time.time()
370 # print('Time to Create Environment Combobox: {:}'.format(
371 # create_environment_combobox_time-create_spinbox_time))
372 # Next a combobox sets the operation
373 if environment_combobox.currentIndex() == 0:
374 operations = [operation for operation in self.command_map]
375 else:
376 environment_name = self.environments[
377 environment_combobox.currentIndex() - 1
378 ]
379 operations = [
380 op for op in self.environment_uis[environment_name].command_map
381 ]
382 operation_combobox = QtWidgets.QComboBox()
383 for op in operations:
384 operation_combobox.addItem(op)
385 operation_combobox.setCurrentIndex(operation_combobox.findText(operation))
386 self.profile_table.setCellWidget(selected_row, 2, operation_combobox)
387 # create_operation_combobox_time = time.time()
388 # print('Time to Create Operation Combobox: {:}'.format(
389 # create_operation_combobox_time-create_environment_combobox_time))
390 data_item = QtWidgets.QTableWidgetItem()
391 data_item.setText(str(data))
392 self.profile_table.setItem(selected_row, 3, data_item)
393 # create_data_entry_time = time.time()
394 # print('Time to Data Entry: {:}'.format(
395 # create_data_entry_time-create_operation_combobox_time))
396 timestamp_spinbox.valueChanged.connect(self.update_profile_plot)
397 environment_combobox.currentIndexChanged.connect(self.update_operations)
398 operation_combobox.currentIndexChanged.connect(self.update_profile_plot)
399 # connect_callbacks_time = time.time()
400 # print('Time to Connect Callbacks: {:}'.format(
401 # connect_callbacks_time-create_data_entry_time))
402 # insert_row_time = connect_callbacks_time
403 self.update_profile_plot()
404 self.profile_table.itemChanged.connect(self.update_profile_plot)
406 except Exception: # pylint: disable=broad-exception-caught
407 print(traceback.format_exc())
409 def event(self, event):
410 """Overload event to capture the initial resizing of the window"""
411 was_processed = super().event(event)
412 if event.type() == QEvent.LayoutRequest:
413 if not self._updated_size:
414 print("Updating Size of Window")
415 self.resize(1500, 667)
416 self._updated_size = True
417 return was_processed
419 def log(self, string):
420 """Pass a message to the log_file_queue along with date/time and task name
422 Parameters
423 ----------
424 string : str
425 Message that will be written to the queue
427 """
428 self.queue_container.log_file_queue.put(
429 f"{datetime.datetime.now()}: {TASK_NAME} -- {string}\n"
430 )
432 def complete_ui(self):
433 """Helper function to complete setting up of the User Interface"""
434 self.ip_lookup_button.hide()
435 self.lanxi_sample_rate_selector.hide()
436 self.lanxi_maximum_acquisition_processes_label.hide()
437 self.lanxi_maximum_acquisition_processes_selector.hide()
438 self.integration_oversample_selector.hide()
439 self.integration_oversample_label.hide()
441 self.channel_table.horizontalHeader().setSectionResizeMode(
442 QtWidgets.QHeaderView.ResizeToContents
443 )
444 # Fill in the channel table with empty strings
445 for row_idx in range(self.channel_table.rowCount()):
446 for col_idx in range(self.channel_table.columnCount()):
447 item = QtWidgets.QTableWidgetItem("")
448 self.channel_table.setItem(row_idx, col_idx, item)
450 # Disable all tabs except the first
451 for i in range(1, self.rattlesnake_tabs.count() - 1):
452 self.rattlesnake_tabs.setTabEnabled(i, False)
454 # Reindex button groups
455 self.streaming_button_group.setId(self.immediate_streaming_radiobutton, 0)
456 self.streaming_button_group.setId(self.test_level_streaming_radiobutton, 1)
457 self.streaming_button_group.setId(self.no_streaming_radiobutton, 2)
458 self.streaming_button_group.setId(self.profile_streaming_radiobutton, 3)
460 # Put values into the environment channel table
461 self.environment_channels_table.setColumnCount(len(self.environments))
462 self.environment_channels_table.setHorizontalHeaderLabels(self.environments)
463 for row in range(self.environment_channels_table.rowCount()):
464 for col in range(self.environment_channels_table.columnCount()):
465 checkbox = QtWidgets.QCheckBox()
466 if len(self.environments) == 1:
467 checkbox.setChecked(True)
468 self.environment_channels_table.setCellWidget(row, col, checkbox)
469 if len(self.environments) == 1:
470 self.environment_channels_table.hide()
471 max_cpus = mp.cpu_count()
472 self.lanxi_maximum_acquisition_processes_selector.setMaximum(max_cpus)
473 self.lanxi_maximum_acquisition_processes_selector.setValue(
474 max_cpus - len(self.environments) if max_cpus > len(self.environments) else 1
475 )
477 def connect_callbacks(self):
478 """Helper function to connect callbacks to widgets in the user interface"""
479 # Stop program
480 self.stop_program_button.clicked.connect(self.stop_program)
481 # Channel Monitor
482 self.channel_monitor_button.clicked.connect(self.show_channel_monitor)
483 self.color_theme_combobox.currentTextChanged.connect(self.change_color_theme)
484 # Channel Table Tab
485 self.ip_lookup_button.clicked.connect(self.ip_lookup)
486 self.load_channel_table_button.clicked.connect(self.load_channel_table)
487 self.save_channel_table_button.clicked.connect(self.save_channel_table)
488 self.initialize_data_acquisition_button.clicked.connect(self.initialize_data_acquisition)
489 self.load_test_file_button.clicked.connect(self.load_test_file)
490 self.hardware_selector.currentIndexChanged.connect(self.hardware_update)
491 self.task_trigger_selector.currentIndexChanged.connect(self.task_trigger_update)
492 self.sample_rate_selector.valueChanged.connect(self.sample_rate_update)
493 channel_table_scroll = self.channel_table.verticalScrollBar()
494 channel_table_scroll.valueChanged.connect(self.sync_environment_table)
495 environment_table_scroll = self.environment_channels_table.verticalScrollBar()
496 environment_table_scroll.valueChanged.connect(self.sync_channel_table)
497 # Copy
498 self.channel_table_action_copy = QtWidgets.QAction("Copy", self.channel_table)
499 self.channel_table_action_copy.setShortcut("Ctrl+C")
500 self.channel_table_action_copy.triggered.connect(self.channel_table_copy)
501 self.channel_table.addAction(self.channel_table_action_copy)
502 # Paste
503 self.channel_table_action_paste = QtWidgets.QAction("Paste", self.channel_table)
504 self.channel_table_action_paste.setShortcut("Ctrl+V")
505 self.channel_table_action_paste.triggered.connect(self.channel_table_paste)
506 self.channel_table.addAction(self.channel_table_action_paste)
507 # Delete
508 self.channel_table_action_delete = QtWidgets.QAction("Delete", self.channel_table)
509 self.channel_table_action_delete.setShortcut("Del")
510 self.channel_table_action_delete.triggered.connect(self.channel_table_delete)
511 self.channel_table.addAction(self.channel_table_action_delete)
513 # Control Definition Tab
514 self.initialize_environments_button.clicked.connect(self.initialize_environment_parameters)
516 # Profile Callbacks
517 self.initialize_profile_button.clicked.connect(self.initialize_profile)
518 self.save_profile_button.clicked.connect(self.save_profile)
519 self.load_profile_button.clicked.connect(self.load_profile)
520 self.add_profile_event_button.clicked.connect(self.add_profile_event)
521 self.remove_profile_event_button.clicked.connect(self.remove_profile_event)
523 # Run Test Tab
524 self.select_streaming_file_button.clicked.connect(self.select_control_streaming_file)
525 self.arm_test_button.clicked.connect(self.arm_test)
526 self.disarm_test_button.clicked.connect(self.disarm_test)
527 self.start_profile_button.clicked.connect(self.start_profile)
528 self.stop_profile_button.clicked.connect(self.stop_profile)
529 self.manual_streaming_radiobutton.toggled.connect(self.show_hide_manual_streaming)
530 self.manual_streaming_trigger_button.clicked.connect(self.start_stop_streaming)
532 # GUI Updater Signals
533 self.gui_updater.signals.update.connect(self.update_gui)
534 self.controller_instructions_collector.signals.update.connect(
535 self.handle_controller_instructions
536 )
538 # %% Utility Functions
539 def get_channel_table_strings(self):
540 """Collect the strings in the channel table"""
541 string_array = []
542 for row_idx in range(self.channel_table.rowCount()):
543 string_array.append([])
544 for col_idx in range(self.channel_table.columnCount()):
545 value = self.channel_table.item(row_idx, col_idx).text()
546 string_array[-1].append(value)
547 return string_array
549 # %% Data Acquisition Callbacks
551 def load_channel_table(self, clicked, filename=None): # pylint: disable=unused-argument
552 """Loads a channel table using a file dialog or the specified filename
554 Parameters
555 ----------
556 clicked :
557 The clicked event that triggered the callback.
558 filename :
559 File name defining the channel table for bypassing the callback when
560 loading from a file (Default value = None).
562 """
563 self.channel_table.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Fixed)
564 num_environments = len(self.environments)
565 if filename is None:
566 filename, _ = QtWidgets.QFileDialog.getOpenFileName(
567 self, "Load Channel Table", filter="Spreadsheets (*.xlsx *.csv *.txt)"
568 )
569 if filename == "":
570 return
571 self.log(f"Loading Channel Table {filename}")
572 _, file_type = os.path.splitext(filename)
573 if file_type == ".xlsx":
574 workbook = openpyxl.load_workbook(filename, read_only=True, data_only=True)
575 sheets = workbook.sheetnames
576 if len(sheets) > 1:
577 sheets = [sheet for sheet in sheets if "channel" in sheet.lower()]
578 if len(sheets) > 1:
579 error_dialog = QtWidgets.QErrorMessage()
580 error_dialog.showMessage(
581 "Could not identify channel table in Excel Spreadsheet\n"
582 'If multiple sheets exist, only 1 should have the word "channel" in it'
583 )
584 return
585 worksheet = workbook[sheets[0]]
586 data_array = []
587 environment_names = [worksheet.cell(2, 24 + i).value for i in range(num_environments)]
588 for row in worksheet.iter_rows(min_row=3, max_col=23 + num_environments):
589 data_array.append([])
590 for col_idx, cell in enumerate(row):
591 data_array[-1].append(cell.value)
592 if data_array[-1][0] is None:
593 data_array = data_array[:-1]
594 break
595 workbook.close()
596 elif file_type == ".csv" or file_type == ".txt":
597 with open(filename, "r", encoding="utf-8") as f:
598 data_array = []
599 for row_idx, line in enumerate(f):
600 if row_idx < 1:
601 continue
602 elif row_idx == 1:
603 environment_names = [val.strip() for val in line.split(",")][23:]
604 continue
605 data_array.append([val.strip() for val in line.split(",")])
606 # Now split the data array off into the environment table
607 channel_table_data_array = [row[:23] for row in data_array]
608 environment_data_array = [row[23:] for row in data_array]
609 # Now complete the table
610 for row_idx, row_data in enumerate(channel_table_data_array):
611 for col_idx, cell_data in enumerate(row_data):
612 if col_idx == 0:
613 continue
614 self.channel_table.item(row_idx, col_idx - 1).setText(
615 "" if cell_data is None else str(cell_data)
616 )
617 if num_environments > 1:
618 for environment_index, environment_name in enumerate(environment_names):
619 try:
620 environment_table_column = self.environments.index(environment_name)
621 except ValueError:
622 error_message_qt(
623 "Invalid Environment Name",
624 "Invalid Environment Name {environment_name}, Valid Environments are "
625 "{self.environments}.\n\nEnvironment channels not defined.",
626 )
627 return
628 for row_index, table_row in enumerate(environment_data_array):
629 try:
630 value = not (
631 table_row[environment_index] == ""
632 or table_row[environment_index] is None
633 )
634 except IndexError:
635 value = False
636 self.environment_channels_table.cellWidget(
637 row_index, environment_table_column
638 ).setChecked(value)
640 self.channel_table.horizontalHeader().setSectionResizeMode(
641 QtWidgets.QHeaderView.ResizeToContents
642 )
644 def save_channel_table(self):
645 """Save the channel table to a file"""
646 filename, _ = QtWidgets.QFileDialog.getSaveFileName(
647 self,
648 "Save Channel Table",
649 filter="Excel File (*.xlsx);;Comma-separated Values (*.csv)",
650 )
651 if filename == "":
652 return
653 self.log(f"Saving Channel Table {filename}")
654 string_array = self.get_channel_table_strings()
655 _, file_type = os.path.splitext(filename)
656 if file_type == ".xlsx":
657 # Create the header
658 workbook = openpyxl.Workbook()
659 worksheet = workbook.active
660 worksheet.title = "Channel Table"
661 # Create the header
662 worksheet.cell(row=1, column=2, value="Test Article Definition")
663 worksheet.merge_cells(start_row=1, start_column=2, end_row=1, end_column=4)
664 worksheet.cell(row=1, column=5, value="Instrument Definition")
665 worksheet.merge_cells(start_row=1, start_column=5, end_row=1, end_column=11)
666 worksheet.cell(row=1, column=12, value="Channel Definition")
667 worksheet.merge_cells(start_row=1, start_column=12, end_row=1, end_column=19)
668 worksheet.cell(row=1, column=20, value="Output Feedback")
669 worksheet.merge_cells(start_row=1, start_column=20, end_row=1, end_column=21)
670 worksheet.cell(row=1, column=22, value="Limits")
671 worksheet.merge_cells(start_row=1, start_column=22, end_row=1, end_column=23)
672 for col_idx, val in enumerate(
673 [
674 "Channel Index",
675 "Node Number",
676 "Node Direction",
677 "Comment",
678 "Serial Number",
679 "Triax DoF",
680 "Sensitivity (mV/EU)",
681 "Engineering Unit",
682 "Make",
683 "Model",
684 "Calibration Exp Date",
685 "Physical Device",
686 "Physical Channel",
687 "Type",
688 "Minimum Value (V)",
689 "Maximum Value (V)",
690 "Coupling",
691 "Current Excitation Source",
692 "Current Excitation Value",
693 "Physical Device",
694 "Physical Channel",
695 "Warning Level (EU)",
696 "Abort Level (EU)",
697 ]
698 ):
699 worksheet.cell(row=2, column=1 + col_idx, value=val)
700 for row_idx, row in enumerate(string_array):
701 worksheet.cell(row=row_idx + 3, column=1, value=row_idx + 1)
702 for col_idx, col in enumerate(row):
703 if col == "":
704 continue
705 worksheet.cell(row=row_idx + 3, column=col_idx + 2, value=col)
706 # Now do the environment
707 if len(self.environments) > 1:
708 bool_array = get_table_bools(self.environment_channels_table)
709 worksheet.cell(row=1, column=24, value="Environments")
710 for index, name in enumerate(self.environments):
711 worksheet.cell(row=2, column=24 + index, value=name)
712 for row_idx, row in enumerate(bool_array):
713 for col_idx, col in enumerate(row):
714 if col:
715 worksheet.cell(row=row_idx + 3, column=col_idx + 24, value="X")
716 workbook.save(filename)
717 elif file_type == ".csv" or file_type == ".txt":
718 error_message_qt("Not Implemented!", "Output to CSV Not Implemented Yet!")
720 def load_test_file(self, filename, hardware=True):
721 """Loads a test file using a file dialog"""
722 if not filename:
723 filename, _ = QtWidgets.QFileDialog.getOpenFileName(
724 self,
725 "Load Test NetCDF File",
726 filter="NetCDF File (*.nc4);;All Files (*.*)",
727 )
728 if filename == "":
729 return
730 dataset = netCDF4.Dataset(filename) # pylint: disable=no-member
731 # Channel Table
732 channel_table = dataset["channels"]
733 # Node Number
734 data = channel_table["node_number"][...]
735 for row_idx, value in enumerate(data):
736 self.channel_table.item(row_idx, 0).setText(value)
737 # Node Direction
738 data = channel_table["node_direction"][...]
739 for row_idx, value in enumerate(data):
740 self.channel_table.item(row_idx, 1).setText(value)
741 # Comment
742 data = channel_table["comment"][...]
743 for row_idx, value in enumerate(data):
744 self.channel_table.item(row_idx, 2).setText(value)
745 # SN
746 data = channel_table["serial_number"][...]
747 for row_idx, value in enumerate(data):
748 self.channel_table.item(row_idx, 3).setText(value)
749 # Triax Dof
750 data = channel_table["triax_dof"][...]
751 for row_idx, value in enumerate(data):
752 self.channel_table.item(row_idx, 4).setText(value)
753 # Sensitivity
754 data = channel_table["sensitivity"][...]
755 for row_idx, value in enumerate(data):
756 self.channel_table.item(row_idx, 5).setText(str(value))
757 # Units
758 data = channel_table["unit"][...]
759 for row_idx, value in enumerate(data):
760 self.channel_table.item(row_idx, 6).setText(value)
761 # Make
762 data = channel_table["make"][...]
763 for row_idx, value in enumerate(data):
764 self.channel_table.item(row_idx, 7).setText(value)
765 # Model
766 data = channel_table["model"][...]
767 for row_idx, value in enumerate(data):
768 self.channel_table.item(row_idx, 8).setText(value)
769 # Expiration Date
770 data = channel_table["expiration"][...]
771 for row_idx, value in enumerate(data):
772 self.channel_table.item(row_idx, 9).setText(value)
773 # Read Device
774 data = channel_table["physical_device"][...]
775 for row_idx, value in enumerate(data):
776 self.channel_table.item(row_idx, 10).setText(value)
777 # Read Channel
778 data = channel_table["physical_channel"][...]
779 for row_idx, value in enumerate(data):
780 self.channel_table.item(row_idx, 11).setText(value)
781 # Type
782 data = channel_table["channel_type"][...]
783 for row_idx, value in enumerate(data):
784 self.channel_table.item(row_idx, 12).setText(value)
785 # Min Volts
786 data = channel_table["minimum_value"][...]
787 for row_idx, value in enumerate(data):
788 self.channel_table.item(row_idx, 13).setText(str(value))
789 # Max Volts
790 data = channel_table["maximum_value"][...]
791 for row_idx, value in enumerate(data):
792 self.channel_table.item(row_idx, 14).setText(str(value))
793 # Coupling
794 data = channel_table["coupling"][...]
795 for row_idx, value in enumerate(data):
796 self.channel_table.item(row_idx, 15).setText(value)
797 # Excitation Source
798 data = channel_table["excitation_source"][...]
799 for row_idx, value in enumerate(data):
800 self.channel_table.item(row_idx, 16).setText(value)
801 # Excitation
802 data = channel_table["excitation"][...]
803 for row_idx, value in enumerate(data):
804 self.channel_table.item(row_idx, 17).setText(str(value))
805 # Output Device
806 data = channel_table["feedback_device"][...]
807 for row_idx, value in enumerate(data):
808 self.channel_table.item(row_idx, 18).setText(value)
809 # Output Channel
810 data = channel_table["feedback_channel"][...]
811 for row_idx, value in enumerate(data):
812 self.channel_table.item(row_idx, 19).setText(value)
813 # Output Device
814 data = channel_table["warning_level"][...]
815 for row_idx, value in enumerate(data):
816 self.channel_table.item(row_idx, 20).setText(value)
817 # Output Channel
818 data = channel_table["abort_level"][...]
819 for row_idx, value in enumerate(data):
820 self.channel_table.item(row_idx, 21).setText(value)
821 # Environment Table
822 for saved_environment_index, saved_environment_name in enumerate(
823 dataset.variables["environment_names"][...]
824 ):
825 try:
826 environment_index = self.environments.index(saved_environment_name)
827 except ValueError:
828 if len(dataset.variables["environment_names"][...]) == 1:
829 environment_index = 0
830 print(
831 f"Warning: saved environment ({saved_environment_name}) is different from "
832 f"current environment ({self.environments[environment_index]})"
833 )
834 for channel_index, bool_row in enumerate(
835 dataset.variables["environment_active_channels"][:, saved_environment_index]
836 ):
837 boolean = bool(bool_row)
838 widget = self.environment_channels_table.cellWidget(
839 channel_index, environment_index
840 )
841 widget.setChecked(boolean)
842 if hardware:
843 # Hardware
844 self.hardware_selector.blockSignals(True)
845 try:
846 self.hardware_selector.setCurrentIndex(dataset.hardware)
847 self.hardware_file = (
848 None if dataset.hardware_file == "None" else dataset.hardware_file
849 )
850 except AttributeError:
851 self.hardware_selector.setCurrentIndex(0)
852 self.hardware_file = None
853 self.hardware_selector.blockSignals(False)
854 # Show the right widgets
855 self.hardware_update(select_file=False)
856 if self.hardware_selector.currentIndex() == 1:
857 self.lanxi_sample_rate_selector.setCurrentIndex(np.log2(dataset.sample_rate // 4096))
858 self.lanxi_maximum_acquisition_processes_selector.setValue(
859 dataset.maximum_acquisition_processes
860 )
861 else:
862 self.sample_rate_selector.setValue(dataset.sample_rate)
863 self.integration_oversample_selector.setValue(dataset.output_oversample)
864 self.time_per_read_selector.setValue(dataset.time_per_read)
865 self.time_per_write_selector.setValue(dataset.time_per_write)
866 # Initialize files
867 self.initialize_data_acquisition()
868 # Set the test parameters
869 for environment in self.environments:
870 self.environment_uis[environment].retrieve_metadata(dataset)
871 self.initialize_environment_parameters()
873 def channel_table_paste(self):
874 """Function to paste clipboard starting from top left cell"""
875 selection_range = self.channel_table.selectedRanges()
876 self.channel_table.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Fixed)
877 if selection_range:
878 # Get top left cell
879 top_left_row = selection_range[0].topRow()
880 top_left_column = selection_range[0].leftColumn()
881 # Get clipboard text
882 clipboard = QtWidgets.QApplication.clipboard()
883 if clipboard.mimeData().hasText():
884 clipboard_text = clipboard.text()
885 # Split clipboard text with newlines between rows
886 rows = clipboard_text.splitlines()
887 # Split clipboard text with tabs between columns
888 array_text = [row.split("\t") for row in rows]
889 # Paste the text into the table
890 for i, row in enumerate(array_text):
891 for j, cell_text in enumerate(row):
892 cell_text = cell_text if cell_text is not None else ""
893 item = QtWidgets.QTableWidgetItem(cell_text)
894 self.channel_table.setItem(top_left_row + i, top_left_column + j, item)
895 self.channel_table.horizontalHeader().setSectionResizeMode(
896 QtWidgets.QHeaderView.ResizeToContents
897 )
899 def channel_table_copy(self):
900 """Function to copy text from channel table in a format that Excel recognizes"""
901 clipboard = QtWidgets.QApplication.clipboard()
902 selected_ranges = self.channel_table.selectedRanges()
903 if selected_ranges:
904 # Get selected range
905 selected_range = selected_ranges[0]
906 copied_text = ""
907 rows = range(selected_range.topRow(), selected_range.bottomRow() + 1)
908 columns = range(selected_range.leftColumn(), selected_range.rightColumn() + 1)
909 # Put tabs inbetween columns, newlines inbetween rows
910 copied_text = []
911 for row in rows:
912 row_data = []
913 for column in columns:
914 item = self.channel_table.item(row, column)
915 row_data.append(
916 item.text() if item else ""
917 ) # Empty cells should be "" not None
918 copied_text.append("\t".join(row_data)) # Tab betewen columns
919 copied_text = "\n".join(copied_text) # Newline between rows
920 clipboard.setText(copied_text)
922 def channel_table_delete(self):
923 """Function to delete text from a channel table when delete is pressed"""
924 selection_range = self.channel_table.selectedRanges()
925 self.channel_table.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Fixed)
926 if selection_range:
927 # Get the selected range
928 selected_range = selection_range[0]
929 rows = range(selected_range.topRow(), selected_range.bottomRow() + 1)
930 columns = range(selected_range.leftColumn(), selected_range.rightColumn() + 1)
931 # Clear the selected cells
932 for row in rows:
933 for column in columns:
934 clear_item = QtWidgets.QTableWidgetItem("")
935 self.channel_table.setItem(row, column, clear_item)
936 self.channel_table.horizontalHeader().setSectionResizeMode(
937 QtWidgets.QHeaderView.ResizeToContents
938 )
940 def hardware_update(self, current_index=None, select_file=True):
941 """Callback to provide options when hardware is selected"""
942 current_index = self.hardware_selector.currentIndex()
943 if current_index == 0: # NIDAQmx
944 self.sample_rate_selector.show()
945 self.ip_lookup_button.hide()
946 self.lanxi_sample_rate_selector.hide()
947 self.lanxi_maximum_acquisition_processes_label.hide()
948 self.lanxi_maximum_acquisition_processes_selector.hide()
949 self.integration_oversample_selector.hide()
950 self.integration_oversample_label.hide()
951 self.task_trigger_label.show()
952 self.task_trigger_selector.show()
953 self.hardware_file = None
954 elif current_index == 1: # LAN-XI
955 self.sample_rate_selector.hide()
956 self.ip_lookup_button.show()
957 self.lanxi_sample_rate_selector.show()
958 self.lanxi_maximum_acquisition_processes_label.show()
959 self.lanxi_maximum_acquisition_processes_selector.show()
960 self.integration_oversample_selector.hide()
961 self.integration_oversample_label.hide()
962 self.task_trigger_label.hide()
963 self.task_trigger_selector.hide()
964 self.hardware_file = None
965 elif current_index == 2: # DP Quattro
966 # Load in the library file
967 if select_file:
968 filename, _ = QtWidgets.QFileDialog.getOpenFileName(
969 self, "Data Physics API", filter="Quattro API (DpQuattro.dll)"
970 )
971 if filename == "":
972 self.hardware_selector.setCurrentIndex(0)
973 return
974 else:
975 self.hardware_file = filename
976 self.sample_rate_selector.show()
977 self.ip_lookup_button.hide()
978 self.lanxi_sample_rate_selector.hide()
979 self.lanxi_maximum_acquisition_processes_label.hide()
980 self.lanxi_maximum_acquisition_processes_selector.hide()
981 self.integration_oversample_selector.hide()
982 self.integration_oversample_label.hide()
983 self.task_trigger_label.hide()
984 self.task_trigger_selector.hide()
985 self.sample_rate_update()
986 elif current_index == 3: # DP 900
987 # Load in the library file
988 if select_file:
989 filename, _ = QtWidgets.QFileDialog.getOpenFileName(
990 self, "Data Physics API", filter="DP900 API (Dp900Matlab.dll)"
991 )
992 if filename == "":
993 self.hardware_selector.setCurrentIndex(0)
994 return
995 else:
996 self.hardware_file = filename
997 self.sample_rate_selector.show()
998 self.ip_lookup_button.hide()
999 self.lanxi_sample_rate_selector.hide()
1000 self.lanxi_maximum_acquisition_processes_label.hide()
1001 self.lanxi_maximum_acquisition_processes_selector.hide()
1002 self.integration_oversample_selector.hide()
1003 self.integration_oversample_label.hide()
1004 self.task_trigger_label.hide()
1005 self.task_trigger_selector.hide()
1006 elif current_index == 4: # Exodus
1007 # Load in an exodus file
1008 if select_file:
1009 filename, _ = QtWidgets.QFileDialog.getOpenFileName(
1010 self,
1011 "Load Exodus File with Eigensolution",
1012 filter="Exodus File (*.exo *.e)",
1013 )
1014 if filename == "":
1015 self.hardware_selector.setCurrentIndex(0)
1016 return
1017 else:
1018 self.hardware_file = filename
1019 self.sample_rate_selector.show()
1020 self.ip_lookup_button.hide()
1021 self.lanxi_sample_rate_selector.hide()
1022 self.lanxi_maximum_acquisition_processes_label.hide()
1023 self.lanxi_maximum_acquisition_processes_selector.hide()
1024 self.integration_oversample_selector.show()
1025 self.integration_oversample_label.show()
1026 self.task_trigger_label.hide()
1027 self.task_trigger_selector.hide()
1028 elif current_index == 5: # State Space File
1029 # Load in a state space file
1030 if select_file:
1031 filename, _ = QtWidgets.QFileDialog.getOpenFileName(
1032 self,
1033 "Load Numpy or Matlab File with State Space Matrices A B C D",
1034 filter="Matlab or Numpy File (*.mat *.npz)",
1035 )
1036 if filename == "":
1037 self.hardware_selector.setCurrentIndex(0)
1038 return
1039 else:
1040 self.hardware_file = filename
1041 self.sample_rate_selector.show()
1042 self.ip_lookup_button.hide()
1043 self.lanxi_sample_rate_selector.hide()
1044 self.lanxi_maximum_acquisition_processes_label.hide()
1045 self.lanxi_maximum_acquisition_processes_selector.hide()
1046 self.integration_oversample_selector.show()
1047 self.integration_oversample_label.show()
1048 self.task_trigger_label.hide()
1049 self.task_trigger_selector.hide()
1050 elif current_index == 6:
1051 # Load in an sdynpy system
1052 if select_file:
1053 filename, _ = QtWidgets.QFileDialog.getOpenFileName(
1054 self, "Load a SDynPy System", filter="Numpy File (*.npz)"
1055 )
1056 if filename == "":
1057 self.hardware_selector.setCurrentIndex(0)
1058 return
1059 else:
1060 self.hardware_file = filename
1061 self.sample_rate_selector.show()
1062 self.ip_lookup_button.hide()
1063 self.lanxi_sample_rate_selector.hide()
1064 self.lanxi_maximum_acquisition_processes_label.hide()
1065 self.lanxi_maximum_acquisition_processes_selector.hide()
1066 self.integration_oversample_selector.show()
1067 self.integration_oversample_label.show()
1068 self.task_trigger_label.hide()
1069 self.task_trigger_selector.hide()
1070 elif current_index == 7:
1071 if select_file:
1072 filename, _ = QtWidgets.QFileDialog.getOpenFileName(
1073 self,
1074 "Load a SDynPy TransferFunctionArray",
1075 filter="Numpy File (*.npz)",
1076 )
1077 if filename == "":
1078 self.hardware_selector.setCurrentIndex(0)
1079 return
1080 else:
1081 self.hardware_file = filename
1082 self.sample_rate_selector.show()
1083 self.ip_lookup_button.hide()
1084 self.lanxi_sample_rate_selector.hide()
1085 self.lanxi_maximum_acquisition_processes_label.hide()
1086 self.lanxi_maximum_acquisition_processes_selector.hide()
1087 self.integration_oversample_selector.show()
1088 self.integration_oversample_label.show()
1089 self.task_trigger_label.hide()
1090 self.task_trigger_selector.hide()
1091 else:
1092 error_message_qt(
1093 "Invalid Hardware Type!",
1094 "You have selected an invalid hardware type. How did you do this?!",
1095 )
1096 self.task_trigger_update()
1098 def ip_lookup(self):
1099 """Creates an IP Lookup window"""
1100 ipv4_pattern = r"^((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])(\.(?!$)|$)){4}$"
1101 ipv6_pattern = r"\[\s*([0-9a-fA-F]{1,4}:){0,7}(:[0-9a-fA-F]{1,4})*%?\d*\s*\]"
1102 stored_addresses = self.lanxi_ip_addresses
1104 bknum = []
1105 ipv4 = []
1106 ipv6 = []
1107 for ip_address in stored_addresses:
1108 bknum.append(ip_address.host_name)
1109 ipv4.append(ip_address.ipv4_address)
1110 ipv6.append(ip_address.ipv6_address)
1112 # Loop through table devices and append unique IP addresses
1113 for row in range(self.channel_table.rowCount()):
1114 table_text = self.channel_table.item(row, 10).text()
1115 if re.search(ipv4_pattern, table_text) is not None:
1116 if table_text not in ipv4:
1117 stored_addresses.append(IPAddress(None, table_text, None))
1118 ipv4.append(table_text)
1119 elif re.search(ipv6_pattern, table_text) is not None:
1120 if table_text not in ipv6:
1121 stored_addresses.append(IPAddress(None, None, table_text))
1122 ipv6.append(table_text)
1123 elif table_text != "":
1124 if table_text not in bknum:
1125 stored_addresses.append(IPAddress(table_text, None, None))
1126 bknum.append(table_text)
1128 ip_manager = IPAddressManager(stored_addresses)
1129 # TODO: I don't think the check for equality does anything here. Show isn't blocking, so
1130 # the dialog wouldn't have been accepted yet.
1131 # ok_clicked = ip_manager.show() == QtWidgets.QDialog.Accepted
1132 ip_manager.show()
1134 def sample_rate_update(self):
1135 """Updates the sample rate selector based on valid available rates"""
1136 if self.hardware_selector.currentIndex() == 2:
1137 current_value = self.sample_rate_selector.value()
1138 valid_dp_sample_rates = np.array(
1139 [
1140 16,
1141 20,
1142 25,
1143 32,
1144 40,
1145 50,
1146 64,
1147 80,
1148 100,
1149 128,
1150 160,
1151 200,
1152 256,
1153 320,
1154 400,
1155 512,
1156 640,
1157 800,
1158 1024,
1159 1280,
1160 1600,
1161 2048,
1162 2560,
1163 3200,
1164 4096,
1165 5120,
1166 6400,
1167 8192,
1168 10240,
1169 12800,
1170 20480,
1171 25600,
1172 40960,
1173 51200,
1174 102400,
1175 ]
1176 )
1177 closest_index = np.argmin(abs(valid_dp_sample_rates - current_value))
1178 closest_rate = valid_dp_sample_rates[closest_index]
1179 # Check if it is either one above or one below a previous rate
1180 if (
1181 current_value - closest_rate == 1
1182 and closest_index != len(valid_dp_sample_rates) - 1
1183 ):
1184 closest_index += 1
1185 closest_rate = valid_dp_sample_rates[closest_index]
1186 elif current_value - closest_rate == -1 and closest_index != 0:
1187 closest_index -= 1
1188 closest_rate = valid_dp_sample_rates[closest_index]
1189 self.sample_rate_selector.blockSignals(True)
1190 self.sample_rate_selector.setValue(closest_rate)
1191 self.sample_rate_selector.blockSignals(False)
1193 def task_trigger_update(self):
1194 """Updates task trigger widgets based on other widget's selections"""
1195 if (
1196 self.hardware_selector.currentIndex() == 0
1197 and self.task_trigger_selector.currentIndex() == 2
1198 ):
1199 self.task_trigger_output_selector.show()
1200 self.task_trigger_output_label.show()
1201 else:
1202 self.task_trigger_output_selector.hide()
1203 self.task_trigger_output_label.hide()
1205 def initialize_data_acquisition(self):
1206 """Initializes the data acquisition hardware
1208 This function collects the information from the channel table as well
1209 as the hardware information to create a DataAcquisitionParameters object
1210 that gets passed to each environment through its command queue.
1212 It also sends the data acquisition parameters to the acquisition and
1213 output subtasks.
1214 """
1215 self.log("Initializing Data Acquisition")
1216 channels = []
1217 environment_booleans = []
1218 channel_table_strings = self.get_channel_table_strings()
1219 environment_channels = get_table_bools(self.environment_channels_table)
1220 # print('User Interface {:} Channels'.format(len(channel_table_strings)))
1221 for index, (row, environment_bools) in enumerate(
1222 zip(channel_table_strings, environment_channels)
1223 ):
1224 try:
1225 channel = Channel.from_channel_table_row(row)
1226 except ValueError as e:
1227 self.log(f"Bad Entry in Channel {index + 1}, {e}")
1228 error_message_qt(
1229 "Channel Table Error",
1230 f"Bad Entry in Channel {index + 1}\n\n{e}",
1231 )
1232 return
1233 if channel is not None:
1234 channels.append(channel)
1235 environment_booleans.append(environment_bools)
1236 # Go through and initialize the channel information for each environment
1237 environment_booleans = np.array(environment_booleans)
1238 environment_channel_indices = {}
1239 extra_parameters = {}
1240 if self.hardware_selector.currentIndex() == 0:
1241 sample_rate = self.sample_rate_selector.value()
1242 extra_parameters["task_trigger"] = self.task_trigger_selector.currentIndex()
1243 extra_parameters["task_trigger_output_channel"] = (
1244 self.task_trigger_output_selector.text()
1245 )
1246 output_oversample = 1
1247 elif self.hardware_selector.currentIndex() == 1:
1248 sample_rate = 2 ** self.lanxi_sample_rate_selector.currentIndex() * 4096
1249 output_oversample = 16384 // sample_rate
1250 if output_oversample == 0:
1251 output_oversample = 1
1252 extra_parameters["maximum_acquisition_processes"] = (
1253 self.lanxi_maximum_acquisition_processes_selector.value()
1254 )
1255 elif self.hardware_selector.currentIndex() in [4, 5, 6, 7]:
1256 sample_rate = self.sample_rate_selector.value()
1257 output_oversample = self.integration_oversample_selector.value()
1258 else:
1259 sample_rate = self.sample_rate_selector.value()
1260 output_oversample = 1
1261 for environment_index, environment in enumerate(self.environments):
1262 environment_channel_list = copy.deepcopy(
1263 [
1264 channel
1265 for channel, environment_bool in zip(channels, environment_booleans)
1266 if environment_bool[environment_index]
1267 ]
1268 )
1269 environment_channel_indices[environment] = [
1270 index
1271 for index, environment_bool in enumerate(environment_booleans)
1272 if environment_bool[environment_index]
1273 ]
1274 environment_daq_parameters = DataAcquisitionParameters(
1275 environment_channel_list,
1276 sample_rate,
1277 round(sample_rate * self.time_per_read_selector.value()),
1278 round(sample_rate * self.time_per_write_selector.value() * output_oversample),
1279 self.hardware_selector.currentIndex(),
1280 self.hardware_file,
1281 self.environments,
1282 environment_booleans,
1283 output_oversample,
1284 **extra_parameters,
1285 )
1286 self.queue_container.environment_command_queues[environment].put(
1287 TASK_NAME,
1288 (
1289 GlobalCommands.INITIALIZE_DATA_ACQUISITION,
1290 environment_daq_parameters,
1291 ),
1292 )
1293 self.environment_uis[environment].initialize_data_acquisition(
1294 environment_daq_parameters
1295 )
1296 self.global_daq_parameters = DataAcquisitionParameters(
1297 channels,
1298 sample_rate,
1299 round(sample_rate * self.time_per_read_selector.value()),
1300 round(sample_rate * self.time_per_write_selector.value() * output_oversample),
1301 self.hardware_selector.currentIndex(),
1302 self.hardware_file,
1303 self.environments,
1304 environment_booleans,
1305 output_oversample,
1306 **extra_parameters,
1307 )
1308 self.queue_container.acquisition_command_queue.put(
1309 TASK_NAME,
1310 (
1311 GlobalCommands.INITIALIZE_DATA_ACQUISITION,
1312 (self.global_daq_parameters, environment_channel_indices),
1313 ),
1314 )
1315 self.queue_container.output_command_queue.put(
1316 TASK_NAME,
1317 (
1318 GlobalCommands.INITIALIZE_DATA_ACQUISITION,
1319 (self.global_daq_parameters, environment_channel_indices),
1320 ),
1321 )
1322 self.channel_monitor_button.setEnabled(True)
1323 if self.channel_monitor_window is not None:
1324 self.channel_monitor_window.update_channel_list(self.global_daq_parameters)
1325 for i in range(2, self.rattlesnake_tabs.count() - 1):
1326 self.rattlesnake_tabs.setTabEnabled(i, False)
1327 self.rattlesnake_tabs.setTabEnabled(1, True)
1328 self.rattlesnake_tabs.setCurrentIndex(1)
1330 # %% Test Parameters Callbacks
1332 def initialize_environment_parameters(self):
1333 """Initializes the environment parameters
1335 This function initializes the environment-specific parameters for each
1336 environment by calling the initialize_environment function of each
1337 environment-specific user interface."""
1338 for environment in self.environments:
1339 environment_parameters = self.environment_uis[environment].initialize_environment()
1340 self.environment_metadata[environment] = environment_parameters
1341 self.queue_container.environment_command_queues[environment].put(
1342 TASK_NAME,
1343 (
1344 GlobalCommands.INITIALIZE_ENVIRONMENT_PARAMETERS,
1345 environment_parameters,
1346 ),
1347 )
1349 # Enable the next section
1350 self.rattlesnake_tabs.setTabEnabled(2, True)
1351 self.rattlesnake_tabs.setCurrentIndex(2)
1353 # If there are test predictions
1354 if self.has_test_predictions:
1355 self.rattlesnake_tabs.setTabEnabled(3, True)
1357 # %% Run test callbacks
1358 def select_control_streaming_file(self):
1359 """Selects a file to stream data to disk"""
1360 filename, _ = QtWidgets.QFileDialog.getSaveFileName(
1361 self,
1362 "Select NetCDF File to Save Control Data",
1363 filter="NetCDF File (*.nc4)",
1364 )
1365 if filename == "":
1366 return
1367 self.streaming_file_display.setText(filename)
1369 def arm_test(self):
1370 """Starts the data acquisition running in preparation for control"""
1371 if (
1372 not self.no_streaming_radiobutton.isChecked()
1373 and len(self.streaming_file_display.text()) == 0
1374 ):
1375 error_message_qt(
1376 "No Streaming File Selected",
1377 "Please select a file into which data will be streamed.",
1378 )
1379 return
1380 self.log("Arming Test Hardware")
1381 self.queue_container.controller_communication_queue.put(
1382 TASK_NAME, (GlobalCommands.RUN_HARDWARE, None)
1383 )
1384 self.no_streaming_radiobutton.setEnabled(False)
1385 self.profile_streaming_radiobutton.setEnabled(False)
1386 self.test_level_streaming_radiobutton.setEnabled(False)
1387 self.streaming_environment_select_combobox.setEnabled(False)
1388 self.immediate_streaming_radiobutton.setEnabled(False)
1389 self.select_streaming_file_button.setEnabled(False)
1390 self.manual_streaming_radiobutton.setEnabled(False)
1391 self.manual_streaming_trigger_button.setEnabled(True)
1392 self.arm_test_button.setEnabled(False)
1393 self.disarm_test_button.setEnabled(True)
1394 self.start_profile_button.setEnabled(True)
1395 self.stop_profile_button.setEnabled(True)
1396 for i in range(self.run_environment_tabs.count()):
1397 self.run_environment_tabs.widget(i).setEnabled(True)
1398 for _, ui in self.environment_uis.items():
1399 try:
1400 ui.disable_system_id_daq_armed()
1401 except AttributeError:
1402 pass
1403 if (
1404 self.profile_streaming_radiobutton.isChecked()
1405 or self.test_level_streaming_radiobutton.isChecked()
1406 or self.immediate_streaming_radiobutton.isChecked()
1407 or self.manual_streaming_radiobutton.isChecked()
1408 ):
1409 file_path = self.streaming_file_display.text()
1410 self.queue_container.streaming_command_queue.put(
1411 TASK_NAME,
1412 (
1413 GlobalCommands.INITIALIZE_STREAMING,
1414 (file_path, self.global_daq_parameters, self.environment_metadata),
1415 ),
1416 )
1417 if self.immediate_streaming_radiobutton.isChecked():
1418 self.start_streaming()
1420 def disarm_test(self):
1421 """Stops the data acquisition from running and shuts down all environments"""
1422 self.log("Disarming Test Hardware")
1423 self.queue_container.controller_communication_queue.put(
1424 TASK_NAME, (GlobalCommands.STOP_HARDWARE, None)
1425 )
1426 for _, ui in self.environment_uis.items():
1427 ui.stop_control()
1428 # for environment,queue in self.queue_container.environment_command_queues.items():
1429 # queue.put(TASK_NAME,(GlobalCommands.STOP_ENVIRONMENT,None))
1430 self.no_streaming_radiobutton.setEnabled(True)
1431 self.profile_streaming_radiobutton.setEnabled(True)
1432 self.test_level_streaming_radiobutton.setEnabled(True)
1433 self.streaming_environment_select_combobox.setEnabled(True)
1434 self.immediate_streaming_radiobutton.setEnabled(True)
1435 self.manual_streaming_radiobutton.setEnabled(True)
1436 self.manual_streaming_trigger_button.setEnabled(False)
1437 self.manual_streaming_trigger_button.setText("Start\nStreaming")
1438 self.select_streaming_file_button.setEnabled(True)
1439 self.arm_test_button.setEnabled(True)
1440 self.disarm_test_button.setEnabled(False)
1441 self.start_profile_button.setEnabled(False)
1442 self.stop_profile_button.setEnabled(False)
1443 for i in range(self.run_environment_tabs.count()):
1444 self.run_environment_tabs.widget(i).setEnabled(False)
1445 for _, ui in self.environment_uis.items():
1446 try:
1447 ui.enable_system_id_daq_disarmed()
1448 except AttributeError:
1449 pass
1451 def start_profile(self):
1452 """Starts running the test profile"""
1453 self.log("Running Profile")
1454 # Create the QTimers
1455 self.profile_timers = []
1456 for timestamp, environment_name, operation, data in self.profile_events:
1457 timer = ProfileTimer(environment_name, operation, data)
1458 timer.setSingleShot(True)
1459 timer.timeout.connect(self.fire_profile_event)
1460 timer.start(int(timestamp * 1000))
1461 self.profile_timers.append(timer)
1462 self.profile_list_update_timer = QTimer()
1463 self.profile_list_update_timer.timeout.connect(self.update_profile_list)
1464 self.profile_list_update_timer.start(250)
1466 def fire_profile_event(self):
1467 """Activates a given profile event"""
1468 widget = self.sender()
1469 environment_name = widget.environment
1470 operation = widget.operation
1471 data = widget.data
1472 self.log(f"Profile Firing Event {environment_name} {operation} {data}")
1473 if self.show_profile_change_checkbox.isChecked():
1474 if not environment_name == "Global":
1475 environment_index = self.environments.index(environment_name)
1476 self.run_environment_tabs.setCurrentIndex(environment_index)
1477 if environment_name == "Global":
1478 if operation == "Start Streaming" and (
1479 not self.profile_streaming_radiobutton.isChecked()
1480 ):
1481 return
1482 self.command_map[operation]()
1483 elif operation in ["Start Control", "Stop Control"]:
1484 self.environment_uis[environment_name].command_map[operation]()
1485 else:
1486 self.environment_uis[environment_name].command_map[operation](data)
1488 def update_profile_list(self):
1489 """Updates the list of upcoming profile events."""
1490 profile_representation = []
1491 for timer, profile_event in zip(self.profile_timers, self.profile_events):
1492 remaining_time = timer.remainingTime() / 1000
1493 if remaining_time > 0:
1494 profile_representation.append([remaining_time] + profile_event[1:])
1495 self.upcoming_instructions_list.clear()
1496 self.upcoming_instructions_list.addItems(
1497 [
1498 "{:0.2f} {:} {:} {:}".format( # pylint: disable=consider-using-f-string
1499 *profile_event
1500 )
1501 for profile_event in sorted(profile_representation)
1502 ]
1503 )
1504 if len(profile_representation) == 0:
1505 self.stop_profile()
1507 def stop_profile(self):
1508 """Stops running the profile"""
1509 for timer in self.profile_timers:
1510 timer.stop()
1511 self.profile_list_update_timer.stop()
1513 def initialize_profile(self):
1514 """Initializes the profile list in the controller"""
1515 self.profile_events = []
1516 for row in range(self.profile_table.rowCount()):
1517 self.profile_events.append(
1518 [
1519 float(self.profile_table.cellWidget(row, 0).value()),
1520 self.profile_table.cellWidget(row, 1).currentText(),
1521 self.profile_table.cellWidget(row, 2).currentText(),
1522 self.profile_table.item(row, 3).text(),
1523 ]
1524 )
1525 if len(self.profile_events) == 0:
1526 self.run_profile_widget.hide()
1527 else:
1528 self.run_profile_widget.show()
1529 self.upcoming_instructions_list.clear()
1530 self.upcoming_instructions_list.addItems(
1531 [
1532 "{:0.2f} {:} {:} {:}".format( # pylint: disable=consider-using-f-string
1533 *profile_event
1534 )
1535 for profile_event in sorted(self.profile_events)
1536 ]
1537 )
1538 for i in range(self.rattlesnake_tabs.count() - 1):
1539 self.rattlesnake_tabs.setTabEnabled(i, True)
1541 self.rattlesnake_tabs.setCurrentIndex(self.rattlesnake_tabs.count() - 2)
1543 def save_profile(self):
1544 """Save the profile to a spreadsheet file"""
1545 filename, _ = QtWidgets.QFileDialog.getSaveFileName(
1546 self, "Save Test Profile", filter="Excel File (*.xlsx)"
1547 )
1548 if filename == "":
1549 return
1550 workbook = openpyxl.Workbook()
1551 worksheet = workbook.active
1552 worksheet.title = "Test Profile"
1553 worksheet.cell(1, 1, "Time (s)")
1554 worksheet.cell(1, 2, "Environment")
1555 worksheet.cell(1, 3, "Operation")
1556 worksheet.cell(1, 4, "Data")
1557 for row in range(self.profile_table.rowCount()):
1558 worksheet.cell(row + 2, 1, float(self.profile_table.cellWidget(row, 0).value()))
1559 worksheet.cell(row + 2, 2, self.profile_table.cellWidget(row, 1).currentText())
1560 worksheet.cell(row + 2, 3, self.profile_table.cellWidget(row, 2).currentText())
1561 worksheet.cell(row + 2, 4, self.profile_table.item(row, 3).text())
1562 workbook.save(filename)
1564 def load_profile(self):
1565 """Load a profile from a spreadsheet file"""
1566 filename, _ = QtWidgets.QFileDialog.getOpenFileName(
1567 self, "Load Test Profile", filter="Excel File (*.xlsx)"
1568 )
1569 if filename == "":
1570 return
1571 workbook = openpyxl.load_workbook(filename)
1572 profile_sheet = workbook["Test Profile"]
1573 index = 2
1574 while True:
1575 timestamp = profile_sheet.cell(index, 1).value
1576 environment = profile_sheet.cell(index, 2).value
1577 operation = profile_sheet.cell(index, 3).value
1578 data = profile_sheet.cell(index, 4).value
1579 if timestamp is None or (isinstance(timestamp, str) and timestamp.strip() == ""):
1580 break
1581 self.add_profile_event(None, timestamp, environment, operation, data)
1582 index += 1
1584 def add_profile_event(
1585 self,
1586 clicked=None, # pylint: disable=unused-argument
1587 timestamp=None,
1588 environment=None,
1589 operation=None,
1590 data=None,
1591 ):
1592 """Adds an event to the profile either by clicking a button or by specifying it
1594 Parameters
1595 ----------
1596 clicked :
1597 The clicked event. (Default value = None)
1598 timestamp :
1599 Optional timestamp to give to the controller (Default value = None)
1600 environment :
1601 Optional environment the profile instruction corresponds to
1602 (Default value = None)
1603 operation :
1604 Optional operation specified by the profile instruction
1605 (Default value = None)
1606 data :
1607 Optional data needed by the operation (Default value = None)
1609 """
1610 # start_time = time.time()
1611 # Create the row in the profile table
1612 selected_row = self.profile_table.rowCount()
1613 self.profile_table.insertRow(selected_row)
1614 # insert_row_time = time.time()
1615 # print('Time to Insert Row: {:}'.format(insert_row_time-start_time))
1616 # First entry is a spinbox
1617 timestamp_spinbox = QtWidgets.QDoubleSpinBox()
1618 timestamp_spinbox.setMaximum(1e6)
1619 self.profile_table.setCellWidget(selected_row, 0, timestamp_spinbox)
1620 # create_spinbox_time = time.time()
1621 # print('Time to Create Spinbox: {:}'.format(create_spinbox_time-insert_row_time))
1622 # Next a combobox sets the environment
1623 environment_combobox = QtWidgets.QComboBox()
1624 environment_combobox.addItem("Global")
1625 for environment_name in self.environments:
1626 environment_combobox.addItem(environment_name)
1627 self.profile_table.setCellWidget(selected_row, 1, environment_combobox)
1628 # create_environment_combobox_time = time.time()
1629 # print('Time to Create Environment Combobox: {:}'.format(
1630 # create_environment_combobox_time-create_spinbox_time))
1631 # Next a combobox sets the operation
1632 operation_combobox = QtWidgets.QComboBox()
1633 for op in self.command_map:
1634 operation_combobox.addItem(op)
1635 self.profile_table.setCellWidget(selected_row, 2, operation_combobox)
1636 # create_operation_combobox_time = time.time()
1637 # print('Time to Create Operation Combobox: {:}'.format(
1638 # create_operation_combobox_time-create_environment_combobox_time))
1639 data_item = QtWidgets.QTableWidgetItem()
1640 self.profile_table.setItem(selected_row, 3, data_item)
1641 # create_data_entry_time = time.time()
1642 # print('Time to Data Entry: {:}'.format(
1643 # create_data_entry_time-create_operation_combobox_time))
1644 # Connect the callbacks
1645 timestamp_spinbox.valueChanged.connect(self.update_profile_plot)
1646 environment_combobox.currentIndexChanged.connect(self.update_operations)
1647 operation_combobox.currentIndexChanged.connect(self.update_profile_plot)
1648 # connect_callbacks_time = time.time()
1649 # print('Time to Connect Callbacks: {:}'.format(
1650 # connect_callbacks_time-create_data_entry_time))
1651 # Initialize parameters if necessary
1652 if timestamp is not None:
1653 timestamp_spinbox.setValue(float(timestamp))
1654 # initialize_time_time = time.time()
1655 # print('Time to Initialize Timestamp: {:}'.format(
1656 # initialize_time_time-connect_callbacks_time))
1657 if environment is not None:
1658 environment_combobox.setCurrentIndex(environment_combobox.findText(environment))
1659 # initialize_environment_time = time.time()
1660 # print('Time to Initialize Timestamp: {:}'.format(
1661 # initialize_environment_time-initialize_time_time))
1662 if operation is not None:
1663 operation_combobox.setCurrentIndex(operation_combobox.findText(operation))
1664 # initialize_operation_time = time.time()
1665 # print('Time to Initialize Timestamp: {:}'.format(
1666 # initialize_operation_time-initialize_environment_time))
1667 if data is not None:
1668 data_item.setText(str(data))
1669 # initialize_data_time = time.time()
1670 # print('Time to Initialize Data: {:}'.format(
1671 # initialize_data_time-initialize_operation_time))
1672 # Update the plot
1673 self.update_profile_plot()
1674 # update_plot_time = time.time()
1675 # print('Time to Update Plot: {:}'.format(update_plot_time-initialize_data_time))
1677 def update_operations(self):
1678 """Update profile operations given a selected environment"""
1679 widget = self.sender()
1680 if widget.currentIndex() == 0:
1681 operations = [operation for operation in self.command_map]
1682 else:
1683 environment_name = self.environments[widget.currentIndex() - 1]
1684 operations = [
1685 operation for operation in self.environment_uis[environment_name].command_map
1686 ]
1687 for row in range(self.profile_table.rowCount()):
1688 if widget is self.profile_table.cellWidget(row, 1):
1689 print(f"Found Widget at {row}")
1690 break
1691 operation_combobox = self.profile_table.cellWidget(row, 2)
1692 operation_combobox.blockSignals(True)
1693 operation_combobox.clear()
1694 for operation in operations:
1695 operation_combobox.addItem(operation)
1696 operation_combobox.blockSignals(False)
1697 self.update_profile_plot()
1699 def update_profile_plot(self):
1700 """Updates the plot of profile events"""
1701 plot_item = self.profile_timeline_plot.getPlotItem()
1702 plot_item.clear()
1703 plot_item.showGrid(True, True, 0.25)
1704 plot_item.disableAutoRange()
1705 max_time = 0
1706 for row in range(self.profile_table.rowCount()):
1707 time_val = self.profile_table.cellWidget(row, 0).value()
1708 if time_val > max_time:
1709 max_time = time_val
1710 plot_item.plot(
1711 [time_val],
1712 [self.profile_table.cellWidget(row, 1).currentIndex()],
1713 pen=None,
1714 symbol="o",
1715 pxMode=True,
1716 )
1717 text_item = pyqtgraph.TextItem(
1718 f"{row + 1}: "
1719 + self.profile_table.cellWidget(row, 2).currentText()
1720 + (
1721 ": " + self.profile_table.item(row, 3).text()
1722 if self.profile_table.item(row, 3).text().strip() != ""
1723 else ""
1724 ),
1725 color=(0, 0, 0),
1726 angle=-15,
1727 )
1728 plot_item.addItem(text_item)
1729 text_item.setPos(time_val, self.profile_table.cellWidget(row, 1).currentIndex())
1730 axis = plot_item.getAxis("left")
1731 axis.setTicks([[(i, name) for i, name in enumerate(["Global"] + self.environments)], []])
1732 plot_item.setXRange(0, max_time * 1.1)
1733 plot_item.setYRange(-1, len(self.environments))
1735 def remove_profile_event(self):
1736 """Removes a profile event from the list of events"""
1737 selected_row = self.profile_table.currentRow()
1738 if selected_row >= 0:
1739 self.profile_table.removeRow(selected_row)
1740 self.update_profile_plot()
1742 def start_streaming(self):
1743 """Tells acquisition to start sending data to streaming"""
1744 self.queue_container.acquisition_command_queue.put(
1745 TASK_NAME, (GlobalCommands.START_STREAMING, None)
1746 )
1748 def stop_streaming(self):
1749 """Tells the acquisition to stop sending data to streaming"""
1750 self.queue_container.acquisition_command_queue.put(
1751 TASK_NAME, (GlobalCommands.STOP_STREAMING, None)
1752 )
1754 def show_hide_manual_streaming(self):
1755 """Shows or hides the manual streaming button depending on which streaming type is chosen"""
1756 if self.manual_streaming_radiobutton.isChecked():
1757 self.manual_streaming_trigger_button.setVisible(True)
1758 else:
1759 self.manual_streaming_trigger_button.setVisible(False)
1761 def start_stop_streaming(self):
1762 """Starts or stops streaming manually"""
1763 if self.manual_streaming_trigger_button.text() == "Stop\nStreaming":
1764 self.manual_streaming_trigger_button.setText("Start\nStreaming")
1765 self.queue_container.acquisition_command_queue.put(
1766 TASK_NAME, (GlobalCommands.STOP_STREAMING, None)
1767 )
1768 else:
1769 self.manual_streaming_trigger_button.setText("Stop\nStreaming")
1770 self.queue_container.acquisition_command_queue.put(
1771 TASK_NAME, (GlobalCommands.START_STREAMING, None)
1772 )
1774 # %% Other Callbacks
1775 def sync_environment_table(self):
1776 """Callback to synchronize scrolling between channel tables"""
1777 self.environment_channels_table.verticalScrollBar().setValue(
1778 self.channel_table.verticalScrollBar().value()
1779 )
1781 def sync_channel_table(self):
1782 """Callback to synchronize scrolling between channel tables"""
1783 self.channel_table.verticalScrollBar().setValue(
1784 self.environment_channels_table.verticalScrollBar().value()
1785 )
1787 def stop_program(self):
1788 """Callback to stop the entire program"""
1789 self.close()
1791 def update_gui(self, queue_data):
1792 """Update the graphical interface for the main controller
1794 Parameters
1795 ----------
1796 queue_data :
1797 A 2-tuple consisting of ``(message,data)`` pairs where the message
1798 denotes what to change and the data contains the information needed
1799 to be displayed.
1801 """
1802 message, data = queue_data
1803 # self.log('Updating GUI {:}'.format(message))
1804 if message == "error":
1805 error_message_qt(data[0], data[1])
1806 return
1807 elif message in self.environments:
1808 self.environment_uis[message].update_gui(data)
1809 elif message == "monitor":
1810 if self.channel_monitor_window is not None:
1811 if not self.channel_monitor_window.isVisible():
1812 self.channel_monitor_window = None
1813 else:
1814 self.channel_monitor_window.update(data)
1815 elif message == "update_metadata":
1816 environment_name, metadata = data
1817 self.environment_metadata[environment_name] = metadata
1818 elif message == "stop":
1819 self.disarm_test()
1820 elif message == "enable":
1821 widget = getattr(self, data)
1822 widget.setEnabled(True)
1823 elif message == "disable":
1824 widget = getattr(self, data)
1825 widget.setEnabled(False)
1826 elif message == "enable_tab":
1827 self.rattlesnake_tabs.setTabEnabled(data, True)
1828 self.rattlesnake_tabs.setCurrentIndex(data)
1829 elif message == "disable_tab":
1830 self.rattlesnake_tabs.setTabEnabled(data, False)
1831 else:
1832 widget = getattr(self, message)
1833 if isinstance(widget, QtWidgets.QDoubleSpinBox):
1834 widget.setValue(data)
1835 elif isinstance(widget, QtWidgets.QSpinBox):
1836 widget.setValue(data)
1837 elif isinstance(widget, QtWidgets.QLineEdit):
1838 widget.setText(data)
1839 elif isinstance(widget, QtWidgets.QListWidget):
1840 widget.clear()
1841 widget.addItems([f"{d:.3f}" for d in data])
1843 # self.log('Update took {:} seconds'.format(time.time()-start_time))
1845 def handle_controller_instructions(self, queue_data):
1846 """Handler function for global controller instructions
1848 Parameters
1849 ----------
1850 queue_data :
1851 A 2-tuple consisting of ``(message,data)`` pairs where the message
1852 denotes what to change and the data contains the information needed
1853 to be displayed.
1855 """
1856 message, data = queue_data
1857 self.log(f"Received Global Instruction {message.name}")
1858 if message == GlobalCommands.QUIT:
1859 self.stop_program()
1860 elif message == GlobalCommands.INITIALIZE_DATA_ACQUISITION:
1861 self.initialize_data_acquisition()
1862 elif message == GlobalCommands.INITIALIZE_ENVIRONMENT_PARAMETERS:
1863 self.initialize_environment_parameters()
1864 elif message == GlobalCommands.UPDATE_METADATA:
1865 environment, metadata = data
1866 self.environment_metadata[environment] = metadata
1867 elif message == GlobalCommands.RUN_HARDWARE:
1868 self.queue_container.acquisition_command_queue.put(
1869 TASK_NAME, (GlobalCommands.RUN_HARDWARE, data)
1870 )
1871 self.queue_container.output_command_queue.put(
1872 TASK_NAME, (GlobalCommands.RUN_HARDWARE, data)
1873 )
1874 elif message == GlobalCommands.STOP_HARDWARE:
1875 self.queue_container.acquisition_command_queue.put(
1876 TASK_NAME, (GlobalCommands.STOP_HARDWARE, data)
1877 )
1878 self.queue_container.output_command_queue.put(
1879 TASK_NAME, (GlobalCommands.STOP_HARDWARE, data)
1880 )
1881 elif message == GlobalCommands.INITIALIZE_STREAMING:
1882 self.queue_container.streaming_command_queue.put(
1883 TASK_NAME,
1884 (
1885 GlobalCommands.INITIALIZE_STREAMING,
1886 (data, self.global_daq_parameters, self.environment_metadata),
1887 ),
1888 )
1889 elif message == GlobalCommands.STREAMING_DATA:
1890 self.queue_container.streaming_command_queue.put(
1891 TASK_NAME, (GlobalCommands.STREAMING_DATA, data)
1892 )
1893 elif message == GlobalCommands.FINALIZE_STREAMING:
1894 self.queue_container.streaming_command_queue.put(
1895 TASK_NAME, (GlobalCommands.FINALIZE_STREAMING, data)
1896 )
1897 elif message == GlobalCommands.START_ENVIRONMENT:
1898 self.queue_container.output_command_queue.put(
1899 TASK_NAME, (GlobalCommands.START_ENVIRONMENT, data)
1900 )
1901 elif message == GlobalCommands.STOP_ENVIRONMENT:
1902 self.queue_container.acquisition_command_queue.put(
1903 TASK_NAME, (GlobalCommands.STOP_ENVIRONMENT, data)
1904 )
1905 elif message == GlobalCommands.START_STREAMING:
1906 self.start_streaming()
1907 elif message == GlobalCommands.STOP_STREAMING:
1908 self.queue_container.acquisition_command_queue.put(
1909 TASK_NAME, (GlobalCommands.STOP_STREAMING, data)
1910 )
1911 elif message == GlobalCommands.COMPLETED_SYSTEM_ID:
1912 environment, _ = data
1913 self.complete_system_ids[environment] = True
1914 if all([flag for environment, flag in self.complete_system_ids.items()]):
1915 if self.has_test_predictions:
1916 self.rattlesnake_tabs.setTabEnabled(4, True)
1917 else:
1918 self.rattlesnake_tabs.setTabEnabled(3, True)
1919 elif message == GlobalCommands.AT_TARGET_LEVEL:
1920 environment_name = data
1921 if (
1922 self.test_level_streaming_radiobutton.isChecked()
1923 and self.streaming_environment_select_combobox.currentText() == environment_name
1924 ):
1925 self.start_streaming()
1927 def closeEvent(self, event): # pylint: disable=invalid-name
1928 """Event triggered when closing the software to gracefully shut down.
1930 Parameters
1931 ----------
1932 event :
1933 The close event, which is accepted.
1935 """
1936 for (
1937 _,
1938 command_queue,
1939 ) in self.queue_container.environment_command_queues.items():
1940 command_queue.put(TASK_NAME, (GlobalCommands.QUIT, None))
1942 self.queue_container.gui_update_queue.put((GlobalCommands.QUIT, None))
1943 self.queue_container.controller_communication_queue.put(
1944 TASK_NAME, (GlobalCommands.QUIT, None)
1945 )
1947 for command_queue in [
1948 self.queue_container.acquisition_command_queue,
1949 self.queue_container.output_command_queue,
1950 self.queue_container.streaming_command_queue,
1951 ]:
1952 command_queue.put(TASK_NAME, (GlobalCommands.QUIT, None))
1954 event.accept()
1956 def change_color_theme(self, text: str):
1957 """Updates the color scheme of the UI"""
1958 if text == "Light":
1959 self.setStyleSheet("")
1960 elif text == "Dark":
1961 dark_theme_path = os.path.join(directory, "themes", "dark_theme.txt")
1962 with open(dark_theme_path, encoding="utf-8") as file:
1963 stylesheet = file.read()
1964 images_path = os.path.join(directory, "themes", "images").replace("\\", "/")
1965 print(f"Images Path: {images_path}")
1966 stylesheet.replace(r"%%IMAGES_PATH%%", images_path)
1967 self.setStyleSheet(stylesheet)
1969 def show_channel_monitor(self):
1970 """
1971 Shows the channel monitor window
1972 """
1973 if (self.channel_monitor_window is None) or (not self.channel_monitor_window.isVisible()):
1974 self.channel_monitor_window = ChannelMonitor(None, self.global_daq_parameters)
1975 else:
1976 pass # TODO Need to raise the window to the front, or close and reopen