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

1# -*- coding: utf-8 -*- 

2""" 

3Controller subsystem to handle the user interface, including callback 

4assignment and displaying results. 

5 

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. 

10 

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. 

15 

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. 

20 

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""" 

24 

25import copy 

26import datetime 

27import multiprocessing as mp 

28 

29# pyqtgraph.setConfigOption('leftButtonPan',False) 

30import os 

31import re 

32import time 

33import traceback 

34 

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) 

50 

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) 

68 

69pyqtgraph.setConfigOption("background", "w") 

70pyqtgraph.setConfigOption("foreground", "k") 

71 

72directory = os.path.split(__file__)[0] 

73QDir.addSearchPath("images", os.path.join(directory, "themes", "images")) 

74 

75TASK_NAME = "UI" 

76 

77 

78class UpdaterSignals(QObject): 

79 """Defines the signals that will be sent from the GUI Updater to the GUI 

80 

81 Supported signals are: 

82 

83 finished 

84 empty 

85 

86 update 

87 `tuple` (widget_id,data) 

88 """ 

89 

90 finished = Signal() 

91 update = Signal(tuple) 

92 

93 

94class Updater(QRunnable): 

95 """Updater thread to collect results from the subsystems and reflect the 

96 changes in the GUI 

97 """ 

98 

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. 

103 

104 Parameters 

105 ---------- 

106 update_queue : mp.queues.Queue 

107 Queue from which events will be captured. 

108 

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) 

114 

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) 

128 

129 

130class Ui(QtWidgets.QMainWindow): 

131 """Main user interface from which the controller will be controlled.""" 

132 

133 def __init__(self, environments, queue_container: QueueContainer, profile_file=None): 

134 """ 

135 Create the user interface with the specified parameters and queues 

136 

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. 

147 

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 = [] 

161 

162 # Create the user interface 

163 super(Ui, self).__init__() 

164 uic.loadUi(ui_path, self) 

165 

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 ) 

180 

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 

199 

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 

210 

211 self.streaming_environment_select_combobox.addItems(self.environments) 

212 

213 self.manual_streaming_trigger_button.setVisible(False) 

214 

215 for i in range(self.run_environment_tabs.count()): 

216 self.run_environment_tabs.widget(i).setEnabled(False) 

217 

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 ) 

224 

225 # Start Workers 

226 self.threadpool.start(self.gui_updater) 

227 self.threadpool.start(self.controller_instructions_collector) 

228 

229 # Complete the remaining user interface 

230 self.complete_ui() 

231 self.connect_callbacks() 

232 

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 } 

239 

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() 

245 

246 # Hide the task trigger fields if necessary 

247 self.task_trigger_update() 

248 

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) 

405 

406 except Exception: # pylint: disable=broad-exception-caught 

407 print(traceback.format_exc()) 

408 

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 

418 

419 def log(self, string): 

420 """Pass a message to the log_file_queue along with date/time and task name 

421 

422 Parameters 

423 ---------- 

424 string : str 

425 Message that will be written to the queue 

426 

427 """ 

428 self.queue_container.log_file_queue.put( 

429 f"{datetime.datetime.now()}: {TASK_NAME} -- {string}\n" 

430 ) 

431 

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() 

440 

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) 

449 

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) 

453 

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) 

459 

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 ) 

476 

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) 

512 

513 # Control Definition Tab 

514 self.initialize_environments_button.clicked.connect(self.initialize_environment_parameters) 

515 

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) 

522 

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) 

531 

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 ) 

537 

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 

548 

549 # %% Data Acquisition Callbacks 

550 

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 

553 

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). 

561 

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) 

639 

640 self.channel_table.horizontalHeader().setSectionResizeMode( 

641 QtWidgets.QHeaderView.ResizeToContents 

642 ) 

643 

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!") 

719 

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() 

872 

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 ) 

898 

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) 

921 

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 ) 

939 

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() 

1097 

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 

1103 

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) 

1111 

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) 

1127 

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() 

1133 

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) 

1192 

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() 

1204 

1205 def initialize_data_acquisition(self): 

1206 """Initializes the data acquisition hardware 

1207 

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. 

1211 

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) 

1329 

1330 # %% Test Parameters Callbacks 

1331 

1332 def initialize_environment_parameters(self): 

1333 """Initializes the environment parameters 

1334 

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 ) 

1348 

1349 # Enable the next section 

1350 self.rattlesnake_tabs.setTabEnabled(2, True) 

1351 self.rattlesnake_tabs.setCurrentIndex(2) 

1352 

1353 # If there are test predictions 

1354 if self.has_test_predictions: 

1355 self.rattlesnake_tabs.setTabEnabled(3, True) 

1356 

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) 

1368 

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() 

1419 

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 

1450 

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) 

1465 

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) 

1487 

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() 

1506 

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() 

1512 

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) 

1540 

1541 self.rattlesnake_tabs.setCurrentIndex(self.rattlesnake_tabs.count() - 2) 

1542 

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) 

1563 

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 

1583 

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 

1593 

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) 

1608 

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)) 

1676 

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() 

1698 

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)) 

1734 

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() 

1741 

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 ) 

1747 

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 ) 

1753 

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) 

1760 

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 ) 

1773 

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 ) 

1780 

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 ) 

1786 

1787 def stop_program(self): 

1788 """Callback to stop the entire program""" 

1789 self.close() 

1790 

1791 def update_gui(self, queue_data): 

1792 """Update the graphical interface for the main controller 

1793 

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. 

1800 

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]) 

1842 

1843 # self.log('Update took {:} seconds'.format(time.time()-start_time)) 

1844 

1845 def handle_controller_instructions(self, queue_data): 

1846 """Handler function for global controller instructions 

1847 

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. 

1854 

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() 

1926 

1927 def closeEvent(self, event): # pylint: disable=invalid-name 

1928 """Event triggered when closing the software to gracefully shut down. 

1929 

1930 Parameters 

1931 ---------- 

1932 event : 

1933 The close event, which is accepted. 

1934 

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)) 

1941 

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 ) 

1946 

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)) 

1953 

1954 event.accept() 

1955 

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) 

1968 

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