Python使用Tkinter编写串口通信调试工具

Written by

in

文章目录
  • 串口调试助手是一款基于Python和Tkinter开发的串口通信调试工具, 专为工程师和开发者设计。它提供了简洁清爽的用户界面, 支持完整的串口配置、数据收发、自动发送、日志记录等功能, 是进行串口通信测试和调试的理想工具。 效果图如下:
  • 串口配置:支持完整的串口参数配置,包括串口号、波特率、数据位、停止位、校验位等,满足各种串口通信需求。 数据接收:实时接收串口数据,支持文本和十六进制两种显示模式,自动统计接收字节数。 数据发送:支持手动发送和自动发送两种模式,可发送文本或十六进制数据,自动统计发送字节数。 自动发送:可设置自动发送间隔,支持自定义发送内容,适合进行周期性数据测试。 日志保存:支持将接收到的数据保存为日志文件,方便后续分析和调试。 配置管理:自动保存和加载配置参数,下次启动时自动恢复上次的设置。
  • 串口参数配置支持波特率:300/600/1200/2400/4800/9600/14400/19200/38400/57600/115200 数据位设置支持5/6/7/8位数据位 停止位设置支持1/1.5/2位停止位 校验位设置支持None/Even/Odd/Mark/Space五种校验方式 十六进制显示接收区域支持十六进制和文本两种显示模式切换 十六进制发送发送区域支持十六进制和文本两种发送模式切换 数据统计实时显示RX(接收)和TX(发送)的字节数统计 清空功能支持清空接收区域和发送区域的内容 状态提示底部状态栏实时显示当前操作状态
  • 选择串口在工具栏中选择要使用的串口号,点击"刷新"按钮可更新串口列表 配置参数设置波特率、数据位、停止位、校验位等串口参数,确保与目标设备一致 打开串口点击"打开串口"按钮,程序将尝试连接并打开指定的串口 发送数据在发送区域输入要发送的数据,点击"发送"按钮即可发送,也可勾选"十六进制发送"发送十六进制数据 接收数据接收区域会自动显示接收到的数据,可勾选"十六进制显示"以十六进制格式查看 自动发送在自动发送区域勾选"启用自动发送",设置发送间隔,程序将自动周期性发送数据 保存日志点击"保存日志"按钮,可将接收区域的内容保存为文本文件 关闭串口使用完毕后,点击"关闭串口"按钮断开连接
  • Python开发:基于Python 3.x开发,跨平台兼容 Tkinter界面:使用Tkinter构建简洁清爽的GUI界面 PySerial库:使用PySerial库实现串口通信 多线程处理:使用多线程实现异步数据接收 配置持久化:使用JSON格式保存和加载配置 简约设计:界面简洁清爽,操作直观便捷
  • 系统要求 Windows / Linux / macOS # 安装依赖库 pip install pyserial # 运行程序 python serial_assistant.py
  • 工具栏:位于窗口顶部,包含串口配置、打开/关闭按钮等控制组件,高度60px,简约风格设计。 接收区域:位于左侧,显示接收到的数据,支持十六进制显示,包含清空和保存日志按钮。 发送区域:位于右侧,用于输入和发送数据,支持十六进制发送,包含发送和清空按钮。 状态栏:位于窗口底部,显示当前状态、RX/TX计数等信息。
  • “”” 串口调试助手 – 简化版 功能:串口配置、数据收发、自动发送、日志记录等 UI:简洁清爽,易于使用 “”” import tkinter as tk from tkinter import ttk, scrolledtext, messagebox, filedialog import serial import serial.tools.list_ports import threading import time from datetime import datetime import json import os class SerialAssistant: “””串口调试助手主类””” def __init__(self, root): self.root = root self.root.title(“串口调试助手”) self.root.geometry(“900×650″) self.root.resizable(True, True) # 串口对象 self.serial_port = None self.is_open = False self.receive_thread = None self.is_running = False self.auto_send_timer = None # 简化配色方案 self.COLORS = { ‘primary’: ‘#2196F3’, ‘success’: ‘#4CAF50’, ‘danger’: ‘#F44336’, ‘warning’: ‘#FF9800’, ‘bg’: ‘#FFFFFF’, ‘text’: ‘#000000’, ‘border’: ‘#CCCCCC’, ‘bg_light’: ‘#F5F5F5’ } # 配置参数 self.config = { ‘port’: ”, ‘baudrate’: ‘9600’, ‘bytesize’: ‘8’, ‘parity’: ‘None’, ‘stopbits’: ‘1’, ‘send_hex’: False, ‘receive_hex’: False, ‘auto_send’: False, ‘auto_send_interval’: 1000, ‘auto_send_data’: ” } # 加载配置 self.load_config() # 创建界面 self.create_widgets() # 刷新串口列表 self.refresh_ports() # 窗口关闭事件 self.root.protocol(“WM_DELETE_WINDOW”, self.on_close) def create_widgets(self): “””创建界面组件””” # 主容器 main_container = tk.Frame(self.root, bg=self.COLORS[‘bg’]) main_container.pack(fill=”both”, expand=True) # 顶部工具栏 self.create_toolbar(main_container) # 中间内容区域 content_area = tk.Frame(main_container, bg=self.COLORS[‘bg’]) content_area.pack(fill=”both”, expand=True, padx=10, pady=10) # 左侧接收区域 self.create_receive_area(content_area) # 右侧发送区域 self.create_send_area(content_area) # 底部状态栏 self.create_status_bar(main_container) def create_toolbar(self, parent): “””创建顶部工具栏 – 简约风格””” toolbar = tk.Frame(parent, bg=self.COLORS[‘bg_light’], height=60) toolbar.pack(fill=”x”, side=”top”, padx=5, pady=5) toolbar.pack_propagate(False) # 第一行:标题和串口配置 # 标题 title_label = tk.Label( toolbar, text=”串口调试助手”, font=(“Arial”, 11, “bold”), fg=self.COLORS[‘primary’], bg=self.COLORS[‘bg_light’] ) title_label.grid(row=0, column=0, columnspan=2, pady=2, sticky=”w”) # 串口选择 tk.Label(toolbar, text=”串口:”, bg=self.COLORS[‘bg_light’]).grid(row=1, column=0, padx=2, pady=2) self.port_combo = ttk.Combobox(toolbar, width=10, state=”readonly”) self.port_combo.grid(row=1, column=1, padx=2, pady=2) # 波特率 tk.Label(toolbar, text=”波特率:”, bg=self.COLORS[‘bg_light’]).grid(row=1, column=2, padx=3) self.baudrate_combo = ttk.Combobox( toolbar, width=8, values=[‘300’, ‘600’, ‘1200’, ‘2400’, ‘4800’, ‘9600’, ‘14400’, ‘19200’, ‘38400’, ‘57600’, ‘115200’], state=”readonly” ) self.baudrate_combo.grid(row=1, column=3, padx=3) self.baudrate_combo.set(self.config[‘baudrate’]) # 数据位 tk.Label(toolbar, text=”数据位:”, bg=self.COLORS[‘bg_light’]).grid(row=1, column=4, padx=3) self.bytesize_combo = ttk.Combobox( toolbar, width=4, values=[‘5’, ‘6’, ‘7’, ‘8’], state=”readonly” ) self.bytesize_combo.grid(row=1, column=5, padx=3) self.bytesize_combo.set(self.config[‘bytesize’]) # 停止位 tk.Label(toolbar, text=”停止位:”, bg=self.COLORS[‘bg_light’]).grid(row=1, column=6, padx=3) self.stopbits_combo = ttk.Combobox( toolbar, width=4, values=[‘1’, ‘1.5’, ‘2’], state=”readonly” ) self.stopbits_combo.grid(row=1, column=7, padx=3) self.stopbits_combo.set(self.config[‘stopbits’]) # 校验位 tk.Label(toolbar, text=”校验位:”, bg=self.COLORS[‘bg_light’]).grid(row=2, column=0, padx=3) self.parity_combo = ttk.Combobox( toolbar, width=8, values=[‘None’, ‘Even’, ‘Odd’, ‘Mark’, ‘Space’], state=”readonly” ) self.parity_combo.grid(row=2, column=1, padx=3) self.parity_combo.set(self.config[‘parity’]) # 刷新按钮 refresh_btn = tk.Button( toolbar, text=”刷新”, command=self.refresh_ports, width=8 ) refresh_btn.grid(row=2, column=2, padx=5, pady=5) # 打开/关闭按钮 self.open_btn = tk.Button( toolbar, text=”打开串口”, command=self.toggle_port, bg=self.COLORS[‘success’], fg=’white’, width=12 ) self.open_btn.grid(row=2, column=3, columnspan=2, padx=5, pady=5) def create_receive_area(self, parent): “””创建接收区域 – 简约风格””” receive_frame = tk.Frame(parent, bg=self.COLORS[‘bg’]) receive_frame.pack(side=”left”, fill=”both”, expand=True, padx=3) # 标题 tk.Label( receive_frame, text=”接收区域”, font=(“Arial”, 10, “bold”), bg=self.COLORS[‘bg’] ).pack(pady=5) # 接收选项 options_frame = tk.Frame(receive_frame, bg=self.COLORS[‘bg’]) options_frame.pack(fill=”x”, padx=5, pady=5) self.receive_hex_var = tk.BooleanVar(value=self.config[‘receive_hex’]) tk.Checkbutton( options_frame, text=”十六进制显示”, variable=self.receive_hex_var, bg=self.COLORS[‘bg’], command=self.update_receive_hex ).pack(side=”left”) # 接收文本框 self.receive_text = scrolledtext.ScrolledText( receive_frame, height=20, width=50, font=(“Consolas”, 10), state=’disabled’ ) self.receive_text.pack(fill=”both”, expand=True, padx=5, pady=5) # 按钮区域 btn_frame = tk.Frame(receive_frame, bg=self.COLORS[‘bg’]) btn_frame.pack(fill=”x”, padx=5, pady=5) tk.Button( btn_frame, text=”清空接收”, command=self.clear_receive, width=10 ).pack(side=”left”, padx=5) tk.Button( btn_frame, text=”保存日志”, command=self.save_log, width=10 ).pack(side=”left”, padx=5) def create_send_area(self, parent): “””创建发送区域 – 简约风格””” send_frame = tk.Frame(parent, bg=self.COLORS[‘bg’]) send_frame.pack(side=”right”, fill=”both”, expand=True, padx=3) # 标题 tk.Label( send_frame, text=”发送区域”, font=(“Arial”, 10, “bold”), bg=self.COLORS[‘bg’] ).pack(pady=5) # 发送选项 options_frame = tk.Frame(send_frame, bg=self.COLORS[‘bg’]) options_frame.pack(fill=”x”, padx=5, pady=5) self.send_hex_var = tk.BooleanVar(value=self.config[‘send_hex’]) tk.Checkbutton( options_frame, text=”十六进制发送”, variable=self.send_hex_var, bg=self.COLORS[‘bg’], command=self.update_send_hex ).pack(side=”left”) # 发送文本框 – 使用ScrolledText与接收区域保持一致 self.send_text = scrolledtext.ScrolledText( send_frame, height=20, width=50, font=(“Consolas”, 10) ) self.send_text.pack(fill=”both”, expand=True, padx=5, pady=5) self.send_text.insert(“1.0″, self.config[‘auto_send_data’]) # 按钮区域 – 与接收区域保持一致的布局 btn_frame = tk.Frame(send_frame, bg=self.COLORS[‘bg’]) btn_frame.pack(fill=”x”, padx=5, pady=5) tk.Button( btn_frame, text=”发送”, command=self.send_data, bg=self.COLORS[‘primary’], fg=’white’, width=10 ).pack(side=”left”, padx=5) tk.Button( btn_frame, text=”清空发送”, command=self.clear_send, width=10 ).pack(side=”left”, padx=5) # 自动发送区域 auto_frame = tk.LabelFrame(send_frame, text=”自动发送”, bg=self.COLORS[‘bg’]) auto_frame.pack(fill=”x”, padx=5, pady=10) self.auto_send_var = tk.BooleanVar(value=self.config[‘auto_send’]) tk.Checkbutton( auto_frame, text=”启用自动发送”, variable=self.auto_send_var, bg=self.COLORS[‘bg’], command=self.toggle_auto_send ).pack(pady=5) interval_frame = tk.Frame(auto_frame, bg=self.COLORS[‘bg’]) interval_frame.pack(pady=5) tk.Label(interval_frame, text=”间隔(ms):”, bg=self.COLORS[‘bg’]).pack(side=”left”) self.interval_entry = tk.Entry(interval_frame, width=10) self.interval_entry.pack(side=”left”, padx=5) self.interval_entry.insert(0, str(self.config[‘auto_send_interval’])) def create_status_bar(self, parent): “””创建状态栏””” status_bar = tk.Frame(parent, bg=self.COLORS[‘bg_light’], height=30) status_bar.pack(fill=”x”, side=”bottom”) status_bar.pack_propagate(False) self.status_label = tk.Label( status_bar, text=”就绪”, bg=self.COLORS[‘bg_light’], anchor=”w” ) self.status_label.pack(side=”left”, padx=10, pady=5) # 分隔线 tk.Frame(status_bar, width=2, bg=self.COLORS[‘border’]).pack(side=”left”, padx=10, fill=”y”) self.rx_count_label = tk.Label( status_bar, text=”RX: 0″, bg=self.COLORS[‘bg_light’] ) self.rx_count_label.pack(side=”left”, padx=10) self.tx_count_label = tk.Label( status_bar, text=”TX: 0″, bg=self.COLORS[‘bg_light’] ) self.tx_count_label.pack(side=”left”, padx=10) def refresh_ports(self): “””刷新串口列表””” ports = serial.tools.list_ports.comports() port_list = [port.device for port in ports] self.port_combo[‘values’] = port_list if port_list and not self.port_combo.get(): self.port_combo.current(0) self.update_status(f”发现 {len(port_list)} 个串口”) def toggle_port(self): “””打开/关闭串口””” if self.is_open: self.close_port() else: self.open_port() def open_port(self): “””打开串口””” port = self.port_combo.get() if not port: messagebox.showwarning(“警告”, “请选择串口”) return try: self.serial_port = serial.Serial( port=port, baudrate=int(self.baudrate_combo.get()), bytesize=int(self.bytesize_combo.get()), parity=self.parity_combo.get()[0] if self.parity_combo.get() != ‘None’ else ‘N’, stopbits=float(self.stopbits_combo.get()), timeout=1 ) self.is_open = True self.is_running = True self.open_btn.config(text=”关闭串口”, bg=self.COLORS[‘danger’]) self.update_status(f”串口 {port} 已打开”) # 启动接收线程 self.receive_thread = threading.Thread(target=self.receive_data, daemon=True) self.receive_thread.start() # 保存配置 self.save_config() except Exception as e: messagebox.showerror(“错误”, f”打开串口失败: {str(e)}”) self.update_status(f”打开串口失败: {str(e)}”) def close_port(self): “””关闭串口””” if self.auto_send_var.get(): self.toggle_auto_send() self.is_running = False if self.serial_port and self.serial_port.is_open: self.serial_port.close() self.is_open = False self.open_btn.config(text=”打开串口”, bg=self.COLORS[‘success’]) self.update_status(“串口已关闭”) def receive_data(self): “””接收数据线程””” rx_count = 0 while self.is_running: try: if self.serial_port and self.serial_port.is_open: data = self.serial_port.read_all() if data: rx_count += len(data) self.root.after(0, self.update_receive_text, data) self.root.after(0, self.update_rx_count, rx_count) time.sleep(0.01) except Exception as e: self.root.after(0, self.update_status, f”接收错误: {str(e)}”) break def update_receive_text(self, data): “””更新接收文本””” self.receive_text.config(state=’normal’) if self.receive_hex_var.get(): hex_str = ‘ ‘.join([f'{byte:02X}’ for byte in data]) self.receive_text.insert(“end”, hex_str + ‘n’) else: try: text = data.decode(‘utf-8′, errors=’ignore’) self.receive_text.insert(“end”, text) except: hex_str = ‘ ‘.join([f'{byte:02X}’ for byte in data]) self.receive_text.insert(“end”, hex_str + ‘n’) self.receive_text.see(“end”) self.receive_text.config(state=’disabled’) def send_data(self): “””发送数据””” if not self.is_open: messagebox.showwarning(“警告”, “请先打开串口”) return data_str = self.send_text.get(“1.0”, “end-1c”) if not data_str: messagebox.showwarning(“警告”, “请输入要发送的数据”) return try: if self.send_hex_var.get(): # 十六进制发送 hex_str = data_str.replace(‘ ‘, ”).replace(‘n’, ”) data = bytes.fromhex(hex_str) else: # 文本发送 data = data_str.encode(‘utf-8’) self.serial_port.write(data) self.update_status(f”已发送 {len(data)} 字节”) # 更新发送计数 if not hasattr(self, ‘tx_count’): self.tx_count = 0 self.tx_count += len(data) self.update_tx_count(self.tx_count) except Exception as e: messagebox.showerror(“错误”, f”发送失败: {str(e)}”) self.update_status(f”发送失败: {str(e)}”) def toggle_auto_send(self): “””切换自动发送””” if self.auto_send_var.get(): try: interval = int(self.interval_entry.get()) if interval < 10: messagebox.showwarning(“警告”, “发送间隔不能小于10ms”) self.auto_send_var.set(False) return self.config[‘auto_send’] = True self.config[‘auto_send_interval’] = interval self.auto_send_timer = self.root.after(interval, self.auto_send) self.update_status(f”自动发送已启动,间隔 {interval}ms”) except ValueError: messagebox.showwarning(“警告”, “请输入有效的间隔时间”) self.auto_send_var.set(False) else: self.config[‘auto_send’] = False if self.auto_send_timer: self.root.after_cancel(self.auto_send_timer) self.auto_send_timer = None self.update_status(“自动发送已停止”) self.save_config() def auto_send(self): “””自动发送””” if self.auto_send_var.get() and self.is_open: self.send_data() interval = int(self.interval_entry.get()) self.auto_send_timer = self.root.after(interval, self.auto_send) def clear_receive(self): “””清空接收区””” self.receive_text.config(state=’normal’) self.receive_text.delete(“1.0”, “end”) self.receive_text.config(state=’disabled’) self.update_status(“接收区已清空”) def clear_send(self): “””清空发送区””” self.send_text.delete(“1.0”, “end”) self.update_status(“发送区已清空”) def save_log(self): “””保存日志””” content = self.receive_text.get(“1.0”, “end-1c”) if not content: messagebox.showinfo(“提示”, “没有可保存的日志”) return timestamp = datetime.now().strftime(“%Y%m%d_%H%M%S”) filename = f”serial_log_{timestamp}.txt” file_path = filedialog.asksaveasfilename( defaultextension=”.txt”, initialfile=filename, filetypes=[(“文本文件”, “*.txt”), (“所有文件”, “*.*”)] ) if file_path: try: with open(file_path, ‘w’, encoding=’utf-8′) as f: f.write(content) messagebox.showinfo(“成功”, f”日志已保存到 {file_path}”) self.update_status(f”日志已保存”) except Exception as e: messagebox.showerror(“错误”, f”保存失败: {str(e)}”) def update_status(self, message): “””更新状态栏””” self.status_label.config(text=message) def update_rx_count(self, count): “””更新接收计数””” self.rx_count_label.config(text=f”RX: {count}”) def update_tx_count(self, count): “””更新发送计数””” self.tx_count_label.config(text=f”TX: {count}”) def update_receive_hex(self): “””更新接收十六进制设置””” self.config[‘receive_hex’] = self.receive_hex_var.get() self.save_config() def update_send_hex(self): “””更新发送十六进制设置””” self.config[‘send_hex’] = self.send_hex_var.get() self.save_config() def save_config(self): “””保存配置””” self.config[‘port’] = self.port_combo.get() self.config[‘baudrate’] = self.baudrate_combo.get() self.config[‘bytesize’] = self.bytesize_combo.get() self.config[‘parity’] = self.parity_combo.get() self.config[‘stopbits’] = self.stopbits_combo.get() try: with open(‘serial_config.json’, ‘w’, encoding=’utf-8′) as f: json.dump(self.config, f, indent=4, ensure_ascii=False) except Exception as e: print(f”保存配置失败: {e}”) def load_config(self): “””加载配置””” try: if os.path.exists(‘serial_config.json’): with open(‘serial_config.json’, ‘r’, encoding=’utf-8′) as f: config = json.load(f) self.config.update(config) except Exception as e: print(f”加载配置失败: {e}”) def on_close(self): “””窗口关闭事件””” if self.is_open: self.close_port() self.root.destroy() def main(): “””主函数””” root = tk.Tk() app = SerialAssistant(root) root.mainloop() if __name__ == “__main__”: main() 配置文件serial_config.json { “port”: “COM1”, “baudrate”: “115200”, “bytesize”: “8”, “parity”: “None”, “stopbits”: “1”, “send_hex”: false, “receive_hex”: false, “auto_send”: true, “auto_send_interval”: 1000, “auto_send_data”: “” } 以上就是Python使用Tkinter编写串口通信调试工具的详细内容,更多关于Python串口通信调试的资料请关注风君子博客其它相关文章! 您可能感兴趣的文章: 利用Python调试串口的示例代码 基于Python编写一个串口调试工具 Python中串口通信库pyserial基础知识 python serial串口通信示例详解 Python通过串口实现收发文件 Python串口通信的接收与发送的实现 python实现串口通信的示例代码
  • 目录
    • 简介
    • 核心功能
    • 功能详解
    • 使用说明
    • 技术特点
    • 运行要求
    • 界面说明
    • python 代码

    串口调试助手是一款基于Python和Tkinter开发的串口通信调试工具, 专为工程师和开发者设计。它提供了简洁清爽的用户界面, 支持完整的串口配置、数据收发、自动发送、日志记录等功能, 是进行串口通信测试和调试的理想工具。

    效果图如下:

    串口配置:支持完整的串口参数配置,包括串口号、波特率、数据位、停止位、校验位等,满足各种串口通信需求。

    数据接收:实时接收串口数据,支持文本和十六进制两种显示模式,自动统计接收字节数。

    数据发送:支持手动发送和自动发送两种模式,可发送文本或十六进制数据,自动统计发送字节数。

    自动发送:可设置自动发送间隔,支持自定义发送内容,适合进行周期性数据测试。

    日志保存:支持将接收到的数据保存为日志文件,方便后续分析和调试。

    配置管理:自动保存和加载配置参数,下次启动时自动恢复上次的设置。

    • 串口参数配置支持波特率:300/600/1200/2400/4800/9600/14400/19200/38400/57600/115200
    • 数据位设置支持5/6/7/8位数据位
    • 停止位设置支持1/1.5/2位停止位
    • 校验位设置支持None/Even/Odd/Mark/Space五种校验方式
    • 十六进制显示接收区域支持十六进制和文本两种显示模式切换
    • 十六进制发送发送区域支持十六进制和文本两种发送模式切换
    • 数据统计实时显示RX(接收)和TX(发送)的字节数统计
    • 清空功能支持清空接收区域和发送区域的内容
    • 状态提示底部状态栏实时显示当前操作状态

    • 选择串口在工具栏中选择要使用的串口号,点击"刷新"按钮可更新串口列表
    • 配置参数设置波特率、数据位、停止位、校验位等串口参数,确保与目标设备一致
    • 打开串口点击"打开串口"按钮,程序将尝试连接并打开指定的串口
    • 发送数据在发送区域输入要发送的数据,点击"发送"按钮即可发送,也可勾选"十六进制发送"发送十六进制数据
    • 接收数据接收区域会自动显示接收到的数据,可勾选"十六进制显示"以十六进制格式查看
    • 自动发送在自动发送区域勾选"启用自动发送",设置发送间隔,程序将自动周期性发送数据
    • 保存日志点击"保存日志"按钮,可将接收区域的内容保存为文本文件
    • 关闭串口使用完毕后,点击"关闭串口"按钮断开连接

    Python开发:基于Python 3.x开发,跨平台兼容

    Tkinter界面:使用Tkinter构建简洁清爽的GUI界面

    PySerial库:使用PySerial库实现串口通信

    多线程处理:使用多线程实现异步数据接收

    配置持久化:使用JSON格式保存和加载配置

    简约设计:界面简洁清爽,操作直观便捷

    系统要求

    Windows / Linux / macOS

    # 安装依赖库
    pip install pyserial
    # 运行程序
    python serial_assistant.py

    工具栏:位于窗口顶部,包含串口配置、打开/关闭按钮等控制组件,高度60px,简约风格设计。

    接收区域:位于左侧,显示接收到的数据,支持十六进制显示,包含清空和保存日志按钮。

    发送区域:位于右侧,用于输入和发送数据,支持十六进制发送,包含发送和清空按钮。

    状态栏:位于窗口底部,显示当前状态、RX/TX计数等信息。

    """
    串口调试助手 - 简化版
    功能:串口配置、数据收发、自动发送、日志记录等
    UI:简洁清爽,易于使用
    """
    
    import tkinter as tk
    from tkinter import ttk, scrolledtext, messagebox, filedialog
    import serial
    import serial.tools.list_ports
    import threading
    import time
    from datetime import datetime
    import json
    import os
    
    
    class SerialAssistant:
        """串口调试助手主类"""
        
        def __init__(self, root):
            self.root = root
            self.root.title("串口调试助手")
            self.root.geometry("900x650")
            self.root.resizable(True, True)
            
            # 串口对象
            self.serial_port = None
            self.is_open = False
            self.receive_thread = None
            self.is_running = False
            self.auto_send_timer = None
            
            # 简化配色方案
            self.COLORS = {
                'primary': '#2196F3',
                'success': '#4CAF50',
                'danger': '#F44336',
                'warning': '#FF9800',
                'bg': '#FFFFFF',
                'text': '#000000',
                'border': '#CCCCCC',
                'bg_light': '#F5F5F5'
            }
            
            # 配置参数
            self.config = {
                'port': '',
                'baudrate': '9600',
                'bytesize': '8',
                'parity': 'None',
                'stopbits': '1',
                'send_hex': False,
                'receive_hex': False,
                'auto_send': False,
                'auto_send_interval': 1000,
                'auto_send_data': ''
            }
            
            # 加载配置
            self.load_config()
            
            # 创建界面
            self.create_widgets()
            
            # 刷新串口列表
            self.refresh_ports()
            
            # 窗口关闭事件
            self.root.protocol("WM_DELETE_WINDOW", self.on_close)
        
        def create_widgets(self):
            """创建界面组件"""
            # 主容器
            main_container = tk.Frame(self.root, bg=self.COLORS['bg'])
            main_container.pack(fill="both", expand=True)
            
            # 顶部工具栏
            self.create_toolbar(main_container)
            
            # 中间内容区域
            content_area = tk.Frame(main_container, bg=self.COLORS['bg'])
            content_area.pack(fill="both", expand=True, padx=10, pady=10)
            
            # 左侧接收区域
            self.create_receive_area(content_area)
            
            # 右侧发送区域
            self.create_send_area(content_area)
            
            # 底部状态栏
            self.create_status_bar(main_container)
        
        def create_toolbar(self, parent):
            """创建顶部工具栏 - 简约风格"""
            toolbar = tk.Frame(parent, bg=self.COLORS['bg_light'], height=60)
            toolbar.pack(fill="x", side="top", padx=5, pady=5)
            toolbar.pack_propagate(False)
            
            # 第一行:标题和串口配置
            # 标题
            title_label = tk.Label(
                toolbar,
                text="串口调试助手",
                font=("Arial", 11, "bold"),
                fg=self.COLORS['primary'],
                bg=self.COLORS['bg_light']
            )
            title_label.grid(row=0, column=0, columnspan=2, pady=2, sticky="w")
            
            # 串口选择
            tk.Label(toolbar, text="串口:", bg=self.COLORS['bg_light']).grid(row=1, column=0, padx=2, pady=2)
            self.port_combo = ttk.Combobox(toolbar, width=10, state="readonly")
            self.port_combo.grid(row=1, column=1, padx=2, pady=2)
            
            # 波特率
            tk.Label(toolbar, text="波特率:", bg=self.COLORS['bg_light']).grid(row=1, column=2, padx=3)
            self.baudrate_combo = ttk.Combobox(
                toolbar,
                width=8,
                values=['300', '600', '1200', '2400', '4800', '9600', '14400', '19200', '38400', '57600', '115200'],
                state="readonly"
            )
            self.baudrate_combo.grid(row=1, column=3, padx=3)
            self.baudrate_combo.set(self.config['baudrate'])
            
            # 数据位
            tk.Label(toolbar, text="数据位:", bg=self.COLORS['bg_light']).grid(row=1, column=4, padx=3)
            self.bytesize_combo = ttk.Combobox(
                toolbar,
                width=4,
                values=['5', '6', '7', '8'],
                state="readonly"
            )
            self.bytesize_combo.grid(row=1, column=5, padx=3)
            self.bytesize_combo.set(self.config['bytesize'])
            
            # 停止位
            tk.Label(toolbar, text="停止位:", bg=self.COLORS['bg_light']).grid(row=1, column=6, padx=3)
            self.stopbits_combo = ttk.Combobox(
                toolbar,
                width=4,
                values=['1', '1.5', '2'],
                state="readonly"
            )
            self.stopbits_combo.grid(row=1, column=7, padx=3)
            self.stopbits_combo.set(self.config['stopbits'])
            
            # 校验位
            tk.Label(toolbar, text="校验位:", bg=self.COLORS['bg_light']).grid(row=2, column=0, padx=3)
            self.parity_combo = ttk.Combobox(
                toolbar,
                width=8,
                values=['None', 'Even', 'Odd', 'Mark', 'Space'],
                state="readonly"
            )
            self.parity_combo.grid(row=2, column=1, padx=3)
            self.parity_combo.set(self.config['parity'])
            
            # 刷新按钮
            refresh_btn = tk.Button(
                toolbar,
                text="刷新",
                command=self.refresh_ports,
                width=8
            )
            refresh_btn.grid(row=2, column=2, padx=5, pady=5)
            
            # 打开/关闭按钮
            self.open_btn = tk.Button(
                toolbar,
                text="打开串口",
                command=self.toggle_port,
                bg=self.COLORS['success'],
                fg='white',
                width=12
            )
            self.open_btn.grid(row=2, column=3, columnspan=2, padx=5, pady=5)
        
        def create_receive_area(self, parent):
            """创建接收区域 - 简约风格"""
            receive_frame = tk.Frame(parent, bg=self.COLORS['bg'])
            receive_frame.pack(side="left", fill="both", expand=True, padx=3)
            
            # 标题
            tk.Label(
                receive_frame,
                text="接收区域",
                font=("Arial", 10, "bold"),
                bg=self.COLORS['bg']
            ).pack(pady=5)
            
            # 接收选项
            options_frame = tk.Frame(receive_frame, bg=self.COLORS['bg'])
            options_frame.pack(fill="x", padx=5, pady=5)
            
            self.receive_hex_var = tk.BooleanVar(value=self.config['receive_hex'])
            tk.Checkbutton(
                options_frame,
                text="十六进制显示",
                variable=self.receive_hex_var,
                bg=self.COLORS['bg'],
                command=self.update_receive_hex
            ).pack(side="left")
            
            # 接收文本框
            self.receive_text = scrolledtext.ScrolledText(
                receive_frame,
                height=20,
                width=50,
                font=("Consolas", 10),
                state='disabled'
            )
            self.receive_text.pack(fill="both", expand=True, padx=5, pady=5)
            
            # 按钮区域
            btn_frame = tk.Frame(receive_frame, bg=self.COLORS['bg'])
            btn_frame.pack(fill="x", padx=5, pady=5)
            
            tk.Button(
                btn_frame,
                text="清空接收",
                command=self.clear_receive,
                width=10
            ).pack(side="left", padx=5)
            
            tk.Button(
                btn_frame,
                text="保存日志",
                command=self.save_log,
                width=10
            ).pack(side="left", padx=5)
        
        def create_send_area(self, parent):
            """创建发送区域 - 简约风格"""
            send_frame = tk.Frame(parent, bg=self.COLORS['bg'])
            send_frame.pack(side="right", fill="both", expand=True, padx=3)
            
            # 标题
            tk.Label(
                send_frame,
                text="发送区域",
                font=("Arial", 10, "bold"),
                bg=self.COLORS['bg']
            ).pack(pady=5)
            
            # 发送选项
            options_frame = tk.Frame(send_frame, bg=self.COLORS['bg'])
            options_frame.pack(fill="x", padx=5, pady=5)
            
            self.send_hex_var = tk.BooleanVar(value=self.config['send_hex'])
            tk.Checkbutton(
                options_frame,
                text="十六进制发送",
                variable=self.send_hex_var,
                bg=self.COLORS['bg'],
                command=self.update_send_hex
            ).pack(side="left")
            
            # 发送文本框 - 使用ScrolledText与接收区域保持一致
            self.send_text = scrolledtext.ScrolledText(
                send_frame,
                height=20,
                width=50,
                font=("Consolas", 10)
            )
            self.send_text.pack(fill="both", expand=True, padx=5, pady=5)
            self.send_text.insert("1.0", self.config['auto_send_data'])
            
            # 按钮区域 - 与接收区域保持一致的布局
            btn_frame = tk.Frame(send_frame, bg=self.COLORS['bg'])
            btn_frame.pack(fill="x", padx=5, pady=5)
            
            tk.Button(
                btn_frame,
                text="发送",
                command=self.send_data,
                bg=self.COLORS['primary'],
                fg='white',
                width=10
            ).pack(side="left", padx=5)
            
            tk.Button(
                btn_frame,
                text="清空发送",
                command=self.clear_send,
                width=10
            ).pack(side="left", padx=5)
            
            # 自动发送区域
            auto_frame = tk.LabelFrame(send_frame, text="自动发送", bg=self.COLORS['bg'])
            auto_frame.pack(fill="x", padx=5, pady=10)
            
            self.auto_send_var = tk.BooleanVar(value=self.config['auto_send'])
            tk.Checkbutton(
                auto_frame,
                text="启用自动发送",
                variable=self.auto_send_var,
                bg=self.COLORS['bg'],
                command=self.toggle_auto_send
            ).pack(pady=5)
            
            interval_frame = tk.Frame(auto_frame, bg=self.COLORS['bg'])
            interval_frame.pack(pady=5)
            
            tk.Label(interval_frame, text="间隔(ms):", bg=self.COLORS['bg']).pack(side="left")
            self.interval_entry = tk.Entry(interval_frame, width=10)
            self.interval_entry.pack(side="left", padx=5)
            self.interval_entry.insert(0, str(self.config['auto_send_interval']))
        
        def create_status_bar(self, parent):
            """创建状态栏"""
            status_bar = tk.Frame(parent, bg=self.COLORS['bg_light'], height=30)
            status_bar.pack(fill="x", side="bottom")
            status_bar.pack_propagate(False)
            
            self.status_label = tk.Label(
                status_bar,
                text="就绪",
                bg=self.COLORS['bg_light'],
                anchor="w"
            )
            self.status_label.pack(side="left", padx=10, pady=5)
            
            # 分隔线
            tk.Frame(status_bar, width=2, bg=self.COLORS['border']).pack(side="left", padx=10, fill="y")
            
            self.rx_count_label = tk.Label(
                status_bar,
                text="RX: 0",
                bg=self.COLORS['bg_light']
            )
            self.rx_count_label.pack(side="left", padx=10)
            
            self.tx_count_label = tk.Label(
                status_bar,
                text="TX: 0",
                bg=self.COLORS['bg_light']
            )
            self.tx_count_label.pack(side="left", padx=10)
        
        def refresh_ports(self):
            """刷新串口列表"""
            ports = serial.tools.list_ports.comports()
            port_list = [port.device for port in ports]
            self.port_combo['values'] = port_list
            if port_list and not self.port_combo.get():
                self.port_combo.current(0)
            self.update_status(f"发现 {len(port_list)} 个串口")
        
        def toggle_port(self):
            """打开/关闭串口"""
            if self.is_open:
                self.close_port()
            else:
                self.open_port()
        
        def open_port(self):
            """打开串口"""
            port = self.port_combo.get()
            if not port:
                messagebox.showwarning("警告", "请选择串口")
                return
            
            try:
                self.serial_port = serial.Serial(
                    port=port,
                    baudrate=int(self.baudrate_combo.get()),
                    bytesize=int(self.bytesize_combo.get()),
                    parity=self.parity_combo.get()[0] if self.parity_combo.get() != 'None' else 'N',
                    stopbits=float(self.stopbits_combo.get()),
                    timeout=1
                )
                self.is_open = True
                self.is_running = True
                self.open_btn.config(text="关闭串口", bg=self.COLORS['danger'])
                self.update_status(f"串口 {port} 已打开")
                
                # 启动接收线程
                self.receive_thread = threading.Thread(target=self.receive_data, daemon=True)
                self.receive_thread.start()
                
                # 保存配置
                self.save_config()
                
            except Exception as e:
                messagebox.showerror("错误", f"打开串口失败: {str(e)}")
                self.update_status(f"打开串口失败: {str(e)}")
        
        def close_port(self):
            """关闭串口"""
            if self.auto_send_var.get():
                self.toggle_auto_send()
            
            self.is_running = False
            if self.serial_port and self.serial_port.is_open:
                self.serial_port.close()
            
            self.is_open = False
            self.open_btn.config(text="打开串口", bg=self.COLORS['success'])
            self.update_status("串口已关闭")
        
        def receive_data(self):
            """接收数据线程"""
            rx_count = 0
            
            while self.is_running:
                try:
                    if self.serial_port and self.serial_port.is_open:
                        data = self.serial_port.read_all()
                        if data:
                            rx_count += len(data)
                            self.root.after(0, self.update_receive_text, data)
                            self.root.after(0, self.update_rx_count, rx_count)
                    time.sleep(0.01)
                except Exception as e:
                    self.root.after(0, self.update_status, f"接收错误: {str(e)}")
                    break
        
        def update_receive_text(self, data):
            """更新接收文本"""
            self.receive_text.config(state='normal')
            
            if self.receive_hex_var.get():
                hex_str = ' '.join([f'{byte:02X}' for byte in data])
                self.receive_text.insert("end", hex_str + 'n')
            else:
                try:
                    text = data.decode('utf-8', errors='ignore')
                    self.receive_text.insert("end", text)
                except:
                    hex_str = ' '.join([f'{byte:02X}' for byte in data])
                    self.receive_text.insert("end", hex_str + 'n')
            
            self.receive_text.see("end")
            self.receive_text.config(state='disabled')
        
        def send_data(self):
            """发送数据"""
            if not self.is_open:
                messagebox.showwarning("警告", "请先打开串口")
                return
            
            data_str = self.send_text.get("1.0", "end-1c")
            if not data_str:
                messagebox.showwarning("警告", "请输入要发送的数据")
                return
            
            try:
                if self.send_hex_var.get():
                    # 十六进制发送
                    hex_str = data_str.replace(' ', '').replace('n', '')
                    data = bytes.fromhex(hex_str)
                else:
                    # 文本发送
                    data = data_str.encode('utf-8')
                
                self.serial_port.write(data)
                self.update_status(f"已发送 {len(data)} 字节")
                
                # 更新发送计数
                if not hasattr(self, 'tx_count'):
                    self.tx_count = 0
                self.tx_count += len(data)
                self.update_tx_count(self.tx_count)
                
            except Exception as e:
                messagebox.showerror("错误", f"发送失败: {str(e)}")
                self.update_status(f"发送失败: {str(e)}")
        
        def toggle_auto_send(self):
            """切换自动发送"""
            if self.auto_send_var.get():
                try:
                    interval = int(self.interval_entry.get())
                    if interval < 10:
                        messagebox.showwarning("警告", "发送间隔不能小于10ms")
                        self.auto_send_var.set(False)
                        return
                    
                    self.config['auto_send'] = True
                    self.config['auto_send_interval'] = interval
                    self.auto_send_timer = self.root.after(interval, self.auto_send)
                    self.update_status(f"自动发送已启动,间隔 {interval}ms")
                    
                except ValueError:
                    messagebox.showwarning("警告", "请输入有效的间隔时间")
                    self.auto_send_var.set(False)
            else:
                self.config['auto_send'] = False
                if self.auto_send_timer:
                    self.root.after_cancel(self.auto_send_timer)
                    self.auto_send_timer = None
                self.update_status("自动发送已停止")
            
            self.save_config()
        
        def auto_send(self):
            """自动发送"""
            if self.auto_send_var.get() and self.is_open:
                self.send_data()
                interval = int(self.interval_entry.get())
                self.auto_send_timer = self.root.after(interval, self.auto_send)
        
        def clear_receive(self):
            """清空接收区"""
            self.receive_text.config(state='normal')
            self.receive_text.delete("1.0", "end")
            self.receive_text.config(state='disabled')
            self.update_status("接收区已清空")
        
        def clear_send(self):
            """清空发送区"""
            self.send_text.delete("1.0", "end")
            self.update_status("发送区已清空")
        
        def save_log(self):
            """保存日志"""
            content = self.receive_text.get("1.0", "end-1c")
            if not content:
                messagebox.showinfo("提示", "没有可保存的日志")
                return
            
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            filename = f"serial_log_{timestamp}.txt"
            
            file_path = filedialog.asksaveasfilename(
                defaultextension=".txt",
                initialfile=filename,
                filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")]
            )
            
            if file_path:
                try:
                    with open(file_path, 'w', encoding='utf-8') as f:
                        f.write(content)
                    messagebox.showinfo("成功", f"日志已保存到 {file_path}")
                    self.update_status(f"日志已保存")
                except Exception as e:
                    messagebox.showerror("错误", f"保存失败: {str(e)}")
        
        def update_status(self, message):
            """更新状态栏"""
            self.status_label.config(text=message)
        
        def update_rx_count(self, count):
            """更新接收计数"""
            self.rx_count_label.config(text=f"RX: {count}")
        
        def update_tx_count(self, count):
            """更新发送计数"""
            self.tx_count_label.config(text=f"TX: {count}")
        
        def update_receive_hex(self):
            """更新接收十六进制设置"""
            self.config['receive_hex'] = self.receive_hex_var.get()
            self.save_config()
        
        def update_send_hex(self):
            """更新发送十六进制设置"""
            self.config['send_hex'] = self.send_hex_var.get()
            self.save_config()
        
        def save_config(self):
            """保存配置"""
            self.config['port'] = self.port_combo.get()
            self.config['baudrate'] = self.baudrate_combo.get()
            self.config['bytesize'] = self.bytesize_combo.get()
            self.config['parity'] = self.parity_combo.get()
            self.config['stopbits'] = self.stopbits_combo.get()
            
            try:
                with open('serial_config.json', 'w', encoding='utf-8') as f:
                    json.dump(self.config, f, indent=4, ensure_ascii=False)
            except Exception as e:
                print(f"保存配置失败: {e}")
        
        def load_config(self):
            """加载配置"""
            try:
                if os.path.exists('serial_config.json'):
                    with open('serial_config.json', 'r', encoding='utf-8') as f:
                        config = json.load(f)
                        self.config.update(config)
            except Exception as e:
                print(f"加载配置失败: {e}")
        
        def on_close(self):
            """窗口关闭事件"""
            if self.is_open:
                self.close_port()
            self.root.destroy()
    
    
    def main():
        """主函数"""
        root = tk.Tk()
        app = SerialAssistant(root)
        root.mainloop()
    
    
    if __name__ == "__main__":
        main()

    配置文件serial_config.json

    {
        "port": "COM1",
        "baudrate": "115200",
        "bytesize": "8",
        "parity": "None",
        "stopbits": "1",
        "send_hex": false,
        "receive_hex": false,
        "auto_send": true,
        "auto_send_interval": 1000,
        "auto_send_data": ""
    }

    以上就是Python使用Tkinter编写串口通信调试工具的详细内容,更多关于Python串口通信调试的资料请关注风君子博客其它相关文章!

    您可能感兴趣的文章:

    • 利用Python调试串口的示例代码
    • 基于Python编写一个串口调试工具
    • Python中串口通信库pyserial基础知识
    • python serial串口通信示例详解
    • Python通过串口实现收发文件
    • Python串口通信的接收与发送的实现
    • python实现串口通信的示例代码

    站内搜索