串口助手带波形显示

一,效果

在这里插入图片描述

二,源码

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  # 最多显示100个数据点
        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)  # 默认115200
        
        # 数据位
        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)  # 默认8
        
        # 停止位
        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)  # 默认1
        
        # 校验位
        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)  # 默认N
        
        # 刷新端口按钮
        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)
        
        # 创建Matplotlib画布
        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)
        
        # 显示ASCII
        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")  # 默认1000ms
        
        # 自动发送按钮
        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)  # 出错后等待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:
                # 1. 高效读取所有可用数据
                data = self.serial_port.read(9)
                if not data:
                    time.sleep(0.001)  # 无数据时短暂休眠
                    continue
                    
                buffer.extend(data)
                
                # 2. 处理完整的数据包
                if buffer:
                    # 3. 解析数据包中的value值
                    if len(buffer) >= 9:  # 假设数据包至少9字节
                        value_bytes = buffer[3:7]  # 第3到第6字节(Python切片是左闭右开)
                        value = int.from_bytes(value_bytes, byteorder='big', signed=True)
                        
                        # 将value值添加到波形数据
                        self.waveform_data.append(value)
                        if len(self.waveform_data) > self.max_data_points:
                            self.waveform_data.pop(0)
                        
                        # 打印解析结果(调试用)
                        print(value)
                    
                    # 4. 转换为HEX和ASCII格式
                    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]
                    
                    # 5. 使用队列传输完整数据包
                    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)  # 每50ms更新一次界面
        
    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', '')
            
            # 检查是否为有效的HEX
            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()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值