一、小程序功能设计
1、自定义协议帧格式设计
(1)功能背景:
①在软件开发岗位的工作中,经常会涉及企业自定义的私有协议,这些私有协议不像CAN通信、USB通信、串口通信,它们往往定义在应用层,没有专门的软件对数据流进行整合或解析。
②为了减少自定义协议帧格式带来的额外工作量,可以设计程序实现协议模板定义功能,直接在小程序中编辑协议帧的各字段定义并保存为模板,用户可以使用该模板快速生成或解析自定义协议的数据帧。
(2)协议模板管理功能定义:
①打开小程序后有三个页面选项卡,其中第一个选项卡“协议模板编辑器”就是该功能的界面。

②该功能的定义与自定义协议编辑工具一致,具体可参考《基于Python开发自定义协议编辑工具》,此处不过多赘述。
2、常用报文库配置
(1)功能背景:
①自定义协议编辑工具只支持应用层协议报文的生成和解析,而在嵌入式调试中,经常需要使用上位机发送报文,如果每次发送前都在自定义协议编辑工具内部生成数据流,然后复制到其它通用的收发工具中发送,会非常繁琐。
②为了减少这种重复的工作量,可以基于自定义的协议帧格式,直接配置常用报文保存在本地,并按照使用的模板和报文用途对它们进行分类和命名,这样,后续需要使用该报文测试时,不需要重新在编辑工具中生成,而是直接在常用报文库中根据报文名找到保存的报文即可。
(2)功能定义:
①打开小程序后有三个页面选项卡,其中第二个选项卡“常用报文库”就是该功能的界面。

②报文分类管理:
[1]为避免报文库出现空分类的情况,小程序默认存在一个“默认分类”,如果用户不自行新增分类,直接配置报文,则报文默认归属于“默认分类”。

[2]点击“新增分类”,然后在弹出的输入框中输入分类名称,再点击“OK”,即可新增分类。

[3]下拉框选择想要删除的分类,然后点击“删除分类”,然后在弹出的对话框中点击“是”,即可删除分类,同时该分类下的所有报文也会一并被删除。

③根据模板配置常用报文:
[1]先在下拉框选择协议模板(如果没有对应的协议模板,则回到“协议模板编辑器”选项卡先新建、编辑、保存模板,再回到本选项卡选择协议模板)。

[2]选好报文分类以后,点击“配置报文并保存入库”,填写报文字段信息、报文名称和报文用途,然后点击“生成并保存报文”,即可在报文库中添加一条常用报文。

④编辑和删除已配置报文:
[1]在报文列表中双击报文条目,在弹出的窗口中可以编辑已配置报文,编辑完毕后点击“生成并保存报文”即可,如果报文中有CRC校验字段,则CRC校验码会重新计算。

[2]在报文列表中单击选中报文条目,然后点击“删除选中”,即可删除已配置报文。

⑤在报文列表中单击选中报文条目,然后点击“复制Hex”,即可复制报文的内容(十六进制数据流)。

3、总线发送控制台
(1)功能背景:
①如果只有常用报文库功能,那还是没办法省去复制黏贴的工作量,对此继续开发小程序功能,将发送报文的功能引入,发送控制台能直接读取报文库中的报文,用户只需配置通信接口的参数、报文的发送方式,选择想要发送的应用层报文,然后控制发送即可,数据链路层的协议封装由小程序和通信协议硬件自动实现。
②小程序计划开发串口(TTL/RS232)、CAN和USB三个比较常用的通信接口(考虑到RS485通常和Modbus配合使用,Modbus有专门的上位机,所以暂不开发RS485通信接口),在实际开发中如果有其它通信接口需求,可在该小程序的基础上继续增加功能。
③小程序在各通信接口数据链路层的处理范围:
[1]串口(TTL/RS232):

[2]CAN:

[3]USB(CDC/HID):

(2)通信接口的选择和配置:
①串口(TTL/RS232):
[1]通信通道选择“串口(TTL/RS232)”,即可选择通信接口为串口(不支持RS485)。

[2]在通信参数配置区可选择串行COM端口(小程序会自动识别可打开的COM端口,如果无法识别,点击“刷新端口”即可),然后配置通信参数,双方的通信参数必须保持一致。

[3]配置通信参数后,点击“打开/关闭串口”,即可打开串口通道,再次点击“打开/关闭串口”,即可关闭串口通道,可根据日志窗口输出的日志判断是否操作成功。

②CAN:
[1]通信通道选择“CAN”,即可选择通信接口为CAN。

[2]在通信参数配置区可配置通信参数,包括CAN/CAN FD、DLC、发送报文ID、波特率、是否为扩展帧等。

[3]配置通信参数后,点击“打开/关闭CAN通道”,即可打开CAN通道,再次点击“打开/关闭CAN通道”,即可关闭CAN通道,可根据日志窗口输出的日志判断是否操作成功。
③USB-CDC:
[1]通信通道选择“USB-CDC”,即可选择通信接口为USB-CDC。

[2]在通信参数配置区可选择CDC-COM端口(小程序会自动识别可打开的COM端口,如果无法识别,点击“刷新端口”即可),然后配置通信参数,但需要注意的是,USB-CDC的实际波特率由USB外设决定。

[3]配置通信参数后,点击“打开/关闭USB-CDC”,即可打开USB-CDC通道,再次点击“打开/关闭USB-CDC”,即可关闭USB-CDC通道,可根据日志窗口输出的日志判断是否操作成功。

④USB-HID:
[1]通信通道选择“USB-HID”,即可选择通信接口为USB-HID。

[2]在通信参数配置区可配置VID和PID。

[3]配置通信参数后,点击“打开/关闭USB-HID”,即可打开USB-HID通道,再次点击“打开/关闭USB-HID”,即可关闭USB-HID通道,可根据日志窗口输出的日志判断是否操作成功。
(3)报文发送列表管理:
①添加待发送报文:选择报文分类,然后选择分类下的具体报文,再点击“添加到发送列表”,此时会弹出输入框,如果希望该报文能够周期性地自动发送,则输入报文周期(单位为ms),如果只希望该报文能够手动单次发送,则输入“0”即可。

②编辑报文发送参数:双击待发送报文列表中的报文条目,或者选中它,再点击“编辑选中”,可修改报文的发送周期;需要注意的是,这里不能编辑报文的内容,如果报文库中的报文被修改,则待发送报文列表的报文需要重新添加。

③删除待发送报文:单击待发送报文列表中的报文条目,再点击“删除选中”,即可将该报文条目从待发送报文列表中移除;如果点击“清空列表”,将会移除待发送报文列表中的全部报文条目。

(4)报文发送控制(以串口发送为例):
[1]单次发送:选中待发送报文列表中的报文条目,然后点击“发送选中报文(单次)”,即可通过串口将选中报文发出,且仅发出一次。

[2]周期发送:直接勾选“全局启动周期发送”,待发送列表中启用周期发送的报文将自动按照配置的周期发送。

(5)收发日志窗口:
[1]清空日志:点击“清空日志”,即可清除日志窗口中的全部内容。

[2]接收数据日志的打印:取消勾选“打印接收数据日志”,即可不打印小程序接收到的数据(防止总线上密集的报文将日志窗口刷屏)。

二、小程序开发
1、开发环境
(1)需要确认安装的Python是否自带tkinter库(图形依赖),有则无需单独安装。
(2)安装pyinstaller库,该库主要用于将.py打包成可执行文件.exe,使小程序运行能够脱离Python环境。
(3)安装serial库、can库、hid库,这几个库用于支持通信接口开发。
2、程序源码
(1)JSON配置文件示例:
①协议模板配置文件protocol_templates.json示例:
[
{
"name": "自定义协议帧",
"fields": [
{
"name": "普通字段",
"type": "普通",
"length": 1,
"var_mode": "fixed",
"bind_field": "",
"endian": "BE(大端)",
"enum_dict": {},
"crc_model": "CRC-8",
"crc_start_field": "",
"crc_range_field": "",
"crc_custom": {
"width": 8,
"poly": 0,
"init": 0,
"xorout": 0,
"refin": false,
"refout": false
}
},
{
"name": "枚举字段",
"type": "枚举",
"length": 1,
"var_mode": "fixed",
"bind_field": "",
"endian": "BE(大端)",
"enum_dict": {
"枚举1": 0,
"枚举2": 1
},
"crc_model": "CRC-8",
"crc_start_field": "",
"crc_range_field": "",
"crc_custom": {
"width": 8,
"poly": 0,
"init": 0,
"xorout": 0,
"refin": false,
"refout": false
}
},
{
"name": "校验字段",
"type": "CRC",
"length": 2,
"var_mode": "fixed",
"bind_field": "",
"endian": "BE(大端)",
"enum_dict": {},
"crc_model": "自定义",
"crc_start_field": "普通字段",
"crc_range_field": "枚举字段",
"crc_custom": {
"width": 16,
"poly": 0,
"init": 0,
"xorout": 0,
"refin": false,
"refout": false
}
},
{
"name": "有效数据长度",
"type": "普通",
"length": 1,
"var_mode": "fixed",
"bind_field": "",
"endian": "BE(大端)",
"enum_dict": {},
"crc_model": "CRC-8",
"crc_start_field": "",
"crc_range_field": "",
"crc_custom": {
"width": 8,
"poly": 0,
"init": 0,
"xorout": 0,
"refin": false,
"refout": false
}
},
{
"name": "数据字段",
"type": "普通",
"length": 1,
"var_mode": "bind",
"bind_field": "有效数据长度",
"endian": "BE(大端)",
"enum_dict": {},
"crc_model": "CRC-8",
"crc_start_field": "",
"crc_range_field": "",
"crc_custom": {
"width": 8,
"poly": 0,
"init": 0,
"xorout": 0,
"refin": false,
"refout": false
}
},
{
"name": "帧结束标志",
"type": "普通",
"length": 1,
"var_mode": "fixed",
"bind_field": "",
"endian": "BE(大端)",
"enum_dict": {},
"crc_model": "CRC-8",
"crc_start_field": "",
"crc_range_field": "",
"crc_custom": {
"width": 8,
"poly": 0,
"init": 0,
"xorout": 0,
"refin": false,
"refout": false
}
}
]
},
{
"name": "伪CAN数据帧",
"fields": [
{
"name": "ID仲裁段",
"type": "普通",
"length": 3,
"var_mode": "fixed",
"bind_field": "",
"endian": "BE(大端)",
"enum_dict": {},
"crc_model": "CRC-8",
"crc_start_field": "",
"crc_range_field": "",
"crc_custom": {
"width": 8,
"poly": 0,
"init": 0,
"xorout": 0,
"refin": false,
"refout": false
}
},
{
"name": "控制段",
"type": "枚举",
"length": 1,
"var_mode": "fixed",
"bind_field": "",
"endian": "BE(大端)",
"enum_dict": {
"读命令": 0,
"写命令": 1,
"读返回": 2,
"写返回": 3
},
"crc_model": "CRC-8",
"crc_start_field": "",
"crc_range_field": "",
"crc_custom": {
"width": 8,
"poly": 0,
"init": 0,
"xorout": 0,
"refin": false,
"refout": false
}
},
{
"name": "有效数据长度",
"type": "普通",
"length": 1,
"var_mode": "fixed",
"bind_field": "",
"endian": "BE(大端)",
"enum_dict": {},
"crc_model": "CRC-8",
"crc_start_field": "",
"crc_range_field": "",
"crc_custom": {
"width": 8,
"poly": 0,
"init": 0,
"xorout": 0,
"refin": false,
"refout": false
}
},
{
"name": "数据段",
"type": "普通",
"length": 1,
"var_mode": "bind",
"bind_field": "有效数据长度",
"endian": "BE(大端)",
"enum_dict": {},
"crc_model": "CRC-8",
"crc_start_field": "",
"crc_range_field": "",
"crc_custom": {
"width": 8,
"poly": 0,
"init": 0,
"xorout": 0,
"refin": false,
"refout": false
}
},
{
"name": "CRC段",
"type": "CRC",
"length": 2,
"var_mode": "fixed",
"bind_field": "",
"endian": "BE(大端)",
"enum_dict": {},
"crc_model": "CRC-16/MODBUS",
"crc_start_field": "ID仲裁段",
"crc_range_field": "数据段",
"crc_custom": {
"width": 8,
"poly": 0,
"init": 0,
"xorout": 0,
"refin": false,
"refout": false
}
},
{
"name": "帧结束",
"type": "普通",
"length": 1,
"var_mode": "fixed",
"bind_field": "",
"endian": "BE(大端)",
"enum_dict": {},
"crc_model": "CRC-8",
"crc_start_field": "",
"crc_range_field": "",
"crc_custom": {
"width": 8,
"poly": 0,
"init": 0,
"xorout": 0,
"refin": false,
"refout": false
}
}
]
},
{
"name": "令牌伪USB数据帧",
"fields": [
{
"name": "PID",
"type": "枚举",
"length": 1,
"var_mode": "fixed",
"bind_field": "",
"endian": "BE(大端)",
"enum_dict": {
"IN": 0,
"OUT": 1,
"SETUP": 2
},
"crc_model": "CRC-8",
"crc_start_field": "",
"crc_range_field": "",
"crc_custom": {
"width": 8,
"poly": 0,
"init": 0,
"xorout": 0,
"refin": false,
"refout": false
}
},
{
"name": "ADDR",
"type": "普通",
"length": 1,
"var_mode": "fixed",
"bind_field": "",
"endian": "BE(大端)",
"enum_dict": {},
"crc_model": "CRC-8",
"crc_start_field": "",
"crc_range_field": "",
"crc_custom": {
"width": 8,
"poly": 0,
"init": 0,
"xorout": 0,
"refin": false,
"refout": false
}
},
{
"name": "EP",
"type": "普通",
"length": 1,
"var_mode": "fixed",
"bind_field": "",
"endian": "BE(大端)",
"enum_dict": {},
"crc_model": "CRC-8",
"crc_start_field": "",
"crc_range_field": "",
"crc_custom": {
"width": 8,
"poly": 0,
"init": 0,
"xorout": 0,
"refin": false,
"refout": false
}
},
{
"name": "CRC-8",
"type": "CRC",
"length": 1,
"var_mode": "fixed",
"bind_field": "",
"endian": "BE(大端)",
"enum_dict": {},
"crc_model": "CRC-8",
"crc_start_field": "PID",
"crc_range_field": "EP",
"crc_custom": {
"width": 8,
"poly": 0,
"init": 0,
"xorout": 0,
"refin": false,
"refout": false
}
}
]
},
{
"name": "特殊伪USB数据帧",
"fields": [
{
"name": "PID",
"type": "枚举",
"length": 1,
"var_mode": "fixed",
"bind_field": "",
"endian": "BE(大端)",
"enum_dict": {
"SOF": 3
},
"crc_model": "CRC-8",
"crc_start_field": "",
"crc_range_field": "",
"crc_custom": {
"width": 8,
"poly": 0,
"init": 0,
"xorout": 0,
"refin": false,
"refout": false
}
},
{
"name": "FRAME",
"type": "普通",
"length": 2,
"var_mode": "fixed",
"bind_field": "",
"endian": "BE(大端)",
"enum_dict": {},
"crc_model": "CRC-8",
"crc_start_field": "",
"crc_range_field": "",
"crc_custom": {
"width": 8,
"poly": 0,
"init": 0,
"xorout": 0,
"refin": false,
"refout": false
}
},
{
"name": "CRC-8",
"type": "CRC",
"length": 1,
"var_mode": "fixed",
"bind_field": "",
"endian": "BE(大端)",
"enum_dict": {},
"crc_model": "CRC-8",
"crc_start_field": "PID",
"crc_range_field": "FRAME",
"crc_custom": {
"width": 8,
"poly": 0,
"init": 0,
"xorout": 0,
"refin": false,
"refout": false
}
}
]
},
{
"name": "握手伪USB数据帧",
"fields": [
{
"name": "PID",
"type": "枚举",
"length": 1,
"var_mode": "fixed",
"bind_field": "",
"endian": "BE(大端)",
"enum_dict": {
"ACK": 4,
"NAK": 5,
"STALL": 6
},
"crc_model": "CRC-8",
"crc_start_field": "",
"crc_range_field": "",
"crc_custom": {
"width": 8,
"poly": 0,
"init": 0,
"xorout": 0,
"refin": false,
"refout": false
}
}
]
},
{
"name": "数据伪USB数据帧",
"fields": [
{
"name": "PID",
"type": "枚举",
"length": 1,
"var_mode": "fixed",
"bind_field": "",
"endian": "BE(大端)",
"enum_dict": {
"DATA0": 7,
"DATA1": 8
},
"crc_model": "CRC-8",
"crc_start_field": "",
"crc_range_field": "",
"crc_custom": {
"width": 8,
"poly": 0,
"init": 0,
"xorout": 0,
"refin": false,
"refout": false
}
},
{
"name": "Payload",
"type": "普通",
"length": 1,
"var_mode": "free",
"bind_field": "",
"endian": "BE(大端)",
"enum_dict": {},
"crc_model": "CRC-8",
"crc_start_field": "",
"crc_range_field": "",
"crc_custom": {
"width": 8,
"poly": 0,
"init": 0,
"xorout": 0,
"refin": false,
"refout": false
}
},
{
"name": "CRC-16",
"type": "CRC",
"length": 2,
"var_mode": "fixed",
"bind_field": "",
"endian": "BE(大端)",
"enum_dict": {},
"crc_model": "CRC-16/MODBUS",
"crc_start_field": "PID",
"crc_range_field": "Payload",
"crc_custom": {
"width": 8,
"poly": 0,
"init": 0,
"xorout": 0,
"refin": false,
"refout": false
}
}
]
}
]
②报文分类配置文件msg_category.json示例:
[
"默认分类",
"新增分类测试"
]
③报文库配置文件message_lib.json示例:
[
{
"category": "新增分类测试",
"msg_name": "示例报文",
"template_name": "伪CAN数据帧",
"hex_data": "11 45 14 01 03 14 45 11 21 4F 0A",
"remark": "演示使用",
"field_input": {
"ID仲裁段": "11 45 14",
"控制段": "写命令",
"有效数据长度": "03",
"数据段": "14 45 11",
"帧结束": "0A"
}
},
{
"category": "默认分类",
"msg_name": "发送示例报文",
"template_name": "令牌伪USB数据帧",
"hex_data": "02 13 32 20",
"remark": "演示使用",
"field_input": {
"PID": "SETUP",
"ADDR": "13",
"EP": "32"
}
}
]
④通信接口配置文件comm_config.json示例:
{
"active_comm": "\u4e32\u53e3\uff08TTL/RS232\uff09",
"serial": {
"port": "COM12",
"baud": 9600,
"data": 8,
"stop": 1.0,
"parity": "N"
},
"can": {
"channel": "0",
"arb_baud": 500000,
"fd_baud": 2000000,
"fd_enable": false,
"ext": false,
"tx_id": 2047
},
"usb_cdc": {
"port": "COM11",
"baud": 115200,
"data": 8,
"stop": 1.0,
"parity": "N"
},
"usb_vid": {
"vid": 0,
"pid": 0
},
"period_ms": 1000,
"global_auto": false
}
(2)程序源码文件protocol_sender.py:
import json
import os
import time
import threading
import binascii
import tkinter as tk
from tkinter import ttk, messagebox, scrolledtext, simpledialog
try:
import serial
from serial.tools import list_ports
SERIAL_AVAIL = True
except ImportError:
serial = None
list_ports = None
SERIAL_AVAIL = False
try:
import hid
USB_AVAIL = True
except ImportError:
hid = None
USB_AVAIL = False
try:
import can
CAN_AVAIL = True
except ImportError:
can = None
CAN_AVAIL = False
# ===================== 全局文件常量 =====================
TEMPLATE_FILE = "protocol_templates.json"
MSG_LIB_FILE = "message_lib.json"
MSG_CATEGORY_FILE = "msg_category.json"
COMM_CFG_FILE = "comm_config.json"
CRC_MODEL_LIST = ["CRC-16/XMODEM", "CRC-16/MODBUS", "CRC-8", "CRC-32", "自定义"]
DEFAULT_PRESETS = {
"CRC-16/XMODEM": {"width": 16, "poly": 0x1021, "init": 0x0000, "xorout": 0x0000, "refin": False, "refout": False},
"CRC-16/MODBUS": {"width": 16, "poly": 0xA001, "init": 0xFFFF, "xorout": 0x0000, "refin": True, "refout": True},
"CRC-8": {"width": 8, "poly": 0x07, "init": 0x00, "xorout": 0x00, "refin": False, "refout": False},
"CRC-32": {"width": 32, "poly": 0x04C11DB7, "init": 0xFFFFFFFF, "xorout": 0xFFFFFFFF, "refin": True, "refout": True},
}
# 串口完整参数列表
SERIAL_BAUDS = [1200, 2400, 4800, 9600, 14400, 19200, 38400, 57600, 115200, 230400, 460800, 921600]
SERIAL_STOP = [0.5, 1, 1.5, 2]
SERIAL_DATA = [5, 6, 7, 8]
SERIAL_PARITY = ["N", "O", "E"]
# CAN波特率
CAN_ARB_BAUD = [125000, 250000, 500000, 800000, 1000000]
CAN_FD_BAUD = [500000, 1000000, 2000000, 4000000, 8000000]
# CAN适配器接口
CAN_INTERFACES = ["pcan", "canalystii", "slcan", "usb2can", "vector", "socketcan"]
# ===================== CRC工具函数 =====================
def reverse_bits(value, bit_width):
result = 0
for _ in range(bit_width):
result = (result << 1) | (value & 1)
value >>= 1
return result
def calc_crc(data: bytes, width: int, poly: int, init: int, xorout: int, refin: bool, refout: bool):
mask = (1 << width) - 1
crc = init & mask
for b in data:
byte = b
if refin:
byte = reverse_bits(byte, 8)
crc ^= (byte << (width - 8))
for _ in range(8):
if crc & (1 << (width - 1)):
crc = ((crc << 1) ^ poly) & mask
else:
crc = (crc << 1) & mask
if refout:
crc = reverse_bits(crc, width)
crc = (crc ^ xorout) & mask
return crc
# ===================== Tab0 协议模板编辑器(原版完整无删减) =====================
class ProtocolEditor:
def __init__(self, parent_frame, notify_refresh):
self.parent = parent_frame
self.notify_refresh = notify_refresh
self.templates = []
self.current_template = None
self.field_widgets = []
self.load_templates()
top_frame = ttk.Frame(self.parent)
top_frame.pack(fill="x", padx=10, pady=5)
ttk.Label(top_frame, text="协议模板 NAME:").grid(row=0, column=0, sticky="w")
self.cmb_template = ttk.Combobox(top_frame, width=34)
self.cmb_template.grid(row=0, column=1, padx=5)
ttk.Button(top_frame, text="载入", command=self.load_selected_template).grid(row=0, column=2)
ttk.Button(top_frame, text="新建", command=self.new_template).grid(row=0, column=3, padx=2)
ttk.Button(top_frame, text="保存模板", command=self.save_template).grid(row=0, column=4, padx=2)
ttk.Button(top_frame, text="删除模板", command=self.del_template).grid(row=0, column=5, padx=2)
ttk.Label(self.parent, text="字段定义列表(双击条目编辑)").pack(anchor="w", padx=10)
self.tree = ttk.Treeview(self.parent, columns=("name", "type", "length_info", "endian", "extra"), show="headings", height=9)
self.tree.heading("name", text="字段名")
self.tree.heading("type", text="类型")
self.tree.heading("length_info", text="长度信息")
self.tree.heading("endian", text="大小端")
self.tree.heading("extra", text="附加配置")
self.tree.pack(fill="x", padx=10)
self.tree.bind("<Double-1>", self.on_tree_double_click)
btn_field = ttk.Frame(self.parent)
btn_field.pack(anchor="w", padx=10, pady=3)
ttk.Button(btn_field, text="添加字段", command=self.add_field).pack(side="left")
ttk.Button(btn_field, text="删除选中", command=self.del_field).pack(side="left", padx=5)
ttk.Label(self.parent, text="帧参数输入面板").pack(anchor="w", padx=10, pady=8)
self.form_frame = ttk.Frame(self.parent)
self.form_frame.pack(fill="x", padx=10)
btn_frame = ttk.Frame(self.parent)
btn_frame.pack(pady=8)
ttk.Button(btn_frame, text="生成协议帧", command=self.build_frame).pack(side="left", padx=5)
ttk.Button(btn_frame, text="解析接收报文", command=self.parse_frame).pack(side="left", padx=5)
ttk.Button(btn_frame, text="清空输入", command=self.clear_input_values).pack(side="left", padx=5)
out_frame = ttk.Frame(self.parent)
out_frame.pack(fill="x", padx=10)
out_frame.columnconfigure(1, weight=1)
ttk.Label(out_frame, text="待解析Hex / 生成结果:").grid(row=0, column=0, sticky="w")
self.var_hex = tk.StringVar()
ttk.Entry(out_frame, textvariable=self.var_hex, font=("Consolas", 11)).grid(row=0, column=1, padx=6, sticky="ew")
ttk.Button(out_frame, text="复制", command=lambda: self.parent.clipboard_append(self.var_hex.get())).grid(row=0, column=2)
self.refresh_template_combo()
def load_templates(self):
if os.path.exists(TEMPLATE_FILE):
try:
with open(TEMPLATE_FILE, "r", encoding="utf-8") as f:
self.templates = json.load(f)
except:
self.templates = []
else:
self.templates = []
def save_templates(self):
with open(TEMPLATE_FILE, "w", encoding="utf-8") as f:
json.dump(self.templates, f, ensure_ascii=False, indent=2)
def refresh_template_combo(self):
self.cmb_template["values"] = [t["name"] for t in self.templates]
def new_template(self):
dlg = tk.Toplevel(self.parent.winfo_toplevel())
dlg.title("新建协议模板")
dlg.geometry("340x130")
dlg.transient(self.parent.winfo_toplevel())
dlg.grab_set()
dlg.columnconfigure(1, weight=1)
var_name = tk.StringVar()
ttk.Label(dlg, text="模板名称:").grid(row=0, column=0, padx=10, pady=12, sticky="w")
entry = ttk.Entry(dlg, textvariable=var_name)
entry.grid(row=0, column=1, padx=10, pady=12, sticky="ew")
entry.focus()
def confirm():
name = var_name.get().strip()
if not name:
messagebox.showwarning("提示", "模板名称不能为空", parent=dlg)
return
self.current_template = {"name": name, "fields": []}
self.refresh_tree()
self.build_form()
dlg.destroy()
messagebox.showinfo("完成", f"新建模板:{name}", parent=self.parent.winfo_toplevel())
if self.notify_refresh:
self.notify_refresh()
ttk.Button(dlg, text="确定", command=confirm).grid(row=1, column=0, columnspan=2, pady=5)
dlg.bind("<Return>", lambda e: confirm())
def save_template(self):
if not self.current_template:
messagebox.showwarning("提示", "请先新建/加载模板")
return
name = self.current_template["name"]
found = False
for idx, t in enumerate(self.templates):
if t["name"] == name:
self.templates[idx] = self.current_template
found = True
break
if not found:
self.templates.append(self.current_template)
self.save_templates()
self.refresh_template_combo()
if self.notify_refresh:
self.notify_refresh()
messagebox.showinfo("成功", "模板已保存")
def del_template(self):
name = self.cmb_template.get()
self.templates = [t for t in self.templates if t["name"] != name]
self.save_templates()
self.refresh_template_combo()
self.current_template = None
self.refresh_tree()
self.clear_form()
if self.notify_refresh:
self.notify_refresh()
messagebox.showinfo("完成", "模板已删除")
def load_selected_template(self):
name = self.cmb_template.get()
for t in self.templates:
if t["name"] == name:
self.current_template = t
self.refresh_tree()
self.build_form()
return
def refresh_tree(self):
for item in self.tree.get_children():
self.tree.delete(item)
if not self.current_template:
return
for f in self.current_template["fields"]:
var_mode = f.get("var_mode", "fixed")
bind_name = f.get("bind_field", "")
if var_mode == "fixed":
len_info = f"{f['length']}字节"
elif var_mode == "bind":
len_info = f"不定长(绑定:{bind_name})"
else:
len_info = "不定长(自由段)"
ftype = f["type"]
if ftype == "CRC":
extra = f["crc_model"]
elif ftype == "枚举":
extra = str(f.get("enum_dict", {}))
else:
extra = ""
if f.get("var_mode") in ("bind", "free"):
endian_show = "原始数据流"
else:
endian_show = f.get("endian", "BE(大端)")
self.tree.insert("", "end", values=(f["name"], f["type"], len_info, endian_show, extra))
def on_tree_double_click(self, event=None):
sel = self.tree.selection()
if not sel:
return
idx = self.tree.index(sel[0])
field = self.current_template["fields"][idx]
self.edit_field_window(field, idx)
def add_field(self):
empty_field = {
"name": "", "type": "普通", "length": 1, "var_mode": "fixed", "bind_field": "",
"endian": "BE(大端)", "enum_dict": {},
"crc_model": "CRC-8",
"crc_start_field": "",
"crc_range_field": "",
"crc_custom": {"width": 8, "poly": 0, "init": 0, "xorout": 0, "refin": False, "refout": False}
}
self.edit_field_window(empty_field, -1)
def del_enum(self, tree):
tree.focus()
sel = tree.selection()
if not sel:
messagebox.showwarning("提示", "请先选中一行枚举条目!")
return
tree.delete(sel[0])
def edit_field_window(self, field, index):
win = tk.Toplevel(self.parent.winfo_toplevel())
win.title("字段配置")
win.geometry("720x660")
win.transient(self.parent.winfo_toplevel())
win.grab_set()
win.columnconfigure(1, weight=1)
vars = {
"name": tk.StringVar(value=field["name"]),
"ftype": tk.StringVar(value=field["type"]),
"length": tk.StringVar(value=str(field["length"])),
"var_mode": tk.StringVar(value=field.get("var_mode", "fixed")),
"bind_field": tk.StringVar(value=field.get("bind_field", "")),
"endian": tk.StringVar(value=field.get("endian", "BE(大端)")),
"crc_model": tk.StringVar(value=field.get("crc_model", "CRC-8")),
"crc_start_field": tk.StringVar(value=field.get("crc_start_field", "")),
"crc_range_field": tk.StringVar(value=field.get("crc_range_field", "")),
"crc_width": tk.StringVar(value=str(field.get("crc_custom", {}).get("width", 8))),
"crc_poly": tk.StringVar(value=hex(field.get("crc_custom", {}).get("poly", 0))[2:]),
"crc_init": tk.StringVar(value=hex(field.get("crc_custom", {}).get("init", 0))[2:]),
"crc_xorout": tk.StringVar(value=hex(field.get("crc_custom", {}).get("xorout", 0))[2:]),
"crc_refin": tk.BooleanVar(value=field.get("crc_custom", {}).get("refin", False)),
"crc_refout": tk.BooleanVar(value=field.get("crc_custom", {}).get("refout", False)),
}
enum_dict = field.get("enum_dict", {}).copy()
types_list = ["普通", "枚举", "CRC"]
container = ttk.Frame(win)
container.pack(fill="both", expand=True, padx=8, pady=8)
container.columnconfigure(1, weight=1)
ttk.Label(container, text="字段名称:").grid(row=0, column=0, sticky="w", padx=6, pady=6)
ttk.Entry(container, textvariable=vars["name"]).grid(row=0, column=1, sticky="ew", padx=6)
ttk.Label(container, text="字段类型:").grid(row=1, column=0, sticky="w", padx=6, pady=6)
cb_type = ttk.Combobox(container, textvariable=vars["ftype"], values=types_list)
cb_type.grid(row=1, column=1, sticky="ew", padx=6)
row_len = 2
lbl_len = ttk.Label(container, text="字节长度:")
ent_len = ttk.Entry(container, textvariable=vars["length"])
lbl_len.grid(row=row_len, column=0, sticky="w", padx=6, pady=6)
ent_len.grid(row=row_len, column=1, sticky="ew", padx=6)
ttk.Label(container, text="大小端:").grid(row=3, column=0, sticky="w", padx=6, pady=6)
cb_endian = ttk.Combobox(container, values=["BE(大端)", "LE(小端)"], textvariable=vars["endian"])
cb_endian.grid(row=3, column=1, sticky="ew", padx=6)
var_frame = ttk.Frame(container)
var_frame.grid(row=4, column=0, columnspan=2, sticky="ew", padx=4, pady=4)
rb_fixed = ttk.Radiobutton(var_frame, text="定长字段", variable=vars["var_mode"], value="fixed", command=lambda: refresh_ui())
rb_bind = ttk.Radiobutton(var_frame, text="不定长-绑定前置长度字段", variable=vars["var_mode"], value="bind", command=lambda: refresh_ui())
rb_free = ttk.Radiobutton(var_frame, text="不定长-自由段(最多1个)", variable=vars["var_mode"], value="free", command=lambda: refresh_ui())
rb_fixed.pack(side="left")
rb_bind.pack(side="left", padx=4)
rb_free.pack(side="left")
bind_frame = ttk.Frame(container)
bind_frame.grid(row=5, column=0, columnspan=2,
sticky="ew", padx=2, pady=2)
ttk.Label(bind_frame, text="绑定长度字段:").pack(side="left")
cb_bind = ttk.Combobox(bind_frame, textvariable=vars["bind_field"])
cb_bind.pack(side="left", fill="x", expand=True, padx=4)
bind_names = []
if self.current_template:
for idx, f in enumerate(self.current_template["fields"]):
if index == -1 or idx < index:
bind_names.append(f["name"])
cb_bind["values"] = bind_names
cfg_frame = ttk.Frame(container)
cfg_frame.grid(row=6, column=0, columnspan=2,
sticky="ew", padx=6, pady=6)
def refresh_ui():
tp = vars["ftype"].get()
mode = vars["var_mode"].get()
endian_combo = cb_endian
if mode != "fixed":
endian_combo.config(state="disabled")
else:
endian_combo.config(state="readonly")
if tp == "普通":
var_frame.grid()
if mode == "fixed":
ent_len.config(state="normal")
bind_frame.grid_remove()
elif mode == "bind":
ent_len.config(state="disabled")
bind_frame.grid()
elif mode == "free":
ent_len.config(state="disabled")
bind_frame.grid_remove()
else:
vars["var_mode"].set("fixed")
var_frame.grid_remove()
bind_frame.grid_remove()
ent_len.config(state="normal")
endian_combo.config(state="readonly")
for w in cfg_frame.winfo_children():
w.destroy()
if tp == "CRC":
r = 0
ttk.Label(cfg_frame, text="CRC模型:").grid(row=r, column=0, sticky="w", padx=6)
cb_crc = ttk.Combobox(cfg_frame, textvariable=vars["crc_model"], values=CRC_MODEL_LIST)
cb_crc.grid(row=r, column=1, sticky="ew", padx=6)
r += 1
ttk.Label(cfg_frame, text="校验起始字段:").grid(row=r, column=0, sticky="w", padx=6)
cb_crc_start = ttk.Combobox(cfg_frame, textvariable=vars["crc_start_field"])
crc_range_names = []
if self.current_template:
for idx, f in enumerate(self.current_template["fields"]):
if index == -1 or idx < index:
crc_range_names.append(f["name"])
cb_crc_start["values"] = [""] + crc_range_names
cb_crc_start.grid(row=r, column=1, sticky="ew", padx=6)
r += 1
ttk.Label(cfg_frame, text="校验截止字段:").grid(row=r, column=0, sticky="w", padx=6)
cb_crc_range = ttk.Combobox(cfg_frame, textvariable=vars["crc_range_field"])
cb_crc_range["values"] = [""] + crc_range_names
cb_crc_range.grid(row=r, column=1, sticky="ew", padx=6)
r += 1
custom_frame = ttk.Frame(cfg_frame)
def refresh_crc_custom():
for w in custom_frame.winfo_children():
w.destroy()
if vars["crc_model"].get() == "自定义":
ttk.Label(custom_frame, text="宽度WIDTH").grid(row=0, column=0, sticky="w")
ttk.Entry(custom_frame, textvariable=vars["crc_width"]).grid(row=0, column=1, sticky="ew")
ttk.Label(custom_frame, text="POLY(Hex)").grid(row=1, column=0, sticky="w")
ttk.Entry(custom_frame, textvariable=vars["crc_poly"]).grid(row=1, column=1, sticky="ew")
ttk.Label(custom_frame, text="INIT(Hex)").grid(row=2, column=0, sticky="w")
ttk.Entry(custom_frame, textvariable=vars["crc_init"]).grid(row=2, column=1, sticky="ew")
ttk.Label(custom_frame, text="XOROUT(Hex)").grid(row=3, column=0, sticky="w")
ttk.Entry(custom_frame, textvariable=vars["crc_xorout"]).grid(row=3, column=1, sticky="ew")
ck1 = ttk.Checkbutton(custom_frame, text="REFIN", variable=vars["crc_refin"])
ck1.grid(row=4, column=0, sticky="w")
ck2 = ttk.Checkbutton(custom_frame, text="REFOUT", variable=vars["crc_refout"])
ck2.grid(row=4, column=1, sticky="w")
cb_crc.bind("<<ComboboxSelected>>", lambda e: refresh_crc_custom())
custom_frame.grid(row=r, column=0, columnspan=2, sticky="ew", padx=4, pady=4)
refresh_crc_custom()
elif tp == "枚举":
top_bar = ttk.Frame(cfg_frame)
top_bar.pack(fill="x")
ttk.Label(top_bar, text="枚举列表(双击条目弹出编辑窗口)").pack(side="left")
ttk.Button(top_bar, text="+", command=lambda: tree_enum.insert("", "end", values=("名称", "0x00"))).pack(side="right", padx=2)
ttk.Button(top_bar, text="-", command=lambda: self.del_enum(tree_enum)).pack(side="right")
tree_enum = ttk.Treeview(cfg_frame, columns=("name", "val"), show="headings", height=4)
tree_enum.heading("name", text="枚举名称")
tree_enum.heading("val", text="数值(0xXX)")
tree_enum.pack(fill="x", padx=4)
def enum_double_pop(event):
item = tree_enum.focus()
if not item:
return
vals = tree_enum.item(item, "values")
old_name, old_val = vals[0], vals[1]
dlg = tk.Toplevel(win)
dlg.title("修改枚举项")
dlg.geometry("320x160")
dlg.transient(win)
dlg.grab_set()
var_n = tk.StringVar(value=old_name)
var_v = tk.StringVar(value=old_val)
ttk.Label(dlg, text="枚举名称:").grid(row=0, column=0, padx=8, pady=8, sticky="w")
ttk.Entry(dlg, textvariable=var_n).grid(row=0, column=1, padx=8, pady=8, sticky="ew")
ttk.Label(dlg, text="枚举数值(Hex):").grid(row=1, column=0, padx=8, pady=8, sticky="w")
ttk.Entry(dlg, textvariable=var_v).grid(row=1, column=1, padx=8, pady=8, sticky="ew")
def confirm():
nn = var_n.get().strip()
nv = var_v.get().strip()
if not nn or not nv:
messagebox.showwarning("提示", "名称和数值不能为空", parent=dlg)
return
tree_enum.item(item, values=(nn, nv))
dlg.destroy()
ttk.Button(dlg, text="确定", command=confirm).grid(row=2, column=0, columnspan=2, pady=10)
dlg.columnconfigure(1, weight=1)
tree_enum.bind("<Double-1>", enum_double_pop)
for k, v in enum_dict.items():
tree_enum.insert("", "end", values=(k, f"0x{v:X}"))
def save_enum():
nonlocal enum_dict
enum_dict.clear()
for it in tree_enum.get_children():
n, v = tree_enum.item(it, "values")
enum_dict[n] = int(v, 0)
ttk.Button(cfg_frame, text="应用枚举列表", command=save_enum).pack()
cb_type.bind("<<ComboboxSelected>>", lambda e: refresh_ui())
refresh_ui()
def confirm_save():
try:
w = int(vars["length"].get())
var_mode = vars["var_mode"].get()
new_fields = self.current_template["fields"].copy() if self.current_template else []
if index >= 0:
new_fields.pop(index)
free_cnt = sum(1 for f in new_fields if f.get("var_mode", "free") == "free")
if var_mode == "free" and free_cnt >= 1:
messagebox.showerror("限制", "协议最多只能存在1个【不定长-自由段】")
return
bind_name = vars["bind_field"].get().strip()
if var_mode == "bind" and bind_name == "":
messagebox.showerror("参数错误", "绑定模式必须选择前置长度字段!")
return
crc_custom = {
"width": int(vars["crc_width"].get()),
"poly": int(vars["crc_poly"].get(), 16) if vars["crc_poly"].get().strip() else 0,
"init": int(vars["crc_init"].get(), 16) if vars["crc_init"].get().strip() else 0,
"xorout": int(vars["crc_xorout"].get(), 16) if vars["crc_xorout"].get().strip() else 0,
"refin": vars["crc_refin"].get(),
"refout": vars["crc_refout"].get(),
}
new_field = {
"name": vars["name"].get().strip(),
"type": vars["ftype"].get(),
"length": w,
"var_mode": var_mode,
"bind_field": bind_name,
"endian": vars["endian"].get(),
"enum_dict": enum_dict.copy(),
"crc_model": vars["crc_model"].get().strip(),
"crc_start_field": vars["crc_start_field"].get(),
"crc_range_field": vars["crc_range_field"].get(),
"crc_custom": crc_custom
}
if index == -1:
self.current_template["fields"].append(new_field)
else:
self.current_template["fields"][index] = new_field
self.refresh_tree()
self.build_form()
win.destroy()
except Exception as e:
messagebox.showerror("参数错误", str(e))
ttk.Button(container, text="确认保存字段", command=confirm_save).grid(row=7, column=0, columnspan=2, padx=14, pady=14)
def del_field(self):
sel = self.tree.selection()
if not sel:
return
idx = self.tree.index(sel[0])
del self.current_template["fields"][idx]
self.refresh_tree()
self.build_form()
def clear_form(self):
for w in self.form_frame.winfo_children():
w.destroy()
self.field_widgets = []
self.var_hex.set("")
def clear_input_values(self):
for wdg in self.field_widgets:
if "var" in wdg:
wdg["var"].set("")
def build_form(self):
self.clear_form()
if not self.current_template:
return
for fd in self.current_template["fields"]:
fr = ttk.Frame(self.form_frame)
fr.pack(anchor="w", padx=2, pady=2, fill="x")
fr.columnconfigure(0, weight=0)
fr.columnconfigure(1, weight=1)
label_text = f"{fd['name']} [{fd['type']}]"
if fd["type"] == "普通":
label_text += " 十六进制空格分隔"
lbl = ttk.Label(fr, text=label_text, width=68)
lbl.grid(row=0, column=0, padx=10, sticky="w")
ftype = fd["type"]
enum_dict = fd.get("enum_dict", {})
if ftype == "枚举":
keys = list(enum_dict.keys())
var = tk.StringVar(value=keys[0] if keys else "")
cb = ttk.Combobox(fr, textvariable=var, values=keys)
cb.grid(row=0, column=1, padx=100, pady=0)
self.field_widgets.append(
{"field": fd, "var": var, "enum": enum_dict})
elif ftype == "CRC":
ttk.Label(fr, text="【自动计算】").grid(row=0, column=1)
self.field_widgets.append({"field": fd})
else:
var = tk.StringVar()
entry = ttk.Entry(fr, textvariable=var, width=30)
entry.grid(row=0, column=1, padx=100)
self.field_widgets.append({"field": fd, "var": var})
def _get_raw_field_bytes(self, field_name):
for wdg in self.field_widgets:
fd = wdg["field"]
if fd["name"] != field_name:
continue
tp = fd["type"]
endian = "big" if fd["endian"] == "BE(大端)" else "little"
if tp == "枚举":
val = wdg["enum"][wdg["var"].get()]
return val.to_bytes(fd["length"], byteorder=endian)
elif tp == "普通":
var_mode = fd.get("var_mode", "fixed")
if var_mode == "fixed":
raw_str = wdg["var"].get().strip().replace(" ", "")
num = int(raw_str, 16) if raw_str else 0
return num.to_bytes(fd["length"], byteorder=endian)
else:
raw = wdg["var"].get().strip()
return bytes.fromhex(raw) if raw else b""
return b""
def _get_bind_length_value(self, bind_field_name):
try:
target_fd = None
for wdg in self.field_widgets:
if wdg["field"]["name"] == bind_field_name:
target_fd = wdg["field"]
break
if target_fd is None:
return 0
endian = "big" if target_fd["endian"] == "BE(大端)" else "little"
data = self._get_raw_field_bytes(bind_field_name)
val = int.from_bytes(data, byteorder=endian)
return max(0, min(val, 4096))
except Exception:
return 0
def _get_raw_undetermined_data(self, field_name):
for wdg in self.field_widgets:
fd = wdg["field"]
if fd["name"] == field_name and fd["type"] == "普通":
raw = wdg["var"].get().strip()
if raw:
return bytes.fromhex(raw)
return b""
return b""
def build_frame(self):
try:
final_buf = b""
for wdg in self.field_widgets:
fd = wdg["field"]
tp = fd["type"]
endian = "big" if fd["endian"] == "BE(大端)" else "little"
var_mode = fd.get("var_mode", "fixed")
if tp == "枚举":
val = wdg["enum"][wdg["var"].get()]
final_buf += val.to_bytes(fd["length"], byteorder=endian)
elif tp == "普通":
if var_mode == "fixed":
bdata = self._get_raw_field_bytes(fd["name"])
final_buf += bdata
elif var_mode == "bind":
bind_len = self._get_bind_length_value(fd["bind_field"])
raw_data = self._get_raw_undetermined_data(fd["name"])
chunk = raw_data[:bind_len]
if len(chunk) < bind_len:
chunk = raw_data[:bind_len].ljust(bind_len, b"\x00")
final_buf += chunk
elif var_mode == "free":
raw = wdg["var"].get().strip()
chunk = bytes.fromhex(raw) if raw else b""
final_buf += chunk
elif tp == "CRC":
crc_start_name = fd.get("crc_start_field", "")
crc_end_name = fd.get("crc_range_field", "")
start_ptr = 0
end_ptr = len(final_buf)
if crc_start_name:
pos = 0
for f_wdg in self.field_widgets:
f = f_wdg["field"]
if f["name"] == crc_start_name:
start_ptr = pos
break
if f["type"] == "CRC":
pos += f["length"]
elif f.get("var_mode") == "bind":
pos += self._get_bind_length_value(f["bind_field"])
elif f.get("free"):
pos += 4096
else:
pos += f["length"]
if crc_end_name:
pos = 0
for f_wdg in self.field_widgets:
f = f_wdg["field"]
pos += self._get_field_actual_length(f_wdg)
if f["name"] == crc_end_name:
end_ptr = pos
break
calc_data = final_buf[start_ptr:end_ptr]
if fd["crc_model"] == "自定义":
cfg = fd["crc_custom"]
crc_cfg = {"width": cfg["width"], "poly": cfg["poly"], "init": cfg["init"],
"xorout": cfg["xorout"], "refin": cfg["refin"], "refout": cfg["refout"]}
else:
crc_cfg = DEFAULT_PRESETS[fd["crc_model"]]
crc_val = calc_crc(calc_data, **crc_cfg)
# CRC计算得到的整数,固定大端输出,refin/refout已经处理比特反转
crc_bytes = crc_val.to_bytes(fd["length"], byteorder="big")
# 如果模板字段配置是小端,对crc字节做字节反转
if endian == "little":
crc_bytes = crc_bytes[::-1]
final_buf += crc_bytes
hex_out = " ".join(f"{b:02X}" for b in final_buf)
self.var_hex.set(hex_out)
return hex_out
except ValueError as ve:
messagebox.showerror("组帧失败", f"十六进制格式错误:{str(ve)}")
return ""
except Exception as e:
messagebox.showerror("组帧失败", str(e))
return ""
def _get_field_actual_length(self, wdg):
fd = wdg["field"]
vm = fd.get("var_mode", "fixed")
if vm == "bind":
return self._get_bind_length_value(fd["bind_field"])
elif vm == "free":
return 4096
else:
return fd["length"]
def parse_frame(self):
hex_str = self.var_hex.get().strip()
if not hex_str:
messagebox.showwarning("提示", "请粘贴十六进制报文")
return
try:
data = bytes.fromhex(hex_str)
ptr = 0
for wdg in self.field_widgets:
fd = wdg["field"]
tp = fd["type"]
var_mode = fd.get("var_mode", "fixed")
endian = "big" if fd["endian"] == "BE(大端)" else "little"
if tp == "CRC":
ptr += fd["length"]
continue
if var_mode == "fixed":
if ptr + fd["length"] > len(data):
messagebox.showwarning("长度不足", f"{fd['name']}超出报文")
break
chunk = data[ptr:ptr+fd["length"]]
ptr += fd["length"]
if tp == "枚举":
val = int.from_bytes(chunk, byteorder=endian)
rev = {v: k for k, v in wdg["enum"].items()}
wdg["var"].set(rev.get(val, ""))
else:
chunk = chunk[::-1] if endian == "little" else chunk
hex_str_out = " ".join(f"{b:02X}" for b in chunk)
wdg["var"].set(hex_str_out)
elif var_mode == "bind":
blen = self._get_bind_length_value(fd["bind_field"])
if ptr + blen > len(data):
messagebox.showwarning("长度不足", f"{fd['name']}超出报文")
break
chunk = data[ptr:ptr+blen]
ptr += blen
wdg["var"].set(" ".join(f"{b:02X}" for b in chunk))
elif var_mode == "free":
chunk = data[ptr:]
ptr = len(data)
wdg["var"].set(" ".join(f"{b:02X}" for b in chunk))
except Exception as e:
messagebox.showerror("解析异常", str(e))
# ===================== Tab1 常用报文库(重构:删除分类+弹窗一体化) =====================
class MessageLibTab:
def __init__(self, parent, notify_template_change, switch_tab, editor_ref):
self.parent = parent
self.notify_refresh = notify_template_change
self.switch_tab = switch_tab
self.editor = editor_ref
self.msg_list = []
self.category_list = []
self.load_file()
self.build_ui()
def load_file(self):
if os.path.exists(MSG_LIB_FILE):
try:
with open(MSG_LIB_FILE, "r", encoding="utf-8") as f:
self.msg_list = json.load(f)
except:
self.msg_list = []
if os.path.exists(MSG_CATEGORY_FILE):
try:
with open(MSG_CATEGORY_FILE, "r", encoding="utf-8") as f:
self.category_list = json.load(f)
except:
self.category_list = ["默认分类"]
else:
self.category_list = ["默认分类"]
def save_msg_lib(self):
with open(MSG_LIB_FILE, "w", encoding="utf-8") as f:
json.dump(self.msg_list, f, ensure_ascii=False, indent=2)
self.refresh_msg_tree()
def save_category_file(self):
with open(MSG_CATEGORY_FILE, "w", encoding="utf-8") as f:
json.dump(self.category_list, f, ensure_ascii=False, indent=2)
self.refresh_category_cmb()
def refresh_category_cmb(self):
self.cmb_category["values"] = self.category_list
def refresh_template_cmb(self, editor):
if editor and editor.templates:
self.cmb_tmpl["values"] = [t["name"] for t in editor.templates]
else:
self.cmb_tmpl["values"] = []
def refresh_msg_tree(self):
for i in self.tree.get_children():
self.tree.delete(i)
for idx, item in enumerate(self.msg_list):
self.tree.insert("", "end", iid=str(idx), values=(item["category"], item["msg_name"], item["template_name"], item["hex_data"], item["remark"]))
def build_ui(self):
top_frame = ttk.Frame(self.parent)
top_frame.pack(fill="x", padx=5, pady=5)
ttk.Label(top_frame, text="报文分类:").pack(side="left")
self.cmb_category = ttk.Combobox(top_frame, width=15, values=self.category_list)
self.cmb_category.set(self.category_list[0])
self.cmb_category.pack(side="left", padx=4)
ttk.Button(top_frame, text="新增分类", command=self.add_category).pack(side="left", padx=2)
ttk.Button(top_frame, text="删除分类", command=self.del_category).pack(side="left", padx=2)
ttk.Label(top_frame, text="选择模板:").pack(side="left", padx=10)
self.cmb_tmpl = ttk.Combobox(top_frame, width=25)
self.cmb_tmpl.pack(side="left", padx=4)
mid_frame = ttk.LabelFrame(self.parent, text="新建报文(选择模板后点击配置)")
mid_frame.pack(fill="x", padx=5, pady=5)
ttk.Button(mid_frame, text="配置报文并保存入库", command=lambda: self.popup_build_full_msg(edit_item=None)).grid(row=0, column=0, padx=8, pady=8)
list_frame = ttk.LabelFrame(self.parent, text="全部报文列表")
list_frame.pack(fill="both", expand=True, padx=5, pady=5)
self.tree = ttk.Treeview(list_frame, columns=("cat", "name", "tmpl", "hex", "remark"), show="headings")
self.tree.heading("cat", text="分类")
self.tree.heading("name", text="报文名")
self.tree.heading("tmpl", text="模板")
self.tree.heading("hex", text="报文Hex")
self.tree.heading("remark", text="备注")
self.tree.column("hex", width=320)
self.tree.pack(fill="both", expand=True)
btn_bar = ttk.Frame(list_frame)
btn_bar.pack(fill="x", pady=3)
ttk.Button(btn_bar, text="删除选中", command=self.del_msg).pack(side="left")
ttk.Button(btn_bar, text="复制Hex", command=self.copy_hex).pack(side="left", padx=5)
self.tree.bind("<Double-1>", self.load_selected_msg)
self.refresh_category_cmb()
self.refresh_msg_tree()
def del_category(self):
cat = self.cmb_category.get().strip()
if cat == "默认分类":
messagebox.showerror("禁止操作", "默认分类不能删除")
return
if cat not in self.category_list:
return
res = messagebox.askyesno("确认删除", f"确定删除分类【{cat}】?该分类下所有报文会丢失!")
if not res:
return
# 清除该分类所有报文
self.msg_list = [m for m in self.msg_list if m["category"] != cat]
self.category_list.remove(cat)
self.save_category_file()
self.save_msg_lib()
messagebox.showinfo("完成", "分类已删除")
def add_category(self):
cat = simpledialog.askstring("新增分类", "输入分类名称")
if not cat or cat.strip() == "" or cat in self.category_list:
return
self.category_list.append(cat)
self.save_category_file()
def popup_build_full_msg(self, edit_item=None):
tmpl_name = self.cmb_tmpl.get().strip()
target_cat = self.cmb_category.get().strip()
edit_data = None
# 如果是编辑模式,覆盖原有模板、分类
if edit_item is not None:
edit_data = edit_item
tmpl_name = edit_item["template_name"]
target_cat = edit_item["category"]
self.cmb_tmpl.set(tmpl_name)
self.cmb_category.set(target_cat)
if not tmpl_name:
messagebox.showwarning("提示", "请先选择协议", parent=self.parent)
return
# 找到对应模板
target_tmpl = None
for t in self.editor.templates:
if t["name"] == tmpl_name:
target_tmpl = t
break
if target_tmpl is None:
messagebox.showerror("错误", "模板不存在,请先在Tab0配置模板")
return
# 创建弹窗
win = tk.Toplevel(self.parent.winfo_toplevel())
win.title("配置报文并保存入库" if edit_item is None else "编辑报文")
win.geometry("750x620")
win.transient(self.parent.winfo_toplevel())
win.grab_set()
win.columnconfigure(1, weight=1)
vars_field = {}
# 1. 报文名称、备注
top_cfg = ttk.LabelFrame(win, text="报文基础信息")
top_cfg.pack(fill="x", padx=6, pady=6)
ttk.Label(top_cfg, text="报文名称:").grid(row=0, column=0, sticky="w", padx=4, pady=6)
var_msg_name = tk.StringVar()
if edit_data is not None:
var_msg_name.set(edit_data["msg_name"])
ttk.Entry(top_cfg, textvariable=var_msg_name, width=22).grid(row=0, column=1, padx=4)
ttk.Label(top_cfg, text="用途备注:").grid(row=0, column=2, sticky="w", padx=10)
var_remark = tk.StringVar()
if edit_data is not None:
var_remark.set(edit_data["remark"])
ttk.Entry(top_cfg, textvariable=var_remark, width=38).grid(row=0, column=3, padx=4)
# 2. 字段输入区
field_frame = ttk.LabelFrame(win, text="协议字段填写(十六进制)")
field_frame.pack(fill="both", expand=True, padx=6, pady=6)
r_idx = 0
if edit_item is not None and "field_input" in edit_item:
saved_input = edit_item["field_input"]
else:
saved_input = {}
for fd in target_tmpl["fields"]:
fname = fd["name"]
ftype = fd["type"]
ttk.Label(field_frame, text=f"{fname} [{ftype}]").grid(row=r_idx, column=0, sticky="w", padx=3, pady=4)
if ftype == "枚举":
enum_map = fd["enum_dict"]
init_val = saved_input.get(fname, list(enum_map.keys())[0] if enum_map else "")
var_f = tk.StringVar(value=init_val)
cb_f = ttk.Combobox(field_frame, textvariable=var_f, values=list(enum_map.keys()), width=28)
cb_f.grid(row=r_idx, column=1, padx=3)
elif ftype == "CRC":
var_f = tk.StringVar()
ttk.Label(field_frame, text="自动计算无需填写").grid(row=r_idx, column=1)
else:
var_f = tk.StringVar()
ent_f = ttk.Entry(field_frame, textvariable=var_f, width=32)
ent_f.grid(row=r_idx, column=1, padx=3)
if fname in saved_input:
var_f.set(saved_input[fname])
vars_field[fname] = {"var": var_f, "field": fd}
r_idx += 1
# 底部按钮
btn_frame = ttk.Frame(win)
btn_frame.pack(fill="x", padx=6, pady=8)
def save_all():
try:
msg_name = var_msg_name.get().strip()
remark = var_remark.get().strip()
if not msg_name:
messagebox.showwarning("提示", "报文名称不能为空", parent=win)
return
field_input_save = {}
temp_fields = []
# 1. 先把弹窗字段数据缓存,复刻编辑器field_widgets结构
for fd in target_tmpl["fields"]:
fn = fd["name"]
fdata = vars_field[fn]
v = fdata["var"].get().strip()
temp_fields.append((fd, v))
if fd["type"] != "CRC":
field_input_save[fn] = v
final_buf = b""
crc_cache = {}
# 2. 完全复刻编辑器遍历逻辑,逐字段流式拼接
for fd, v in temp_fields:
tp = fd["type"]
endian = "big" if fd["endian"] == "BE(大端)" else "little"
var_mode = fd.get("var_mode", "fixed")
if tp == "枚举":
val = fd["enum_dict"][v]
final_buf += val.to_bytes(fd["length"], byteorder=endian)
elif tp == "普通":
if var_mode == "fixed":
raw_str = v.replace(" ", "")
num = int(raw_str, 16) if raw_str else 0
bdata = num.to_bytes(fd["length"], byteorder=endian)
final_buf += bdata
elif var_mode == "bind":
bind_fd = None
bind_val_str = ""
for f, vv in temp_fields:
if f["name"] == fd["bind_field"]:
bind_fd = f
bind_val_str = vv.replace(" ", "")
break
bind_len = int(bind_val_str, 16) if bind_val_str else 0
bind_len = min(bind_len, 4096)
raw_data = bytes.fromhex(v.replace(" ", "")) if v else b""
chunk = raw_data[:bind_len]
if len(chunk) < bind_len:
chunk = chunk.ljust(bind_len, b"\x00")
final_buf += chunk
elif var_mode == "free":
raw = bytes.fromhex(v.replace(" ", "")) if v else b""
final_buf += raw
elif tp == "CRC":
crc_start_name = fd.get("crc_start_field", "")
crc_end_name = fd.get("crc_range_field", "")
start_ptr = 0
end_ptr = len(final_buf)
if crc_start_name:
pos = 0
for f, vv in temp_fields:
if f["name"] == crc_start_name:
start_ptr = pos
break
if f["type"] == "CRC":
pos += f["length"]
elif f.get("var_mode") == "bind":
# 计算bind真实长度
bv = ""
for ff, vvv in temp_fields:
if ff["name"] == f["bind_field"]:
bf = ff
bv = vvv
break
blen = int(bv.replace(" ", ""), 16) if bv else 0
pos += min(blen, 4096)
elif f.get("var_mode") == "free":
pos += 4096
else:
pos += f["length"]
if crc_end_name:
pos = 0
for f, vv in temp_fields:
flen = 0
ftp = f["type"]
fvm = f.get("var_mode", "fixed")
if ftp == "CRC":
flen = f["length"]
elif fvm == "bind":
bf = None
bv = ""
for ff, vvv in temp_fields:
if ff["name"] == f["bind_field"]:
bv = vvv
break
blen = int(bv.replace(" ", ""), 16) if bv else 0
flen = min(blen, 4096)
elif fvm == "free":
flen = 4096
else:
flen = f["length"]
pos += flen
if f["name"] == crc_end_name:
end_ptr = pos
break
calc_data = final_buf[start_ptr:end_ptr]
if fd["crc_model"] == "自定义":
crc_cfg = fd["crc_custom"]
else:
crc_cfg = DEFAULT_PRESETS[fd["crc_model"]]
crc_val = calc_crc(calc_data, **crc_cfg)
crc_bytes = crc_val.to_bytes(fd["length"], byteorder="big")
if endian == "little":
crc_bytes = crc_bytes[::-1]
crc_cache[fd["name"]] = crc_bytes
final_buf += crc_bytes
hex_str = " ".join(f"{b:02X}" for b in final_buf)
if edit_item is None:
for m in self.msg_list:
if m["category"] == target_cat and m["msg_name"] == msg_name:
messagebox.showerror("重名", "当前分类下该报文名称已存在", parent=win)
return
self.msg_list.append({
"category": target_cat,
"msg_name": msg_name,
"template_name": tmpl_name,
"hex_data": hex_str,
"remark": remark,
"field_input": field_input_save
})
else:
edit_idx = self.msg_list.index(edit_item)
for idx, m in enumerate(self.msg_list):
if idx != edit_idx and m["category"] == target_cat and m["msg_name"] == msg_name:
messagebox.showerror("重名", "当前分类下该报文名称已存在", parent=win)
return
self.msg_list[edit_idx] = {
"category": target_cat,
"msg_name": msg_name,
"template_name": tmpl_name,
"hex_data": hex_str,
"remark": remark,
"field_input": field_input_save
}
self.save_msg_lib()
self.refresh_msg_tree()
win.destroy()
except Exception as err:
import traceback
err_info = traceback.format_exc()
messagebox.showerror("生成失败", f"错误详情:{err_info}", parent=win)
ttk.Button(btn_frame, text="生成并保存报文", command=save_all).pack(side="left", padx=15)
ttk.Button(btn_frame, text="取消", command=win.destroy).pack(side="left")
def del_msg(self):
sel = self.tree.selection()
if not sel:
return
idx = int(sel[0])
del self.msg_list[idx]
self.save_msg_lib()
def copy_hex(self):
sel = self.tree.selection()
if not sel:
return
idx = int(sel[0])
hex_txt = self.msg_list[idx]["hex_data"]
self.parent.clipboard_clear()
self.parent.clipboard_append(hex_txt)
def load_selected_msg(self, event):
sel = self.tree.selection()
if not sel:
return
idx = int(sel[0])
item = self.msg_list[idx]
# 双击直接打开编辑弹窗,传入旧报文对象
self.popup_build_full_msg(edit_item=item)
# ===================== Tab2 通信发送控制台 =====================
class CommSendTab:
def __init__(self, parent, get_msg_lib):
self.parent = parent
self.get_msg_lib = get_msg_lib
self.ser = None
self.can_bus = None
self.timer_id = None
self.send_thread = None
self.thread_run = False
self.send_list = []
self.load_comm_cfg()
# 筛选下拉变量
self.send_cat_var = tk.StringVar()
self.cmb_send_cat = None
self.send_msg_var = tk.StringVar()
self.cmb_send_msg = None
# 串口变量
self.ser_port_var = tk.StringVar()
self.ser_baud_var = tk.StringVar()
self.ser_databit_var = tk.StringVar()
self.ser_stopbit_var = tk.StringVar()
self.ser_parity_var = tk.StringVar()
self.ser_recv_thread = None
self.ser_recv_run = False
# CAN变量
self.can_chan_var = tk.StringVar()
self.can_interface_var = tk.StringVar(value="pcan")
self.can_arb_baud_var = tk.StringVar()
self.can_fd_var = tk.StringVar()
self.can_fd_en_var = tk.BooleanVar()
self.can_ext_var = tk.BooleanVar()
self.can_txid_var = tk.StringVar()
self.can_mode_var = tk.StringVar(value="CAN2.0")
self.can_dlc_var = tk.StringVar(value="8")
self.dlc_list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64]
self.cb_dlc = None
self.can_recv_thread = None
self.can_recv_run = False
# USB‑CDC
self.cdc_ser = None
self.cdc_port_var = tk.StringVar()
self.cdc_baud_var = tk.StringVar()
self.cdc_databit_var = tk.StringVar()
self.cdc_stopbit_var = tk.StringVar()
self.cdc_parity_var = tk.StringVar()
self.cdc_recv_thread = None
self.cdc_recv_run = False
# USB-HID
self.usb_vid_var = tk.StringVar()
self.usb_pid_var = tk.StringVar()
self.usb_rlen_var = tk.StringVar()
self.usb_dev = None
self.usb_recv_thread = None
self.usb_recv_run = False
self.send_tree = None
self.enable_recv_log = tk.BooleanVar(value=True)
self.build_ui()
self.refresh_send_tree()
def load_comm_cfg(self):
if os.path.exists(COMM_CFG_FILE):
try:
with open(COMM_CFG_FILE, "r", encoding="utf-8") as f:
self.cfg = json.load(f)
except:
self.cfg = {}
else:
self.cfg = {
"active_comm": "串口(TTL/RS232)",
"serial": {"port": "", "baud": 115200, "data": 8, "stop": 1, "parity": "N"},
"can": {"channel": "0", "arb_baud": 500000, "fd_baud": 2000000, "fd_enable": False, "ext": False, "tx_id": 0x7FF, "interface": "pcan"},
"usb_cdc": {"port": "", "baud": 115200, "data": 8, "stop": 1, "parity": "N"},
"usb_vid": {"vid": 0, "pid": 0, "report_len": 64},
"period_ms": 1000,
"global_auto": False
}
def save_cfg(self):
with open(COMM_CFG_FILE, "w", encoding="utf-8") as f:
json.dump(self.cfg, f, indent=2)
def double_click_edit_period(self, event=None):
sel = self.send_tree.selection()
if not sel:
return
idx = int(sel[0])
item = self.send_list[idx]
win = tk.Toplevel(self.parent.winfo_toplevel())
win.title("修改发送周期")
win.geometry("400x160")
win.transient(self.parent)
win.grab_set()
win.columnconfigure(1, weight=1)
ttk.Label(win, text=f"报文:{item['name']}").grid(row=0, column=0, columnspan=2, pady=8)
ttk.Label(win, text="周期ms:").grid(row=1, column=0, sticky="w", padx=10)
var_p = tk.StringVar(value=str(item["period"]))
ttk.Entry(win, textvariable=var_p, width=20).grid(row=1, column=1, padx=10)
def save():
try:
new_p = int(var_p.get())
except ValueError:
messagebox.showerror("输入错误", "请输入数字", parent=win)
return
item["period"] = new_p
self.refresh_send_tree()
win.destroy()
ttk.Button(win, text="保存", command=save).grid(row=2, column=0, columnspan=2, pady=12)
def refresh_msg_cmb(self, event=None):
lib = self.get_msg_lib()
target_cat = self.send_cat_var.get()
if not target_cat:
self.cmb_send_msg["values"] = []
self.send_msg_var.set("")
self.cmb_send_msg.config(state="disabled")
return
msg_names = [m["msg_name"] for m in lib.msg_list if m["category"] == target_cat]
self.cmb_send_msg["values"] = msg_names
self.cmb_send_msg.config(state="readonly")
def refresh_cat_cmb(self):
lib = self.get_msg_lib()
cats = lib.category_list
self.cmb_send_cat["values"] = cats
self.send_cat_var.set("")
self.send_msg_var.set("")
self.cmb_send_msg["values"] = []
self.cmb_send_msg.config(state="disabled")
def add_msg_from_select(self):
lib = self.get_msg_lib()
sel_cat = self.send_cat_var.get()
sel_msg = self.send_msg_var.get()
if not sel_msg:
messagebox.showwarning("提示", "请选择报文")
return
# 查找报文
target = None
for m in lib.msg_list:
if m["category"] == sel_cat and m["msg_name"] == sel_msg:
target = m
break
if target is None:
return
win = tk.Toplevel(self.parent.winfo_toplevel())
win.title("添加发送报文")
win.geometry("420x180")
win.transient(self.parent)
win.grab_set()
win.columnconfigure(1, weight=1)
ttk.Label(win, text=f"选中报文:{sel_msg}", font=("SimSun", 11)).grid(row=0, column=0, columnspan=2, pady=10)
ttk.Label(win, text="发送周期(ms,0仅手动):").grid(row=1, column=0, sticky="w", padx=10)
var_per = tk.StringVar(value="1000")
ttk.Entry(win, textvariable=var_per, width=18).grid(row=1, column=1, padx=10)
def confirm():
try:
per = int(var_per.get())
except:
messagebox.showerror("错误", "周期必须是数字", parent=win)
return
self.send_list.append({
"name": sel_msg,
"hex": target["hex_data"],
"period": per,
"enable": True
})
self.refresh_send_tree()
win.destroy()
ttk.Button(win, text="确认添加", command=confirm).grid(row=2, column=0, columnspan=2, pady=15)
def build_ui(self):
# 顶部通道选择行
top_sel = ttk.Frame(self.parent)
top_sel.pack(fill="x", padx=5, pady=5)
ttk.Label(top_sel, text="通信通道切换:").pack(side="left")
self.comm_type_var = tk.StringVar(value=self.cfg["active_comm"])
self.comm_sel = ttk.Combobox(top_sel, values=["串口(TTL/RS232)", "CAN", "USB‑CDC", "USB-HID"], textvariable=self.comm_type_var, state="readonly")
self.comm_sel.pack(side="left", padx=5)
self.comm_sel.bind("<<ComboboxSelected>>", self.on_comm_switch)
# 独立参数配置容器
self.comm_param_frame = ttk.LabelFrame(self.parent, text="通道参数配置")
self.comm_param_frame.pack(fill="x", padx=5, pady=5)
filter_frame = ttk.LabelFrame(self.parent, text="报文快速选择")
filter_frame.pack(fill="x", padx=5, pady=5)
ttk.Label(filter_frame, text="分类:").grid(row=0, column=0, padx=3, pady=4)
self.send_cat_var = tk.StringVar()
self.cmb_send_cat = ttk.Combobox(filter_frame, textvariable=self.send_cat_var, state="readonly", width=16)
self.cmb_send_cat.grid(row=0, column=1, padx=3)
self.cmb_send_cat.bind("<<ComboboxSelected>>", self.refresh_msg_cmb)
ttk.Label(filter_frame, text="报文:").grid(row=0, column=2, padx=8, pady=4)
self.send_msg_var = tk.StringVar()
self.cmb_send_msg = ttk.Combobox(filter_frame, textvariable=self.send_msg_var, state="disabled", width=30)
self.cmb_send_msg.grid(row=0, column=3, padx=3)
ttk.Button(filter_frame, text="添加到发送列表", command=self.add_msg_from_select).grid(row=0, column=4, padx=10)
# 待发送报文列表
list_frame = ttk.LabelFrame(self.parent, text="待发送报文列表(双击修改周期)")
list_frame.pack(fill="both", expand=True, padx=5, pady=5)
btn_list = ttk.Frame(list_frame)
btn_list.pack(fill="x", padx=3, pady=3)
ttk.Button(btn_list, text="编辑选中", command=self.edit_send_item).pack(side="left")
ttk.Button(btn_list, text="删除选中", command=self.del_send_item).pack(side="left", padx=3)
ttk.Button(btn_list, text="清空列表", command=self.clear_send_list).pack(side="left")
self.send_tree = ttk.Treeview(list_frame, columns=("name", "hex", "period", "enable"), show="headings")
self.send_tree.heading("name", text="报文名称")
self.send_tree.heading("hex", text="Hex载荷")
self.send_tree.heading("period", text="周期ms(0=仅手动)")
self.send_tree.heading("enable", text="周期启用")
self.send_tree.column("hex", width=360)
self.send_tree.pack(fill="both", expand=True)
# 双击绑定:打开修改周期弹窗
self.send_tree.bind("<Double-1>", self.double_click_edit_period)
# 全局发送控制
ctrl_frame = ttk.LabelFrame(self.parent, text="全局发送控制")
ctrl_frame.pack(fill="x", padx=5, pady=5)
self.var_global_auto = tk.BooleanVar(value=self.cfg["global_auto"])
ttk.Checkbutton(ctrl_frame, text="全局启动周期发送", variable=self.var_global_auto,
command=self.toggle_thread).pack(side="left", padx=5)
ttk.Button(ctrl_frame, text="发送选中报文(单次)", command=self.send_single).pack(side="left", padx=10)
self.status_var = tk.StringVar(value="通道:未打开")
ttk.Label(ctrl_frame, textvariable=self.status_var).pack(side="right", padx=10)
# 收发日志
log_frame = ttk.LabelFrame(self.parent, text="收发日志")
log_frame.pack(fill="both", expand=True, padx=5, pady=5)
log_ctrl = ttk.Frame(log_frame)
log_ctrl.pack(fill="x")
ttk.Checkbutton(log_frame, text="打印接收数据日志", variable=self.enable_recv_log).pack(side="left")
ttk.Button(log_frame, text="清空日志", command=lambda: self.log_text.delete(1.0, tk.END)).pack(anchor="w")
self.log_text = scrolledtext.ScrolledText(log_frame, height=10)
self.log_text.pack(fill="both", expand=True)
# 初始化下拉数据
self.refresh_cat_cmb()
self.refresh_send_tree()
self.on_comm_switch()
def on_can_mode_change(self, event=None):
mode = self.can_mode_var.get()
if mode == "CAN-FD":
self.can_fd_en_var.set(True)
else:
self.can_fd_en_var.set(False)
self.fd_switch()
# 同步过滤DLC
if mode == "CAN-FD":
self.cb_dlc["values"] = [str(x) for x in self.dlc_list]
else:
self.cb_dlc["values"] = [str(x) for x in self.dlc_list if x <= 8]
if int(self.can_dlc_var.get()) > 8:
self.can_dlc_var.set("8")
def on_comm_switch(self, event=None):
if self.ser and self.ser.is_open:
self.ser_recv_run = False
if self.ser_recv_thread is not None:
self.ser_recv_thread.join(0.2)
self.ser.close()
self.ser = None
self.log("切换通道,串口已关闭")
if self.cdc_ser and self.cdc_ser.is_open:
self.cdc_recv_run = False
if self.cdc_recv_thread is not None:
self.cdc_recv_thread.join(0.2)
self.cdc_ser.close()
self.cdc_ser = None
self.log("切换通道,USB‑CDC已关闭")
if self.can_bus is not None:
self.can_recv_run = False
if self.can_recv_thread:
self.can_recv_thread.join(0.2)
self.can_bus.shutdown()
self.can_bus = None
self.log("切换通道,CAN通道已关闭")
if self.usb_dev is not None:
self.usb_recv_run = False
if self.usb_recv_thread:
self.usb_recv_thread.join(0.2)
try:
self.usb_dev.close()
except Exception:
pass
self.usb_dev = None
self.log("切换通道,USB‑HID已断开")
self.status_var.set("通道:未打开")
# 清空参数面板所有旧控件
for widget in self.comm_param_frame.winfo_children():
widget.destroy()
# 根据选择渲染对应面板,传入父容器
sel = self.comm_sel.get()
if sel == "串口(TTL/RS232)":
self.build_serial_panel(parent=self.comm_param_frame)
elif sel == "CAN":
self.build_can_panel(parent=self.comm_param_frame)
elif sel == "USB‑CDC":
self.build_usb_cdc_panel(parent=self.comm_param_frame)
elif sel == "USB-HID":
self.build_usb_panel(parent=self.comm_param_frame)
def build_serial_panel(self, parent):
f = parent
for w in f.winfo_children():
w.destroy()
ser_cfg = self.cfg["serial"]
self.ser_port_var.set(ser_cfg["port"])
self.ser_baud_var.set(str(ser_cfg["baud"]))
self.ser_databit_var.set(str(ser_cfg["data"]))
self.ser_stopbit_var.set(str(ser_cfg["stop"]))
self.ser_parity_var.set(ser_cfg["parity"])
row = 0
ttk.Label(f, text="串行COM端口:").grid(row=row, column=0, sticky="w", padx=3, pady=4)
self.cmb_serial_port = ttk.Combobox(f, textvariable=self.ser_port_var, width=15, state="readonly")
self.cmb_serial_port.grid(row=row, column=1, padx=3)
ttk.Button(f, text="刷新端口", command=self.refresh_serial_ports).grid(row=row, column=2)
row += 1
ttk.Label(f, text="波特率:").grid(row=row, column=0, sticky="w", padx=3, pady=4)
ttk.Combobox(f, textvariable=self.ser_baud_var, values=[str(x) for x in SERIAL_BAUDS], state="readonly").grid(row=row, column=1, padx=3)
row += 1
ttk.Label(f, text="数据位:").grid(row=row, column=0, sticky="w", padx=3, pady=4)
ttk.Combobox(f, textvariable=self.ser_databit_var, values=[str(x) for x in SERIAL_DATA], state="readonly").grid(row=row, column=1, padx=3)
row += 1
ttk.Label(f, text="停止位:").grid(row=row, column=0, sticky="w", padx=3, pady=4)
ttk.Combobox(f, textvariable=self.ser_stopbit_var, values=[str(x) for x in SERIAL_STOP], state="readonly").grid(row=row, column=1, padx=3)
row += 1
ttk.Label(f, text="校验位:").grid(row=row, column=0, sticky="w", padx=3, pady=4)
ttk.Combobox(f, textvariable=self.ser_parity_var, values=SERIAL_PARITY, state="readonly").grid(row=row, column=1, padx=3)
row += 1
ttk.Button(f, text="打开/关闭串口", command=self.serial_toggle).grid(row=row, column=0, columnspan=3, pady=6)
self.refresh_serial_ports()
if not SERIAL_AVAIL:
for w in f.winfo_children():
w.config(state="disabled")
def refresh_serial_ports(self):
if not SERIAL_AVAIL:
return
ports = [p.device for p in list_ports.comports()]
self.cmb_serial_port["values"] = ports
if ports and self.ser_port_var.get() not in ports:
self.ser_port_var.set(ports[0])
def can_toggle(self):
if not CAN_AVAIL:
messagebox.showwarning("依赖缺失", "未安装 python-can 库,无法使用CAN通道")
return
if self.can_bus is not None:
self.can_recv_run = False
if self.can_recv_thread is not None:
self.can_recv_thread.join(0.2)
try:
self.can_bus.shutdown()
self.log("CAN通道关闭")
except Exception as e:
self.log(f"CAN关闭异常:{e}")
self.can_bus = None
self.status_var.set("通道:未打开")
return
try:
chan = self.can_chan_var.get()
arb = int(self.can_arb_baud_var.get())
fd_baud = int(self.can_fd_var.get())
try:
tx_id = int(self.can_txid_var.get(), 16)
except:
tx_id = 0x7FF
interface = self.can_interface_var.get()
self.cfg["can"]["interface"] = interface
self.cfg["can"]["tx_id"] = tx_id
self.cfg["can"]["ext"] = self.can_ext_var.get()
self.save_cfg()
is_can_fd = (self.can_mode_var.get() == "CAN-FD")
if is_can_fd:
self.can_bus = can.Bus(interface=interface, channel=chan, fd=True, bitrate=arb, data_bitrate=fd_baud)
else:
self.can_bus = can.Bus(interface=interface, channel=chan, bitrate=arb)
self.log(f"CAN通道打开成功 接口={interface} 通道={chan}")
self.status_var.set("CAN 已打开")
self.can_recv_run = True
self.can_recv_thread = threading.Thread(target=self.can_recv_loop, daemon=True)
self.can_recv_thread.start()
except Exception as e:
self.can_bus = None
self.log(f"CAN打开失败:{e}")
def build_can_panel(self, parent):
f = parent
for w in f.winfo_children():
w.destroy()
can_cfg = self.cfg["can"]
self.can_interface_var.set(str(can_cfg.get("interface", "pcan")))
self.can_chan_var.set(str(can_cfg["channel"]))
self.can_arb_baud_var.set(str(can_cfg["arb_baud"]))
self.can_fd_var.set(str(can_cfg["fd_baud"]))
self.can_fd_en_var.set(can_cfg["fd_enable"])
self.can_ext_var.set(can_cfg["ext"])
self.can_txid_var.set(hex(can_cfg.get("tx_id", 0x7FF))[2:])
row = 0
ttk.Label(f, text="CAN模式:").grid(row=row, column=0, sticky="w", padx=3, pady=4)
cb_can_mode = ttk.Combobox(f, textvariable=self.can_mode_var, values=["CAN2.0", "CAN-FD"], state="readonly")
cb_can_mode.grid(row=row, column=1, padx=3)
cb_can_mode.bind("<<ComboboxSelected>>", self.on_can_mode_change)
row += 1
ttk.Label(f, text="DLC:").grid(row=row, column=0, sticky="w", padx=3, pady=4)
self.cb_dlc = ttk.Combobox(f, textvariable=self.can_dlc_var, values=[str(x) for x in self.dlc_list], state="readonly", width=8)
self.cb_dlc.grid(row=row, column=1, padx=3)
row += 1
ttk.Label(f, text="适配器接口:").grid(row=row, column=0, sticky="w", padx=3, pady=4)
ttk.Combobox(f, textvariable=self.can_interface_var, values=CAN_INTERFACES, state="readonly", width=12).grid(row=row, column=1, padx=3)
row += 1
ttk.Label(f, text="CAN适配器通道:").grid(row=row, column=0, sticky="w", padx=3, pady=4)
ttk.Entry(f, textvariable=self.can_chan_var, width=10).grid(row=row, column=1, padx=3)
row += 1
ttk.Label(f, text="发送ID(十六进制):").grid(row=row, column=0, sticky="w", padx=3, pady=4)
ttk.Entry(f, textvariable=self.can_txid_var, width=10).grid(row=row, column=1, padx=3)
row += 1
ttk.Label(f, text="仲裁波特率:").grid(row=row, column=0, sticky="w", padx=3, pady=4)
ttk.Combobox(f, textvariable=self.can_arb_baud_var, values=[str(x) for x in CAN_ARB_BAUD], state="readonly").grid(row=row, column=1, padx=3)
row += 1
ttk.Label(f, text="FD数据波特率:").grid(row=row, column=0, sticky="w", padx=3, pady=4)
self.cb_fd = ttk.Combobox(f, textvariable=self.can_fd_var, values=[str(x) for x in CAN_FD_BAUD], state="readonly")
self.cb_fd.grid(row=row, column=1, padx=3)
self.fd_switch()
row += 1
ttk.Checkbutton(f, text="扩展帧ID", variable=self.can_ext_var).grid(row=row, column=0)
row += 1
ttk.Button(f, text="打开/关闭CAN通道", command=self.can_toggle).grid(row=row, column=0, columnspan=3, pady=6)
self.on_can_mode_change()
if not CAN_AVAIL:
for w in f.winfo_children():
w.config(state="disabled")
ttk.Label(f, text="未安装 python-can 库,CAN通道不可用", foreground="#882200").grid(
row=row + 1, column=0, columnspan=3, pady=2)
def fd_switch(self):
if not hasattr(self, "cb_fd") or self.cb_fd is None:
return
if self.can_fd_en_var.get():
self.cb_fd.config(state="readonly")
else:
self.cb_fd.config(state="disabled")
def build_usb_cdc_panel(self, parent):
f = parent
for w in f.winfo_children():
w.destroy()
cdc_cfg = self.cfg.setdefault("usb_cdc", {"port": "", "baud": 115200, "data": 8, "stop": 1, "parity": "N"})
self.cdc_port_var.set(cdc_cfg["port"])
self.cdc_baud_var.set(str(cdc_cfg["baud"]))
self.cdc_databit_var.set(str(cdc_cfg["data"]))
self.cdc_stopbit_var.set(str(cdc_cfg["stop"]))
self.cdc_parity_var.set(cdc_cfg["parity"])
row = 0
ttk.Label(f, text="CDC‑COM端口:").grid(row=row, column=0, sticky="w", padx=3, pady=4)
self.cmb_cdc_port = ttk.Combobox(f, textvariable=self.cdc_port_var, width=15, state="readonly")
self.cmb_cdc_port.grid(row=row, column=1, padx=3)
ttk.Button(f, text="刷新端口", command=self.refresh_cdc_ports).grid(row=row, column=2)
row += 1
ttk.Label(f, text="波特率(仅通知MCU):").grid(row=row, column=0, sticky="w", padx=3, pady=4)
ttk.Combobox(f, textvariable=self.cdc_baud_var, values=[str(x) for x in SERIAL_BAUDS], state="readonly").grid(row=row, column=1, padx=3)
row += 1
ttk.Label(f, text="提示:USB‑CDC波特率仅通知单片机,实际速度由USB决定", foreground="#882200").grid(row=row, column=0, columnspan=3, pady=2)
row += 1
ttk.Button(f, text="打开/关闭 USB‑CDC", command=self.usb_cdc_toggle).grid(row=row, column=0, columnspan=3, pady=6)
self.refresh_cdc_ports()
if not SERIAL_AVAIL:
for w in f.winfo_children():
w.config(state="disabled")
def refresh_cdc_ports(self):
if not SERIAL_AVAIL:
return
ports = [p.device for p in list_ports.comports()]
self.cmb_cdc_port["values"] = ports
if ports and self.cdc_port_var.get() not in ports:
self.cdc_port_var.set(ports[0])
def usb_cdc_toggle(self):
if self.cdc_ser and self.cdc_ser.is_open:
self.cdc_recv_run = False
if self.cdc_recv_thread is not None:
self.cdc_recv_thread.join(0.2)
self.cdc_ser.close()
self.cdc_ser = None
self.log("USB‑CDC虚拟串口已关闭")
self.status_var.set("通道:未打开")
return
try:
port = self.cdc_port_var.get()
baud = int(self.cdc_baud_var.get())
databit = int(self.cdc_databit_var.get())
stopbit = float(self.cdc_stopbit_var.get())
parity = self.cdc_parity_var.get()
self.cdc_ser = serial.Serial(
port,
baudrate=baud,
bytesize=databit,
stopbits=stopbit,
parity=parity,
timeout=0,
write_timeout=0.2,
rtscts=False,
dsrdtr=False
)
self.cdc_ser.reset_input_buffer()
self.cdc_ser.reset_output_buffer()
# 保存配置
self.cfg["usb_cdc"]["port"] = port
self.cfg["usb_cdc"]["baud"] = baud
self.cfg["usb_cdc"]["data"] = databit
self.cfg["usb_cdc"]["stop"] = stopbit
self.cfg["usb_cdc"]["parity"] = parity
self.save_cfg()
# 启动接收线程
self.cdc_recv_run = True
self.cdc_recv_thread = threading.Thread(target=self.usb_cdc_recv_loop, daemon=True)
self.cdc_recv_thread.start()
self.log(f"USB‑CDC打开成功 {port} (虚拟串口)")
self.status_var.set(f"USB‑CDC {port} 已打开")
except Exception as e:
self.cdc_ser = None
self.log(f"USB‑CDC打开失败:{e}")
messagebox.showerror("USB‑CDC打开失败", str(e))
def usb_cdc_recv_loop(self):
buffer = b""
last_byte_time = time.time()
IDLE_THRESHOLD = 0.01
while self.cdc_recv_run and self.cdc_ser and self.cdc_ser.is_open:
try:
cnt = self.cdc_ser.in_waiting
if cnt > 0:
data = self.cdc_ser.read(cnt)
buffer += data
last_byte_time = time.time()
if len(buffer) > 4096:
buffer = buffer[-2048:]
else:
delta = time.time() - last_byte_time
if len(buffer) > 0 and delta >= IDLE_THRESHOLD:
hex_str = " ".join(f"{b:02X}" for b in buffer)
if self.enable_recv_log.get():
self.log(f"USB‑CDC接收: {hex_str}")
buffer = b""
time.sleep(0.001)
except Exception as e:
self.log(f"USB‑CDC接收异常: {e}")
buffer = b""
time.sleep(0.01)
def build_usb_panel(self, parent):
f = parent
for w in f.winfo_children():
w.destroy()
usb_cfg = self.cfg["usb_vid"]
self.usb_vid_var = tk.StringVar(value=hex(usb_cfg.get("vid", 0))[2:])
self.usb_pid_var = tk.StringVar(value=hex(usb_cfg.get("pid", 0))[2:])
self.usb_rlen_var.set(str(usb_cfg.get("report_len", 64)))
row = 0
ttk.Label(f, text="VID(十六进制):").grid(row=row, column=0, sticky="w", padx=3, pady=4)
ttk.Entry(f, textvariable=self.usb_vid_var, width=12).grid(row=row, column=1, padx=3)
row += 1
ttk.Label(f, text="PID(十六进制):").grid(row=row, column=0, sticky="w", padx=3, pady=4)
ttk.Entry(f, textvariable=self.usb_pid_var, width=12).grid(row=row, column=1, padx=3)
row += 1
ttk.Label(f, text="报告长度(字节):").grid(row=row, column=0, sticky="w", padx=3, pady=4)
ttk.Entry(f, textvariable=self.usb_rlen_var, width=12).grid(row=row, column=1, padx=3)
row += 1
ttk.Button(f, text="打开/断开USB-HID", command=self.usb_toggle).grid(row=row, column=0, columnspan=2, pady=8)
if not USB_AVAIL:
for w in f.winfo_children():
w.config(state="disabled")
ttk.Label(f, text="未安装 hidapi 库,USB-HID通道不可用", foreground="#882200").grid(
row=row + 1, column=0, columnspan=2, pady=2)
def serial_recv_loop(self):
buffer = b""
last_byte_time = time.time()
# 空闲阈值,单位秒,可调:10ms ~30ms
IDLE_THRESHOLD = 0.01
while self.ser_recv_run and self.ser and self.ser.is_open:
try:
cnt = self.ser.in_waiting
if cnt > 0:
data = self.ser.read(cnt)
buffer += data
last_byte_time = time.time()
# 防止缓存无限膨胀
if len(buffer) > 4096:
buffer = buffer[-2048:]
else:
# 没有新字节,判断是否空闲超时
delta = time.time() - last_byte_time
if len(buffer) > 0 and delta >= IDLE_THRESHOLD:
# 空闲超时,判定为完整一帧
hex_str = " ".join(f"{b:02X}" for b in buffer)
if self.enable_recv_log.get():
self.log(f"串口接收: {hex_str}")
buffer = b""
time.sleep(0.001)
except Exception as e:
self.log(f"串口接收异常: {e}")
buffer = b""
time.sleep(0.01)
def can_recv_loop(self):
while self.can_bus is not None and self.can_recv_run:
try:
msg = self.can_bus.recv(timeout=0.01)
if msg is not None:
hex_str = " ".join(f"{b:02X}" for b in msg.data)
if self.enable_recv_log.get():
ext_tag = "扩展帧" if msg.is_extended_id else "标准帧"
fd_tag = "[CAN-FD]" if msg.is_fd else ""
self.log(f"CAN接收 ID=0x{msg.arbitration_id:X} {ext_tag}{fd_tag} DATA:{hex_str}")
except Exception:
time.sleep(0.01)
def usb_recv_loop(self):
report_len = int(self.usb_rlen_var.get())
while self.usb_recv_run and self.usb_dev is not None:
try:
data = self.usb_dev.read(report_len, timeout_ms=50)
if data:
hex_str = " ".join(f"{b:02X}" for b in data)
if self.enable_recv_log.get():
self.log(f"USB-HID接收: {hex_str}")
except Exception:
time.sleep(0.05)
def usb_toggle(self):
if not USB_AVAIL:
messagebox.showwarning("依赖缺失", "未安装 hidapi 库,无法使用USB-HID通道")
return
if self.usb_dev is not None:
# 关闭设备、停止接收线程
self.usb_recv_run = False
if self.usb_recv_thread is not None:
self.usb_recv_thread.join(0.2)
try:
self.usb_dev.close()
self.log("USB‑HID设备已断开")
except Exception as e:
self.log(f"USB‑HID关闭时异常:{e}")
self.usb_dev = None
self.status_var.set("通道:未打开")
return
try:
vid = int(self.usb_vid_var.get(), 16)
pid = int(self.usb_pid_var.get(), 16)
report_len = int(self.usb_rlen_var.get())
# 打开HID设备
self.usb_dev = hid.device()
self.usb_dev.open(vid, pid)
# 保存配置
self.cfg["usb_vid"]["vid"] = vid
self.cfg["usb_vid"]["pid"] = pid
self.cfg["usb_vid"]["report_len"] = report_len
self.save_cfg()
self.log(f"USB‑HID打开成功 VID:{vid:04X} PID:{pid:04X}")
self.status_var.set("USB‑HID 已连接")
# 启动后台接收线程
self.usb_recv_run = True
self.usb_recv_thread = threading.Thread(target=self.usb_recv_loop, daemon=True)
self.usb_recv_thread.start()
except Exception as e:
self.usb_dev = None
self.log(f"USB‑HID打开失败:{str(e)}")
messagebox.showerror("USB错误", f"设备打开失败:{e}")
def serial_toggle(self):
if self.ser and self.ser.is_open:
# 停止接收线程
self.ser_recv_run = False
if self.ser_recv_thread is not None:
self.ser_recv_thread.join(0.2)
self.ser.close()
self.ser = None
self.log("串口已关闭")
self.status_var.set("通道:未打开")
return
try:
port = self.ser_port_var.get()
baud = int(self.ser_baud_var.get())
databit = int(self.ser_databit_var.get())
stopbit = float(self.ser_stopbit_var.get())
parity = self.ser_parity_var.get()
self.ser = serial.Serial(port, baudrate=baud, bytesize=databit, stopbits=stopbit, parity=parity, timeout=0, write_timeout=0.2)
self.ser.reset_input_buffer()
self.ser.reset_output_buffer()
self.cfg["serial"]["port"] = port
self.cfg["serial"]["baud"] = baud
self.cfg["serial"]["data"] = databit
self.cfg["serial"]["stop"] = stopbit
self.cfg["serial"]["parity"] = parity
self.save_cfg()
# 启动接收线程
self.ser_recv_run = True
self.ser_recv_thread = threading.Thread(target=self.serial_recv_loop, daemon=True)
self.ser_recv_thread.start()
self.log(f"串口打开成功 {port} {baud}")
self.status_var.set(f"串口 {port} 已打开")
except Exception as e:
self.log(f"串口打开失败:{e}")
self.ser = None
def log(self, text):
t = time.strftime("%Y-%m-%d %H:%M:%S")
self.log_text.insert(tk.END, f"[{t}] {text}\n")
self.log_text.see(tk.END)
def refresh_send_tree(self):
for i in self.send_tree.get_children():
self.send_tree.delete(i)
for idx, item in enumerate(self.send_list):
if item["period"] <= 0:
en_text = "关闭"
else:
en_text = "开启" if item["enable"] else "关闭"
self.send_tree.insert("", "end", iid=str(idx), values=(item["name"], item["hex"], item["period"], en_text))
def add_send_item(self):
lib = self.get_msg_lib()
msg_names = [m["msg_name"] for m in lib.msg_list]
sel = simpledialog.askstring("选择报文", "从报文库选择名称", initialvalue=msg_names[0] if msg_names else "")
if sel not in msg_names:
return
target = None
for m in lib.msg_list:
if m["msg_name"] == sel:
target = m
break
per = simpledialog.askinteger("周期ms", "0代表仅手动发送", initialvalue=1000)
self.send_list.append({
"name": sel,
"hex": target["hex_data"],
"period": per,
"enable": True
})
self.refresh_send_tree()
def edit_send_item(self):
sel = self.send_tree.selection()
if not sel:
return
idx = int(sel[0])
item = self.send_list[idx]
win = tk.Toplevel(self.parent.winfo_toplevel())
win.title("修改发送周期")
win.geometry("400x160")
win.transient(self.parent)
win.grab_set()
win.columnconfigure(1, weight=1)
ttk.Label(win, text=f"报文:{item['name']}").grid(row=0, column=0, columnspan=2, pady=8)
ttk.Label(win, text="周期ms:").grid(row=1, column=0, sticky="w", padx=10)
var_p = tk.StringVar(value=str(item["period"]))
ttk.Entry(win, textvariable=var_p, width=20).grid(row=1, column=1, padx=10)
def save():
try:
new_p = int(var_p.get())
except ValueError:
messagebox.showerror("输入错误", "请输入数字", parent=win)
return
item["period"] = new_p
self.refresh_send_tree()
win.destroy()
ttk.Button(win, text="保存", command=save).grid(row=2, column=0, columnspan=2, pady=12)
def del_send_item(self):
sel = self.send_tree.selection()
if not sel:
return
idx = int(sel[0])
del self.send_list[idx]
self.refresh_send_tree()
def clear_send_list(self):
self.send_list.clear()
self.refresh_send_tree()
def send_single(self):
active = self.comm_sel.get()
sel = self.send_tree.selection()
if not sel:
messagebox.showwarning("提示", "请选中一条报文")
return
idx = int(sel[0])
hex_str = self.send_list[idx]["hex"].strip()
try:
raw = binascii.unhexlify(hex_str.replace(" ", ""))
except:
self.log("报文HEX格式错误")
messagebox.showerror("发送失败", "报文十六进制格式错误")
return
if active == "串口(TTL/RS232)":
if not (self.ser and self.ser.is_open):
msg = "串口通道未打开,请先配置并打开串口"
self.log(msg)
messagebox.showwarning("通道未就绪", msg)
return
try:
self.ser.write(raw)
self.ser.flush()
self.ser.reset_output_buffer()
self.log(f"串口发送: {raw.hex(' ')}")
except Exception as send_err:
self.log(f"串口发送失败:{send_err}")
messagebox.showerror("发送错误", f"报文发送失败:{send_err}")
elif active == "CAN":
if self.can_bus is None:
msg = "CAN通道未打开,请先配置并打开CAN"
self.log(msg)
messagebox.showwarning("通道未就绪", msg)
return
try:
tx_id = int(self.can_txid_var.get(), 16)
except:
tx_id = 0x7FF
dlc = int(self.can_dlc_var.get())
data = bytearray(raw[:dlc])
data.extend(b"\x00" * (dlc - len(data)))
msg = can.Message(arbitration_id=tx_id, data=bytes(data), dlc=dlc,
is_extended_id=self.can_ext_var.get(),
is_fd=(self.can_mode_var.get() == "CAN-FD"))
self.can_bus.send(msg)
self.log(f"CAN发送 ID=0x{tx_id:X} DATA:{bytes(data).hex(' ')}")
elif active == "USB‑CDC":
if not (self.cdc_ser and self.cdc_ser.is_open):
msg = "USB‑CDC未打开"
self.log(msg)
messagebox.showwarning("通道未就绪", msg)
return
try:
self.cdc_ser.write(raw)
self.cdc_ser.flush()
self.log(f"USB‑CDC发送: {raw.hex(' ')}")
except Exception as send_err:
self.log(f"USB‑CDC发送失败:{send_err}")
messagebox.showerror("发送错误", f"报文发送失败:{send_err}")
elif active == "USB-HID":
if self.usb_dev is None:
msg = "USB-HID设备未打开"
self.log(msg)
messagebox.showwarning("通道未就绪", msg)
return
try:
report_len = int(self.usb_rlen_var.get())
send_buf = bytearray(report_len)
send_buf[0] = 0
copy_len = min(len(raw), report_len - 1)
send_buf[1:1 + copy_len] = raw[:copy_len]
n = self.usb_dev.write(bytes(send_buf))
if n != len(send_buf):
raise IOError(f"仅写入{n}/{len(send_buf)}字节")
self.log(f"USB-HID发送: {send_buf.hex(' ')}")
except Exception as e:
self.log(f"USB发送失败:{e}")
messagebox.showerror("发送异常", str(e))
else:
msg = "未识别的通信通道"
self.log(msg)
messagebox.showerror("错误", msg)
def toggle_thread(self):
self.thread_run = self.var_global_auto.get()
if self.thread_run:
self.send_thread = threading.Thread(target=self.send_loop, daemon=True)
self.send_thread.start()
else:
self.send_thread = None
def send_loop(self):
last_send = {}
while self.thread_run:
now = time.time() * 1000
for item in self.send_list:
if not item["enable"] or item["period"] <= 0:
continue
name = item["name"]
if now - last_send.get(name, 0) >= item["period"]:
try:
raw = binascii.unhexlify(item["hex"].replace(" ", ""))
self.trans_raw(raw)
last_send[name] = now
except:
pass
time.sleep(0.005)
def trans_raw(self, raw):
active = self.comm_sel.get()
if active == "串口(TTL/RS232)":
if self.ser and self.ser.is_open:
try:
self.ser.write(raw)
self.ser.flush()
self.ser.reset_output_buffer()
self.log(f"周期串口发送 {raw.hex(' ')}")
except Exception as e:
self.log(f"周期发送异常:{e}")
else:
self.log("周期发送:串口未打开,跳过")
elif active == "CAN" and self.can_bus:
try:
tx_id = int(self.can_txid_var.get(), 16)
except ValueError:
tx_id = 0x7FF
dlc = int(self.can_dlc_var.get())
data = bytearray(raw[:dlc])
data.extend(b"\x00" * (dlc - len(data)))
msg = can.Message(arbitration_id=tx_id, data=bytes(data), dlc=dlc,
is_extended_id=self.can_ext_var.get(),
is_fd=(self.can_mode_var.get() == "CAN-FD"))
self.can_bus.send(msg)
self.log(f"周期CAN发送 ID=0x{tx_id:X} {bytes(data).hex()}")
elif active == "USB‑CDC":
if self.cdc_ser and self.cdc_ser.is_open:
try:
self.cdc_ser.write(raw)
self.cdc_ser.flush()
self.log(f"周期USB‑CDC发送 {raw.hex(' ')}")
except Exception as e:
self.log(f"USB‑CDC周期发送异常:{e}")
else:
self.log("周期发送:USB‑CDC未打开,跳过")
elif active == "USB-HID":
if self.usb_dev is None:
self.log("周期发送:USB未连接,跳过")
return
try:
report_len = int(self.usb_rlen_var.get())
send_buf = bytearray(report_len)
send_buf[0] = 0
copy_len = min(len(raw), report_len - 1)
send_buf[1:1 + copy_len] = raw[:copy_len]
n = self.usb_dev.write(bytes(send_buf))
if n != len(send_buf):
raise IOError(f"仅写入{n}/{len(send_buf)}字节")
self.log(f"周期USB发送 {send_buf.hex()}")
except Exception as e:
self.log(f"USB周期发送异常:{e}")
# ===================== 主窗口多Tab入口 =====================
class MainApp:
def __init__(self, root):
self.root = root
self.root.title("综合协议工具")
self.root.geometry("1260x840")
notebook = ttk.Notebook(root)
notebook.pack(fill="both", expand=True, padx=6, pady=6)
# Tab0 协议编辑器
frame0 = ttk.Frame(notebook)
notebook.add(frame0, text="协议模板编辑器")
self.editor = ProtocolEditor(frame0, self.on_template_changed)
# Tab1 报文管理
frame1 = ttk.Frame(notebook)
notebook.add(frame1, text="常用报文库")
self.msg_tab = MessageLibTab(frame1, self.on_template_changed, lambda i: notebook.select(), self.editor)
# Tab2 通信发送
frame2 = ttk.Frame(notebook)
notebook.add(frame2, text="总线发送控制台")
self.comm_tab = CommSendTab(frame2, lambda: self.msg_tab)
def on_tab_switch(event):
idx = notebook.index(notebook.select())
if idx == 1:
self.refresh_tmpl_cmb()
elif idx == 2:
self.comm_tab.refresh_cat_cmb()
notebook.bind("<<NotebookTabChanged>>", on_tab_switch)
def on_template_changed(self):
self.msg_tab.refresh_template_cmb(self.editor)
def refresh_tmpl_cmb(self):
self.msg_tab.refresh_template_cmb(self.editor)
def close_event(self):
# 停止发送循环
self.comm_tab.thread_run = False
if self.comm_tab.send_thread is not None:
self.comm_tab.send_thread.join(0.3) # 等待线程退出
if self.comm_tab.ser and self.comm_tab.ser.is_open:
self.comm_tab.ser_recv_run = False
if self.comm_tab.ser_recv_thread:
self.comm_tab.ser_recv_thread.join(0.2)
self.comm_tab.ser.close()
self.comm_tab.ser = None
if self.comm_tab.cdc_ser and self.comm_tab.cdc_ser.is_open:
self.comm_tab.cdc_recv_run = False
if self.comm_tab.cdc_recv_thread:
self.comm_tab.cdc_recv_thread.join(0.2)
self.comm_tab.cdc_ser.close()
self.comm_tab.cdc_ser = None
if self.comm_tab.can_bus is not None:
self.comm_tab.can_recv_run = False
if self.comm_tab.can_recv_thread:
self.comm_tab.can_recv_thread.join(0.2)
self.comm_tab.can_bus.shutdown()
self.comm_tab.can_bus = None
if self.comm_tab.usb_dev is not None:
self.comm_tab.usb_recv_run = False
if self.comm_tab.usb_recv_thread:
self.comm_tab.usb_recv_thread.join(0.2)
self.comm_tab.usb_dev.close()
self.comm_tab.usb_dev = None
self.root.destroy()
if __name__ == "__main__":
try:
root = tk.Tk()
app = MainApp(root)
root.protocol("WM_DELETE_WINDOW", app.close_event)
root.mainloop()
except Exception as err:
import tkinter.messagebox
tkinter.messagebox.showerror("启动异常", str(err))
3、可执行程序生成及注意事项
(1)在程序源码文件protocol_sender.py所在目录下打开终端,输入命令“pyinstaller -w -F protocol_sender.py”,即可生成可执行文件protocol_sender.exe。
(2)不管是运行protocol_sender.py还是protocol_sender.exe,其所在目录下如果没有小程序需要读写的JSON文件,都会直接新建,切勿随意修改它们。
(3)在实际开发中如果有其它通信接口需求,可在该小程序的基础上继续增加功能。

1227

被折叠的 条评论
为什么被折叠?



