一,效果

二,源码
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox
import serial
import serial.tools.list_ports
import threading
import queue
import time
import binascii
from datetime import datetime
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import numpy as np
class HexSerialMonitor:
def __init__(self, root):
self.root = root
self.root.title("高性能HEX串口助手")
self.root.geometry("1200x900")
self.serial_port = None
self.is_serial_open = False
self.receive_thread = None
self.send_thread = None
self.should_receive = False
self.receive_queue = queue.Queue()
self.send_queue = queue.Queue()
self.receive_count = 0
self.total_received = 0
self.total_sent = 0
self.last_update_time = time.time()
self.auto_send_active = False
self.auto_send_thread = None
self.default_auto_send_data = "01 03 00 01 00 02 95 CB"
self.waveform_data = []
self.max_data_points = 500
self.fig, self.ax = plt.subplots(figsize=(8, 4))
self.line, = self.ax.plot([], [], color='red')
self.ax.set_facecolor('black')
self.ax.set_xlabel('时间')
self.ax.set_ylabel('值')
self.ax.grid(True)
self.create_widgets()
self.refresh_ports()
def create_widgets(self):
top_frame = ttk.LabelFrame(self.root, text="串口配置", padding=10)
top_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(top_frame, text="端口:").grid(row=0, column=0, sticky=tk.W)
self.port_combobox = ttk.Combobox(top_frame, width=15)
self.port_combobox.grid(row=0, column=1, sticky=tk.W)
ttk.Label(top_frame, text="波特率:").grid(row=0, column=2, sticky=tk.W)
self.baudrate_combobox = ttk.Combobox(top_frame, width=10, values=[
"9600", "19200", "38400", "57600", "115200", "230400", "460800", "1250000"
])
self.baudrate_combobox.grid(row=0, column=3, sticky=tk.W)
self.baudrate_combobox.current(7)
ttk.Label(top_frame, text="数据位:").grid(row=0, column=4, sticky=tk.W)
self.databits_combobox = ttk.Combobox(top_frame, width=5, values=["5", "6", "7", "8"])
self.databits_combobox.grid(row=0, column=5, sticky=tk.W)
self.databits_combobox.current(3)
ttk.Label(top_frame, text="停止位:").grid(row=0, column=6, sticky=tk.W)
self.stopbits_combobox = ttk.Combobox(top_frame, width=5, values=["1", "1.5", "2"])
self.stopbits_combobox.grid(row=0, column=7, sticky=tk.W)
self.stopbits_combobox.current(0)
ttk.Label(top_frame, text="校验位:").grid(row=0, column=8, sticky=tk.W)
self.parity_combobox = ttk.Combobox(top_frame, width=5, values=["N", "E", "O", "M", "S"])
self.parity_combobox.grid(row=0, column=9, sticky=tk.W)
self.parity_combobox.current(0)
self.refresh_btn = ttk.Button(top_frame, text="刷新端口", command=self.refresh_ports)
self.refresh_btn.grid(row=0, column=10, padx=5)
self.connect_btn = ttk.Button(top_frame, text="打开串口", command=self.toggle_serial)
self.connect_btn.grid(row=0, column=11, padx=5)
mid_frame = ttk.Frame(self.root)
mid_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
data_frame = ttk.LabelFrame(mid_frame, text="接收数据 (HEX)", padding=10)
data_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.receive_text = scrolledtext.ScrolledText(
data_frame, wrap=tk.WORD, width=60, height=20, font=('Courier', 10)
)
self.receive_text.pack(fill=tk.BOTH, expand=True)
wave_frame = ttk.LabelFrame(mid_frame, text="波形显示", padding=10)
wave_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
self.canvas = FigureCanvasTkAgg(self.fig, master=wave_frame)
self.canvas.draw()
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
bottom_left_frame = ttk.Frame(self.root, padding=10)
bottom_left_frame.pack(side=tk.LEFT, fill=tk.X, padx=5, pady=5)
self.clear_btn = ttk.Button(bottom_left_frame, text="清空显示", command=self.clear_display)
self.clear_btn.pack(side=tk.LEFT, padx=5)
self.wrap_var = tk.IntVar(value=1)
self.wrap_cb = ttk.Checkbutton(bottom_left_frame, text="自动换行", variable=self.wrap_var,
command=self.toggle_wrap)
self.wrap_cb.pack(side=tk.LEFT, padx=5)
self.timestamp_var = tk.IntVar(value=1)
self.timestamp_cb = ttk.Checkbutton(bottom_left_frame, text="时间戳", variable=self.timestamp_var)
self.timestamp_cb.pack(side=tk.LEFT, padx=5)
self.ascii_var = tk.IntVar(value=0)
self.ascii_cb = ttk.Checkbutton(bottom_left_frame, text="显示ASCII", variable=self.ascii_var)
self.ascii_cb.pack(side=tk.LEFT, padx=5)
self.clear_wave_btn = ttk.Button(bottom_left_frame, text="清空波形", command=self.clear_waveform)
self.clear_wave_btn.pack(side=tk.LEFT, padx=5)
bottom_right_frame = ttk.Frame(self.root, padding=10)
bottom_right_frame.pack(side=tk.RIGHT, fill=tk.X, padx=5, pady=5)
self.stat_label = ttk.Label(bottom_right_frame, text="接收: 0 字节 | 发送: 0 字节 | 速率: 0 B/s")
self.stat_label.pack(side=tk.RIGHT)
send_frame = ttk.LabelFrame(self.root, text="发送数据 (HEX)", padding=10)
send_frame.pack(fill=tk.X, padx=5, pady=5)
self.send_entry = ttk.Entry(send_frame)
self.send_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
self.send_btn = ttk.Button(send_frame, text="发送", command=self.send_data)
self.send_btn.pack(side=tk.LEFT, padx=5)
auto_send_frame = ttk.LabelFrame(self.root, text="自动发送", padding=10)
auto_send_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(auto_send_frame, text="发送数据:").grid(row=0, column=0, sticky=tk.W)
self.auto_send_entry = ttk.Entry(auto_send_frame, width=30)
self.auto_send_entry.grid(row=0, column=1, sticky=tk.W)
self.auto_send_entry.insert(0, self.default_auto_send_data)
ttk.Label(auto_send_frame, text="间隔(ms):").grid(row=0, column=2, sticky=tk.W)
self.auto_send_interval = ttk.Entry(auto_send_frame, width=10)
self.auto_send_interval.grid(row=0, column=3, sticky=tk.W)
self.auto_send_interval.insert(0, "1000")
self.auto_send_btn = ttk.Button(auto_send_frame, text="开始自动发送", command=self.toggle_auto_send)
self.auto_send_btn.grid(row=0, column=4, padx=5)
self.root.after(100, self.update_ui)
def update_waveform(self, value):
"""更新波形图"""
self.waveform_data.append(value)
if len(self.waveform_data) > self.max_data_points:
self.waveform_data = self.waveform_data[-self.max_data_points:]
x_data = np.arange(len(self.waveform_data))
self.line.set_data(x_data, self.waveform_data)
self.ax.relim()
self.ax.autoscale_view()
self.canvas.draw()
def clear_waveform(self):
"""清空波形数据"""
self.waveform_data = []
self.line.set_data([], [])
self.ax.relim()
self.ax.autoscale_view()
self.canvas.draw()
self.log_message("波形数据已清空")
def toggle_auto_send(self):
"""切换自动发送状态"""
if not self.auto_send_active:
if not self.is_serial_open or not self.serial_port:
messagebox.showerror("错误", "串口未打开!")
return
try:
interval = int(self.auto_send_interval.get())
if interval <= 0:
raise ValueError("间隔时间必须大于0")
except ValueError as e:
messagebox.showerror("错误", f"无效的间隔时间: {str(e)}")
return
self.auto_send_active = True
self.auto_send_btn.config(text="停止自动发送")
self.auto_send_thread = threading.Thread(target=self.auto_send_loop, daemon=True)
self.auto_send_thread.start()
self.log_message(f"开始自动发送,间隔: {interval}ms")
else:
self.auto_send_active = False
self.auto_send_btn.config(text="开始自动发送")
if self.auto_send_thread and self.auto_send_thread.is_alive():
self.auto_send_thread.join(timeout=0.1)
self.log_message("自动发送已停止")
def auto_send_loop(self):
"""自动发送循环"""
while self.auto_send_active and self.is_serial_open and self.serial_port:
try:
data = self.auto_send_entry.get().strip()
interval = int(self.auto_send_interval.get()) / 1000
data = data.replace(' ', '').replace('\n', '').replace('\t', '').replace(',', '').replace('0x', '')
if not all(c in '0123456789abcdefABCDEF' for c in data):
self.log_message("自动发送错误: 包含非HEX字符", error=True)
continue
if len(data) % 2 != 0:
data = '0' + data
binary_data = binascii.unhexlify(data)
sent_bytes = self.serial_port.write(binary_data)
self.total_sent += sent_bytes
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
hex_str = ' '.join([data[i:i+2] for i in range(0, len(data), 2)])
self.log_message(f"[{timestamp}] 自动发送: {hex_str}")
start_time = time.time()
while self.auto_send_active and (time.time() - start_time) < interval:
time.sleep(0.01)
except Exception as e:
self.log_message(f"自动发送错误: {str(e)}", error=True)
time.sleep(1)
def refresh_ports(self):
"""刷新可用串口列表"""
ports = serial.tools.list_ports.comports()
self.port_combobox['values'] = [port.device for port in ports]
if ports:
self.port_combobox.current(0)
def toggle_serial(self):
"""打开或关闭串口"""
if not self.is_serial_open:
port = self.port_combobox.get()
if not port:
messagebox.showerror("错误", "请选择串口!")
return
try:
self.serial_port = serial.Serial(
port=port,
baudrate=int(self.baudrate_combobox.get()),
bytesize=int(self.databits_combobox.get()),
stopbits=float(self.stopbits_combobox.get()),
parity=self.parity_combobox.get(),
timeout=0.1
)
self.is_serial_open = True
self.connect_btn.config(text="关闭串口")
self.should_receive = True
self.receive_thread = threading.Thread(target=self.receive_data, daemon=True)
self.receive_thread.start()
self.log_message(f"已连接到 {port}")
except Exception as e:
messagebox.showerror("错误", f"无法打开串口: {str(e)}")
else:
self.should_receive = False
self.auto_send_active = False
if self.receive_thread and self.receive_thread.is_alive():
self.receive_thread.join(timeout=0.1)
if self.auto_send_thread and self.auto_send_thread.is_alive():
self.auto_send_thread.join(timeout=0.1)
if self.serial_port and self.serial_port.is_open:
self.serial_port.close()
self.is_serial_open = False
self.connect_btn.config(text="打开串口")
self.auto_send_btn.config(text="开始自动发送")
self.log_message("串口已关闭")
def receive_data(self):
"""高性能串口数据接收线程函数"""
buffer = bytearray()
last_process_time = time.time()
while self.should_receive and self.serial_port and self.serial_port.is_open:
try:
data = self.serial_port.read(9)
if not data:
time.sleep(0.001)
continue
buffer.extend(data)
if buffer:
if len(buffer) >= 9:
value_bytes = buffer[3:7]
value = int.from_bytes(value_bytes, byteorder='big', signed=True)
self.waveform_data.append(value)
if len(self.waveform_data) > self.max_data_points:
self.waveform_data.pop(0)
print(value)
hex_str = binascii.hexlify(buffer).decode('ascii')
ascii_str = ''.join([chr(b) if 32 <= b < 127 else '.' for b in buffer])
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
self.receive_queue.put((hex_str, ascii_str, timestamp, len(buffer)))
self.receive_count += len(buffer)
self.total_received += len(buffer)
buffer.clear()
except serial.SerialException as e:
self.receive_queue.put(("ERROR", f"串口错误: {str(e)}", "", 0))
time.sleep(0.1)
except Exception as e:
self.receive_queue.put(("ERROR", f"系统错误: {str(e)}", "", 0))
time.sleep(0.1)
def update_ui(self):
"""更新界面显示"""
while not self.receive_queue.empty():
hex_str, ascii_str, timestamp, length = self.receive_queue.get()
if hex_str == "ERROR":
self.log_message(f"接收错误: {ascii_str}", error=True)
continue
lines = []
hex_pairs = [hex_str[i:i+2] for i in range(0, len(hex_str), 2)]
for i in range(0, len(hex_pairs), 16):
chunk = hex_pairs[i:i+16]
hex_line = ' '.join(chunk)
if self.ascii_var.get():
ascii_chunk = ascii_str[i:i+16]
ascii_line = ' | ' + ascii_chunk
else:
ascii_line = ''
if self.timestamp_var.get():
time_part = f"[{timestamp}] " if i == 0 else " " * (len(timestamp) + 3)
else:
time_part = ""
lines.append(f"{time_part}{hex_line.ljust(47)}{ascii_line}")
self.receive_text.insert(tk.END, '\n'.join(lines) + '\n')
self.receive_text.see(tk.END)
if self.waveform_data:
x_data = np.arange(len(self.waveform_data))
self.line.set_data(x_data, self.waveform_data)
self.ax.relim()
self.ax.autoscale_view()
self.canvas.draw()
current_time = time.time()
elapsed = current_time - self.last_update_time
if elapsed >= 1:
rate = self.receive_count / elapsed
self.stat_label.config(
text=f"接收: {self.total_received} 字节 | 发送: {self.total_sent} 字节 | 速率: {rate:.1f} B/s"
)
self.receive_count = 0
self.last_update_time = current_time
self.root.after(50, self.update_ui)
def send_data(self):
"""发送HEX数据"""
if not self.is_serial_open or not self.serial_port:
messagebox.showerror("错误", "串口未打开!")
return
data = self.send_entry.get().strip()
if not data:
messagebox.showwarning("警告", "请输入要发送的HEX数据!")
return
try:
data = data.replace(' ', '').replace('\n', '').replace('\t', '').replace(',', '').replace('0x', '')
if not all(c in '0123456789abcdefABCDEF' for c in data):
raise ValueError("包含非HEX字符")
if len(data) % 2 != 0:
data = '0' + data
binary_data = binascii.unhexlify(data)
sent_bytes = self.serial_port.write(binary_data)
self.total_sent += sent_bytes
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
hex_str = ' '.join([data[i:i+2] for i in range(0, len(data), 2)])
self.log_message(f"[{timestamp}] 发送: {hex_str}")
except Exception as e:
messagebox.showerror("错误", f"发送失败: {str(e)}")
def log_message(self, message, error=False):
"""在接收框中显示消息"""
tag = "error" if error else "info"
self.receive_text.insert(tk.END, message + "\n", tag)
self.receive_text.see(tk.END)
def clear_display(self):
"""清空接收框"""
self.receive_text.delete(1.0, tk.END)
def toggle_wrap(self):
"""切换自动换行"""
wrap = tk.WORD if self.wrap_var.get() else tk.NONE
self.receive_text.config(wrap=wrap)
def on_closing(self):
"""窗口关闭时的清理工作"""
self.should_receive = False
self.auto_send_active = False
if self.is_serial_open and self.serial_port:
self.serial_port.close()
if self.receive_thread and self.receive_thread.is_alive():
self.receive_thread.join(timeout=0.1)
if self.auto_send_thread and self.auto_send_thread.is_alive():
self.auto_send_thread.join(timeout=0.1)
self.root.destroy()
if __name__ == "__main__":
root = tk.Tk()
app = HexSerialMonitor(root)
app.receive_text.tag_config("error", foreground="red")
app.receive_text.tag_config("info", foreground="blue")
root.protocol("WM_DELETE_WINDOW", app.on_closing)
root.mainloop()