-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMain.py
More file actions
1461 lines (1274 loc) · 76.9 KB
/
Main.py
File metadata and controls
1461 lines (1274 loc) · 76.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import sys
import time
import pyvisa
import os
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QGridLayout, QLabel, QLineEdit, QPushButton, QTextEdit,
QDoubleSpinBox, QMessageBox, QGroupBox, QSizePolicy, QFrame,
QDialog, QListWidget, QListWidgetItem) # 导入QDialog, QListWidget, QListWidgetItem
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QObject, pyqtSlot, QTimer, QUrl, QSettings
from PyQt5.QtGui import QPalette, QColor, QFont, QIcon
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent
# --- 设备工作器 (在独立线程中执行VISA操作) ---
class DeviceWorker(QObject):
# 工作器 -> GUI 的信号
connected = pyqtSignal(str, str) # 设备名称, IDN字符串
disconnected = pyqtSignal(str) # 设备名称
error = pyqtSignal(str, str, str) # 设备名称, 错误标题, 错误信息
log_message_signal = pyqtSignal(str) # 日志消息
# 电源特定更新信号
ps_settings_updated = pyqtSignal(float, float) # 设定电压, 设定电流上限
ps_measurements_updated = pyqtSignal(float, float, float) # 测量电压, 测量电流, 测量功率
ps_output_status_updated = pyqtSignal(str) # "ON", "OFF" 或其他状态
# 电子负载特定更新信号
el_settings_updated = pyqtSignal(float) # 设定电流
el_measurements_updated = pyqtSignal(float, float, float) # 测量电压, 测量电流, 测量功率
el_input_status_updated = pyqtSignal(str) # "ON", "OFF" 或其他状态
def __init__(self, rm, resource_string, device_name):
super().__init__()
self.rm = rm
self.resource_string = resource_string
self.device_name = device_name
self.instrument = None
self._is_connected = False
@pyqtSlot()
def connect_device(self):
"""尝试连接VISA设备。此方法将在工作线程中执行。"""
if self._is_connected:
self.log_message_signal.emit(f"<font color='#FFC107'>警告:</font> {self.device_name} 已经连接。") # 警告色
return
self.log_message_signal.emit(f"正在连接到 <font color='#17A2B8'>{self.device_name}</font> (<font color='#17A2B8'>{self.resource_string}</font>)...") # 信息色
try:
self.instrument = self.rm.open_resource(self.resource_string)
self.instrument.timeout = 5000 # 5秒超时
self.instrument.read_termination = '\n'
self.instrument.write_termination = '\n'
self.instrument.write("*CLS") # 清除设备错误
idn = self.instrument.query("*IDN?").strip()
self.log_message_signal.emit(f"<font color='#28A745'>成功:</font> {self.device_name} 已连接: {idn}") # 成功色
try:
# 尝试进入远程模式,某些设备可能不需要或不支持
self.instrument.write("SYST:REM")
self.log_message_signal.emit(f"{self.device_name}: 已发送 <font color='#17A2B8'>SYST:REM</font> (进入远程模式) 命令。") # 信息色
except pyvisa.errors.VisaIOError as e:
self.log_message_signal.emit(f"<font color='#FFC107'>警告:</font> {self.device_name}: 发送 SYST:REM 命令失败 (可能不需要): {e}") # 警告色
self._is_connected = True
self.connected.emit(self.device_name, idn)
except pyvisa.errors.VisaIOError as e:
err_msg = f"连接 {self.device_name} 失败: {e}"
self.log_message_signal.emit(f"<font color='#DC3545'>错误:</font> {err_msg}") # 错误色
self.error.emit(self.device_name, f"连接 {self.device_name} 错误", err_msg)
if self.instrument: self.instrument.close()
self.instrument = None
self._is_connected = False
except Exception as e:
err_msg = f"连接 {self.device_name} 时发生未知错误: {e}"
self.log_message_signal.emit(f"<font color='#DC3545'>错误:</font> {err_msg}") # 错误色
self.error.emit(self.device_name, f"{self.device_name} 未知错误", err_msg)
if self.instrument: self.instrument.close()
self.instrument = None
self._is_connected = False
@pyqtSlot()
def disconnect_device(self):
"""断开VISA设备连接。此方法将在工作线程中执行。"""
if not self._is_connected or not self.instrument:
self.log_message_signal.emit(f"<font color='#FFC107'>警告:</font> {self.device_name} 未连接,无需断开。") # 警告色
return
self.log_message_signal.emit(f"正在断开 <font color='#17A2B8'>{self.device_name}</font>...") # 信息色
try:
try:
# 尝试返回本地模式
self.instrument.write("SYST:LOC")
self.log_message_signal.emit(f"{self.device_name}: 已发送 <font color='#17A2B8'>SYST:LOC</font> (返回本地模式) 命令。") # 信息色
except pyvisa.errors.VisaIOError as e:
self.log_message_signal.emit(f"<font color='#FFC107'>警告:</font> {self.device_name}: 发送 SYST:LOC 命令失败 (可能设备已不响应或无此命令): {e}") # 警告色
self.instrument.close()
self.instrument = None
self._is_connected = False
self.log_message_signal.emit(f"<font color='#28A745'>成功:</font> {self.device_name} 设备已断开。</font>") # 成功色
self.disconnected.emit(self.device_name)
except Exception as e:
err_msg = f"断开 {self.device_name} 时发生错误: {e}"
self.log_message_signal.emit(f"<font color='#DC3545'>错误:</font> {err_msg}") # 错误色
self.error.emit(self.device_name, f"{self.device_name} 断开错误", err_msg)
@pyqtSlot(str, str, bool, str)
def process_command(self, command, param, is_query, caller_id=""):
"""
在工作线程中处理SCPI命令。
:param command: SCPI命令 (例如 "VOLT")
:param param: 命令参数 (例如 "1.0")
:param is_query: True表示查询命令 (例如 "VOLT?")
:param caller_id: 用于区分不同命令类型,以便精确地发射更新信号
"""
if not self._is_connected or not self.instrument:
self.log_message_signal.emit(f"<font color='#DC3545'>错误:</font> {self.device_name} 未连接,无法执行命令: {command} {param}") # 错误色
self.error.emit(self.device_name, f"{self.device_name} 命令错误", f"{self.device_name} 未连接。")
return
full_command = f"{command} {param}" if param else command
try:
self.log_message_signal.emit(f"{self.device_name} 发送: <font color='#17A2B8'>{full_command}</font>") # 信息色
if is_query:
response = self.instrument.query(full_command).strip()
self.log_message_signal.emit(f"{self.device_name} 收到: <font color='#20B2AA'>{response}</font>") # 成功色 (偏青)
# 根据设备类型和命令发射特定信号
if self.device_name == "电源":
if command == "VOLT?":
self.ps_settings_updated.emit(float(response), -1.0) # -1.0表示不更新电流
elif command == "CURR?":
self.ps_settings_updated.emit(-1.0, float(response)) # -1.0表示不更新电压
elif command == "OUTP?":
self.ps_output_status_updated.emit(response)
elif command == "MEAS:VOLT?":
# 测量值查询通常在刷新时批量处理,这里可以单独处理,但为了避免重复刷新,只在需要时更新UI
pass
elif self.device_name == "电子负载":
if command == "CURR?":
self.el_settings_updated.emit(float(response))
elif command == "INP?":
self.el_input_status_updated.emit(response)
elif command == "MEAS:VOLT?":
pass
else: # 写入命令
self.instrument.write(full_command)
self.log_message_signal.emit(f"<font color='#28A745'>成功:</font> {self.device_name}: 命令 '{full_command}' 已发送成功。</font>") # 成功色
# 写入命令后,立即查询状态以更新UI
if self.device_name == "电源" and command == "OUTP":
# 短暂延迟以确保设备处理完写入命令
time.sleep(0.1)
self.process_command("OUTP?", "", True, "query_output_status")
elif self.device_name == "电子负载" and command == "INP":
time.sleep(0.1)
self.process_command("INP?", "", True, "query_input_status")
except pyvisa.errors.VisaIOError as e:
err_msg = f"执行 '{full_command}' 失败: {e}"
self.log_message_signal.emit(f"<font color='#DC3545'>错误:</font> {self.device_name} 命令 ({full_command}) VISA I/O 错误: {e}</font>") # 错误色
self.error.emit(self.device_name, f"{self.device_name} 命令错误", err_msg)
except ValueError:
self.log_message_signal.emit(f"<font color='#DC3545'>错误:</font> {self.device_name} 响应 '{response}' 数据解析失败。</font>") # 错误色
self.error.emit(self.device_name, f"{self.device_name} 数据解析错误", f"无法解析设备响应: {response}")
except Exception as e:
err_msg = f"执行 '{full_command}' 时发生未知错误: {e}"
self.log_message_signal.emit(f"<font color='#DC3545'>错误:</font> {err_msg}") # 错误色
self.error.emit(self.device_name, f"{self.device_name} 未知错误", err_msg)
@pyqtSlot()
def refresh_status_and_measurements(self):
"""
在工作线程中刷新设备的设定值、状态和测量值。
"""
if not self._is_connected or not self.instrument:
self.log_message_signal.emit(f"<font color='#FFC107'>警告:</font> {self.device_name} 未连接,无法刷新。") # 警告色
return
self.log_message_signal.emit(f"正在刷新 <font color='#17A2B8'>{self.device_name}</font> 所有状态和测量值...") # 信息色
try:
if self.device_name == "电源":
v_set_str = self.instrument.query("VOLT?").strip()
time.sleep(0.05)
i_set_str = self.instrument.query("CURR?").strip()
self.ps_settings_updated.emit(float(v_set_str), float(i_set_str))
time.sleep(0.05)
outp_status_str = self.instrument.query("OUTP?").strip()
self.ps_output_status_updated.emit(outp_status_str)
time.sleep(0.05)
v_meas = float(self.instrument.query("MEAS:VOLT?").strip())
time.sleep(0.05)
i_meas = float(self.instrument.query("MEAS:CURR?").strip())
time.sleep(0.05)
p_meas = float(self.instrument.query("MEAS:POW?").strip())
self.ps_measurements_updated.emit(v_meas, i_meas, p_meas)
self.log_message_signal.emit(f"<font color='#28A745'>成功:</font> {self.device_name}: 刷新完成。</font>") # 成功色
elif self.device_name == "电子负载":
i_set_str = self.instrument.query("CURR?").strip()
self.el_settings_updated.emit(float(i_set_str))
time.sleep(0.05)
inp_status_str = self.instrument.query("INP?").strip()
self.el_input_status_updated.emit(inp_status_str)
time.sleep(0.05)
v_meas = float(self.instrument.query("MEAS:VOLT?").strip())
time.sleep(0.05)
i_meas = float(self.instrument.query("MEAS:CURR?").strip())
time.sleep(0.05)
p_meas = float(self.instrument.query("MEAS:POW?").strip())
self.el_measurements_updated.emit(v_meas, i_meas, p_meas)
self.log_message_signal.emit(f"<font color='#28A745'>成功:</font> {self.device_name}: 刷新完成。</font>") # 成功色
except pyvisa.errors.VisaIOError as e:
err_msg = f"刷新 {self.device_name} 状态失败: {e}"
self.log_message_signal.emit(f"<font color='#DC3545'>错误:</font> {err_msg}") # 错误色
self.error.emit(self.device_name, f"{self.device_name} 刷新错误", err_msg)
except ValueError:
self.log_message_signal.emit(f"<font color='#DC3545'>错误:</font> {self.device_name} 刷新数据解析失败。</font>") # 错误色
self.error.emit(self.device_name, f"{self.device_name} 刷新错误", "无法解析刷新数据。")
except Exception as e:
err_msg = f"刷新 {self.device_name} 状态时发生未知错误: {e}"
self.log_message_signal.emit(f"<font color='#DC3545'>错误:</font> {err_msg}") # 错误色
self.error.emit(self.device_name, f"{self.device_name} 刷新错误", err_msg)
# --- VISA 扫描对话框 ---
class VisaScanDialog(QDialog):
def __init__(self, rm, parent=None):
super().__init__(parent)
self.setWindowTitle("扫描 VISA 资源")
self.setGeometry(200, 200, 600, 400)
self.rm = rm
self.selected_resource = None
self.init_ui()
self.apply_stylesheet()
self.scan_resources() # 自动扫描一次
def init_ui(self):
layout = QVBoxLayout()
self.resource_list_widget = QListWidget()
self.resource_list_widget.itemDoubleClicked.connect(self.accept) # 双击选择并关闭
layout.addWidget(self.resource_list_widget)
button_layout = QHBoxLayout()
self.scan_button = QPushButton("重新扫描")
self.scan_button.clicked.connect(self.scan_resources)
button_layout.addWidget(self.scan_button)
self.select_button = QPushButton("选择")
self.select_button.clicked.connect(self.on_select_clicked)
self.select_button.setEnabled(False) # 初始禁用,直到有选择
button_layout.addWidget(self.select_button)
self.cancel_button = QPushButton("取消")
self.cancel_button.clicked.connect(self.reject)
button_layout.addWidget(self.cancel_button)
layout.addLayout(button_layout)
self.setLayout(layout)
self.resource_list_widget.itemSelectionChanged.connect(self.on_selection_changed)
def apply_stylesheet(self):
"""为对话框应用样式,与主窗口保持一致。"""
self.setStyleSheet("""
QDialog {
background-color: #F0F4F8;
color: #333333;
}
QListWidget {
background-color: #FFFFFF;
color: #333333;
border: 1px solid #B0C4DE;
border-radius: 5px;
padding: 5px;
font-size: 10pt;
}
QListWidget::item:selected {
background-color: #4682B4; /* 选中项背景色 */
color: white; /* 选中项文字颜色 */
}
QPushButton {
background-color: #4682B4; /* 按钮默认蓝色 (Steel Blue) */
color: white;
border: none;
border-radius: 5px;
padding: 8px 16px; /* 增加按钮内边距 */
font-size: 10pt;
font-weight: bold;
min-width: 80px;
}
QPushButton:hover {
background-color: #5A9BD6;
}
QPushButton:pressed {
background-color: #3A6F9B;
}
QPushButton:disabled {
background-color: #AAAAAA;
color: #666666;
}
""")
def scan_resources(self):
"""扫描可用的VISA资源并显示在列表中。"""
self.resource_list_widget.clear()
try:
resources = self.rm.list_resources()
if resources:
for r in resources:
item = QListWidgetItem(r)
self.resource_list_widget.addItem(item)
else:
self.resource_list_widget.addItem("未找到任何 VISA 资源。")
except Exception as e:
QMessageBox.critical(self, "扫描错误", f"扫描 VISA 资源时发生错误: {e}")
self.resource_list_widget.addItem(f"扫描错误: {e}")
self.on_selection_changed() # 更新选择按钮状态
def on_selection_changed(self):
"""根据列表选择状态更新选择按钮的可用性。"""
self.select_button.setEnabled(len(self.resource_list_widget.selectedItems()) > 0)
def on_select_clicked(self):
"""处理选择按钮点击事件,保存选中资源并接受对话框。"""
selected_items = self.resource_list_widget.selectedItems()
if selected_items:
self.selected_resource = selected_items[0].text()
self.accept()
else:
QMessageBox.warning(self, "无选择", "请从列表中选择一个 VISA 资源。")
# --- 主GUI窗口 ---
class UnifiedControllerGUI(QMainWindow):
# GUI -> 工作器 的信号
ps_connect_request = pyqtSignal()
ps_disconnect_request = pyqtSignal()
ps_command_request = pyqtSignal(str, str, bool, str) # command, param, is_query, caller_id
ps_refresh_request = pyqtSignal()
el_connect_request = pyqtSignal()
el_disconnect_request = pyqtSignal()
el_command_request = pyqtSignal(str, str, bool, str)
el_refresh_request = pyqtSignal()
# 默认VISA资源字符串 (如果 QSettings 没有找到,则使用这些默认值)
DEFAULT_PS_VISA_RESOURCE = 'USB0::0x2EC7::0x6000::803982200797740009::INSTR'
DEFAULT_EL_VISA_RESOURCE = 'USB0::0x2EC7::0x8900::803280023806740001::INSTR'
DEFAULT_PS_VOLTAGE = 1.0
DEFAULT_PS_CURRENT = 0.1
DEFAULT_EL_CURRENT = 1.0
# 音效文件路径 (重要:修改此处,使其在打包后也能正确找到)
if getattr(sys, 'frozen', False):
# 如果是Nuitka/PyInstaller打包的,sys.frozen 为 True,使用 sys.executable 的目录
application_path = os.path.dirname(sys.executable)
else:
# 如果是作为普通Python脚本运行,使用当前文件所在的目录
application_path = os.path.dirname(os.path.abspath(__file__))
# 构建 sound 目录的路径
SOUND_DIR = os.path.join(application_path, "sound")
ON_SOUND_PATH = os.path.join(SOUND_DIR, "on.mp3")
OFF_SOUND_PATH = os.path.join(SOUND_DIR, "off.mp3")
def __init__(self):
super().__init__()
self.setWindowTitle("ITECH 设备统一控制器 (IT6000 & IT8902E)")
self.setGeometry(100, 100, 1050, 850) # 调整窗口大小,留出更多空间
self.rm = pyvisa.ResourceManager()
self.ps_worker = None
self.ps_thread = None
self.el_worker = None
self.el_thread = None
# 初始化 QSettings
# 使用公司名和应用程序名来确保配置的唯一性
self.settings = QSettings("YourCompanyName", "ITECHUnifiedController")
# Store original class properties for measurement labels for flashing
# 这些将用于在闪烁后恢复标签的原始QSS类样式(包括警告色)
self._ps_voltage_label_original_class = "measurement_value"
self._ps_current_label_original_class = "measurement_value"
self._ps_power_label_original_class = "measurement_value"
self._el_voltage_label_original_class = "measurement_value"
self._el_current_label_original_class = "measurement_value"
self._el_power_label_original_class = "measurement_value"
# 初始化 QMediaPlayer
self.media_player = QMediaPlayer()
self.init_ui() # 先初始化UI,确保所有控件都已创建
# 在UI初始化完成后再加载设置,此时 status_log_edit 已经存在
self.load_settings()
self.apply_stylesheet()
self.log_message("<font color='#666666'>应用程序已启动。</font> 请连接设备。") # 普通日志颜色
def load_settings(self):
"""从QSettings加载保存的配置。"""
# 从设置中获取并设置到UI元素
self.ps_visa_entry.setText(self.settings.value("ps_visa_resource", self.DEFAULT_PS_VISA_RESOURCE))
self.el_visa_entry.setText(self.settings.value("el_visa_resource", self.DEFAULT_EL_VISA_RESOURCE))
try:
self.ps_voltage_spinbox.setValue(float(self.settings.value("ps_voltage_default_on_connect", str(self.DEFAULT_PS_VOLTAGE))))
except ValueError:
self.ps_voltage_spinbox.setValue(self.DEFAULT_PS_VOLTAGE)
try:
self.ps_current_limit_spinbox.setValue(float(self.settings.value("ps_current_limit_default_on_connect", str(self.DEFAULT_PS_CURRENT))))
except ValueError:
self.ps_current_limit_spinbox.setValue(self.DEFAULT_PS_CURRENT)
try:
self.el_current_spinbox.setValue(float(self.settings.value("el_current_default_on_connect", str(self.DEFAULT_EL_CURRENT))))
except ValueError:
self.el_current_spinbox.setValue(self.DEFAULT_EL_CURRENT)
self.log_message("<font color='#666666'>已加载用户设置。</font>") # 普通日志颜色
def save_settings(self):
"""将当前配置保存到QSettings。"""
self.settings.setValue("ps_visa_resource", self.ps_visa_entry.text())
self.settings.setValue("el_visa_resource", self.el_visa_entry.text())
self.settings.setValue("ps_voltage_default_on_connect", self.ps_voltage_spinbox.value())
self.settings.setValue("ps_current_limit_default_on_connect", self.ps_current_limit_spinbox.value())
self.settings.setValue("el_current_default_on_connect", self.el_current_spinbox.value())
self.log_message("<font color='#666666'>用户设置已保存。</font>") # 普通日志颜色
def apply_stylesheet(self):
"""应用新的浅色主题和现代化UI样式。"""
self.setStyleSheet("""
QMainWindow {
background-color: #F0F4F8; /* 浅灰色/蓝灰色主背景 */
color: #333333; /* 默认文字颜色 */
}
QGroupBox {
background-color: #E0E6F0; /* 组框背景色,比主背景稍暗 */
color: #333333;
border: 1px solid #C0D0E0; /* 组框边框 */
border-radius: 8px;
margin-top: 2.5ex; /* 顶部留出标题空间 */
font-weight: bold;
font-size: 11pt;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top center;
padding: 0 15px; /* 增加标题内边距 */
background-color: #607B8B; /* 标题背景色 ( muted blue-gray) */
color: #FFFFFF; /* 标题文字颜色 */
border-radius: 6px;
font-size: 12pt; /* 增大标题字体 */
}
QLabel {
color: #333333; /* 普通标签文字颜色 */
font-size: 10pt;
}
QLineEdit, QDoubleSpinBox {
background-color: #FFFFFF; /* 输入框背景色 */
color: #333333;
border: 1px solid #B0C4DE; /* 输入框边框 (Light Steel Blue) */
border-radius: 5px;
padding: 10px; /* 增加内边距 */
font-size: 11pt; /* 增大字体 */
}
QLineEdit:disabled, QDoubleSpinBox:disabled {
background-color: #E8E8E8;
color: #888888;
border: 1px dashed #D0D0D0; /* Disabled border style */
}
QPushButton {
background-color: #4682B4; /* 按钮默认蓝色 (Steel Blue) */
color: white;
border: none;
border-radius: 5px;
padding: 12px 24px; /* 增加按钮内边距 */
font-size: 11pt; /* 增大字体 */
font-weight: bold;
min-width: 120px; /* 增加最小宽度 */
min-height: 35px; /* 增加最小高度 */
/* QSS的'transition'属性在PyQt中可能不完全支持或导致警告,
故为避免“Unknown property transition”警告,此处已移除。
按钮的颜色变化将为即时切换。 */
}
QPushButton:hover {
background-color: #5A9BD6; /* 鼠标悬停时更亮的蓝色 */
}
QPushButton:pressed {
background-color: #3A6F9B; /* 鼠标按下时更暗的蓝色 */
}
QPushButton:disabled {
background-color: #AAAAAA;
color: #666666;
}
QTextEdit {
background-color: #FFFFFF; /* 日志区背景色 */
color: #333333; /* 日志区文字颜色 */
border: 1px solid #B0C4DE;
border-radius: 5px;
font-family: 'Consolas', 'Courier New', monospace;
font-size: 9pt;
padding: 10px; /* 增加内边距 */
}
/* 状态指示器标签 */
QLabel.status_indicator {
min-width: 24px; /* 增大指示器尺寸 */
min-height: 24px;
max-width: 24px;
max-height: 24px;
border-radius: 12px; /* 圆形 */
background-color: #6C757D; /* 默认灰色 (未知) */
border: 1px solid #999999;
}
QLabel.status_indicator.on {
background-color: #28A745; /* 绿色 */
}
QLabel.status_indicator.off {
background-color: #DC3545; /* 红色 */
}
QLabel.status_indicator.connecting {
background-color: #FFC107; /* 琥珀色 (连接中) */
}
QLabel.status_indicator.unknown {
background-color: #6C757D; /* 灰色 */
}
/* 测量值标签样式 - 醒目化 */
QLabel.measurement_value {
font-weight: bold;
color: #007BFF; /* 醒目蓝色 */
font-size: 14pt; /* 进一步增大字体 */
padding: 4px 0; /* 增加垂直内边距 */
}
QLabel.measurement_value.warning { /* 零值或异常值 */
color: #DC3545; /* 红色 */
}
/* 分隔线 */
QFrame#horizontalLine {
background-color: #A9B1BB; /* 浅色主题下的分隔线颜色 */
height: 1px;
margin: 10px 0; /* 增加上下间距 */
}
""")
def init_ui(self):
"""初始化用户界面布局和控件。"""
main_widget = QWidget()
self.setCentralWidget(main_widget)
main_layout = QVBoxLayout(main_widget)
main_layout.setContentsMargins(20, 20, 20, 20) # 增加整体边距
main_layout.setSpacing(25) # 增加主要布局间距
# --- 连接面板 ---
connection_panel = QGroupBox("设备连接")
connection_layout = QGridLayout()
connection_layout.setContentsMargins(20, 30, 20, 20)
connection_layout.setVerticalSpacing(15) # 增加垂直间距
connection_layout.setHorizontalSpacing(20) # 增加水平间距
# 电源连接区域
connection_layout.addWidget(QLabel("电源 VISA 地址:"), 0, 0, Qt.AlignRight)
self.ps_visa_entry = QLineEdit(self.DEFAULT_PS_VISA_RESOURCE)
connection_layout.addWidget(self.ps_visa_entry, 0, 1)
self.ps_browse_button = QPushButton("浏览...")
self.ps_browse_button.setFixedWidth(80) # 固定宽度
self.ps_browse_button.clicked.connect(lambda: self.browse_visa_resource(self.ps_visa_entry))
connection_layout.addWidget(self.ps_browse_button, 0, 2) # 新增浏览按钮
self.ps_connect_button = QPushButton("连接电源")
self.ps_connect_button.clicked.connect(self.toggle_ps_connection)
connection_layout.addWidget(self.ps_connect_button, 0, 3) # 移动连接按钮
self.ps_idn_label = QLabel("电源 IDN: 未连接")
connection_layout.addWidget(self.ps_idn_label, 1, 0, 1, 4) # 占据四列
# 电子负载连接区域
connection_layout.addWidget(QLabel("负载 VISA 地址:"), 2, 0, Qt.AlignRight)
self.el_visa_entry = QLineEdit(self.DEFAULT_EL_VISA_RESOURCE)
connection_layout.addWidget(self.el_visa_entry, 2, 1)
self.el_browse_button = QPushButton("浏览...")
self.el_browse_button.setFixedWidth(80) # 固定宽度
self.el_browse_button.clicked.connect(lambda: self.browse_visa_resource(self.el_visa_entry))
connection_layout.addWidget(self.el_browse_button, 2, 2) # 新增浏览按钮
self.el_connect_button = QPushButton("连接负载")
self.el_connect_button.clicked.connect(self.toggle_el_connection)
connection_layout.addWidget(self.el_connect_button, 2, 3) # 移动连接按钮
self.el_idn_label = QLabel("电子负载 IDN: 未连接")
connection_layout.addWidget(self.el_idn_label, 3, 0, 1, 4)
# VISA 资源扫描按钮 (保留用于全面日志输出)
self.scan_visa_button = QPushButton("扫描所有 VISA 资源 (至日志)")
self.scan_visa_button.clicked.connect(self.scan_visa_resources)
connection_layout.addWidget(self.scan_visa_button, 4, 0, 1, 4) # 占据整行
connection_panel.setLayout(connection_layout)
main_layout.addWidget(connection_panel)
# --- 控制面板 (电源和负载并排) ---
controls_layout = QHBoxLayout()
controls_layout.setSpacing(30) # 增加组框之间的间距
# 电源控制组
ps_controls_group = QGroupBox("电源控制 (IT6000)")
ps_grid = QGridLayout()
ps_grid.setContentsMargins(20, 30, 20, 20)
ps_grid.setVerticalSpacing(12) # 增加垂直间距
ps_grid.setHorizontalSpacing(20) # 增加水平间距
ps_grid.addWidget(QLabel("设定电压 (V):"), 0, 0, Qt.AlignRight)
self.ps_voltage_spinbox = QDoubleSpinBox()
self.ps_voltage_spinbox.setRange(0, 500.0) # 根据IT6000型号调整
self.ps_voltage_spinbox.setDecimals(3)
self.ps_voltage_spinbox.setValue(self.DEFAULT_PS_VOLTAGE) # 初始使用默认值
ps_grid.addWidget(self.ps_voltage_spinbox, 0, 1)
self.ps_set_voltage_button = QPushButton("设定电压")
self.ps_set_voltage_button.clicked.connect(self.set_ps_voltage)
ps_grid.addWidget(self.ps_set_voltage_button, 0, 2)
ps_grid.addWidget(QLabel("设定电流上限 (A):"), 1, 0, Qt.AlignRight)
self.ps_current_limit_spinbox = QDoubleSpinBox()
self.ps_current_limit_spinbox.setRange(0, 180.0) # 根据IT6000型号调整 (例如IT6018B是180A)
self.ps_current_limit_spinbox.setDecimals(3)
self.ps_current_limit_spinbox.setValue(self.DEFAULT_PS_CURRENT) # 初始使用默认值
ps_grid.addWidget(self.ps_current_limit_spinbox, 1, 1)
self.ps_set_current_limit_button = QPushButton("设定电流上限")
self.ps_set_current_limit_button.clicked.connect(self.set_ps_current_limit)
ps_grid.addWidget(self.ps_set_current_limit_button, 1, 2)
# 输出控制和状态
ps_grid.addWidget(QLabel("电源输出:"), 2, 0, Qt.AlignRight)
output_control_layout = QHBoxLayout()
output_control_layout.setSpacing(10) # 按钮间距
self.ps_output_on_button = QPushButton("打开输出")
self.ps_output_on_button.clicked.connect(lambda: self.set_ps_output_state(True))
output_control_layout.addWidget(self.ps_output_on_button)
self.ps_output_off_button = QPushButton("关闭输出")
self.ps_output_off_button.clicked.connect(lambda: self.set_ps_output_state(False))
output_control_layout.addWidget(self.ps_output_off_button)
ps_grid.addLayout(output_control_layout, 2, 1)
output_status_layout = QHBoxLayout()
output_status_layout.setSpacing(8) # 指示器和标签间距
self.ps_output_status_indicator = QLabel()
self.ps_output_status_indicator.setProperty("class", "status_indicator unknown")
self.ps_output_status_indicator._is_pulsing = False # Custom flag for pulsing animation
output_status_layout.addWidget(self.ps_output_status_indicator, alignment=Qt.AlignRight)
self.ps_output_status_label = QLabel("未知")
output_status_layout.addWidget(self.ps_output_status_label)
output_status_layout.addStretch(1) # 确保标签不会被挤压
ps_grid.addLayout(output_status_layout, 2, 2, Qt.AlignLeft)
# 测量值显示
ps_grid.addWidget(self.create_horizontal_line(), 3, 0, 1, 3) # 分隔线占据三列
ps_grid.addWidget(QLabel("实际电压 (V):"), 4, 0, Qt.AlignRight)
self.ps_measured_voltage_label = QLabel("---")
self.ps_measured_voltage_label.setProperty("class", self._ps_voltage_label_original_class)
ps_grid.addWidget(self.ps_measured_voltage_label, 4, 1, 1, 2)
ps_grid.addWidget(QLabel("实际电流 (A):"), 5, 0, Qt.AlignRight)
self.ps_measured_current_label = QLabel("---")
self.ps_measured_current_label.setProperty("class", self._ps_current_label_original_class)
ps_grid.addWidget(self.ps_measured_current_label, 5, 1, 1, 2)
ps_grid.addWidget(QLabel("实际功率 (W):"), 6, 0, Qt.AlignRight)
self.ps_measured_power_label = QLabel("---")
self.ps_measured_power_label.setProperty("class", self._ps_power_label_original_class)
ps_grid.addWidget(self.ps_measured_power_label, 6, 1, 1, 2)
self.ps_refresh_button = QPushButton("刷新电源状态")
self.ps_refresh_button.clicked.connect(self.refresh_ps_status)
ps_grid.addWidget(self.ps_refresh_button, 7, 0, 1, 3)
ps_controls_group.setLayout(ps_grid)
controls_layout.addWidget(ps_controls_group)
self.set_ps_controls_enabled(False) # 初始禁用
# 电子负载控制组
el_controls_group = QGroupBox("电子负载控制 (IT8902E)")
el_grid = QGridLayout()
el_grid.setContentsMargins(20, 30, 20, 20)
el_grid.setVerticalSpacing(12)
el_grid.setHorizontalSpacing(20)
el_grid.addWidget(QLabel("设定电流 (A):"), 0, 0, Qt.AlignRight)
self.el_current_spinbox = QDoubleSpinBox()
self.el_current_spinbox.setRange(0, 240.0) # 根据IT8902E型号调整 (例如240A)
self.el_current_spinbox.setDecimals(3)
self.el_current_spinbox.setValue(self.DEFAULT_EL_CURRENT) # 初始使用默认值
el_grid.addWidget(self.el_current_spinbox, 0, 1)
self.el_set_current_button = QPushButton("设定负载电流")
self.el_set_current_button.clicked.connect(self.set_el_current)
el_grid.addWidget(self.el_set_current_button, 0, 2)
# 负载输入控制和状态
el_grid.addWidget(QLabel("负载输入:"), 1, 0, Qt.AlignRight)
input_control_layout = QHBoxLayout()
input_control_layout.setSpacing(10)
self.el_input_on_button = QPushButton("打开负载")
self.el_input_on_button.clicked.connect(lambda: self.set_el_input_state(True))
input_control_layout.addWidget(self.el_input_on_button)
self.el_input_off_button = QPushButton("关闭负载")
self.el_input_off_button.clicked.connect(lambda: self.set_el_input_state(False))
input_control_layout.addWidget(self.el_input_off_button)
el_grid.addLayout(input_control_layout, 1, 1)
input_status_layout = QHBoxLayout()
input_status_layout.setSpacing(8)
self.el_input_status_indicator = QLabel()
self.el_input_status_indicator.setProperty("class", "status_indicator unknown")
self.el_input_status_indicator._is_pulsing = False # Custom flag for pulsing animation
input_status_layout.addWidget(self.el_input_status_indicator, alignment=Qt.AlignRight)
self.el_input_status_label = QLabel("未知")
input_status_layout.addWidget(self.el_input_status_label)
input_status_layout.addStretch(1)
el_grid.addLayout(input_status_layout, 1, 2, Qt.AlignLeft)
# 负载测量值显示
el_grid.addWidget(self.create_horizontal_line(), 2, 0, 1, 3) # 分隔线
el_grid.addWidget(QLabel("测量电压 (V):"), 3, 0, Qt.AlignRight)
self.el_measured_voltage_label = QLabel("---")
self.el_measured_voltage_label.setProperty("class", self._el_voltage_label_original_class)
el_grid.addWidget(self.el_measured_voltage_label, 3, 1, 1, 2)
el_grid.addWidget(QLabel("测量电流 (A):"), 4, 0, Qt.AlignRight)
self.el_measured_current_label = QLabel("---")
self.el_measured_current_label.setProperty("class", self._el_current_label_original_class)
el_grid.addWidget(self.el_measured_current_label, 4, 1, 1, 2)
el_grid.addWidget(QLabel("测量功率 (W):"), 5, 0, Qt.AlignRight)
self.el_measured_power_label = QLabel("---")
self.el_measured_power_label.setProperty("class", self._el_power_label_original_class)
el_grid.addWidget(self.el_measured_power_label, 5, 1, 1, 2)
self.el_refresh_button = QPushButton("刷新负载状态")
self.el_refresh_button.clicked.connect(self.refresh_el_status)
el_grid.addWidget(self.el_refresh_button, 6, 0, 1, 3)
el_controls_group.setLayout(el_grid)
controls_layout.addWidget(el_controls_group)
self.set_el_controls_enabled(False) # 初始禁用
main_layout.addLayout(controls_layout)
# --- 状态日志面板 ---
log_group = QGroupBox("状态日志")
log_layout = QVBoxLayout()
log_layout.setContentsMargins(20, 30, 20, 20)
self.status_log_edit = QTextEdit() # 在这里创建 status_log_edit
self.status_log_edit.setReadOnly(True)
log_layout.addWidget(self.status_log_edit)
log_group.setLayout(log_layout)
main_layout.addWidget(log_group)
main_layout.setStretchFactor(log_group, 1) # 使日志区域自动扩展
def create_horizontal_line(self):
"""创建水平分隔线。"""
line = QFrame()
line.setFrameShape(QFrame.HLine)
line.setFrameShadow(QFrame.Sunken)
line.setObjectName("horizontalLine")
return line
def log_message(self, message):
"""在状态日志文本框中添加时间戳消息。"""
timestamp = time.strftime('<font color="#666666">%H:%M:%S</font>') # 时间戳颜色
# 确保 self.status_log_edit 在调用此方法时已存在
if hasattr(self, 'status_log_edit') and self.status_log_edit is not None:
self.status_log_edit.append(f"{timestamp} - {message}")
self.status_log_edit.ensureCursorVisible() # 自动滚动到最新消息
else:
# 如果在UI完全初始化前调用,可以暂时打印到控制台
print(f"LOG (pre-UI): {timestamp} - {message}")
def update_status_indicator(self, indicator_label: QLabel, status: str):
"""更新状态指示器的颜色和文本,并处理连接中的脉动效果。"""
# 在设置新状态前,如果当前正在脉动且新状态不是连接中,则停止脉动
if status.upper() != "CONNECTING" and hasattr(indicator_label, '_is_pulsing') and indicator_label._is_pulsing:
indicator_label._is_pulsing = False # 通知脉动函数停止
indicator_label.setStyleSheet("") # 清除直接样式表,让QSS类样式生效
# 确保类属性已正确设置到新状态,以便QSS应用正确的颜色
indicator_label.setProperty("class", f"status_indicator {status.lower()}")
indicator_label.style().polish(indicator_label) # 强制刷新样式
indicator_label.setText("") # 清空文本
if status.upper() == "ON" or status == "1":
indicator_label.setProperty("class", "status_indicator on")
elif status.upper() == "OFF" or status == "0":
indicator_label.setProperty("class", "status_indicator off")
elif status.upper() == "CONNECTING":
indicator_label.setProperty("class", "status_indicator connecting")
self.pulse_indicator(indicator_label, "#FFC107") # 启动脉动 (琥珀色)
else: # UNKNOWN
indicator_label.setProperty("class", "status_indicator unknown")
indicator_label.style().polish(indicator_label) # 强制样式刷新
def pulse_indicator(self, indicator_label: QLabel, target_color_str: str):
"""使指示器在连接状态下脉动。"""
# 只有当指示器确实处于“connecting”状态且未在脉动时才启动
if not indicator_label.property("class") == "status_indicator connecting":
indicator_label._is_pulsing = False # 确保标志正确
return
if not hasattr(indicator_label, '_is_pulsing') or not indicator_label._is_pulsing:
indicator_label._is_pulsing = True # 标记为正在脉动
pulse_color_start = QColor(target_color_str).darker(150) # 较暗的颜色
pulse_color_end = QColor(target_color_str) # 原始颜色
def toggle_pulse_color():
if not indicator_label._is_pulsing: # 检查标志,如果为False则停止脉动
indicator_label.setStyleSheet("") # 清除直接样式,恢复到QSS类样式
indicator_label.style().polish(indicator_label)
return
current_bg_style = indicator_label.styleSheet()
# 检查当前背景色是否为“结束色”,如果是,则切换到“开始色”
# 注意:这里通过字符串匹配,可能会有格式问题,但通常有效
if f"background-color: {pulse_color_end.name()}" in current_bg_style.replace(";", "").replace(" ", ""):
indicator_label.setStyleSheet(f"background-color: {pulse_color_start.name()}; border-radius: 12px; border: 1px solid #999999;") # 保持边框
else: # 否则(包括没有直接样式或处于开始色),切换到“结束色”
indicator_label.setStyleSheet(f"background-color: {pulse_color_end.name()}; border-radius: 12px; border: 1px solid #999999;") # 保持边框
QTimer.singleShot(500, toggle_pulse_color) # 0.5秒后再次切换颜色
# 启动脉动(如果尚未启动)
if indicator_label._is_pulsing:
toggle_pulse_color()
def flash_measurement_label(self, label: QLabel, original_class: str):
"""使测量值标签短暂闪烁以突出更新。"""
# 应用临时的强调样式,通过直接设置样式表
highlight_color = "#FFFF00" # 亮黄色用于闪烁
# 从原始样式中获取 font-weight 和 font-size,并应用于闪烁样式,确保字体样式在闪烁时也保持
font_weight = label.font().weight()
font_size = label.font().pointSize()
label.setStyleSheet(f"color: {highlight_color}; font-weight: {font_weight}; font-size: {font_size}pt;")
# 短暂延迟后,恢复到原始样式
QTimer.singleShot(200, lambda: self.revert_label_style(label, original_class))
def revert_label_style(self, label: QLabel, original_class: str):
"""将测量值标签的样式恢复到其原始的QSS类状态。"""
label.setStyleSheet("") # 清除任何直接样式表,以便QSS类规则能够生效
label.setProperty("class", original_class) # 重新应用原始的类属性
label.style().polish(label) # 强制重新计算和应用QSS规则
def play_sound(self, file_path):
"""播放指定的音效文件。"""
if not os.path.exists(file_path):
self.log_message(f"<font color='#FFC107'>警告:</font> 音效文件未找到: {file_path}")
return
url = QUrl.fromLocalFile(file_path)
content = QMediaContent(url)
if content.isNull():
self.log_message(f"<font color='#FFC107'>警告:</font> 无法加载或解析音效文件内容: {file_path}")
return
self.media_player.setMedia(content)
self.media_player.play()
self.log_message(f"<font color='#666666'>播放音效: {os.path.basename(file_path)}</font>")
def scan_visa_resources(self):
"""扫描并列出所有可用的VISA资源到日志。"""
self.log_message("<font color='#17A2B8'>正在扫描可用的 VISA 资源...</font>") # 信息色
try:
resources = self.rm.list_resources()
if resources:
self.log_message("<font color='#28A745'>扫描完成!以下是可用的 VISA 资源:</font>") # 成功色
for r in resources:
self.log_message(f" - <font color='#20B2AA'>{r}</font>") # 偏青色
self.log_message("<font color='#666666'>请将需要的地址复制到输入框中,或使用 '浏览...' 按钮。</font>")
else:
self.log_message("<font color='#FFC107'>未找到任何 VISA 资源。请确保 VISA 驱动已正确安装并设备已连接。</font>") # 警告色
except Exception as e:
self.log_message(f"<font color='#DC3545'>扫描 VISA 资源时发生错误: {e}</font>") # 错误色
QMessageBox.critical(self, "VISA 扫描错误",
f"扫描 VISA 资源时发生错误: {e}\n\n请确认PyVISA后端已正确配置,并且设备驱动已安装。")
def browse_visa_resource(self, target_entry: QLineEdit):
"""打开VISA扫描对话框,让用户选择资源。"""
dialog = VisaScanDialog(self.rm, self)
if dialog.exec_() == QDialog.Accepted:
if dialog.selected_resource:
target_entry.setText(dialog.selected_resource)
self.log_message(f"<font color='#666666'>已选择 VISA 资源:</font> <font color='#20B2AA'>{dialog.selected_resource}</font>")
else:
self.log_message("<font color='#FFC107'>警告:</font> 未选择任何 VISA 资源。")
# --- 电源 (PS) 特定功能 ---
def toggle_ps_connection(self):
"""根据当前连接状态切换电源的连接/断开。"""
if self.ps_worker and self.ps_worker._is_connected:
self.disconnect_ps()
else:
self.connect_ps()
def connect_ps(self):
"""启动线程连接电源设备。"""
visa_resource = self.ps_visa_entry.text().strip()
if not visa_resource:
QMessageBox.critical(self, "连接错误", "请输入有效的电源 VISA 资源名称。")
return
self.ps_connect_button.setEnabled(False)
self.ps_connect_button.setText("连接中...")
self.ps_browse_button.setEnabled(False) # 连接时禁用浏览按钮
self.update_status_indicator(self.ps_output_status_indicator, "CONNECTING")
self.ps_output_status_label.setText("连接中...")
# 初始化并启动工作线程
self.ps_thread = QThread()
# 传入从UI获取的VISA资源字符串
self.ps_worker = DeviceWorker(self.rm, visa_resource, "电源")
self.ps_worker.moveToThread(self.ps_thread)
# 连接工作器信号到GUI槽
self.ps_worker.connected.connect(self.on_ps_connected_threaded)
self.ps_worker.disconnected.connect(self.on_ps_disconnected_threaded)
self.ps_worker.error.connect(self.show_error_message)
self.ps_worker.log_message_signal.connect(self.log_message)
# 连接GUI信号到工作器槽 (用于发送命令)
self.ps_connect_request.connect(self.ps_worker.connect_device)
self.ps_disconnect_request.connect(self.ps_worker.disconnect_device)
self.ps_command_request.connect(self.ps_worker.process_command)
self.ps_refresh_request.connect(self.ps_worker.refresh_status_and_measurements)
# 连接工作器更新信号到GUI更新槽
self.ps_worker.ps_settings_updated.connect(self.update_ps_settings_ui)
self.ps_worker.ps_measurements_updated.connect(self.update_ps_measurements_ui)
self.ps_worker.ps_output_status_updated.connect(self.update_ps_output_status_ui)
self.ps_thread.started.connect(self.ps_connect_request) # 线程启动时发出连接请求
self.ps_thread.start()
@pyqtSlot(str, str)
def on_ps_connected_threaded(self, device_name, idn):
"""电源连接成功后的回调函数 (在GUI线程中执行)。"""
self.ps_idn_label.setText(f"电源 IDN: {idn}")
self.ps_connect_button.setText("断开电源")
self.ps_visa_entry.setEnabled(False)
self.ps_browse_button.setEnabled(False) # 连接后禁用浏览按钮
self.set_ps_controls_enabled(True)
# 初始设定并刷新状态
# 这些操作现在通过信号发送到工作线程,避免GUI阻塞
# 使用UI中的值进行初始化,这些值已经由 load_settings 加载
self.ps_command_request.emit("VOLT", str(self.ps_voltage_spinbox.value()), False, "set_initial_volt")
self.ps_command_request.emit("CURR", str(self.ps_current_limit_spinbox.value()), False, "set_initial_curr")
# 触发一次全面刷新以获取所有当前状态
self.ps_refresh_request.emit()
self.ps_connect_button.setEnabled(True)
# 状态指示器将在刷新后更新
def disconnect_ps(self):
"""断开电源设备连接。"""
if not self.ps_worker or not self.ps_worker._is_connected:
self.log_message("<font color='#FFC107'>警告:</font> 电源已断开或未连接,无需操作。")
return
self.set_ps_controls_enabled(False) # 立即禁用控件
self.ps_connect_button.setEnabled(False)
self.ps_connect_button.setText("断开中...")
self.update_status_indicator(self.ps_output_status_indicator, "OFF") # 立即显示为关闭状态
# 先发送关闭输出命令,再发送断开连接请求
self.ps_command_request.emit("OUTP", "OFF", False, "disconnect_cleanup")
# 稍微等待确保命令被工作器接收,然后触发断开
QThread.msleep(100) # 非阻塞等待
self.ps_disconnect_request.emit() # 触发工作线程的断开槽
@pyqtSlot(str)
def on_ps_disconnected_threaded(self, device_name):
"""电源断开后的回调函数 (在GUI线程中执行)。"""
self.ps_idn_label.setText("电源 IDN: 未连接")
self.ps_connect_button.setText("连接电源")
self.ps_connect_button.setEnabled(True)
self.ps_visa_entry.setEnabled(True)
self.ps_browse_button.setEnabled(True) # 断开后启用浏览按钮
self.set_ps_controls_enabled(False)
self.update_status_indicator(self.ps_output_status_indicator, "UNKNOWN")
self.ps_output_status_label.setText("未知")
self.ps_measured_voltage_label.setText("---")
self.ps_measured_current_label.setText("---")