Python使用Tkinter编写串口通信调试工具
作者:xingzhemengyou1
简介
串口调试助手是一款基于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计数等信息。
python 代码
"""
串口调试助手 - 简化版
功能:串口配置、数据收发、自动发送、日志记录等
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串口通信调试的资料请关注脚本之家其它相关文章!
