根据python编写的串口调试工具,学习布局中,待完善…
界面如下:
跳转到整体实现代码
根据sscom的界面进行实现,首先实现界面的整体布局,之后再实现不同部件的功能,下列创建画布的实现代码:
import tkinter as tk
from tkinter import ttk
class SerialDebugTool:
def __init__(self, root):
self.root = root
self.root.title("串口调试工具") # 设置工具名称
self.root.geometry("620x600") # 设置窗口大小
self.root.resizable(False, False) # 禁止调整窗口大小
if __name__ == "__main__":
root = tk.Tk()
app = SerialDebugTool(root)
root.mainloop()
之后就可以进行界面布局插件的插入了
创建一个 create_widgets() 函数,用于布局用:
self.create_widgets()
def create_widgets(self):
pass
创建数据接收框
这段代码是用来创建一个带有滚动条的文本框,用于显示串口的输出。这里使用了 tkinter 库来创建用户界面。首先创建了一个 Text 组件 self.serial_output,然后创建了一个 Scrollbar 组件 scrollbar_output,并将其与 Text 组件绑定,实现滚动功能。
self.serial_output = tk.Text(self.root, height=25, width=85)
self.serial_output.place(x=15, y=10)
# 添加滚轴
scrollbar_output = tk.Scrollbar(self.root, command=self.serial_output.yview)
scrollbar_output.place(x=600, y=10, height=329)
self.serial_output.config(yscrollcommand=scrollbar_output.set)
在测试过程中,会出现卡顿现象,所以在这里将收到的数据添加到一个任务中,如下所示:
class SerialReaderThread(threading.Thread):
def __init__(self, serial_port, callback, gui):
super().__init__()
self.serial_port = serial_port
self.callback = callback
self.gui = gui
self._is_running = threading.Event()
def stop(self):
self._is_running.clear()
def run(self):
self._is_running.set()
while self._is_running.is_set():
if self.serial_port.in_waiting > 0:
data = self.serial_port.readline()
self.callback(data)
self.gui.update_serial_output(data)
并在class SerialDebugTool中调用
def update_serial_output(self, data):
try:
if self.hex_display_var.get(): # 如果选择了以十六进制显示接收的数据
raw_data = ' '.join(format(byte, '02X') for byte in data)
else:
# 尝试使用 UTF-8 编码解码字节数据
raw_data = data.decode('utf-8').strip()
except UnicodeDecodeError:
# 如果解码失败,则使用 errors='ignore' 参数忽略无法解码的字节
#raw_data = data.decode('utf-8', errors='ignore').strip()
# 如果解码失败,则使用 errors='replace' 参数替换无法解码的字节
raw_data = data.decode('utf-8', errors='replace').strip()
# 记录乱码数据到日志
logging.warning("Received malformed data: %s", raw_data)
if raw_data:
if self.add_timestamp.get(): # 如果勾选了添加时间戳,则在数据前面添加时间戳
timestamp = time.strftime("[%Y-%m-%d %H:%M:%S] ", time.localtime())
raw_data = timestamp + raw_data
# 显示接收到的数据
self.serial_output.insert(tk.END, f"{raw_data}\n")
self.serial_output.see(tk.END) # 滚动到最后一行
串口和波特率选择部分的用户界面
这段代码实现了一个简单的串口选择界面,允许用户从可用的串口列表中选择一个串口,并提供了一个刷新按钮来更新可用串口列表。get_available_ports() 方法用于获取当前系统中可用的串口列表,并将其返回给 ports。然后,创建了一个 Combobox 组件 port_combobox,将可用的串口列表 ports 作为选项值,设置 state=“readonly” 表示用户只能选择而不能编辑。
# 串口标签
self.port_label = ttk.Label(self.root, text="端口号:")
self.port_label.place(x=15, y=355)
# 获取可用端口并设置组合框
ports = self.get_available_ports()
self.port_combobox = ttk.Combobox(self.root, values=ports, state="readonly")
self.port_combobox.place(x=70, y=355)
# 默认显示第一个可用端口
if ports:
self.port_combobox.set(ports[0])
self.refresh_button = ttk.Button(self.root, text="刷新", command=self.refresh_ports)
self.refresh_button.place(x=245, y=353)
刷新按钮则是当有新设备接入时,则可点击,对于的事件为:refresh_ports, 实现如下:
def refresh_ports(self):
ports = self.get_available_ports()
self.port_combobox['values'] = ports
if ports:
self.port_combobox.set(ports[0]) # 刷新后重新设置为第一个端口
获取端口的函数如下:
def get_available_ports(self):
ports = [port.device for port in serial.tools.list_ports.comports()]
return ports
同理,波特率也是一样实现的
self.baudrate_label = ttk.Label(self.root, text="波特率:")
self.baudrate_label.place(x=15, y=390)
self.baudrate_combobox = ttk.Combobox(self.root, values=["9600", "19200", "38400", "57600", "115200"], state="readonly")
self.baudrate_combobox.place(x=70, y=390)
self.baudrate_combobox.set("9600")
当串口打开时,切换波特率和串口,需将原本连接的串口关闭再打开, 则给波特率和port都绑定事件:
# 绑定事件处理函数到端口号组合框的事件上
self.port_combobox.bind("<<ComboboxSelected>>", self.on_baudrate_and_port_combobox_change)
# 绑定事件处理函数
self.baudrate_combobox.bind("<<ComboboxSelected>>", self.on_baudrate_and_port_combobox_change)
def on_baudrate_and_port_combobox_change(self, event):
# Check if currently connected to a different serial port
if self.serial_port is not None and self.serial_port.is_open:
self.disconnect()
self.connect()
创建打开串口按钮及指示器
代码如下:
# 连接按钮和状态指示器
self.connect_button = ttk.Button(self.root, text="打开串口", command=self.toggle_connection)
self.connect_button.place(x=245, y=390)
# 创建Canvas时,设置其背景颜色与页面背景色相同
self.status_indicator = tk.Canvas(self.root, width=20, height=20, bg=self.root.cget('bg'), highlightthickness=0)
# 将状态指示器放置在连接按钮的左侧
self.status_indicator.place(x=335, y=392)
self.update_status_indicator(False)
其当打开串口成功后,则指示灯为绿色,之后按钮变为关闭串口,当串口关闭后,则指示灯对应为红色
def toggle_connection(self):
if not self.connected:
self.connect()
else:
self.disconnect()
def connect(self):
port = self.port_combobox.get()
baudrate = int(self.baudrate_combobox.get())
if not port:
messagebox.showerror("Error", "Please select a serial port.")
return
try:
# 初始化新的串口对象
self.serial_port = serial.Serial(port, baudrate)
self.reader_thread = SerialReaderThread(self.serial_port, self.handle_data, self)
self.reader_thread.start()
self.serial_port.timeout = 0.1 # 设置读取超时时间
self.connected = True
self.connect_button.config(text="关闭串口")
self.update_status_indicator(True)
#self.root.after(100, self.read_serial)
except serial.SerialException as e:
messagebox.showerror("Error", str(e))
def handle_data(self, data):
print("Received:", data)
def disconnect(self):
if self.reader_thread:
self.reader_thread.stop()
self.reader_thread.join()
if self.serial_port and self.serial_port.is_open:
self.serial_port.close()
self.connected = False
self.connect_button.config(text="打开串口")
self.update_status_indicator(False)
创建添加回车换行、时间戳、hex显示及定时发送选项
self.add_newline_checkbox = tk.Checkbutton(self.root, text="加回车换行", variable=self.add_newline)
self.add_newline_checkbox.place(x=15, y=420)
self.add_timestamp_checkbox = tk.Checkbutton(self.root, text="加时间戳", variable=self.add_timestamp)
self.add_timestamp_checkbox.place(x=100, y=420)
self.hex_display_checkbox = tk.Checkbutton(self.root, text="Hex显示", variable=self.hex_display_var)
self.hex_display_checkbox.place(x=180, y=420)
self.timed_transmission_checkbox = tk.Checkbutton(self.root, text="定时发送:", variable=self.timed_transmission, command=self.toggle_timed_transmission)
self.timed_transmission_checkbox.place(x=250, y=420)
self.time_entry = tk.Text(self.root, height=1, width=8)
self.time_entry.insert("1.0", "1000") # 插入默认文本
self.time_entry.place(x=330, y=425)
self.time_label = ttk.Label(self.root, text="ms/s")
self.time_label.place(x=390, y=423)
其选项对应的实现逻辑如下所示:
def toggle_timed_transmission(self):
if self.timed_transmission.get():
self.start_timed_transmission()
else:
self.stop_timed_transmission()
def start_timed_transmission(self):
interval = int(self.time_entry.get("1.0", "end-1c")) / 1000 # 将毫秒转换为秒
self.timed_transmission.set(True) # 设置标志位,让循环开始
self.timed_transmission_loop(interval)
def stop_timed_transmission(self):
if hasattr(self, 'timed_transmission_id'):
self.timed_transmission.set(False) # 设置标志位,让循环退出
self.root.after_cancel(self.timed_transmission_id) # 取消之前创建的定时任务
def timed_transmission_loop(self, interval):
if self.timed_transmission.get():
data = self.send_entry.get("1.0", "end-1c") # 获取输入框中的数据
if data and self.serial_is_open(): # 如果数据不为空且串口打开
self.send_data()
self.timed_transmission_id = self.root.after(int(interval * 1000), self.timed_transmission_loop, interval)
def serial_is_open(self):
# 检查串口是否打开,这里假设返回 True 表示串口已打开
if self.serial_port is not None and self.serial_port.is_open:
return True
else:
return False
创建发送数据输入框
self.send_entry = tk.Text(self.root, height=10, width=85)
self.send_entry.place(x=15, y=453)
在其上方添加发送按钮
self.send_button = ttk.Button(self.root, text="发送", command=self.send_data)
self.send_button.place(x=525, y=420)
其send_data的实现逻辑如下,并将发送的数据显示在数据接收框内:
def send_data(self):
data = self.send_entry.get("1.0", "end-1c")
timestamp = ""
if self.add_timestamp.get(): # 如果勾选了添加时间戳,则记录时间戳
timestamp = time.strftime("[%Y-%m-%d %H:%M:%S] ", time.localtime())
if self.add_newline.get(): # 如果勾选了添加换行符
data += "\r\n"
encoded_data = data.encode('utf-8') # 将字符串编码为字节形式
if self.connected and self.serial_port:
self.serial_port.write(encoded_data)
if self.hex_display_var.get(): # 如果选择了以十六进制显示发送数据
hex_data = binascii.hexlify(encoded_data).decode("utf-8")
hex_data_spaced = ' '.join(hex_data[i:i + 2] for i in range(0, len(hex_data), 2))
if timestamp: # 如果有时间戳,则在显示时添加时间戳
self.serial_output.insert(tk.END, f"{timestamp}{hex_data_spaced}\n")
else:
self.serial_output.insert(tk.END, f"{hex_data_spaced}\n")
else:
if timestamp: # 如果有时间戳,则在显示时添加时间戳
self.serial_output.insert(tk.END, f"{timestamp}{data}\n")
else:
self.serial_output.insert(tk.END, f"{data}\n")
self.serial_output.see(tk.END)
创建清除数据接收框内容按钮
代码如下:
self.clear_button = ttk.Button(self.root, text="清除", command=self.clear_data)
self.clear_button.place(x=435, y=420)
其clear_data的实现逻辑如下:
def clear_data(self):
# 清除文本框中的内容
self.serial_output.config(state="normal")
self.serial_output.delete(1.0, "end")
因为整体有两个任务,则在直接关闭窗口时可绑定事件,对其进行销毁:
def __init__(self, root):
self.root = root
self.root.protocol("WM_DELETE_WINDOW", self.on_close)
self.reader_thread = None
.....
def on_close(self):
if self.reader_thread:
self.reader_thread.stop()
self.stop_timed_transmission()
self.root.destroy()
sys.exit() # 退出整个程序
整体的实现代码如下所示
import serial
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox
import serial.tools.list_ports
import time
import binascii
import threading
import logging
import sys
class SerialReaderThread(threading.Thread):
def __init__(self, serial_port, callback, gui):
super().__init__()
self.serial_port = serial_port
self.callback = callback
self.gui = gui
self._is_running = threading.Event()
def stop(self):
self._is_running.clear()
def run(self):
self._is_running.set()
while self._is_running.is_set():
if self.serial_port.in_waiting > 0:
data = self.serial_port.readline()
self.callback(data)
self.gui.update_serial_output(data)
class SerialDebugTool:
def __init__(self, root):
self.root = root
self.root.protocol("WM_DELETE_WINDOW", self.on_close)
self.root.title("串口调试工具")
self.root.geometry("620x600")
self.root.resizable(False, False) # 禁止调整窗口大小
self.serial_port = None
self.serial_baudrate = 9600
self.connected = False
self.add_newline = tk.BooleanVar() # 用于跟踪是否勾选添加回车换行
self.add_timestamp = tk.BooleanVar() # 用于跟踪是否勾选添加时间戳
self.hex_display_var = tk.BooleanVar() # 用于跟踪是否勾选hex显示
self.timed_transmission = tk.BooleanVar()
self.timed_transmission.set(False)
self.reader_thread = None
self.create_widgets()
def on_close(self):
if self.reader_thread:
self.reader_thread.stop()
self.stop_timed_transmission()
self.root.destroy()
sys.exit() # 退出整个程序
def create_widgets(self):
# 数据接收框
self.serial_output = tk.Text(self.root, height=25, width=85)
self.serial_output.place(x=15, y=10)
# 添加滚轴
scrollbar_output = tk.Scrollbar(self.root, command=self.serial_output.yview)
scrollbar_output.place(x=600, y=10, height=329)
self.serial_output.config(yscrollcommand=scrollbar_output.set)
# 串口和波特率标签
self.port_label = ttk.Label(self.root, text="端口号:")
self.port_label.place(x=15, y=355)
# 获取可用端口并设置组合框
ports = self.get_available_ports_desc()
self.port_combobox = ttk.Combobox(self.root, values=ports, state="readonly")
self.port_combobox.place(x=70, y=355)
# 默认显示第一个可用端口
if ports:
self.port_combobox.set(ports[0])
# 绑定事件处理函数到端口号组合框的事件上
self.port_combobox.bind("<<ComboboxSelected>>", self.on_baudrate_and_port_combobox_change)
self.port_combobox.bind("<Button-1>", self.on_combobox_click)
self.port_combobox.bind("<Leave>", self.on_combobox_leave)
self.refresh_button = ttk.Button(self.root, text="刷新", command=self.refresh_ports)
self.refresh_button.place(x=245, y=353)
self.baudrate_label = ttk.Label(self.root, text="波特率:")
self.baudrate_label.place(x=15, y=390)
self.baudrate_combobox = ttk.Combobox(self.root, values=["9600", "19200", "38400", "57600", "115200"], state="readonly")
self.baudrate_combobox.place(x=70, y=390)
self.baudrate_combobox.set("9600")
# 绑定事件处理函数
self.baudrate_combobox.bind("<<ComboboxSelected>>", self.on_baudrate_and_port_combobox_change)
# 连接按钮和状态指示器
self.connect_button = ttk.Button(self.root, text="打开串口", command=self.toggle_connection)
self.connect_button.place(x=245, y=390)
# 创建Canvas时,设置其背景颜色与页面背景色相同
self.status_indicator = tk.Canvas(self.root, width=20, height=20, bg=self.root.cget('bg'), highlightthickness=0)
# 将状态指示器放置在连接按钮的左侧
self.status_indicator.place(x=335, y=392)
self.update_status_indicator(False)
self.add_newline_checkbox = tk.Checkbutton(self.root, text="加回车换行", variable=self.add_newline)
self.add_newline_checkbox.place(x=15, y=420)
self.add_timestamp_checkbox = tk.Checkbutton(self.root, text="加时间戳", variable=self.add_timestamp)
self.add_timestamp_checkbox.place(x=100, y=420)
self.hex_display_checkbox = tk.Checkbutton(self.root, text="Hex显示", variable=self.hex_display_var)
self.hex_display_checkbox.place(x=180, y=420)
self.timed_transmission_checkbox = tk.Checkbutton(self.root, text="定时发送:", variable=self.timed_transmission, command=self.toggle_timed_transmission)
self.timed_transmission_checkbox.place(x=250, y=420)
self.time_entry = tk.Text(self.root, height=1, width=8)
self.time_entry.insert("1.0", "1000") # 插入默认文本
self.time_entry.place(x=330, y=425)
self.time_label = ttk.Label(self.root, text="ms/s")
self.time_label.place(x=390, y=423)
self.clear_button = ttk.Button(self.root, text="清除", command=self.clear_data)
self.clear_button.place(x=435, y=420)
self.send_button = ttk.Button(self.root, text="发送", command=self.send_data)
self.send_button.place(x=525, y=420)
self.send_entry = tk.Text(self.root, height=10, width=85)
self.send_entry.place(x=15, y=453)
def on_combobox_leave(self, event):
self.port_combobox.config(width=20) # 鼠标离开时恢复下拉框的宽度为10
def on_combobox_click(self, event): # 注意这里多了一个 self 参数
self.port_combobox.config(width=38) # 修改下拉框的宽度为20
def clear_data(self):
# 清除文本框中的内容
self.serial_output.config(state="normal")
self.serial_output.delete(1.0, "end")
#self.serial_output.config(state="disable")
def toggle_timed_transmission(self):
if self.timed_transmission.get():
self.start_timed_transmission()
else:
self.stop_timed_transmission()
def start_timed_transmission(self):
interval = int(self.time_entry.get("1.0", "end-1c")) / 1000 # 将毫秒转换为秒
self.timed_transmission.set(True) # 设置标志位,让循环开始
self.timed_transmission_loop(interval)
def timed_transmission_loop(self, interval):
if self.timed_transmission.get():
data = self.send_entry.get("1.0", "end-1c") # 获取输入框中的数据
if data and self.serial_is_open(): # 如果数据不为空且串口打开
self.send_data()
self.timed_transmission_id = self.root.after(int(interval * 1000), self.timed_transmission_loop, interval)
def stop_timed_transmission(self):
if hasattr(self, 'timed_transmission_id'):
self.timed_transmission.set(False) # 设置标志位,让循环退出
self.root.after_cancel(self.timed_transmission_id) # 取消之前创建的定时任务
def serial_is_open(self):
# 检查串口是否打开,这里假设返回 True 表示串口已打开
if self.serial_port is not None and self.serial_port.is_open:
return True
else:
return False
def on_baudrate_and_port_combobox_change(self, event):
self.port_combobox.config(width=20) # 修改下拉框的宽度为20
# Check if currently connected to a different serial port
if self.serial_port is not None and self.serial_port.is_open:
self.disconnect()
self.connect()
def send_data(self):
data = self.send_entry.get("1.0", "end-1c")
timestamp = ""
if self.add_timestamp.get(): # 如果勾选了添加时间戳,则记录时间戳
timestamp = time.strftime("[%Y-%m-%d %H:%M:%S] ", time.localtime())
if self.add_newline.get(): # 如果勾选了添加换行符
data += "\r\n"
encoded_data = data.encode('utf-8') # 将字符串编码为字节形式
if self.connected and self.serial_port:
self.serial_port.write(encoded_data)
if self.hex_display_var.get(): # 如果选择了以十六进制显示发送数据
hex_data = binascii.hexlify(encoded_data).decode("utf-8")
hex_data_spaced = ' '.join(hex_data[i:i + 2] for i in range(0, len(hex_data), 2))
if timestamp: # 如果有时间戳,则在显示时添加时间戳
self.serial_output.insert(tk.END, f"{timestamp}{hex_data_spaced}\n")
else:
self.serial_output.insert(tk.END, f"{hex_data_spaced}\n")
else:
if timestamp: # 如果有时间戳,则在显示时添加时间戳
self.serial_output.insert(tk.END, f"{timestamp}{data}\n")
else:
self.serial_output.insert(tk.END, f"{data}\n")
self.serial_output.see(tk.END)
def get_available_ports(self):
ports = [port.device for port in serial.tools.list_ports.comports()]
return ports
def get_available_ports_desc(self):
ports_info = serial.tools.list_ports.comports()
ports = [f"{port.device} {port.description.replace(port.device, '').replace('(', '').replace(')', '').strip()}"
for port in ports_info]
return ports
def refresh_ports(self):
ports = self.get_available_ports_desc()
self.port_combobox['values'] = ports
if ports:
self.port_combobox.set(ports[0]) # 刷新后重新设置为第一个端口
def toggle_connection(self):
if not self.connected:
self.connect()
else:
self.disconnect()
def connect(self):
modified_port = self.port_combobox.get()
port = modified_port.split(' ')[0] # 只获取设备信息,去掉描述信息
baudrate = int(self.baudrate_combobox.get())
if not port:
messagebox.showerror("Error", "Please select a serial port.")
return
try:
# 初始化新的串口对象
self.serial_port = serial.Serial(port, baudrate)
self.reader_thread = SerialReaderThread(self.serial_port, self.handle_data, self)
self.reader_thread.start()
self.serial_port.timeout = 0.1 # 设置读取超时时间
self.connected = True
self.connect_button.config(text="关闭串口")
self.update_status_indicator(True)
#self.root.after(100, self.read_serial)
except serial.SerialException as e:
messagebox.showerror("Error", str(e))
def handle_data(self, data):
print("Received:", data)
def disconnect(self):
if self.reader_thread:
self.reader_thread.stop()
self.reader_thread.join()
if self.serial_port and self.serial_port.is_open:
self.serial_port.close()
self.connected = False
self.connect_button.config(text="打开串口")
self.update_status_indicator(False)
def read_serial(self):
if self.connected and self.serial_port:
try:
raw_data = self.serial_port.readline()
if raw_data:
if self.hex_display_var.get(): # 如果选择了以十六进制显示接收的数据
data = ' '.join(format(byte, '02X') for byte in raw_data)
else:
try:
# 尝试使用 UTF-8 编码解码字节数据
data = raw_data.decode('utf-8').strip()
except UnicodeDecodeError:
# 如果解码失败,则使用 errors='ignore' 参数忽略无法解码的字节
data = raw_data.decode('utf-8', errors='ignore').strip()
if data:
if self.add_timestamp.get(): # 如果勾选了添加时间戳,则在数据前面添加时间戳
timestamp = time.strftime("[%Y-%m-%d %H:%M:%S] ", time.localtime())
data = timestamp + data
# 显示接收到的数据
self.serial_output.insert(tk.END, f"{data}\n")
self.serial_output.see(tk.END) # 滚动到最后一行
except serial.SerialException:
pass
# 设置下一次读取串口数据的定时器
self.root.after(100, self.read_serial)
def update_serial_output(self, data):
try:
if self.hex_display_var.get(): # 如果选择了以十六进制显示接收的数据
raw_data = ' '.join(format(byte, '02X') for byte in data)
else:
# 尝试使用 UTF-8 编码解码字节数据
raw_data = data.decode('utf-8').strip()
except UnicodeDecodeError:
# 如果解码失败,则使用 errors='ignore' 参数忽略无法解码的字节
#raw_data = data.decode('utf-8', errors='ignore').strip()
# 如果解码失败,则使用 errors='replace' 参数替换无法解码的字节
raw_data = data.decode('utf-8', errors='replace').strip()
# 记录乱码数据到日志
logging.warning("Received malformed data: %s", raw_data)
if raw_data:
if self.add_timestamp.get(): # 如果勾选了添加时间戳,则在数据前面添加时间戳
timestamp = time.strftime("[%Y-%m-%d %H:%M:%S] ", time.localtime())
raw_data = timestamp + raw_data
# 显示接收到的数据
self.serial_output.insert(tk.END, f"{raw_data}\n")
self.serial_output.see(tk.END) # 滚动到最后一行
def update_status_indicator(self, connected):
color = "green" if connected else "red"
self.status_indicator.delete("all")
oval_size = 15 # 圆形的大小
oval_x = (20 - oval_size) / 2 # 圆形的x坐标
oval_y = (20 - oval_size) / 2 # 圆形的y坐标
outline_color = "black" # 外轮廓颜色
self.status_indicator.create_oval(oval_x, oval_y, oval_x + oval_size, oval_y + oval_size, fill=color,
outline=outline_color) # 使用fill参数填充颜色,并且outline参数设置外轮廓颜色
if __name__ == "__main__":
root = tk.Tk()
app = SerialDebugTool(root)
root.mainloop()