import serial import serial.tools.list_ports import threading import tkinter as tk from tkinter import ttk, scrolledtext, messagebox class SerialAssistant: def __init__(self, master): self.master = master self.master.title("Python 帧格式串口助手") self.master.geometry("900x650") self.master.minsize(750, 500) self.master.columnconfigure(0, weight=1) self.master.rowconfigure(1, weight=1) self.ser = None self.is_running = False self.hex_display = tk.BooleanVar(value=True) # ------------------ 串口设置 ------------------ frame_top = ttk.LabelFrame(master, text="串口设置", padding=10) frame_top.grid(row=0, column=0, sticky="ew", padx=5, pady=5) for i in range(7): frame_top.columnconfigure(i, weight=1) ttk.Label(frame_top, text="端口:").grid(row=0, column=0, sticky="w") self.combo_port = ttk.Combobox(frame_top, width=10, values=self.list_serial_ports()) self.combo_port.grid(row=0, column=1, padx=5, sticky="w") if self.combo_port["values"]: self.combo_port.current(0) ttk.Label(frame_top, text="波特率:").grid(row=0, column=2, sticky="w") self.combo_baud = ttk.Combobox(frame_top, width=10, values=[9600, 19200, 38400, 57600, 115200]) self.combo_baud.current(4) self.combo_baud.grid(row=0, column=3, padx=5, sticky="w") self.btn_open = ttk.Button(frame_top, text="打开串口", command=self.open_serial) self.btn_open.grid(row=0, column=4, padx=5) self.btn_close = ttk.Button(frame_top, text="关闭串口", command=self.close_serial, state=tk.DISABLED) self.btn_close.grid(row=0, column=5, padx=5) ttk.Checkbutton(frame_top, text="16进制显示", variable=self.hex_display).grid(row=0, column=6, padx=10, sticky="w") # ------------------ 接收区 ------------------ frame_rx = ttk.LabelFrame(master, text="接收数据", padding=10) frame_rx.grid(row=1, column=0, sticky="nsew", padx=5, pady=5) frame_rx.columnconfigure(0, weight=1) frame_rx.rowconfigure(0, weight=1) self.text_rx = scrolledtext.ScrolledText(frame_rx, wrap=tk.WORD) self.text_rx.grid(row=0, column=0, sticky="nsew") # ------------------ 发送帧区 ------------------ frame_tx = ttk.LabelFrame(master, text="帧发送设置", padding=10) frame_tx.grid(row=2, column=0, sticky="ew", padx=5, pady=5) for i in range(5): frame_tx.columnconfigure(i, weight=1) # 命令模板 ttk.Label(frame_tx, text="命令模板:").grid(row=0, column=0, sticky="w") self.combo_frame = ttk.Combobox(frame_tx, width=50, state="normal", values=[ "0A 00 00 00 B4 60 FF EE", "08 00 00 00 B4 60 FF EE", "03 0A 00 D0 B4 60 FF EE", "07 03 00 00 B4 60 FF EE", "05 00 00 14 B4 60 FF EE", "06 00 00 14 B4 60 FF EE", "05 00 00 14 B4 60 FF EE" ]) self.combo_frame.grid(row=0, column=1, padx=5, sticky="w") self.combo_frame.current(0) frame_template_btn = ttk.Frame(frame_tx) frame_template_btn.grid(row=0, column=2, padx=5, sticky="w") ttk.Button(frame_template_btn, text="保存模板", command=self.save_template).pack(side="left", padx=2) ttk.Button(frame_template_btn, text="删除模板", command=self.delete_template).pack(side="left", padx=2) # ------------------ 源地址 ------------------ ttk.Label(frame_tx, text="源地址(2B):").grid(row=1, column=0, sticky="w") self.combo_src = ttk.Combobox(frame_tx, width=10, values=["29 8A", "FF FF", "2A 75"]) self.combo_src.grid(row=1, column=1, padx=5, sticky="w") self.combo_src.current(0) ttk.Label(frame_tx, text="新增源地址:").grid(row=2, column=0, sticky="w") self.entry_new_src = ttk.Entry(frame_tx, width=10) self.entry_new_src.grid(row=2, column=1, padx=5, sticky="w") frame_src_btn = ttk.Frame(frame_tx) frame_src_btn.grid(row=2, column=2, columnspan=3, sticky="w") ttk.Button(frame_src_btn, text="添加源地址", command=self.add_src).pack(side="left", padx=2) ttk.Button(frame_src_btn, text="删除选中源地址", command=self.delete_src).pack(side="left", padx=2) # 发送帧按钮 ttk.Button(frame_tx, text="发送帧", command=self.send_frame).grid(row=1, column=2, padx=10) # 状态栏 self.status = ttk.Label(master, text="状态: 未连接", relief="sunken", anchor="w") self.status.grid(row=3, column=0, sticky="ew") # ------------------ 串口相关 ------------------ def list_serial_ports(self): ports = [port.device for port in serial.tools.list_ports.comports()] return ports if ports else ["无可用端口"] def open_serial(self): port = self.combo_port.get() baud = self.combo_baud.get() try: self.ser = serial.Serial(port, baudrate=int(baud), timeout=0) self.is_running = True self.btn_open["state"] = tk.DISABLED self.btn_close["state"] = tk.NORMAL self.status["text"] = f"状态: 已连接 {port} ({baud}bps)" self.text_rx.insert(tk.END, f"[系统] 成功打开串口 {port}\n") threading.Thread(target=self.read_serial, daemon=True).start() except Exception as e: messagebox.showerror("错误", f"无法打开串口: {e}") def close_serial(self): self.is_running = False if self.ser and self.ser.is_open: self.ser.close() self.btn_open["state"] = tk.NORMAL self.btn_close["state"] = tk.DISABLED self.status["text"] = "状态: 未连接" self.text_rx.insert(tk.END, "[系统] 串口已关闭\n") # ------------------ 接收数据 ------------------ def read_serial(self): while self.is_running and self.ser and self.ser.is_open: try: count = self.ser.in_waiting if count: raw_data = self.ser.read(count) display_data = " ".join(f"{b:02X}" for b in raw_data) if self.hex_display.get() else raw_data.decode("utf-8", errors="ignore") self.text_rx.insert(tk.END, display_data + "\n") self.text_rx.see(tk.END) except Exception: break # ------------------ 源地址管理 ------------------ def add_src(self): addr = self.entry_new_src.get().strip() if addr and addr not in self.combo_src["values"]: values = list(self.combo_src["values"]) values.append(addr) self.combo_src["values"] = values self.entry_new_src.delete(0, tk.END) def delete_src(self): addr = self.combo_src.get().strip() values = list(self.combo_src["values"]) if addr in values: values.remove(addr) self.combo_src["values"] = values if values: self.combo_src.current(0) # ------------------ 命令模板管理 ------------------ def save_template(self): new_template = self.combo_frame.get().strip() if not new_template: messagebox.showwarning("提示", "命令模板不能为空!") return values = list(self.combo_frame["values"]) if new_template not in values: values.append(new_template) self.combo_frame["values"] = values messagebox.showinfo("提示", f"模板已保存: {new_template}") else: messagebox.showinfo("提示", "模板已存在,无需重复添加") def delete_template(self): template = self.combo_frame.get().strip() values = list(self.combo_frame["values"]) if template in values: values.remove(template) self.combo_frame["values"] = values if values: self.combo_frame.current(0) else: self.combo_frame.set('') messagebox.showinfo("提示", f"模板已删除: {template}") else: messagebox.showwarning("提示", "模板不存在或未选择") # ------------------ 发送帧 ------------------ def send_frame(self): if not (self.ser and self.ser.is_open): messagebox.showwarning("警告", "请先打开串口!") return try: cmd_template = self.combo_frame.get().replace(" ", "") src_addr = self.combo_src.get().replace(" ", "") if len(src_addr) != 4: raise ValueError("源地址长度非法") # 组帧: 55 BB [命令模板] [源地址] [保留3B] FF EE frame = bytearray() frame.extend(b'\x55\xBB') frame.extend(bytes.fromhex(cmd_template)) frame.extend(bytes.fromhex(src_addr)) if len(cmd_template) < 4: frame.extend(b'\x00\x00\x00') frame.extend(b'\xFF\xEE') self.ser.write(frame) self.text_rx.insert(tk.END, f"[发送] {' '.join(f'{b:02X}' for b in frame)}\n") self.text_rx.see(tk.END) except ValueError as e: messagebox.showerror("输入错误", str(e)) if __name__ == "__main__": root = tk.Tk() app = SerialAssistant(root) root.mainloop()