利用Python調(diào)試串口的示例代碼
概述:為什么需要專(zhuān)業(yè)的串口調(diào)試工具
在嵌入式開(kāi)發(fā)、物聯(lián)網(wǎng)設(shè)備調(diào)試過(guò)程中,串口通信是最基礎(chǔ)的調(diào)試手段。但系統(tǒng)自帶的串口工具功能簡(jiǎn)陋,商業(yè)軟件又價(jià)格昂貴。本文將帶你用Python+ttkbootstrap打造一款高顏值、多功能的串口調(diào)試助手,具備以下亮點(diǎn)功能:
核心功能亮點(diǎn):
- 現(xiàn)代化UI界面 - 基于ttkbootstrap的多主題切換
- 實(shí)時(shí)數(shù)據(jù)統(tǒng)計(jì) - 發(fā)送/接收字節(jié)計(jì)數(shù)
- 自動(dòng)發(fā)送功能 - 可配置間隔時(shí)間
- 發(fā)送歷史記錄 - 支持上下箭頭導(dǎo)航
- 數(shù)據(jù)持久化 - 接收內(nèi)容保存為文件
- 自動(dòng)端口檢測(cè) - 實(shí)時(shí)監(jiān)控串口熱插拔
項(xiàng)目架構(gòu)設(shè)計(jì)
1.1 技術(shù)棧選型
import serial # 串口通信核心庫(kù) import serial.tools.list_ports # 串口設(shè)備枚舉 import threading # 多線(xiàn)程處理 import queue # 線(xiàn)程安全隊(duì)列 import ttkbootstrap as ttk # 現(xiàn)代化UI框架 from tkinter import filedialog # 文件對(duì)話(huà)框
1.2 關(guān)鍵類(lèi)說(shuō)明
SerialTool:主控制類(lèi),采用MVC設(shè)計(jì)模式
數(shù)據(jù)層:serial_port管理物理連接
視圖層:create_widgets()構(gòu)建界面
控制層:事件處理方法群
1.3 線(xiàn)程模型
定時(shí)輪詢(xún)異步推送定時(shí)觸發(fā)主線(xiàn)程接收隊(duì)列接收子線(xiàn)程自動(dòng)發(fā)送任務(wù)
環(huán)境配置指南
2.1 基礎(chǔ)環(huán)境
# 必需庫(kù)安裝 pip install pyserial ttkbootstrap
2.2 硬件準(zhǔn)備
任意USB轉(zhuǎn)串口設(shè)備(如CH340、CP2102等)
開(kāi)發(fā)板或目標(biāo)設(shè)備
2.3 兼容性說(shuō)明
支持Windows/macOS/Linux
測(cè)試Python版本:3.8+
核心功能實(shí)現(xiàn)
3.1 串口通信核心
def open_serial(self): # 參數(shù)映射轉(zhuǎn)換 parity_map = { 'None': serial.PARITY_NONE, 'Even': serial.PARITY_EVEN, # ...其他校驗(yàn)位映射 } self.serial_port = serial.Serial( port=self.port_cb.get(), baudrate=int(self.baudrate_cb.get()), parity=parity_map[self.parity_cb.get()], timeout=0.1 # 非阻塞讀取 )
3.2 多線(xiàn)程數(shù)據(jù)處理
def receive_worker(self): while not self.receive_thread_event.is_set(): try: # 非阻塞讀取 if self.serial_port.in_waiting > 0: data = self.serial_port.read(self.serial_port.in_waiting) self.receive_queue.put(data) except serial.SerialException: break
3.3 自動(dòng)發(fā)送機(jī)制
def auto_send_task(self): if self.auto_send_flag: try: interval = int(self.interval_entry.get()) self.send_data() # 執(zhí)行發(fā)送 self.master.after(interval, self.auto_send_task) # 定時(shí)循環(huán) except ValueError: self.auto_var.set(False)
UI界面詳解
4.1 三欄式布局
main_frame = ttk.Frame(self.master) left_frame = ttk.Labelframe(main_frame, text="串口配置") # 左側(cè)配置區(qū) send_frame = ttk.Labelframe(right_frame, text="數(shù)據(jù)發(fā)送") # 右上發(fā)送區(qū) recv_frame = ttk.Labelframe(right_frame, text="數(shù)據(jù)接收") # 右下接收區(qū)
4.2 主題切換實(shí)現(xiàn)
def change_theme(self): selected_theme = self.theme_cb.get() self.style.theme_use(selected_theme) # 動(dòng)態(tài)切換主題
4.3 控件亮點(diǎn)功能
歷史記錄導(dǎo)航:通過(guò)<Up>/<Down>鍵遍歷
智能滾動(dòng)文本框:自動(dòng)滾動(dòng)到最新內(nèi)容
狀態(tài)欄提示:實(shí)時(shí)顯示連接狀態(tài)
運(yùn)行效果展示
5.1 主題切換演示
5.2 數(shù)據(jù)收發(fā)演示
[2023-08-20 14:25:36] [Send] AT+GMR
[2023-08-20 14:25:36] AT version:2.1.0.0-dev
5.3 統(tǒng)計(jì)功能展示
發(fā)送: 2456 字節(jié) | 接收: 18923 字節(jié)
源碼下載
import serial import serial.tools.list_ports import threading import queue import os import time import ttkbootstrap as ttk from ttkbootstrap.constants import * from ttkbootstrap.dialogs import Messagebox from ttkbootstrap.scrolled import ScrolledText from tkinter import BooleanVar, StringVar, IntVar import platform from tkinter import filedialog import json class SerialTool: def __init__(self, master): self.master = master self.master.title("串口調(diào)試助手") self.master.geometry("900x520") self.master.resizable(False, False) # 禁止調(diào)整窗口大小 self.master.update() # 強(qiáng)制應(yīng)用尺寸限制 # 初始化樣式 self.style = ttk.Style(theme='cosmo') # 配置邊框線(xiàn)為純黑色的樣式 self.style.configure('BlackBorder.TLabelframe', bordercolor='#D3D3D3', relief='solid', borderwidth=1) # 串口參數(shù) self.serial_port = None self.receive_queue = queue.Queue() self.auto_send_flag = False self.send_count = 0 self.receive_count = 0 self.receive_thread = None self.receive_thread_event = threading.Event() # 用于控制接收線(xiàn)程的事件 # 發(fā)送歷史記錄 self.send_history = [] self.history_index = -1 # 自動(dòng)檢測(cè)串口變化 self.last_port_count = 0 # 創(chuàng)建界面 self.create_widgets() self.refresh_ports() self.master.after(100, self.process_queue) self.check_ports_change() # 開(kāi)始檢測(cè)串口變化 def create_widgets(self): """創(chuàng)建三欄式布局""" main_frame = ttk.Frame(self.master) main_frame.pack(fill=BOTH, expand=True, padx=10, pady=10) # 主題切換控件 theme_frame = ttk.Frame(self.master) theme_frame.pack(fill=X, padx=10, pady=(0, 5)) ttk.Label(theme_frame, text="主題:").pack(side=LEFT, padx=5) self.theme_cb = ttk.Combobox( theme_frame, values=sorted(ttk.Style().theme_names()), state='readonly' ) self.theme_cb.pack(side=LEFT, padx=5) self.theme_cb.set('cosmo') self.theme_cb.bind('<<ComboboxSelected>>', self.change_theme) # 左側(cè)串口配置區(qū) left_frame = ttk.Labelframe(main_frame, text="串口配置", padding=15, style='BlackBorder.TLabelframe') left_frame.grid(row=0, column=0, sticky=NSEW, padx=5, pady=5) # 右側(cè)上下分區(qū) right_frame = ttk.Frame(main_frame) right_frame.grid(row=0, column=1, sticky=NSEW, padx=5, pady=5) # 發(fā)送區(qū)(右上) send_frame = ttk.Labelframe(right_frame, text="數(shù)據(jù)發(fā)送", padding=15, style='BlackBorder.TLabelframe') send_frame.pack(fill=BOTH, expand=True, side=TOP) # 接收區(qū)(右下) recv_frame = ttk.Labelframe(right_frame, text="數(shù)據(jù)接收", padding=15, style='BlackBorder.TLabelframe') recv_frame.pack(fill=BOTH, expand=True, side=TOP) # 配置網(wǎng)格權(quán)重 main_frame.columnconfigure(1, weight=1) main_frame.rowconfigure(0, weight=1) right_frame.rowconfigure(1, weight=1) # 創(chuàng)建各區(qū)域組件 self.create_serial_controls(left_frame) self.create_send_controls(send_frame) self.create_recv_controls(recv_frame) # 狀態(tài)欄 self.status_var = StringVar(value="就緒") ttk.Label(self.master, textvariable=self.status_var, bootstyle=(SECONDARY, INVERSE)).pack(fill=X, side=BOTTOM) def change_theme(self, event=None): """切換主題""" selected_theme = self.theme_cb.get() self.style.theme_use(selected_theme) def create_serial_controls(self, parent): """串口參數(shù)控件""" param_frame = ttk.Frame(parent) param_frame.pack(fill=X) # 串口號(hào) ttk.Label(param_frame, text="COM端口:").grid(row=0, column=0, padx=5, pady=5, sticky=W) self.port_cb = ttk.Combobox(param_frame, width=15) self.port_cb.grid(row=0, column=1, padx=5, pady=5) # 波特率 ttk.Label(param_frame, text="波特率:").grid(row=1, column=0, padx=5, pady=5, sticky=W) self.baudrate_cb = ttk.Combobox(param_frame, values=[ '9600', '115200', '57600', '38400', '19200', '14400', '4800', '2400', '1200' ], width=15) self.baudrate_cb.set('9600') self.baudrate_cb.grid(row=1, column=1, padx=5, pady=5) # 校驗(yàn)位 ttk.Label(param_frame, text="校驗(yàn)位:").grid(row=2, column=0, padx=5, pady=5, sticky=W) self.parity_cb = ttk.Combobox(param_frame, values=[ 'None', 'Even', 'Odd', 'Mark', 'Space' ], width=15) self.parity_cb.set('None') self.parity_cb.grid(row=2, column=1, padx=5, pady=5) # 數(shù)據(jù)位 ttk.Label(param_frame, text="數(shù)據(jù)位:").grid(row=3, column=0, padx=5, pady=5, sticky=W) self.databits_cb = ttk.Combobox(param_frame, values=['8', '7', '6', '5'], width=15) self.databits_cb.set('8') self.databits_cb.grid(row=3, column=1, padx=5, pady=5) # 停止位 ttk.Label(param_frame, text="停止位:").grid(row=4, column=0, padx=5, pady=5, sticky=W) self.stopbits_cb = ttk.Combobox(param_frame, values=['1', '1.5', '2'], width=15) self.stopbits_cb.set('1') self.stopbits_cb.grid(row=4, column=1, padx=5, pady=5) # 操作按鈕 # 按鈕容器 btn_frame = ttk.Frame(parent) btn_frame.pack(pady=10, fill=X) # 配置網(wǎng)格列權(quán)重實(shí)現(xiàn)自動(dòng)伸縮 btn_frame.columnconfigure((0, 1, 2), weight=1, uniform='btns') # uniform 確保列寬一致 # 刷新按鈕 ttk.Button( btn_frame, text="刷新端口", command=self.refresh_ports, bootstyle=OUTLINE ).grid(row=0, column=0, padx=5, sticky="ew") # 連接按鈕 self.conn_btn = ttk.Button( btn_frame, text="打開(kāi)串口", command=self.toggle_connection, bootstyle=OUTLINE + SUCCESS ) self.conn_btn.grid(row=0, column=1, padx=5, sticky="ew") # 手動(dòng)發(fā)送按鈕(移動(dòng)到此處) ttk.Button( btn_frame, text="手動(dòng)發(fā)送", command=self.send_data, bootstyle=OUTLINE + PRIMARY ).grid(row=0, column=2, padx=5, sticky="ew") def create_send_controls(self, parent): """發(fā)送區(qū)控件""" # 自動(dòng)發(fā)送設(shè)置 auto_frame = ttk.Frame(parent) auto_frame.pack(fill=X, pady=5) self.auto_var = BooleanVar() ttk.Checkbutton(auto_frame, text="自動(dòng)發(fā)送", variable=self.auto_var, command=self.toggle_auto_send).pack(side=LEFT) ttk.Label(auto_frame, text="間隔(ms):").pack(side=LEFT, padx=5) self.interval_entry = ttk.Entry(auto_frame, width=8) self.interval_entry.insert(0, "1000") self.interval_entry.pack(side=LEFT) # 發(fā)送內(nèi)容 self.send_text = ScrolledText(parent, height=4, autohide=True) self.send_text.pack(fill=BOTH, expand=True) # 綁定上下箭頭鍵用于歷史記錄導(dǎo)航 self.send_text.bind("<Up>", self.prev_history) self.send_text.bind("<Down>", self.next_history) def create_recv_controls(self, parent): """接收區(qū)控件""" # 接收顯示 self.recv_text = ScrolledText(parent, height=5, autohide=True) self.recv_text.pack(fill=BOTH, expand=True) # 統(tǒng)計(jì)欄 stat_frame = ttk.Frame(parent) stat_frame.pack(fill=X, pady=5) ttk.Label(stat_frame, text="發(fā)送:").pack(side=LEFT, padx=5) self.send_label = ttk.Label(stat_frame, text="0") self.send_label.pack(side=LEFT) ttk.Label(stat_frame, text="接收:").pack(side=LEFT, padx=10) self.recv_label = ttk.Label(stat_frame, text="0") self.recv_label.pack(side=LEFT) # 添加保存接收按鈕 ttk.Button(stat_frame, text="保存接收", command=self.save_received, bootstyle=OUTLINE + INFO).pack(side=RIGHT, padx=5) ttk.Button(stat_frame, text="清空", command=self.clear_received, bootstyle=OUTLINE + WARNING).pack(side=RIGHT) def refresh_ports(self): """刷新端口列表""" try: ports = [p.device for p in serial.tools.list_ports.comports()] self.port_cb['values'] = ports self.status_var.set(f"自動(dòng)檢測(cè)到主板有{len(ports)} 個(gè)串口可用,請(qǐng)注意選擇正確的。") self.last_port_count = len(ports) except Exception as e: print(f"Error refreshing ports: {e}") self.status_var.set(f"刷新端口時(shí)出錯(cuò): {e}") def check_ports_change(self): """檢查串口變化""" current_count = len(list(serial.tools.list_ports.comports())) if current_count != self.last_port_count: self.refresh_ports() self.master.after(1000, self.check_ports_change) # 每秒檢查一次 def toggle_connection(self): """切換連接狀態(tài)""" if self.serial_port and self.serial_port.is_open: self.close_serial() else: self.open_serial() def open_serial(self): """打開(kāi)串口""" try: port = self.port_cb.get() if not port: raise ValueError("請(qǐng)選擇串口") parity_map = { 'None': serial.PARITY_NONE, 'Even': serial.PARITY_EVEN, 'Odd': serial.PARITY_ODD, 'Mark': serial.PARITY_MARK, 'Space': serial.PARITY_SPACE } self.serial_port = serial.Serial( port=port, baudrate=int(self.baudrate_cb.get()), parity=parity_map[self.parity_cb.get()], bytesize=int(self.databits_cb.get()), stopbits=float(self.stopbits_cb.get()), timeout=0.1 ) self.conn_btn.configure(text="關(guān)閉串口", bootstyle=OUTLINE + SUCCESS) self.status_var.set(f"已連接 {port}") self.receive_thread_event.clear() # 清除事件標(biāo)志 self.receive_thread = threading.Thread(target=self.receive_worker, daemon=True) self.receive_thread.start() except Exception as e: Messagebox.show_error(f"主板上沒(méi)有這個(gè)串口或你選的被測(cè)端口跟主板端口不對(duì)應(yīng),請(qǐng)?jiān)谠O(shè)備管理器中確認(rèn)正確的端口: {str(e)}", "錯(cuò)誤") self.status_var.set("連接失敗") def close_serial(self): """關(guān)閉串口""" self.receive_thread_event.set() # 設(shè)置事件標(biāo)志,通知接收線(xiàn)程停止 if self.receive_thread and self.receive_thread.is_alive(): self.receive_thread.join() # 等待接收線(xiàn)程結(jié)束 if self.serial_port: try: self.serial_port.close() except Exception as e: print(f"關(guān)閉串口時(shí)出錯(cuò): {e}") self.conn_btn.configure(text="打開(kāi)串口", bootstyle=DANGER) self.status_var.set("已斷開(kāi)連接") def receive_worker(self): """接收線(xiàn)程工作函數(shù)""" while not self.receive_thread_event.is_set() and self.serial_port and self.serial_port.is_open: try: if self.serial_port.in_waiting > 0: data = self.serial_port.read(self.serial_port.in_waiting) self.receive_queue.put(data) except Exception as e: print(f"接收錯(cuò)誤: {e}") break def process_queue(self): """處理接收隊(duì)列""" while not self.receive_queue.empty(): data = self.receive_queue.get() self.display_received(data) self.receive_count += len(data) self.recv_label.configure(text=str(self.receive_count)) self.master.after(100, self.process_queue) def display_received(self, data): """顯示接收數(shù)據(jù)(帶時(shí)間戳)""" timestamp = time.strftime("[%Y-%m-%d %H:%M:%S] ", time.localtime()) try: text = data.decode('utf-8') self.recv_text.insert(END, timestamp + text + '\n') self.recv_text.see(END) except UnicodeDecodeError: self.recv_text.insert(END, timestamp + data.hex(' ') + '\n') self.recv_text.see(END) def toggle_auto_send(self): """切換自動(dòng)發(fā)送""" self.auto_send_flag = self.auto_var.get() if self.auto_send_flag: self.auto_send_task() def auto_send_task(self): """自動(dòng)發(fā)送任務(wù)""" if self.auto_send_flag and self.serial_port and self.serial_port.is_open: try: interval = int(self.interval_entry.get()) self.send_data() self.master.after(interval, self.auto_send_task) except ValueError: self.auto_var.set(False) Messagebox.show_error("無(wú)效的間隔時(shí)間", "錯(cuò)誤") def send_data(self): """發(fā)送數(shù)據(jù)""" if not self.serial_port or not self.serial_port.is_open: Messagebox.show_warning("請(qǐng)先打開(kāi)串口", "警告") return data = self.send_text.get(1.0, END).strip() if not data: return try: # 添加到歷史記錄 if data and (not self.send_history or data != self.send_history[0]): self.send_history.insert(0, data) if len(self.send_history) > 20: # 限制歷史記錄數(shù)量 self.send_history.pop() self.history_index = -1 # 重置歷史索引 self.serial_port.write(data.encode('utf-8')) self.send_count += len(data) self.send_label.configure(text=str(self.send_count)) # 顯示發(fā)送的數(shù)據(jù)(帶時(shí)間戳) timestamp = time.strftime("[%Y-%m-%d %H:%M:%S] ", time.localtime()) self.recv_text.insert(END, f"{timestamp}[Send] {data}\n") self.recv_text.see(END) except Exception as e: Messagebox.show_error(f"發(fā)送失敗: {str(e)}", "錯(cuò)誤") def prev_history(self, event): """上一條歷史記錄""" if self.send_history: if self.history_index < len(self.send_history) - 1: self.history_index += 1 self.send_text.delete(1.0, END) self.send_text.insert(END, self.send_history[self.history_index]) return "break" def next_history(self, event): """下一條歷史記錄""" if self.history_index > 0: self.history_index -= 1 self.send_text.delete(1.0, END) self.send_text.insert(END, self.send_history[self.history_index]) elif self.history_index == 0: self.history_index = -1 self.send_text.delete(1.0, END) return "break" def save_received(self): """保存接收內(nèi)容到文件""" filename = filedialog.asksaveasfilename( defaultextension=".txt", filetypes=[("Text Files", "*.txt"), ("All Files", "*.*")] ) if filename: try: with open(filename, 'w', encoding='utf-8') as f: f.write(self.recv_text.get(1.0, END)) self.status_var.set(f"接收內(nèi)容已保存到 {filename}") except Exception as e: Messagebox.show_error(f"保存文件失敗: {str(e)}", "錯(cuò)誤") def clear_received(self): """清空接收區(qū)""" self.recv_text.delete(1.0, END) self.receive_count = 0 self.recv_label.configure(text="0") self.send_text.delete(1.0, END) self.send_count = 0 self.send_label.configure(text="0") def on_closing(self): """安全關(guān)閉程序""" # 停止自動(dòng)發(fā)送循環(huán) self.auto_send_flag = False # 關(guān)閉串口連接 self.close_serial() # 確保完全退出 self.master.quit() # 終止mainloop self.master.destroy() # 銷(xiāo)毀所有Tkinter對(duì)象 self.master.after(500, self.force_exit) # 500ms后強(qiáng)制退出 def force_exit(self): """最終退出保障""" import os os._exit(0) # 強(qiáng)制終止進(jìn)程 if __name__ == "__main__": root = ttk.Window() app = SerialTool(root) root.protocol("WM_DELETE_WINDOW", app.on_closing) root.mainloop()
總結(jié)與擴(kuò)展
7.1 項(xiàng)目總結(jié)
- 采用生產(chǎn)者-消費(fèi)者模式處理串口數(shù)據(jù)
- 通過(guò)隊(duì)列實(shí)現(xiàn)線(xiàn)程間安全通信
- 現(xiàn)代化UI提升使用體驗(yàn)
7.2 擴(kuò)展方向
- 增加協(xié)議解析功能(Modbus/AT指令等)
- 實(shí)現(xiàn)數(shù)據(jù)圖表可視化
- 添加插件系統(tǒng)支持
以上就是利用Python調(diào)試串口的示例代碼的詳細(xì)內(nèi)容,更多關(guān)于Python調(diào)試串口的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Python自動(dòng)化之批量生成含指定數(shù)據(jù)的word文檔
在平時(shí)工作當(dāng)中,經(jīng)常需要處理文件,特別是Word,我們常常會(huì)機(jī)械的重復(fù)打開(kāi)、修改、保存文檔等一系列操作。本文將主要介紹如何通過(guò)Python批量生成含指定數(shù)據(jù)的word文檔,感興趣的同學(xué)可以來(lái)看一看2021-11-11Python技巧之四種多線(xiàn)程應(yīng)用分享
這篇文章主要介紹了Python中多線(xiàn)程的所有方式,包括使用threading模塊、使用concurrent.futures模塊、使用multiprocessing模塊以及使用asyncio模塊,希望對(duì)大家有所幫助2023-05-05Python Word實(shí)現(xiàn)批量替換文本并生成副本
這篇文章主要為大家詳細(xì)介紹了Python Word如何實(shí)現(xiàn)批量替換文本并生成副本,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-10-10使用Python創(chuàng)建多功能文件管理器的代碼示例
在本文中,我們將探索一個(gè)使用Python的wxPython庫(kù)開(kāi)發(fā)的文件管理器應(yīng)用程序,這個(gè)應(yīng)用程序不僅能夠?yàn)g覽和選擇文件,還支持文件預(yù)覽、壓縮、圖片轉(zhuǎn)換以及生成PPT演示文稿的功能,需要的朋友可以參考下2024-08-08關(guān)于Python Tkinter Button控件command傳參問(wèn)題的解決方式
這篇文章主要介紹了關(guān)于Python Tkinter Button控件command傳參問(wèn)題的解決方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-03-03python 3利用BeautifulSoup抓取div標(biāo)簽的方法示例
這篇文章主要介紹了python 3利用BeautifulSoup抓取div標(biāo)簽的方法,文中給出了詳細(xì)的示例代碼供大家參考學(xué)習(xí),對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起看看吧。2017-05-05