-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathusb.py
More file actions
1242 lines (1047 loc) · 57.7 KB
/
usb.py
File metadata and controls
1242 lines (1047 loc) · 57.7 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
# -*- coding:utf-8 -*-
import ctypes
import cantools
import os
import time
import platform
import threading # 尽管主要用QThread,但queue仍需
import queue # 用于日志队列
# PyQt5 imports
from PyQt5 import QtWidgets, QtCore, QtGui
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, \
QHBoxLayout, QGridLayout, QLabel, QPushButton, QLineEdit, QComboBox, \
QTextEdit, QGroupBox, QFileDialog, QMessageBox, QCheckBox
from PyQt5.QtCore import QObject, QThread, pyqtSignal, QTimer, Qt
from PyQt5.QtGui import QIcon, QFont, QTextCursor
# Matplotlib imports for PyQt5
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT as NavigationToolbar
import numpy as np
import configparser
# --- 从 zlgcan.py 文件中直接复制的 ZLG CAN Wrapper Classes and Definitions ---
# 此部分代码直接来源于您提供的 zlgcan.py,是最核心的 DLL 交互逻辑。
# 它假定 zlgcan.dll (Windows) 或 ControlCAN.dll (Windows) 文件可用。
from ctypes import * # 注意:这里使用 from ctypes import * 会导入 windll
# 重新定义 ZCAN_DEVICE_TYPE,因为 from ctypes import * 会覆盖
ZCAN_DEVICE_TYPE = c_uint
INVALID_DEVICE_HANDLE = 0
INVALID_CHANNEL_HANDLE = 0
'''
设备类型定义
'''
ZCAN_PCI5121 = ZCAN_DEVICE_TYPE(1)
ZCAN_PCI9810 = ZCAN_DEVICE_TYPE(2)
ZCAN_USBCAN1 = ZCAN_DEVICE_TYPE(3)
ZCAN_USBCAN2 = ZCAN_DEVICE_TYPE(4) # 这是我们主要使用的 USBCAN-II
ZCAN_PCI9820 = ZCAN_DEVICE_TYPE(5)
ZCAN_CAN232 = ZCAN_DEVICE_TYPE(6)
ZCAN_PCI5110 = ZCAN_DEVICE_TYPE(7)
ZCAN_CANLITE = ZCAN_DEVICE_TYPE(8)
ZCAN_ISA9620 = ZCAN_DEVICE_TYPE(9)
ZCAN_ISA5420 = ZCAN_DEVICE_TYPE(10)
ZCAN_PC104CAN = ZCAN_DEVICE_TYPE(11)
ZCAN_CANETUDP = ZCAN_DEVICE_TYPE(12)
ZCAN_CANETE = ZCAN_DEVICE_TYPE(12)
ZCAN_DNP9810 = ZCAN_DEVICE_TYPE(13)
ZCAN_PCI9840 = ZCAN_DEVICE_TYPE(14)
ZCAN_PC104CAN2 = ZCAN_DEVICE_TYPE(15)
ZCAN_PCI9820I = ZCAN_DEVICE_TYPE(16)
ZCAN_CANETTCP = ZCAN_DEVICE_TYPE(17)
ZCAN_PCIE_9220 = ZCAN_DEVICE_TYPE(18)
ZCAN_PCI5010U = ZCAN_DEVICE_TYPE(19)
ZCAN_USBCAN_E_U = ZCAN_DEVICE_TYPE(20) # USBCAN-E-U
ZCAN_USBCAN_2E_U = ZCAN_DEVICE_TYPE(21) # USBCAN-2E-U
ZCAN_PCI5020U = ZCAN_DEVICE_TYPE(22)
ZCAN_EG20T_CAN = ZCAN_DEVICE_TYPE(23)
ZCAN_PCIE9221 = ZCAN_DEVICE_TYPE(24)
ZCAN_WIFICAN_TCP = ZCAN_DEVICE_TYPE(25)
ZCAN_WIFICAN_UDP = ZCAN_DEVICE_TYPE(26)
ZCAN_PCIe9120 = ZCAN_DEVICE_TYPE(27)
ZCAN_PCIe9110 = ZCAN_DEVICE_TYPE(28)
ZCAN_PCIe9140 = ZCAN_DEVICE_TYPE(29)
ZCAN_USBCAN_4E_U = ZCAN_DEVICE_TYPE(31)
ZCAN_CANDTU_200UR = ZCAN_DEVICE_TYPE(32)
ZCAN_CANDTU_MINI = ZCAN_DEVICE_TYPE(33)
ZCAN_USBCAN_8E_U = ZCAN_DEVICE_TYPE(34)
ZCAN_CANREPLAY = ZCAN_DEVICE_TYPE(35)
ZCAN_CANDTU_NET = ZCAN_DEVICE_TYPE(36)
ZCAN_CANDTU_100UR = ZCAN_DEVICE_TYPE(37)
ZCAN_PCIE_CANFD_100U = ZCAN_DEVICE_TYPE(38)
ZCAN_PCIE_CANFD_200U = ZCAN_DEVICE_TYPE(39)
ZCAN_PCIE_CANFD_400U = ZCAN_DEVICE_TYPE(40)
ZCAN_USBCANFD_200U = ZCAN_DEVICE_TYPE(41)
ZCAN_USBCANFD_100U = ZCAN_DEVICE_TYPE(42)
ZCAN_USBCANFD_MINI = ZCAN_DEVICE_TYPE(43)
ZCAN_CANFDCOM_100IE = ZCAN_DEVICE_TYPE(44)
ZCAN_CANSCOPE = ZCAN_DEVICE_TYPE(45)
ZCAN_CLOUD = ZCAN_DEVICE_TYPE(46)
ZCAN_CANDTU_NET_400 = ZCAN_DEVICE_TYPE(47)
ZCAN_CANFDNET_200U_TCP = ZCAN_DEVICE_TYPE(48)
ZCAN_CANFDNET_200U_UDP = ZCAN_DEVICE_TYPE(49)
ZCAN_CANFDWIFI_100U_TCP = ZCAN_DEVICE_TYPE(50)
ZCAN_CANFDWIFI_100U_UDP = ZCAN_DEVICE_TYPE(51)
ZCAN_CANFDNET_400U_TCP = ZCAN_DEVICE_TYPE(52)
ZCAN_CANFDNET_400U_UDP = ZCAN_DEVICE_TYPE(53)
ZCAN_CANFDBLUE_200U = ZCAN_DEVICE_TYPE(54)
ZCAN_CANFDNET_100U_TCP = ZCAN_DEVICE_TYPE(55)
ZCAN_CANFDNET_100U_UDP = ZCAN_DEVICE_TYPE(56)
ZCAN_CANFDNET_800U_TCP = ZCAN_DEVICE_TYPE(57)
ZCAN_CANFDNET_800U_UDP = ZCAN_DEVICE_TYPE(58)
ZCAN_USBCANFD_800U = ZCAN_DEVICE_TYPE(59)
ZCAN_PCIE_CANFD_100U_EX = ZCAN_DEVICE_TYPE(60)
ZCAN_PCIE_CANFD_400U_EX = ZCAN_DEVICE_TYPE(61)
ZCAN_PCIE_CANFD_200U_MINI = ZCAN_DEVICE_TYPE(62)
ZCAN_PCIE_CANFD_200U_M2 = ZCAN_DEVICE_TYPE(63)
ZCAN_PCIE_CANFD_200U_EX = ZCAN_DEVICE_TYPE(62) # 注意:这个值与 MINI 冲突,根据zlgcan.py原样保留
ZCAN_CANFDDTU_400_TCP = ZCAN_DEVICE_TYPE(64)
ZCAN_CANFDDTU_400_UDP = ZCAN_DEVICE_TYPE(65)
ZCAN_CANFDWIFI_200U_TCP = ZCAN_DEVICE_TYPE(66)
ZCAN_CANFDWIFI_200U_UDP = ZCAN_DEVICE_TYPE(67)
ZCAN_CANFDDTU_800ER_TCP = ZCAN_DEVICE_TYPE(68)
ZCAN_CANFDDTU_800ER_UDP = ZCAN_DEVICE_TYPE(69)
ZCAN_CANFDDTU_800EWGR_TCP = ZCAN_DEVICE_TYPE(70)
ZCAN_CANFDDTU_800EWGR_UDP = ZCAN_DEVICE_TYPE(71)
ZCAN_CANFDDTU_600EWGR_TCP = ZCAN_DEVICE_TYPE(72)
ZCAN_CANFDDTU_600EWGR_UDP = ZCAN_DEVICE_TYPE(73)
ZCAN_CANFDDTU_CASCADE_TCP = ZCAN_DEVICE_TYPE(74)
ZCAN_CANFDDTU_CASCADE_UDP = ZCAN_DEVICE_TYPE(75)
ZCAN_USBCANFD_400U = ZCAN_DEVICE_TYPE(76)
ZCAN_CANFDDTU_200U = ZCAN_DEVICE_TYPE(77)
ZCAN_CANFDBRIDGE_PLUS = ZCAN_DEVICE_TYPE(80)
ZCAN_CANFDDTU_300U = ZCAN_DEVICE_TYPE(81)
ZCAN_VIRTUAL_DEVICE = ZCAN_DEVICE_TYPE(99) # 虚拟设备类型
'''
接口返回状态码
'''
ZCAN_STATUS_ERR = 0
ZCAN_STATUS_OK = 1
ZCAN_STATUS_ONLINE = 2
ZCAN_STATUS_OFFLINE = 3
ZCAN_STATUS_UNSUPPORTED = 4
'''
CAN 类型
'''
ZCAN_TYPE_CAN = c_uint(0)
ZCAN_TYPE_CANFD = c_uint(1)
'''
设备信息结构体
'''
class ZCAN_DEVICE_INFO(Structure):
_fields_ = [("hw_Version", c_ushort),
("fw_Version", c_ushort),
("dr_Version", c_ushort),
("in_Version", c_ushort),
("irq_Num", c_ushort),
("can_Num", c_ubyte),
("str_Serial_Num", c_ubyte * 20),
("str_hw_Type", c_ubyte * 40),
("reserved", c_ushort * 4)]
def __str__(self):
# 优化输出,使其更易读
return ("硬件版本:%s\n固件版本:%s\n驱动接口:%s\n接口库接口:%s\n中断号:%d\nCAN通道数:%d\n序列号:%s\n硬件类型:%s" % (
self.hw_version, self.fw_version, self.dr_version, self.in_version, self.irq_num, self.can_num, self.serial,
self.hw_type))
def _version(self, version):
# 格式化版本号
return ("V%02x.%02x" if version // 0xFF >= 9 else "V%d.%02x") % (version // 0xFF, version & 0xFF)
@property
def hw_version(self):
return self._version(self.hw_Version)
@property
def fw_version(self):
return self._version(self.fw_Version)
@property
def dr_version(self):
return self._version(self.dr_Version)
@property
def in_version(self):
return self._version(self.in_Version)
@property
def irq_num(self):
return self.irq_Num
@property
def can_num(self):
return self.can_Num
@property
def serial(self):
serial = ''
for c in self.str_Serial_Num:
if c > 0:
serial += chr(c)
else:
break
return serial
@property
def hw_type(self):
hw_type = ''
for c in self.str_hw_Type:
if c > 0:
hw_type += chr(c)
else:
break
return hw_type
# CAN通道初始化配置结构体
class _ZCAN_CHANNEL_CAN_INIT_CONFIG(Structure):
_fields_ = [("acc_code", c_uint),
("acc_mask", c_uint),
("reserved", c_uint),
("filter", c_ubyte),
("timing0", c_ubyte),
("timing1", c_ubyte),
("mode", c_ubyte)]
# CANFD通道初始化配置结构体 (虽然我们当前只用CAN,但为完整性保留)
class _ZCAN_CHANNEL_CANFD_INIT_CONFIG(Structure):
_fields_ = [("acc_code", c_uint),
("acc_mask", c_uint),
("abit_timing", c_uint),
("dbit_timing", c_uint),
("brp", c_uint),
("filter", c_ubyte),
("mode", c_ubyte),
("pad", c_ushort),
("reserved", c_uint)]
# 通道初始化配置联合体 (根据can_type选择使用CAN或CANFD配置)
class _ZCAN_CHANNEL_INIT_CONFIG(Union):
_fields_ = [("can", _ZCAN_CHANNEL_CAN_INIT_CONFIG), ("canfd", _ZCAN_CHANNEL_CANFD_INIT_CONFIG)]
# 完整的通道初始化配置结构体
class ZCAN_CHANNEL_INIT_CONFIG(Structure):
_fields_ = [("can_type", c_uint),
("config", _ZCAN_CHANNEL_INIT_CONFIG)]
# CAN帧结构体 (用于发送和接收数据帧)
class ZCAN_CAN_FRAME(Structure):
_fields_ = [("can_id", c_uint, 29), # CAN ID,位域
("err", c_uint, 1), # 错误标志
("rtr", c_uint, 1), # 远程帧标志
("eff", c_uint, 1), # 扩展帧标志
("can_dlc", c_ubyte), # 数据长度码
("__pad", c_ubyte), # 填充字节
("__res0", c_ubyte), # 保留字节
("__res1", c_ubyte), # 保留字节
("data", c_ubyte * 8)] # 数据字段 (8字节)
# 发送CAN数据结构体
class ZCAN_Transmit_Data(Structure):
_fields_ = [("frame", ZCAN_CAN_FRAME), ("transmit_type", c_uint)]
# 接收CAN数据结构体
class ZCAN_Receive_Data(Structure):
_fields_ = [("frame", ZCAN_CAN_FRAME), ("timestamp", c_ulonglong)]
# ZCAN 封装类,直接来自您提供的 zlgcan.py
class ZCAN(object):
def __init__(self):
self.__dll = None
if platform.system() == "Windows":
try:
# 尝试加载当前目录下的 zlgcan.dll
self.__dll = windll.LoadLibrary("./zlgcan.dll")
print("成功加载 ./zlgcan.dll")
except OSError as e:
# 如果找不到 zlgcan.dll,尝试加载 ControlCAN.dll
try:
self.__dll = windll.LoadLibrary("ControlCAN.dll")
print("成功加载 ControlCAN.dll (位于系统PATH或当前目录)")
except OSError as e_alt:
# 两个都加载失败,抛出错误,提供详细提示
raise RuntimeError(
f"无法加载 DLL 文件。尝试了 zlgcan.dll 和 ControlCAN.dll。错误: {e_alt}. "
"请确保 DLL 文件在当前目录或系统PATH中,并安装了正确的驱动,"
f"且Python位数({platform.architecture()[0]})与DLL位数匹配。"
)
else:
# 这里的逻辑与原始 zlgcan.py 保持一致,但GUI中我们只针对Windows
print("目前不支持非 Windows 操作系统!")
raise RuntimeError("不支持的操作系统。")
if self.__dll is None:
raise RuntimeError("DLL 加载失败!请检查文件是否存在、路径是否正确以及位数是否匹配。")
# 以下所有方法都直接调用 self.__dll.ZCAN_XXX,并捕获可能的异常
# 这些方法与您原 zlgcan.py 文件中的定义完全一致
def OpenDevice(self, device_type, device_index, reserved):
try:
return self.__dll.ZCAN_OpenDevice(device_type, device_index, reserved)
except AttributeError:
raise AttributeError("DLL中未找到 ZCAN_OpenDevice 函数。请检查DLL是否正确。")
except Exception as e:
raise Exception(f"调用 OpenDevice 异常: {e}")
def CloseDevice(self, device_handle):
try:
return self.__dll.ZCAN_CloseDevice(device_handle)
except AttributeError:
raise AttributeError("DLL中未找到 ZCAN_CloseDevice 函数。请检查DLL是否正确。")
except Exception as e:
raise Exception(f"调用 CloseDevice 异常: {e}")
def GetDeviceInf(self, device_handle):
try:
info = ZCAN_DEVICE_INFO()
ret = self.__dll.ZCAN_GetDeviceInf(device_handle, byref(info))
return info if ret == ZCAN_STATUS_OK else None
except AttributeError:
raise AttributeError("DLL中未找到 ZCAN_GetDeviceInf 函数。请检查DLL是否正确。")
except Exception as e:
raise Exception(f"调用 ZCAN_GetDeviceInf 异常: {e}")
def DeviceOnLine(self, device_handle): # 原始zlgcan.py中有,但GUI未使用
try:
return self.__dll.ZCAN_IsDeviceOnLine(device_handle)
except Exception as e:
raise Exception(f"调用 ZCAN_IsDeviceOnLine 异常: {e}")
def InitCAN(self, device_handle, can_index, init_config):
try:
return self.__dll.ZCAN_InitCAN(device_handle, can_index, byref(init_config))
except AttributeError:
raise AttributeError("DLL中未找到 ZCAN_InitCAN 函数。请检查DLL是否正确。")
except Exception as e:
raise Exception(f"调用 ZCAN_InitCAN 异常: {e}")
def StartCAN(self, chn_handle):
try:
return self.__dll.ZCAN_StartCAN(chn_handle)
except AttributeError:
raise AttributeError("DLL中未找到 ZCAN_StartCAN 函数。请检查DLL是否正确。")
except Exception as e:
raise Exception(f"调用 ZCAN_StartCAN 异常: {e}")
def ResetCAN(self, chn_handle):
try:
return self.__dll.ZCAN_ResetCAN(chn_handle)
except AttributeError:
raise AttributeError("DLL中未找到 ZCAN_ResetCAN 函数。请检查DLL是否正确。")
except Exception as e:
raise Exception(f"调用 ZCAN_ResetCAN 异常: {e}")
def ClearBuffer(self, chn_handle): # 原始zlgcan.py中有,但GUI未使用
try:
return self.__dll.ZCAN_ClearBuffer(chn_handle)
except Exception as e:
raise Exception(f"调用 ZCAN_ClearBuffer 异常: {e}")
# ZCAN_CHANNEL_ERR_INFO 和 ZCAN_CHANNEL_STATUS 未在GUI中使用,但原zlgcan.py有相关结构体,省略其方法
def GetReceiveNum(self, chn_handle, can_type=ZCAN_TYPE_CAN):
try:
return self.__dll.ZCAN_GetReceiveNum(chn_handle, can_type)
except AttributeError:
raise AttributeError("DLL中未找到 ZCAN_GetReceiveNum 函数。请检查DLL是否正确。")
except Exception as e:
raise Exception(f"调用 ZCAN_GetReceiveNum 异常: {e}")
def Transmit(self, chn_handle, std_msg, len): # 原始zlgcan.py中有,但GUI未使用
try:
return self.__dll.ZCAN_Transmit(chn_handle, byref(std_msg), len)
except Exception as e:
raise Exception(f"调用 Transmit 异常: {e}")
def Receive(self, chn_handle, rcv_num, wait_time=c_int(-1)):
try:
rcv_can_msgs = (ZCAN_Receive_Data * rcv_num)()
ret = self.__dll.ZCAN_Receive(chn_handle, byref(rcv_can_msgs), rcv_num, wait_time)
return rcv_can_msgs, ret
except AttributeError:
raise AttributeError("DLL中未找到 ZCAN_Receive 函数。请检查DLL是否正确。")
except Exception as e:
raise Exception(f"调用 Receive 异常: {e}")
# TransmitFD, ReceiveFD, GetIProperty, SetValue, GetValue, ReleaseIProperty
# 这些方法在原始 zlgcan.py 中有,但GUI未使用,为保持简洁性,省略其实现。
# 仅保留 ZCAN_SetValue,因为它是GUI中设置波特率的关键
def ZCAN_SetValue(self, device_handle, path, value):
# 原始zlgcan.py中这里是 c_void_p,但实际传入的是 bytes 类型。
# 强制设置 argtypes 确保正确传递。
self.__dll.ZCAN_SetValue.argtypes = [c_void_p, c_char_p, c_void_p]
try:
# value 通常是字符串编码后的字节,所以不需要在这里再编码
return self.__dll.ZCAN_SetValue(device_handle, path.encode("utf-8"), value)
except AttributeError:
raise AttributeError("DLL中未找到 ZCAN_SetValue 函数。请检查DLL是否正确。")
except Exception as e:
raise Exception(f"调用 ZCAN_SetValue 异常: {e}")
# 原始zlgcan.py中的 ZCAN_GetValue 方法未在GUI中使用,省略其实现。
# --- ZLG CAN Wrapper Classes and Definitions END ---
# ==============================================================================
# CAN 数据接收工作线程类
# ==============================================================================
class CanWorker(QObject):
# 定义信号,用于向主线程发送数据和日志
sensor_data_updated = pyqtSignal(str, float) # sensor_id (e.g., "0x3C1"), value
log_message = pyqtSignal(str, str) # message, tag
status_updated = pyqtSignal(str, str) # type (device/channel/dbc), message
def __init__(self, zcan_lib, db_parser, device_handle, channel_handle, show_debug_log_func, parent=None):
super().__init__(parent)
self.zcanlib = zcan_lib
self.db = db_parser
self.device_handle = device_handle
self.channel_handle = channel_handle
self._running = False
self.show_debug_log_get = show_debug_log_func # 获取主线程中show_debug_log的状态
# 历史数据存储,键为CAN ID。在工作线程中更新,但图表更新由主线程调度。
self.history_data = {"0x3C1": [], "0x3C2": []}
self.max_history_points = 100 # 图表最大显示数据点数量
# 日志队列
self.internal_log_queue = queue.Queue()
def run(self):
self._running = True
self.log_message.emit("CAN报文接收线程已启动。", "info")
start_time = time.time() # 记录线程开始时间,用于计算相对时间
MAX_RECEIVE_BUFFER = 100
while self._running:
try:
rcv_num = self.zcanlib.GetReceiveNum(self.channel_handle, ZCAN_TYPE_CAN)
if rcv_num > 0:
rcv_msgs, actual_rcv_num = self.zcanlib.Receive(self.channel_handle, min(rcv_num, MAX_RECEIVE_BUFFER), ctypes.c_int(0))
if actual_rcv_num > 0:
current_time = time.time() - start_time # 计算当前相对时间
for i in range(actual_rcv_num):
frame_id = rcv_msgs[i].frame.can_id
data_len = rcv_msgs[i].frame.can_dlc
data_bytes = bytes(rcv_msgs[i].frame.data[:data_len])
# 始终记录原始报文信息,但只在show_debug_log为True时发送到GUI日志队列
if self.show_debug_log_get():
log_message_raw = f"\n--- 接收到CAN报文 (时间: {current_time:.3f}s) ---\n"
log_message_raw += f"ID: 0x{frame_id:03X}, DLC: {data_len}, 扩展帧: {rcv_msgs[i].frame.eff}, 远程帧: {rcv_msgs[i].frame.rtr}\n"
log_message_raw += f"原始数据: {data_bytes.hex().upper()}\n"
self.internal_log_queue.put((log_message_raw, "raw"))
if self.db:
try:
message = self.db.get_message_by_frame_id(frame_id)
decoded_data = self.db.decode_message(frame_id, data_bytes)
if self.show_debug_log_get():
decoded_message = f"报文名称: {message.name}\n"
decoded_message += f"解析信号: {decoded_data}\n"
self.internal_log_queue.put((decoded_message, "decoded"))
# 直接通过信号发送更新数据到主线程
if frame_id == 0x3C1 and 'Curr2_Data' in decoded_data:
val = decoded_data['Curr2_Data']
self.sensor_data_updated.emit("0x3C1", val)
self._update_history("0x3C1", val, current_time)
elif frame_id == 0x3C2 and 'Curr1_Data' in decoded_data:
val = decoded_data['Curr1_Data']
self.sensor_data_updated.emit("0x3C2", val)
self._update_history("0x3C2", val, current_time)
except KeyError:
if self.show_debug_log_get():
self.internal_log_queue.put((f"DBC文件中未找到ID为0x{frame_id:X}的报文定义。", "error_debug"))
except Exception as e:
self.internal_log_queue.put((f"解析报文失败。错误: {e}", "error"))
else:
if self.show_debug_log_get():
self.internal_log_queue.put(("DBC未加载,无法解析报文。", "info"))
else:
time.sleep(0.001) # 短暂休眠,避免空循环占用CPU
except Exception as e:
self.internal_log_queue.put((f"接收线程发生错误: {e}", "error"))
self._running = False # 停止线程
self.log_message.emit("CAN报文接收线程已退出。", "info")
def stop(self):
self._running = False
self.log_message.emit("正在停止接收线程...", "info")
def _update_history(self, sensor_id, current, timestamp):
"""仅更新历史数据,不触发图表重绘。图表重绘由主线程定时器控制。"""
self.history_data[sensor_id].append((timestamp, current))
if len(self.history_data[sensor_id]) > self.max_history_points:
self.history_data[sensor_id] = self.history_data[sensor_id][-self.max_history_points:]
# ==============================================================================
# 主 GUI 应用程序类
# ==============================================================================
class USBCANDBCParserGUI(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("周立功霍尔传感器电流检测工具")
self.setFixedSize(950, 750) # 固定窗口大小
# 设置应用图标
try:
self.setWindowIcon(QIcon("can_icon.ico"))
except Exception as e:
print(f"警告: 无法加载 can_icon.ico。请确保文件存在且为有效的.ico格式。错误: {e}")
self.db = None # DBC文件数据库对象
self.zcanlib = None # ZCAN DLL封装对象
self.device_handle = INVALID_DEVICE_HANDLE # 设备句柄
self.channel_handle = INVALID_CHANNEL_HANDLE # 通道句柄
self.can_thread = None # QThread实例
self.can_worker = None # CanWorker实例
self.config_file = 'config.ini'
self.config = configparser.ConfigParser()
self.show_debug_log = False # 绑定到QCheckBox的状态
# === PyQt5 Log Handling ===
self.log_queue = queue.Queue() # 主线程的日志队列,用于接收CanWorker的日志
self.log_update_timer = QTimer(self)
self.log_update_timer.setInterval(100) # 每100ms检查一次日志队列
self.log_update_timer.timeout.connect(self._process_log_queue)
self.log_update_timer.start()
# ==========================
# === Matplotlib Chart Update Timer ===
self.chart_update_timer = QTimer(self)
self.chart_update_timer.setInterval(50) # 每50ms更新一次图表
self.chart_update_timer.timeout.connect(self.update_chart)
# =====================================
self.create_widgets()
self._load_settings()
self.setup_matplotlib_fonts() # 设置Matplotlib字体
self.log_to_gui("系统初始化完成,准备就绪。", "info")
def setup_matplotlib_fonts(self):
"""配置Matplotlib中文字体"""
default_sans_serif_fonts = plt.rcParams['font.sans-serif'][:]
chinese_fonts_priority = ['SimHei', 'Microsoft YaHei', 'WenQuanYi Zen Hei', 'Noto Sans CJK SC', 'PingFang SC', 'Heiti SC', 'SimSun']
found_chinese_font = None
for font_name in chinese_fonts_priority:
if fm.fontManager.findfont(font_name):
found_chinese_font = font_name
break
if found_chinese_font:
plt.rcParams['font.sans-serif'] = [found_chinese_font] + default_sans_serif_fonts
self.log_to_gui(f"Matplotlib 已配置中文字体: {found_chinese_font}", "info")
else:
plt.rcParams['font.sans-serif'] = default_sans_serif_fonts
self.log_to_gui("警告: 未找到合适的系统字体支持中文显示。图表中文可能无法正常显示。", "warning")
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示为方块的问题
def _load_settings(self):
"""加载上次保存的程序设置"""
self.config.read(self.config_file, encoding='utf-8')
last_dbc_path = self.config.get('Paths', 'dbc_path',
fallback=r"C:\Users\Administrator\Desktop\耐压测试用例\霍尔传感器检测\HALL_SENSOR_0x3C1_0x3C2.dbc")
self.dbc_path_entry.setText(last_dbc_path)
last_dll_path = self.config.get('Paths', 'dll_path', fallback='')
self.dll_path_entry.setText(last_dll_path)
last_device_type_val = self.config.get('DeviceSettings', 'device_type', fallback="USBCAN-II (4)")
self.device_type_combo.setCurrentText(last_device_type_val)
last_device_idx_str = self.config.get('DeviceSettings', 'device_index', fallback="0")
self.device_index_combo.setCurrentText(last_device_idx_str)
last_can_channel_str = self.config.get('DeviceSettings', 'can_channel', fallback="0")
self.can_channel_combo.setCurrentText(last_can_channel_str)
last_baudrate_str = self.config.get('DeviceSettings', 'baudrate', fallback="250000")
self.baudrate_combo.setCurrentText(last_baudrate_str)
self.show_debug_log = self.config.getboolean('LogSettings', 'show_debug_log', fallback=False)
self.debug_log_checkbox.setChecked(self.show_debug_log)
self.debug_log_checkbox.stateChanged.connect(self._toggle_debug_log)
self.log_to_gui("已加载上次的配置。", "info")
def _save_settings(self):
"""保存当前程序设置"""
if not self.config.has_section('Paths'):
self.config.add_section('Paths')
if not self.config.has_section('DeviceSettings'):
self.config.add_section('DeviceSettings')
if not self.config.has_section('LogSettings'):
self.config.add_section('LogSettings')
self.config.set('Paths', 'dbc_path', self.dbc_path_entry.text())
self.config.set('Paths', 'dll_path', self.dll_path_entry.text())
self.config.set('DeviceSettings', 'device_type', self.device_type_combo.currentText())
self.config.set('DeviceSettings', 'device_index', self.device_index_combo.currentText())
self.config.set('DeviceSettings', 'can_channel', self.can_channel_combo.currentText())
self.config.set('DeviceSettings', 'baudrate', self.baudrate_combo.currentText())
self.config.set('LogSettings', 'show_debug_log', str(self.show_debug_log))
try:
with open(self.config_file, 'w', encoding='utf-8') as f:
self.config.write(f)
self.log_to_gui("配置已保存。", "info")
except Exception as e:
self.log_to_gui(f"保存配置失败: {e}", "error")
def create_widgets(self):
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QHBoxLayout(central_widget)
# 左侧控制面板
control_frame = QGroupBox("设备控制")
control_frame.setStyleSheet("QGroupBox { border: 1px solid gray; border-radius: 5px; margin-top: 1ex; }"
"QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; padding: 0 3px; }")
control_layout = QVBoxLayout(control_frame)
# 状态栏 (使用 PyQt 的 QStatusBar)
self.statusBar().setStyleSheet("QStatusBar { border: 1px solid gray; border-radius: 5px; background-color: #e0e0e0; padding: 2px; }")
self.device_status_label = QLabel("设备状态: 未连接")
self.channel_status_label = QLabel("通道状态: 未启动")
self.dbc_status_label = QLabel("DBC状态: 未加载")
self.statusBar().addWidget(self.device_status_label)
self.statusBar().addWidget(self.channel_status_label)
self.statusBar().addWidget(self.dbc_status_label)
self.device_status_label.setToolTip("当前CAN设备的连接状态")
self.channel_status_label.setToolTip("当前CAN通道的启动状态")
self.dbc_status_label.setToolTip("DBC文件的加载状态")
# DBC文件加载部分
dbc_group_box = QGroupBox("DBC文件配置")
dbc_group_box.setStyleSheet("QGroupBox { border: 1px solid gray; border-radius: 5px; margin-top: 1ex; }"
"QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; padding: 0 3px; }")
dbc_layout = QGridLayout(dbc_group_box)
dbc_layout.addWidget(QLabel("DBC文件路径:"), 0, 0)
self.dbc_path_entry = QLineEdit()
self.dbc_path_entry.setReadOnly(True) # 通常路径由浏览按钮选择
self.dbc_path_entry.setToolTip("DBC文件路径,用于解析CAN数据")
dbc_layout.addWidget(self.dbc_path_entry, 0, 1, 1, 2) # 跨两列
browse_dbc_btn = QPushButton("浏览...")
browse_dbc_btn.clicked.connect(self.browse_dbc_file)
browse_dbc_btn.setToolTip("选择DBC文件")
dbc_layout.addWidget(browse_dbc_btn, 0, 3)
load_dbc_btn = QPushButton("加载DBC")
load_dbc_btn.clicked.connect(self.load_dbc_file)
load_dbc_btn.setToolTip("加载DBC文件以解析CAN数据")
dbc_layout.addWidget(load_dbc_btn, 1, 3)
self.dbc_version_label = QLabel("DBC版本: N/A")
self.dbc_version_label.setStyleSheet("color: blue;")
dbc_layout.addWidget(self.dbc_version_label, 1, 0, 1, 2)
control_layout.addWidget(dbc_group_box)
# 设备参数配置部分
param_group_box = QGroupBox("设备参数")
param_group_box.setStyleSheet("QGroupBox { border: 1px solid gray; border-radius: 5px; margin-top: 1ex; }"
"QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; padding: 0 3px; }")
param_layout = QGridLayout(param_group_box)
param_layout.addWidget(QLabel("设备类型:"), 0, 0)
self.device_type_combo = QComboBox()
self.device_type_combo.addItems(["USBCAN-II (4)", "USBCAN-E-U (20)", "USBCAN-2E-U (21)"])
self.device_type_combo.setToolTip("选择CAN设备类型")
param_layout.addWidget(self.device_type_combo, 0, 1)
param_layout.addWidget(QLabel("设备索引:"), 1, 0)
self.device_index_combo = QComboBox()
self.device_index_combo.addItems([str(i) for i in range(4)])
self.device_index_combo.setToolTip("选择设备索引号(通常为0)")
param_layout.addWidget(self.device_index_combo, 1, 1)
param_layout.addWidget(QLabel("CAN通道:"), 2, 0)
self.can_channel_combo = QComboBox()
self.can_channel_combo.addItems(["0", "1"])
self.can_channel_combo.setToolTip("选择CAN通道(0或1)")
param_layout.addWidget(self.can_channel_combo, 2, 1)
param_layout.addWidget(QLabel("波特率:"), 3, 0)
self.baudrate_combo = QComboBox()
self.baudrate_combo.addItems(["1000000", "500000", "250000", "125000", "100000", "50000"])
self.baudrate_combo.setToolTip("设置CAN总线波特率")
param_layout.addWidget(self.baudrate_combo, 3, 1)
param_layout.addWidget(QLabel("DLL路径:"), 4, 0)
self.dll_path_entry = QLineEdit()
self.dll_path_entry.setReadOnly(True)
self.dll_path_entry.setToolTip("此DLL路径仅作记录。ZCAN类硬编码加载 './zlgcan.dll' 或 'ControlCAN.dll'。")
param_layout.addWidget(self.dll_path_entry, 4, 1)
browse_dll_btn = QPushButton("浏览...")
browse_dll_btn.clicked.connect(self.select_dll_path)
browse_dll_btn.setToolTip("选择ZLG CAN设备DLL文件")
param_layout.addWidget(browse_dll_btn, 4, 2)
control_layout.addWidget(param_group_box)
# 设备控制按钮
btn_layout = QHBoxLayout()
self.open_device_btn = QPushButton("打开设备")
self.open_device_btn.clicked.connect(self.open_device)
self.open_device_btn.setToolTip("打开并初始化CAN设备")
btn_layout.addWidget(self.open_device_btn)
self.start_can_btn = QPushButton("启动CAN")
self.start_can_btn.clicked.connect(self.start_can)
self.start_can_btn.setDisabled(True)
self.start_can_btn.setToolTip("启动CAN通道并开始接收数据")
btn_layout.addWidget(self.start_can_btn)
self.stop_receive_btn = QPushButton("停止接收")
self.stop_receive_btn.clicked.connect(self.stop_receive)
self.stop_receive_btn.setDisabled(True)
self.stop_receive_btn.setToolTip("停止接收CAN数据")
btn_layout.addWidget(self.stop_receive_btn)
self.close_device_btn = QPushButton("关闭设备")
self.close_device_btn.clicked.connect(self.close_device)
self.close_device_btn.setDisabled(True)
self.close_device_btn.setToolTip("关闭CAN设备")
btn_layout.addWidget(self.close_device_btn)
control_layout.addLayout(btn_layout)
# 设备信息显示
info_group_box = QGroupBox("设备信息")
info_group_box.setStyleSheet("QGroupBox { border: 1px solid gray; border-radius: 5px; margin-top: 1ex; }"
"QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; padding: 0 3px; }")
info_layout = QVBoxLayout(info_group_box)
self.device_info_text = QTextEdit()
self.device_info_text.setReadOnly(True)
self.device_info_text.setText("设备信息将在此显示...")
self.device_info_text.setFont(QFont("Consolas", 9))
info_layout.addWidget(self.device_info_text)
control_layout.addWidget(info_group_box)
main_layout.addWidget(control_frame, 1) # 占据1份宽度
# 右侧数据显示区域
data_frame = QWidget()
data_layout = QVBoxLayout(data_frame)
# 实时数据显示
realtime_group_box = QGroupBox("实时数据")
realtime_group_box.setStyleSheet("QGroupBox { border: 1px solid gray; border-radius: 5px; margin-top: 1ex; }"
"QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; padding: 0 3px; }")
realtime_layout = QVBoxLayout(realtime_group_box)
data_display_layout = QHBoxLayout()
# 霍尔传感器1数据(DBC中0x3C2对应Curr1_Data)
hall1_group_box = QGroupBox("霍尔传感器1 (0x3C2)")
hall1_group_box.setStyleSheet("QGroupBox { border: 1px solid gray; border-radius: 5px; margin-top: 1ex; }"
"QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; padding: 0 3px; }")
hall1_layout = QVBoxLayout(hall1_group_box)
hall1_layout.addWidget(QLabel("电流值:"))
hall1_data_layout = QHBoxLayout()
self.hall1_value = QLabel("---")
self.hall1_value.setFont(QFont("Segoe UI", 24, QFont.Bold))
self.hall1_value.setStyleSheet("color: #0066cc;")
hall1_data_layout.addWidget(self.hall1_value)
hall1_data_layout.addWidget(QLabel("mA"))
hall1_data_layout.addStretch(1) # 填充空白
hall1_layout.addLayout(hall1_data_layout)
data_display_layout.addWidget(hall1_group_box)
# 霍尔传感器2数据(DBC中0x3C1对应Curr2_Data)
hall2_group_box = QGroupBox("霍尔传感器2 (0x3C1)")
hall2_group_box.setStyleSheet("QGroupBox { border: 1px solid gray; border-radius: 5px; margin-top: 1ex; }"
"QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; padding: 0 3px; }")
hall2_layout = QVBoxLayout(hall2_group_box)
hall2_layout.addWidget(QLabel("电流值:"))
hall2_data_layout = QHBoxLayout()
self.hall2_value = QLabel("---")
self.hall2_value.setFont(QFont("Segoe UI", 24, QFont.Bold))
self.hall2_value.setStyleSheet("color: #0066cc;")
hall2_data_layout.addWidget(self.hall2_value)
hall2_data_layout.addWidget(QLabel("mA"))
hall2_data_layout.addStretch(1) # 填充空白
hall2_layout.addLayout(hall2_data_layout)
data_display_layout.addWidget(hall2_group_box)
realtime_layout.addLayout(data_display_layout)
# 数据图表
chart_group_box = QGroupBox("数据趋势")
chart_group_box.setStyleSheet("QGroupBox { border: 1px solid gray; border-radius: 5px; margin-top: 1ex; }"
"QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; padding: 0 3px; }")
chart_layout = QVBoxLayout(chart_group_box)
self.fig, self.ax = plt.subplots(figsize=(8, 3))
self.fig.subplots_adjust(bottom=0.2)
self.line1, = self.ax.plot([], [], 'b-', label='传感器1 (0x3C2)')
self.line2, = self.ax.plot([], [], 'r-', label='传感器2 (0x3C1)')
self.ax.set_xlabel('时间 (秒)')
self.ax.set_ylabel('电流 (mA)')
self.ax.set_title('电流变化趋势')
self.ax.grid(True)
self.ax.legend()
self.canvas = FigureCanvasQTAgg(self.fig)
chart_layout.addWidget(self.canvas)
# 可以添加Matplotlib工具栏,可选
# self.toolbar = NavigationToolbar(self.canvas, self)
# chart_layout.addWidget(self.toolbar)
realtime_layout.addWidget(chart_group_box)
data_layout.addWidget(realtime_group_box, 2) # 占据2份高度
# 日志显示
log_group_box = QGroupBox("系统日志")
log_group_box.setStyleSheet("QGroupBox { border: 1px solid gray; border-radius: 5px; margin-top: 1ex; }"
"QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; padding: 0 3px; }")
log_layout = QVBoxLayout(log_group_box)
log_options_layout = QHBoxLayout()
self.debug_log_checkbox = QCheckBox("显示原始报文/调试日志")
self.debug_log_checkbox.setToolTip("勾选后日志区域将显示更详细的原始报文和解析信息。")
log_options_layout.addWidget(self.debug_log_checkbox)
log_options_layout.addStretch(1) # 填充空白
log_layout.addLayout(log_options_layout)
self.message_display = QTextEdit()
self.message_display.setReadOnly(True)
self.message_display.setFont(QFont("Consolas", 9))
# 定义日志消息的颜色格式
self.message_display.setTextColor(QtGui.QColor("#666666")) # raw
self.message_display.setFontWeight(QFont.Normal)
self.message_display.append("<style>"
"span.raw { color: #666666; }"
"span.decoded { color: #006600; font-weight: bold; }"
"span.error { color: #cc0000; }"
"span.info { color: #0000cc; }"
"span.warning { color: #cc7a00; }"
"span.success { color: #009900; }"
"</style>")
log_layout.addWidget(self.message_display)
log_control_layout = QHBoxLayout()
clear_log_btn = QPushButton("清空日志")
clear_log_btn.clicked.connect(self.clear_log)
log_control_layout.addWidget(clear_log_btn)
save_log_btn = QPushButton("保存日志")
save_log_btn.clicked.connect(self.save_log)
log_control_layout.addWidget(save_log_btn)
log_layout.addLayout(log_control_layout)
data_layout.addWidget(log_group_box, 1) # 占据1份高度
main_layout.addWidget(data_frame, 2) # 占据2份宽度
self.reset_ui_state()
def _toggle_debug_log(self, state):
self.show_debug_log = bool(state)
# 如果需要,可以通知CanWorker更新其show_debug_log_get状态,
# 但由于CanWorker在每次判断时会重新调用self.show_debug_log_get(),所以通常不需要额外通知
self.log_to_gui(f"调试日志显示{'开启' if self.show_debug_log else '关闭'}", "info")
def log_to_gui(self, message, tag="info"):
"""将日志消息放入主线程的日志队列"""
self.log_queue.put((message, tag, time.time()))
def _process_log_queue(self):
"""主线程定时检查日志队列,并批量更新日志显示区域"""
messages_to_display = []
while not self.log_queue.empty():
try:
message, tag, timestamp = self.log_queue.get_nowait()
messages_to_display.append((message, tag, timestamp))
except queue.Empty:
break
if messages_to_display:
cursor = self.message_display.textCursor()
cursor.movePosition(QTextCursor.End)
for message, tag, timestamp in messages_to_display:
formatted_time = time.strftime("%H:%M:%S", time.localtime(timestamp))
# Convert specific internal tags to generic display tags or HTML classes
display_tag = tag
if tag == "error_debug": # Specific debug errors can still be shown as 'error' color
display_tag = "error"
# Append using HTML for colored output
cursor.insertHtml(f"[{formatted_time}] <span class=\"{display_tag}\">{message}</span><br>")
self.message_display.setTextCursor(cursor) # Move cursor to end to ensure scrolling
self.message_display.ensureCursorVisible() # Ensure the last message is visible
def clear_log(self):
"""清空日志区域"""
self.message_display.clear() # 清空所有文本
self.message_display.append("<style>" # 重新添加样式,以防clear移除
"span.raw { color: #666666; }"
"span.decoded { color: #006600; font-weight: bold; }"
"span.error { color: #cc0000; }"
"span.info { color: #0000cc; }"
"span.warning { color: #cc7a00; }"
"span.success { color: #009900; }"
"</style>")
self.log_to_gui("日志已清空", "info")
def save_log(self):
"""保存日志到文件"""
log_file, _ = QFileDialog.getSaveFileName(
self, "保存日志文件", "", "文本文件 (*.txt);;所有文件 (*.*)"
)
if log_file:
try:
# QTextEdit.toPlainText() 获取纯文本
with open(log_file, "w", encoding="utf-8") as f:
f.write(self.message_display.toPlainText())
self.log_to_gui(f"日志已保存到: {log_file}", "success")
except Exception as e:
self.log_to_gui(f"保存日志失败: {e}", "error")
def browse_dbc_file(self):
"""浏览DBC文件"""
dbc_file, _ = QFileDialog.getOpenFileName(
self, "选择DBC文件", "", "DBC文件 (*.dbc);;所有文件 (*.*)"
)
if dbc_file:
self.dbc_path_entry.setText(dbc_file)
self.log_to_gui(f"已选择DBC文件: {dbc_file}", "info")
def select_dll_path(self):
"""选择DLL文件路径 (仅作记录,实际加载取决于ZCAN类内部)"""
dll_file, _ = QFileDialog.getOpenFileName(
self, "选择DLL文件", "", "DLL文件 (*.dll);;所有文件 (*.*)"
)
if dll_file:
self.dll_path_entry.setText(dll_file)
self.log_to_gui(f"已选择DLL文件路径: {dll_file} (ZCAN类将尝试加载 './zlgcan.dll' 或 'ControlCAN.dll')", "info")
def load_dbc_file(self):
"""加载DBC文件"""
dbc_path = self.dbc_path_entry.text()
if not dbc_path:
self.log_to_gui("错误: 请选择DBC文件路径", "error")
QMessageBox.critical(self, "错误", "请选择DBC文件路径!")
return
if not os.path.exists(dbc_path):
self.log_to_gui(f"错误: DBC文件不存在: {dbc_path}", "error")
QMessageBox.critical(self, "错误", f"DBC文件不存在: {dbc_path}")
return
try:
self.db = cantools.database.load_file(dbc_path)
dbc_version = getattr(self.db, 'version', 'N/A')
self.dbc_version_label.setText(f"DBC版本: {dbc_version}")
self.dbc_status_label.setText("DBC状态: 已加载")
self.log_to_gui(f"成功加载DBC文件: {dbc_path}", "success")
self.log_to_gui(f"DBC文件版本信息: {dbc_version}", "info")
except Exception as e:
self.db = None
self.dbc_status_label.setText("DBC状态: 加载失败")
self.log_to_gui(f"加载DBC文件失败: {e}", "error")
QMessageBox.critical(self, "错误", f"加载DBC文件失败: {e}")
def open_device(self):
"""打开CAN设备"""
if self.device_handle != INVALID_DEVICE_HANDLE:
self.log_to_gui("警告: 设备已打开,请先关闭", "warning")
return
try:
device_type_str_full = self.device_type_combo.currentText()
device_type_num_str = device_type_str_full.split('(')[-1].split(')')[0]
device_type = int(device_type_num_str)
device_index = int(self.device_index_combo.currentText())
self.log_to_gui("正在尝试初始化 ZCAN 库并打开设备...", "info")
self.zcanlib = ZCAN()
self.device_handle = self.zcanlib.OpenDevice(device_type, device_index, 0)
if self.device_handle == INVALID_DEVICE_HANDLE:
self.log_to_gui(f"打开设备失败!错误码: {self.device_handle} (通常为0表示失败,请检查设备连接和驱动)", "error")