基于Python开发自定义协议编辑工具
一、小程序功能设计
1、自定义协议帧格式设计
(1)功能背景:
①在软件开发岗位的工作中,经常会涉及企业自定义的私有协议,这些私有协议不像CAN通信、USB通信、串口通信,没有专门的软件对数据流进行梳理,对于帧格式的管理,也只能依靠文档。
②为了减少频繁核对协议帧格式带来的重复工作量,可以设计程序实现协议模板存储功能,直接在小程序中编辑协议帧的各字段定义并保存为模板,这样,这个模板不仅维护起来比文档更高效,更关键的是能够用于做数据帧生成和数据帧解析。
(2)协议模板管理功能定义:
①新建协议模板:点击“新建”,输入模板名称,即可新建协议模板(在添加字段前必须新建协议模板,或载入已有的协议模板,否则添加的字段信息无法识别属于哪一个模板)。

②保存协议模板:在载入模板并对字段定义进行修改,或新建模板后,需要点击“保存模板”,才能将编辑的协议模板保存在磁盘中。

③载入协议模板:在下拉框中可选择已保存的模板,然后点击“载入”,即可将协议模板导入,后续可根据该模板生成或解析数据流,也可以继续编辑该协议模板(编辑模板后需要点击“保存模板”)。

④删除协议模板:在下拉框中选择废弃的协议模板,然后点击“删除模板”,即可将该协议模板移除。

(3)协议模板字段编辑功能定义:
①添加字段:点击“添加字段”,即可进入字段配置页面,完成字段配置后,字段定义列表会显示刚刚添加的字段内容(关于字段配置,将在下面详细展开),如果先前已有定义字段,则新添加的字段会追加到已定义字段的后面。

②删除字段:选中需要删除的字段,然后点击“删除选中”,即可将选中字段移除。

③编辑字段:双击字段定义列表的已定义字段,即可重新编辑字段配置。

(4)字段配置功能定义:
①普通字段类型:普通字段只定义字段名称、字段长度以及大小端(不定长字段自动配置为原始数据流,不做端序转换),根据字段长度不同,会细分为三类字段。
[1]定长字段:长度固定,能够配置字段的字节长度;对字段的数据内容没有要求,取决于更上层对该字段的定义。

[2]不定长-绑定前置长度字段:长度不固定,不需要配置字段的字节长度,但是需要绑定指示该不定长字段长度的其它字段,且指示长度的字段需要排在该不定长字段之前;对字段的数据内容没有要求,取决于更上层对该字段的定义。

[3]不定长-自由段:长度不固定,不需要配置字段的字节长度,不需要绑定指示该不定长字段长度的其它字段,但一个协议帧格式中只能存在一个自由长度段,否则在解析数据流时,小程序无法识别不同自由长度段的实际长度;对字段的数据内容没有要求,取决于更上层对该字段的定义。

②枚举字段类型:枚举字段需要定义字段名称、字段长度、大小端以及枚举定义,枚举定义在枚举列表中配置,点击“+”可增加枚举,选中条目点击“-”可删除枚举,双击条目可编辑枚举的名称和对应数值,配置完成后点击“应用枚举列表”。

③校验(CRC)字段类型:校验字段需要定义字段名称、字段长度(字段长度需要与CRC模型匹配,否则会报错)、大小端以及校验模型,校验模型可选择小程序自带的默认配置,也可选择自定义CRC模型,同时也可选择校验的起始字段和截止字段,如果起始字段配置为空,则默认从首字段开始,如果终止字段配置为空,则默认截止在该校验字段的上一字段。

2、根据帧格式生成数据流
(1)功能背景:
①在嵌入式调试过程中,开发者经常需要使用上位机模拟节点,向单片机发送一串数据流,这需要对照协议格式进行数据流编码,不仅容易把数据流和帧格式的对应关系搞错(尤其是字节偏移),对于校验字段,还需要用其它的工具进行计算,非常麻烦。
②为了减少这种重复且容易出错的工作量,可以基于自定义的协议帧格式,直接列出输入面板,开发者只需根据字段填入十六进制信息,就能自动生成对应的数据流,并且校验字段也能自动计算。
(2)功能定义:
①帧参数输入面板:在有载入模板的前提下,帧参数输入面板会提示用户根据字段填入相应的内容,其中枚举字段通过下拉框选择枚举值,校验字段自动计算。

②生成协议帧:点击“生成协议帧”,即可根据输入面板输入的内容生成十六进制数据流,点击“复制”可以复制生成结果。

③清空输入:点击“清空输入”,即可清除生成结果区和输入面板的所有内容。

3、根据帧格式解析数据流
(1)功能背景:
①开发者面对一串数据流,经常要对照协议格式进行解析,如果数据流很长,还非常容易把数据流和帧格式的对应关系搞错(尤其是字节偏移),导致开发者误以为软件有bug,做很多排查问题的无用功,非常浪费时间。
②为了减少这种重复且容易出错的工作量,可以基于自定义的协议帧格式,直接解析数据流,开发者只需将捕获到的数据流填入输入框,小程序就能自动根据协议帧格式解析出各字段的内容。
(2)功能定义:
①解析协议帧:将数据流贴入下方输入框,点击“解析报文”,小程序会把数据流解析,并显示在输入面板中。

②解析协议帧功能默认通过校验,不会计算校验码是否有误,会自动跳过。
二、小程序开发
1、开发环境
(1)需要确认安装的Python是否自带tkinter库(图形依赖),有则无需单独安装。
(2)安装pyinstaller库,该库主要用于将.py打包成可执行文件.exe,使其运行能够脱离Python环境。
2、程序源码
(1)配置文件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
}
}
]
}
]
(2)程序源码文件ssh_quick_tool.py:
import json
import os
import tkinter as tk
from tkinter import ttk, messagebox
TEMPLATE_FILE = "protocol_templates.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},
}
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
class ProtocolEditor:
def __init__(self, root):
self.root = root
self.root.title("自定义私有协议帧编辑器")
self.root.geometry("1120x760")
self.templates = []
self.current_template = None
self.field_widgets = []
self.load_templates()
top_frame = ttk.Frame(root)
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(root, text="字段定义列表(双击条目编辑)").pack(anchor="w", padx=10)
self.tree = ttk.Treeview(root, 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(root)
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(root, text="帧参数输入面板").pack(anchor="w", padx=10, pady=8)
self.form_frame = ttk.Frame(root)
self.form_frame.pack(fill="x", padx=10)
btn_frame = ttk.Frame(root)
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(root)
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.root.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.root)
dlg.title("新建协议模板")
dlg.geometry("340x130")
dlg.transient(self.root)
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.root)
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()
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()
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 = ""
# 不定长bind / free 统一显示原始数据流
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):
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.root)
win.title("字段配置")
win.geometry("720x660")
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",pady=6)
ttk.Entry(container, textvariable=vars["name"]).grid(row=0,column=1,sticky="ew")
ttk.Label(container, text="字段类型:").grid(row=1,column=0,sticky="w",pady=6)
cb_type = ttk.Combobox(container, textvariable=vars["ftype"], values=types_list)
cb_type.grid(row=1,column=1,sticky="ew")
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",pady=6)
ent_len.grid(row=row_len,column=1,sticky="ew")
ttk.Label(container, text="大小端:").grid(row=3,column=0,sticky="w",pady=6)
cb_endian = ttk.Combobox(container, textvariable=vars["endian"], values=["BE(大端)","LE(小端)"])
cb_endian.grid(row=3,column=1,sticky="ew")
var_frame = ttk.Frame(container)
var_frame.grid(row=4,column=0,columnspan=2,sticky="ew",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",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",pady=6)
def refresh_ui():
tp = vars["ftype"].get()
mode = vars["var_mode"].get()
# 找到大小端标签和下拉框
endian_label = None
endian_combo = None
for child in container.winfo_children():
if isinstance(child, ttk.Label) and child.cget("text") == "大小端:":
endian_label = child
if isinstance(child, ttk.Combobox) and child.grid_info().get("row") == 3:
endian_combo = child
# 自由段、绑定不定长全部置灰;仅fixed/枚举/CRC恢复只读
if endian_combo is not None:
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")
if endian_combo is not None:
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")
cb_crc = ttk.Combobox(cfg_frame, textvariable=vars["crc_model"], values=CRC_MODEL_LIST)
cb_crc.grid(row=r,column=1,sticky="ew")
r += 1
ttk.Label(cfg_frame, text="校验起始字段:").grid(row=r,column=0,sticky="w")
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")
r += 1
ttk.Label(cfg_frame, text="校验截止字段:").grid(row=r,column=0,sticky="w")
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")
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",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",pady=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","fixed")=="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().strip(),
"crc_range_field": vars["crc_range_field"].get().strip(),
"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,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):
self.var_hex.set("")
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", 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=(0,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, 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, 0))
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":
# 定长数值:输入hex字符串转整数,再按端序打包
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:
# bind / free 直接原始字节流,不做数值端序转换
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":
# 不定长绑定字段:独立读取原始数据+补齐,不再依赖ptr偏移
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 = chunk.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
# 累加前面所有字段在final_buf里的长度
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("var_mode") == "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_bytes = crc_val.to_bytes(fd["length"], byteorder="big")
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)
except ValueError as ve:
messagebox.showerror("组帧失败", f"十六进制格式错误:{str(ve)}\n请检查输入内容")
except Exception as e:
messagebox.showerror("组帧失败", str(e))
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))
if __name__ == "__main__":
root = tk.Tk()
app = ProtocolEditor(root)
root.mainloop()
3、可执行程序生成及注意事项
(1)在程序源码文件protocol_editor.py所在目录下打开终端,输入命令“pyinstaller -w -F protocol_editor.py”,即可生成可执行文件protocol_editor.exe。
(2)不管是运行protocol_editor.py还是protocol_editor.exe,其所在目录下如果没有protocol_templates.json文件,都会新建一个protocol_templates.json文件,用户保存的协议模板都存储在该文件中。
(3)如果希望增加默认的CRC校验模型,不想每次新增字段都单独配置,可基于上面的源码继续开发CRC校验模型保存功能,或者在DEFAULT_PRESETS中新增默认模型,并在CRC_MODEL_LIST中注册。
(4)当前小程序支持的字段最小长度单位为字节,不支持按位配置,已适用于大部分应用层协议,如果需要按位配置,可基于上面的源码继续开发,将最小单位调整为bit。
三、小程序使用演示
1、演示示例1——伪CAN协议帧
(1)伪CAN协议数据帧格式定义:
|
字段名 |
ID仲裁段 |
控制段 |
有效数据长度 |
数据段 |
CRC段 |
帧结束 |
|
类型 |
普通 |
枚举 |
普通 |
普通 |
校验 |
普通 |
|
长度信息 |
3字节 |
1字节 |
1字节 |
不定长 |
2字节 |
1字节 |
|
大小端 |
大端 |
大端 |
大端 |
大端 |
大端 |
大端 |
|
备注 |
/ |
0x00:读命令 0x01:写命令 0x02:读返回 0x03:写返回 |
/ |
长度取决于“有效数据长度” |
使用CRC-16/MODBUS模型 |
固定为0F |
(2)该示例的私有协议只有一种帧格式,所以只需要配置一个协议模板即可,点击“新建”,将模板名字配置为“伪CAN数据帧”,点击“确定”,完成模板的创建,然后点击“保存模板”,即可在本地保存模板信息。

(3)根据协议帧格式添加字段:

①ID仲裁段配置:

②控制段配置:

③有效数据长度配置:

④数据段配置:

⑤CRC段配置:

⑥帧结束配置:

(4)协议帧格式对应的JSON数据结构:
{
"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
}
}
]
}
2、演示示例2——伪USB协议帧
(1)伪USB协议数据帧格式定义:
①令牌伪USB报文格式:
|
字段名 |
PID |
ADDR |
EP |
CRC-8 |
|
类型 |
枚举 |
普通 |
普通 |
普通 |
|
长度信息 |
1字节 |
1字节 |
1字节 |
1字节 |
|
大小端 |
大端 |
大端 |
大端 |
大端 |
|
备注 |
0x00:IN 0x01:OUT 0x02:SETUP |
从机地址 |
端点编号 |
根据PID+ADDR+EP计算而得的校验码 |
②特殊伪USB报文格式:
|
字段名 |
PID |
FRAME |
CRC-8 |
|
类型 |
枚举 |
普通 |
普通 |
|
长度信息 |
1字节 |
2字节 |
1字节 |
|
大小端 |
大端 |
大端 |
大端 |
|
备注 |
0x03:SOF |
帧计数器FrameNumber |
根据PID+FRAME计算而得的校验码 |
③握手伪USB报文格式:
|
字段名 |
PID |
|
类型 |
枚举 |
|
长度信息 |
1字节 |
|
大小端 |
大端 |
|
备注 |
0x04:ACK 0x05:NAK 0x06:STALL |
④数据伪USB报文格式:
|
字段名 |
PID |
Payload |
CRC-16 |
|
类型 |
枚举 |
普通 |
普通 |
|
长度信息 |
1字节 |
不定长 |
2字节 |
|
大小端 |
大端 |
大端 |
大端 |
|
备注 |
0x07:DATA0 0x08:DATA1 |
长度自由,取决于帧总长度 |
根据PID+Payload计算而得的校验码 |
(2)该示例的私有协议有四种帧格式,所以需要分别配置四个协议模板,分别命名为“令牌伪USB数据帧”、“特殊伪USB数据帧”、“握手伪USB数据帧”、“数据伪USB数据帧”。
(3)根据协议帧格式添加字段:
①令牌伪USB数据帧字段定义列表及JSON数据结构:

{
"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
}
}
]
}
②特殊伪USB数据帧字段定义列表及JSON数据结构:

{
"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
}
}
]
}
③握手伪USB数据帧字段定义列表及JSON数据结构:

{
"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
}
}
]
}
④数据伪USB数据帧字段定义列表及JSON数据结构:

{
"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
}
}
]
}更多推荐

所有评论(0)