跨平台(Win系统、Linux)串口通信工具,内含数据图表可视化、数据分析,支持多语言

背景

        Linux设备如NVIDIA Jetson系列开发板(包括Jetson Nano、Jetson TX2、Jetson Xavier等)虽然具有强大的AI算力,能够运行复杂的计算机视觉算法和深度学习模型,但其GPIO接口功能相对有限。以Jetson Nano为例,其40pin扩展接口中只有部分管脚可用作GPIO,且驱动能力较弱(通常仅3.3V/4mA),难以直接驱动大功率外设如电机、继电器等。

相比之下,专用嵌入式MCU在这方面具有明显优势:

  1. STM32系列(如STM32F103)提供丰富的外设接口,包含多达112个GPIO,支持5V,驱动能力可达20mA
  2. Arduino开发板(如Arduino Mega2560)具备简易的编程环境和丰富的扩展库
  3. ESP32等物联网芯片还集成了WiFi/蓝牙无线功能

典型应用场景示例:

  • 智能小车:Jetson处理视觉识别,STM32控制电机驱动
  • 工业检测:Jetson运行缺陷检测算法,Arduino控制传感器阵列
  • 机器人系统:Jetson执行SLAM建图,ESP32负责物联网通信

为实现高效协同,串口通信(UART)是最可靠的解决方案,其优势包括:

  1. 硬件兼容性:几乎所有MCU都内置UART外设
  2. 协议简单:只需RX/TX两根数据线(可选RTS/CTS流控)
  3. 传输稳定:支持115200等常用波特率,误码率低
  4. 跨平台:Linux端可通过/dev/tty*设备文件操作

具体实现时建议采用以下配置:

  • 物理连接:使用3.3V电平的USB-TTL转换器(如CH340G)
  • 协议设计:定义包含帧头、长度、校验位的自定义协议
  • 错误处理:加入超时重传和CRC校验机制
  • 数据格式:JSON或Protocol Buffers等结构化数据格式

例如在ROS系统中,可通过rosserial软件包实现Jetson与Arduino的串口通信,将MCU作为ROS节点接入系统。这种架构既能发挥Linux设备的计算优势,又可利用嵌入式MCU的实时控制能力。

为了同时发挥出两者的能力需要将两者结合,而两者直接的通讯则可以通过串口通讯连接。

pyserial

PySerial 是一个 Python 库,用于通过串行接口与设备进行通信。它支持跨平台操作(Windows、Linux、MacOS等),提供了统一的接口来访问串行端口,常用于嵌入式设备、Arduino、工业设备、传感器等的通信。

可以通过终端输入下列指令安装:

pip install pyserial

编写下列代码完成通讯:

import serial
import time

ser = serial.Serial('/dev/ttyUSB0', 115200, timeout=0.1)
buffer = bytearray()

try:
    while True:
        data = ser.read(100)  # 每次最多读取100字节
        if data:
            buffer.extend(data)
            while b'\n' in buffer:
                line, _, buffer = buffer.partition(b'\n')
                print(line.decode('utf-8', errors='replace').strip())
        else:
            time.sleep(0.01)

except KeyboardInterrupt:
    print("\n程序终止")
finally:
    ser.close()  

其中ser = serial.Serial('/dev/ttyUSB0', 115200, timeout=0.1)当中的/dev/ttyUSB0代表端口,在win系统下通常是COM#,而在linux下通常是/dev/ttyUSB#。其中的115200代表了波特率

可视化界面

在调试过程当中需要有一个串口的可视化界面,常见的有PuTTY等,但是这些软件没有数据分析工具,需要一款可以自动数据分析读取,操作方便的跨平台的软件,因此编写程序。

首先先创建一个基础的GUI界面,本界面采用tkinter编写:

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import serial
import serial.tools.list_ports
import threading
import time
import csv
from datetime import datetime

class SerialMonitor(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Serial Monitor - Jetson")
        self.geometry("1000x600")
        
        self.serial_port = None
        self.running = False
        self.receive_buffer = bytearray()
        self.language = "中文"
        self.config_data = {
            "port": "/dev/ttyUSB0",
            "baudrate": 115200,
            "bytesize": 8,
            "parity": "None",
            "stopbits": 1,
            "flowcontrol": "None",
            "display": "ASCII",
            "autoline": True,
            "timestamp": True
        }
        
        self.create_menu()
        self.create_toolbar()
        self.create_config_panel()
        self.create_display_panel()
        self.load_default_config()

        self.detect_ports()

    def create_menu(self):
        menubar = tk.Menu(self)
        
        file_menu = tk.Menu(menubar, tearoff=0)
        file_menu.add_command(label="打开配置", command=self.load_config)
        file_menu.add_command(label="保存配置", command=self.save_config)
        file_menu.add_separator()
        file_menu.add_command(label="保存窗口数据", command=self.save_data)
        menubar.add_cascade(label="文件", menu=file_menu)
        
        edit_menu = tk.Menu(menubar, tearoff=0)
        edit_menu.add_command(label="开始", command=self.start_serial)
        edit_menu.add_command(label="停止", command=self.stop_serial)
        edit_menu.add_command(label="重置", command=self.reset_data)
        menubar.add_cascade(label="编辑", menu=edit_menu)
        
        view_menu = tk.Menu(menubar, tearoff=0)
        view_menu.add_radiobutton(label="中文", command=lambda: self.set_language("中文"))
        view_menu.add_radiobutton(label="English", command=lambda: self.set_language("English"))
        menubar.add_cascade(label="视图", menu=view_menu)
        
        tools_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="工具", menu=tools_menu)
        
        self.config(menu=menubar)

    def create_toolbar(self):
        toolbar = ttk.Frame(self)
        toolbar.pack(side=tk.TOP, fill=tk.X)
        
        self.start_btn = ttk.Button(toolbar, text="开始", command=self.start_serial)
        self.stop_btn = ttk.Button(toolbar, text="停止", command=self.stop_serial, state=tk.DISABLED)
        self.reset_btn = ttk.Button(toolbar, text="重置", command=self.reset_data)
        
        self.start_btn.pack(side=tk.LEFT, padx=2)
        self.stop_btn.pack(side=tk.LEFT, padx=2)
        self.reset_btn.pack(side=tk.LEFT, padx=2)

    def create_config_panel(self):
        config_frame = ttk.LabelFrame(self, text="串口配置")
        config_frame.pack(side=tk.LEFT, fill=tk.Y, padx=5, pady=5)
        
        ttk.Label(config_frame, text="COM端口").grid(row=0, column=0, sticky=tk.W)
        self.port_combo = ttk.Combobox(config_frame, width=15)
        self.port_combo.grid(row=0, column=1)
        
        ttk.Label(config_frame, text="波特率").grid(row=1, column=0, sticky=tk.W)
        self.baud_combo = ttk.Combobox(config_frame, values=[9600, 19200, 38400, 57600, 115200], width=15)
        self.baud_combo.grid(row=1, column=1)
        
        ttk.Label(config_frame, text="数据位").grid(row=2, column=0, sticky=tk.W)
        self.data_combo = ttk.Combobox(config_frame, values=[5,6,7,8], width=15)
        self.data_combo.grid(row=2, column=1)
        
        ttk.Label(config_frame, text="校验位").grid(row=3, column=0, sticky=tk.W)
        self.parity_combo = ttk.Combobox(config_frame, values=["None", "Even", "Odd", "Mark", "Space"], width=15)
        self.parity_combo.grid(row=3, column=1)
        
        ttk.Label(config_frame, text="停止位").grid(row=4, column=0, sticky=tk.W)
        self.stop_combo = ttk.Combobox(config_frame, values=[1, 1.5, 2], width=15)
        self.stop_combo.grid(row=4, column=1)
        
        ttk.Label(config_frame, text="流控").grid(row=5, column=0, sticky=tk.W)
        self.flow_combo = ttk.Combobox(config_frame, values=["None", "RTS/CTS"], width=15)
        self.flow_combo.grid(row=5, column=1)
        
        recv_frame = ttk.LabelFrame(config_frame, text="接收设置")
        recv_frame.grid(row=6, columnspan=2, sticky=tk.W)
        
        ttk.Label(recv_frame, text="显示方式").grid(row=0, column=0)
        self.disp_combo = ttk.Combobox(recv_frame, values=["ASCII", "HEX"], width=8)
        self.disp_combo.grid(row=0, column=1)
        
        self.autoline_var = tk.BooleanVar(value=True)
        self.timestamp_var = tk.BooleanVar(value=True)
        ttk.Checkbutton(recv_frame, text="自动换行", variable=self.autoline_var).grid(row=1, columnspan=2, sticky=tk.W)
        ttk.Checkbutton(recv_frame, text="显示时间", variable=self.timestamp_var).grid(row=2, columnspan=2, sticky=tk.W)

    def create_display_panel(self):
        display_frame = ttk.Frame(self)
        display_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5, pady=5)
        
        self.text_disp = tk.Text(display_frame, wrap=tk.NONE)
        scroll_y = ttk.Scrollbar(display_frame, orient=tk.VERTICAL, command=self.text_disp.yview)
        scroll_x = ttk.Scrollbar(display_frame, orient=tk.HORIZONTAL, command=self.text_disp.xview)
        
        self.text_disp.configure(yscrollcommand=scroll_y.set, xscrollcommand=scroll_x.set)
        
        self.text_disp.grid(row=0, column=0, sticky=tk.NSEW)
        scroll_y.grid(row=0, column=1, sticky=tk.NS)
        scroll_x.grid(row=1, column=0, sticky=tk.EW)
        
        display_frame.rowconfigure(0, weight=1)
        display_frame.columnconfigure(0, weight=1)

    def detect_ports(self):
        ports = [port.device for port in serial.tools.list_ports.comports()]
        self.port_combo['values'] = ports
        if ports:
            self.port_combo.current(0)

    def load_default_config(self):
        self.baud_combo.set(115200)
        self.data_combo.set(8)
        self.parity_combo.set("None")
        self.stop_combo.set(1)
        self.flow_combo.set("None")
        self.disp_combo.set("ASCII")

    def start_serial(self):
        try:
            self.serial_port = serial.Serial(
                port=self.port_combo.get(),
                baudrate=int(self.baud_combo.get()),
                bytesize=int(self.data_combo.get()),
                parity=self.parity_combo.get()[0],
                stopbits=float(self.stop_combo.get()),
                timeout=0.1
            )
            self.running = True
            self.start_btn.config(state=tk.DISABLED)
            self.stop_btn.config(state=tk.NORMAL)
            threading.Thread(target=self.read_serial, daemon=True).start()
        except Exception as e:
            messagebox.showerror("错误", f"串口打开失败: {str(e)}")

    def stop_serial(self):
        self.running = False
        if self.serial_port:
            self.serial_port.close()
        self.start_btn.config(state=tk.NORMAL)
        self.stop_btn.config(state=tk.DISABLED)

    def reset_data(self):
        self.text_disp.delete(1.0, tk.END)

    def read_serial(self):
        while self.running:
            try:
                data = self.serial_port.read(self.serial_port.in_waiting or 1)
                if data:
                    self.process_data(data)
            except Exception as e:
                print(f"读取错误: {str(e)}")
                break
            time.sleep(0.001)

    def process_data(self, data):
        display_mode = self.disp_combo.get()
        timestamp = datetime.now().strftime("[%H:%M:%S] ") if self.timestamp_var.get() else ""
        
        if display_mode == "HEX":
            hex_str = ' '.join(f'{b:02X}' for b in data)
            line = f"{timestamp}{hex_str}\n" if self.autoline_var.get() else hex_str + ' '
        else:
            text = data.decode(errors='replace')
            line = f"{timestamp}{text}" if self.autoline_var.get() else text
        
        self.text_disp.insert(tk.END, line)
        self.text_disp.see(tk.END)

    def save_data(self):
        filename = datetime.now().strftime("%Y%m%d_%H%M%S.txt")
        with open(filename, 'w') as f:
            f.write(self.text_disp.get(1.0, tk.END))
        messagebox.showinfo("保存成功", f"数据已保存为 {filename}")

    def load_config(self):
        filepath = filedialog.askopenfilename(filetypes=[("Config files", "*.csv")])
        if filepath:
            try:
                with open(filepath, newline='') as f:
                    reader = csv.DictReader(f)
                    for row in reader:
                        self.config_data.update(row)
                self.apply_config()
            except Exception as e:
                messagebox.showerror("错误", f"配置文件读取失败: {str(e)}")

    def save_config(self):
        filepath = filedialog.asksaveasfilename(defaultextension=".csv")
        if filepath:
            try:
                with open(filepath, 'w', newline='') as f:
                    writer = csv.DictWriter(f, fieldnames=self.config_data.keys())
                    writer.writeheader()
                    writer.writerow(self.config_data)
            except Exception as e:
                messagebox.showerror("错误", f"配置文件保存失败: {str(e)}")

    def set_language(self, lang):
        self.language = lang
        # 需要实现完整的语言切换功能
        messagebox.showinfo("提示", "语言切换功能待实现")

    def apply_config(self):
        self.port_combo.set(self.config_data['port'])
        self.baud_combo.set(self.config_data['baudrate'])
        self.data_combo.set(self.config_data['bytesize'])
        self.parity_combo.set(self.config_data['parity'])
        self.stop_combo.set(self.config_data['stopbits'])
        self.flow_combo.set(self.config_data['flowcontrol'])
        self.disp_combo.set(self.config_data['display'])
        self.autoline_var.set(self.config_data['autoline'])
        self.timestamp_var.set(self.config_data['timestamp'])

if __name__ == "__main__":
    app = SerialMonitor()
    app.mainloop()

该TK界面显示如下:

主要功能有:

  • 读取端口并选择波特率、数据位等串口基本参数;
  • 设置了三个按键开始、停止和重制能够打开串口读取数据并停止,还可以重置清除页面并重新读取;
  • 显示方式可以用ASCII显示,也可以Hex显示;
  • 设置了文件菜单点击内部包含三个选项可以读取当前串口配置的信息并导出保存为csv文件,也可以选择一个csv文件加载串口配置参数,还可以将当前窗口内所有已经显示的数据保存为当前日期.txt;
  • 编辑菜单栏包含了开始停止和重置按钮;
  • 视图设置了语言切换函数(但是函数内部还没编写);
  • 设置了工具函数用于加载数据分析函数(但是这个版本内部工具函数导入还没有添加);

但即便如此这个函数已经基本完成了串口调试的最小所需

但是仅仅此部分还不够,本身期望就是要有工具和语言适配性

工具函数

为了加载工具需要给原函数进行修改:

在SerialMonitor的初始化部分当中先创建对象

self.tools = {}
self.load_tools()

然后才能加载tools函数(以加载部分tools函数为例):

    def load_tools(self):
        tools_dir = os.path.join(os.path.dirname(__file__), "tools")
        
        if os.path.exists(tools_dir):
            for filename in os.listdir(tools_dir):
                if filename.endswith(".py") and not filename.startswith("_"):
                    try:
                        self.load_tool_file(os.path.join(tools_dir, filename))
                    except Exception as e:
                        print(f"Failed to Load Tool {filename} : {str(e)}")
        
        if hasattr(self, 'tools_menu'):
            self.update_tools_menu()

设置 run_tools函数执行这些脚本:

    def run_tool(self, tool_func):
        try:
            if not self.tools_menu.winfo_exists():
                raise RuntimeError("Tools menu not available")
                
            tool_func(self)  
        except Exception as e:
            messagebox.showerror(
                "Tool Error", 
                f"Tool execution failed:\n{str(e)}\n\n"
                f"Please check if the tool is properly configured."
            )

运行得到界面:

其中Tool函数是创建的一类工具函数,放在当前目录的Tools文件夹下,内部的所有py文件只要按照下列格式编写即可被调用:

import serial
# import 其他任何可用库 

TOOL_NAME = "将会在主界面Tools内部显示的名称"

class CustomGUI:

    def __init__(self, master):
        self.master = master
        self.master.title(TOOL_NAME)

    def Custom_function(self):
        pass

def run_tool(master):
    CustomGUI(master)

工具函数自己本身用python不会运行,只有在主函数当中才能被调用。可以在工具函数当中导入任何库,然后设置一个TOOL_NAME,自定义类名称,例如此处CustomGUI,内部包含初始化和其他功能函数,最后写上run_tool函数调用CustomGUI类,在主函数当中即可调用

波特率检测工具

例如我们可以编写一个波特率检测工具函数:

import serial
import time
import re
import tkinter as tk
from tkinter import ttk
import tkinter.scrolledtext as scrolledtext


TOOL_NAME = "Baudrate Detector"

class SerialBaudrateDetectorGUI:
    COMMON_BAUDRATES = [
        115200, 9600, 57600, 38400, 19200, 4800, 2400, 1200, 230400, 460800
    ]

    def __init__(self, master):
        self.master = master
        self.master.title(TOOL_NAME)

        # Port input frame
        frm = ttk.Frame(master)
        frm.pack(padx=10, pady=10, fill=tk.X)

        ttk.Label(frm, text="Serial Port:").pack(side=tk.LEFT)
        self.port_entry = ttk.Entry(frm, width=15)
        self.port_entry.pack(side=tk.LEFT, padx=5)
        self.port_entry.insert(0, "COM3")  # default value

        self.btn_detect = ttk.Button(frm, text="Start Detection", command=self.start_detection)
        self.btn_detect.pack(side=tk.LEFT, padx=5)

        # Text box for output log
        self.text = scrolledtext.ScrolledText(master, width=60, height=20)
        self.text.pack(padx=10, pady=10, fill=tk.BOTH, expand=True)

        self.baudrate_list = self.COMMON_BAUDRATES[:]
        self.timeout = 1.5
        self.max_bytes = 200

        self._detect_index = 0
        self._current_port = None

    def log(self, msg):
        self.text.insert(tk.END, msg + "\n")
        self.text.see(tk.END)

    def is_data_valid(self, data: bytes) -> bool:
        """Check if data contains valid text or numeric format."""
        try:
            text = data.decode("utf-8", errors="ignore")
            if not any(c.isalnum() or c in ":,.-_" for c in text):
                return False
            if re.search(r"[A-Za-z0-9_.-]+\s*[:=]\s*-?\d+(\.\d+)?", text):
                return True
            if len(text.strip()) > 10:
                return True
        except Exception:
            return False
        return False

    def detect_next_baud(self):
        if self._detect_index >= len(self.baudrate_list):
            self.log("Failed to detect valid baudrate. Data invalid or device not connected.")
            self.btn_detect.config(state=tk.NORMAL)
            return

        baud = self.baudrate_list[self._detect_index]
        self._detect_index += 1

        try:
            self.log(f"Trying baudrate {baud} ...")
            with serial.Serial(self._current_port, baudrate=baud, timeout=self.timeout) as ser:
                time.sleep(0.5)
                data = ser.read(self.max_bytes)
                if self.is_data_valid(data):
                    self.log(f"[✓] Detection succeeded: Baudrate {baud}")
                    self.btn_detect.config(state=tk.NORMAL)
                    return
                else:
                    self.log(f"[×] Baudrate {baud} invalid")
        except Exception as e:
            self.log(f"[!] Baudrate {baud} error: {e}")

        # Continue testing next baudrate, use after to avoid UI blocking
        self.master.after(100, self.detect_next_baud)

    def start_detection(self):
        port = self.port_entry.get().strip()
        if not port:
            self.log("Please enter a serial port.")
            return
        self._current_port = port
        self._detect_index = 0
        self.text.delete("1.0", tk.END)
        self.btn_detect.config(state=tk.DISABLED)
        self.detect_next_baud()


def run_tool(master):
    SerialBaudrateDetectorGUI(master)

将这个函数命名为任意名字(不包含中文和特殊字符即可)放到tools文件夹下

运行得到:

同样的还编写了其他工具函数

均值、标准差、最大最小以及中位数等数据的实时采集计算

核心串口数据解析和数据计算部分:

def parse_data(self):

        text = self.master.text_disp.get("1.0", "end-1c")
       
        pattern = r"\[\d+:\d+:\d+\.\d+\]\s+(.+?)(?=\[|$)"
        data_blocks = re.findall(pattern, text)
        
        current_time = time.time()
        for block in data_blocks:

            kv_pattern = r"([A-Za-z_]\w*):\s*(-?\d+\.?\d*)\b"
            for match in re.finditer(kv_pattern, block):
                channel, value = match.groups()
                try:
                    num = float(value)
                    self.data_store[channel].append( (current_time, num) )
                except ValueError:
                    continue


def calculate_stats(self, data):

        stats = {
            'mean': np.mean(data),
            'std': np.std(data),
            'min': np.min(data),
            'max': np.max(data),
        }
        
        if self.advanced_stats:
            stats.update({
                'median': np.median(data),
                'var': np.var(data),
                'cv': np.std(data)/np.mean(data) if np.mean(data) !=0 else 0
            })
        return stats

数据频率分析器

核心时间戳截取部分和频率计算函数

def parse_timestamps(self):

        text = self.master.text_disp.get("1.0", "end-1c")
        lines = text.strip().splitlines()
        timestamp_pattern = r"\[(\d+):(\d+):(\d+)\.(\d+)\]"

        extracted = []
        for line in lines:
            match = re.match(timestamp_pattern, line)
            if match:
                h, m, s, ms = map(int, match.groups())
                t = h * 3600 + m * 60 + s + ms / 1000.0
                extracted.append(t)
        return extracted

def compute_frequencies(self):

        current_time = time.time()
        raw_timestamps = self.parse_timestamps()

        self.timestamps = deque()
        for t in raw_timestamps:
            if not self.timestamps or t > self.timestamps[-1]:
                self.timestamps.append(t)

        freqs = []
        for i in range(1, len(self.timestamps)):
            delta = self.timestamps[i] - self.timestamps[i - 1]
            if 0 < delta < 10:
                freqs.append(1.0 / delta)

        now = self.timestamps[-1] if self.timestamps else 0
        valid_freqs = [
            f for i, f in enumerate(freqs)
            if (now - self.timestamps[i+1]) <= 30
        ]

        if valid_freqs:
            latest = valid_freqs[-1]
            f_max = max(valid_freqs)
            f_min = min(valid_freqs)
            self.display_label.config(
                text=(
                    f"Current Frequency: {latest:.2f} Hz    "
                    f"Max: {f_max:.2f} Hz    Min: {f_min:.2f} Hz"
                )
            )
        else:
            self.display_label.config(text="Frequency Tracker: no valid data")

数据可视化界面

核心串口数据解析和plot显示部分:

def parse_data(self, new_text):
        pattern = r"([A-Za-z]+):\s*(-?\d+\.?\d*)"
        matches = re.findall(pattern, new_text)

        current_time = time.time()

        for label, value in matches:
            num = float(value)

            if self.data[label]['color'] is None:
                self.data[label]['color'] = self.colors[self.current_color % len(self.colors)]
                self.current_color += 1

            self.data[label]['values'].append(num)
            self.data[label]['timestamps'].append(current_time)

            if self.live_tracking:
                self.data[label]['min'] = min(self.data[label]['min'], num)
                self.data[label]['max'] = max(self.data[label]['max'], num)
            else:
                self.data[label]['min'] = min(self.data[label]['min'], *self.data[label]['values'])
                self.data[label]['max'] = max(self.data[label]['max'], *self.data[label]['values'])


def update_plot(self, frame):
        if self.is_paused:
            return []

        full_text = self.master.text_disp.get("1.0", "end-1c")
        lines = full_text.splitlines()

        new_lines = lines[self.last_data_index:]
        new_text = "\n".join(new_lines)
        self.last_data_index = len(lines)

        self.parse_data(new_text)

        for line in self.lines.values():
            line[0].remove()
        self.lines.clear()

        current_time = time.time()
        time_min = current_time - self.time_window

        self.ax.clear()
        if self.split_var.get() and len(self.data) > 1:
            self.create_subplots()
        else:
            self.plot_combined()

        self.ax.set_xlabel('Time (s)')
        self.ax.set_ylabel('Value')
        self.ax.grid(True)

        return []

数据滤波和数值微积分(目前不稳定)

该函数采用了窗口滤波和数值微分(差分计算式)以及数值积分(梯形公式)作为核心计算的,但是效果不好。
 

串口发送功能

同样调用pyserial编写串口数据发送:

def send_data(self):

        if not self.serial_port or not self.serial_port.is_open:
            messagebox.showerror("Error", "Serial port is not open!")
            return
            
        data = self.send_entry.get()
        if not data:
            return
            
        try:
            if not data.endswith('\n'):
                data += '\n'
                
            self.serial_port.write(data.encode())
            self.send_entry.delete(0, tk.END)  
            

            timestamp = datetime.now().strftime("[%H:%M:%S] ") if self.timestamp_var.get() else ""
            self.text_disp.insert(tk.END, f"{timestamp}[SENT] {data}")
            self.text_disp.see(tk.END)
            
        except Exception as e:
            messagebox.showerror("Error", f"Failed to send data: {str(e)}")

该函数实现了串口数据发送功能,其工作流程如下:首先检测串口状态,然后从TKinter图形界面获取用户输入的发送数据,在数据末尾添加换行符后,最终将数据通过串口发送至连接设备。

语言拓展功能

为实现该程序的通用性增加语言切换功能,具体逻辑是创建一个语言字典,将页面的所有语言翻译为多种语言文本存在这个字典当中,默认是英语打开。同时还有一个语言更新函数当用户选择语言后直接刷新GUI界面的文本(文本从字典索引)。

首先创建一个多语言字典(内部语言还可以增加,目前支持中、英、德、日、法、意、俄、中繁、韩):

self.translations = {
    "Send": {"中文": "发送", "English": "Send", "Deutsch": "Senden", "日本語": "送信", "Français": "Envoyer", "Italiano": "Invia", "Русский": "Отправить", "中文(繁體)": "傳送", "한국어": "전송"},
    "Send:": {"中文": "发送:", "English": "Send:", "Deutsch": "Senden:", "日本語": "送信:", "Français": "Envoyer:", "Italiano": "Invia:", "Русский": "Отправить:", "中文(繁體)": "傳送:", "한국어": "전송:"},
    "⇱ file": {"中文": "⇱ 文件", "English": "⇱ file", "Deutsch": "⇱ Datei", "日本語": "⇱ ファイル", "Français": "⇱ fichier", "Italiano": "⇱ file", "Русский": "⇱ файл", "中文(繁體)": "⇱ 檔案", "한국어": "⇱ 파일"},
    "Open Configs": {"中文": "打开配置", "English": "Open Configs", "Deutsch": "Konfiguration öffnen", "日本語": "設定を開く", "Français": "Ouvrir les configurations", "Italiano": "Apri le configurazioni", "Русский": "Открыть конфигурации", "中文(繁體)": "開啟設定", "한국어": "설정 열기"},
    "Save Configs": {"中文": "保存配置", "English": "Save Configs", "Deutsch": "Konfiguration speichern", "日本語": "設定を保存", "Français": "Enregistrer les configurations", "Italiano": "Salva le configurazioni", "Русский": "Сохранить конфигурации", "中文(繁體)": "儲存設定", "한국어": "설정 저장"},
    "Save serial contents": {"中文": "保存串口内容", "English": "Save serial contents", "Deutsch": "Serielle Daten speichern", "日本語": "シリアル内容を保存", "Français": "Enregistrer les données série", "Italiano": "Salva i contenuti seriali", "Русский": "Сохранить содержимое последовательного порта", "中文(繁體)": "儲存串口內容", "한국어": "시리얼 내용 저장"},
    "✎ Edit": {"中文": "✎ 编辑", "English": "✎ Edit", "Deutsch": "✎ Bearbeiten", "日本語": "✎ 編集", "Français": "✎ Éditer", "Italiano": "✎ Modifica", "Русский": "✎ Редактировать", "中文(繁體)": "✎ 編輯", "한국어": "✎ 편집"},
    "Start ▶": {"中文": "开始 ▶", "English": "Start ▶", "Deutsch": "Start ▶", "日本語": "開始 ▶", "Français": "Démarrer ▶", "Italiano": "Avvia ▶", "Русский": "Старт ▶", "中文(繁體)": "開始 ▶", "한국어": "시작 ▶"},
    "Stop ||": {"中文": "停止 ||", "English": "Stop ||", "Deutsch": "Stopp ||", "日本語": "停止 ||", "Français": "Arrêter ||", "Italiano": "Ferma ||", "Русский": "Стоп ||", "中文(繁體)": "停止 ||", "한국어": "중지 ||"},
    "Reset ↺": {"中文": "重置 ↺", "English": "Reset ↺", "Deutsch": "Zurücksetzen ↺", "日本語": "リセット ↺", "Français": "Réinitialiser ↺", "Italiano": "Resetta ↺", "Русский": "Сброс ↺", "中文(繁體)": "重置 ↺", "한국어": "재설정 ↺"},
    "◉ View": {"中文": "◉ 视图", "English": "◉ View", "Deutsch": "◉ Ansicht", "日本語": "◉ 表示", "Français": "◉ Vue", "Italiano": "◉ Vista", "Русский": "◉ Вид", "中文(繁體)": "◉ 視圖", "한국어": "◉ 보기"},
    "⚙ Tools": {"中文": "⚙ 工具", "English": "⚙ Tools", "Deutsch": "⚙ Werkzeuge", "日本語": "⚙ ツール", "Français": "⚙ Outils", "Italiano": "⚙ Strumenti", "Русский": "⚙ Инструменты", "中文(繁體)": "⚙ 工具", "한국어": "⚙ 도구"},
    "(No tools found)": {"中文": "(未找到工具)", "English": "(No tools found)", "Deutsch": "(Keine Werkzeuge gefunden)", "日本語": "(ツールが見つかりません)", "Français": "(Aucun outil trouvé)", "Italiano": "(Nessuno strumento trovato)", "Русский": "(Инструменты не найдены)", "中文(繁體)": "(未找到工具)", "한국어": "(툴을 찾을 수 없음)"},
    "Serial Port Setting": {"中文": "串口设置", "English": "Serial Port Setting", "Deutsch": "Serieller Port Einstellungen", "日本語": "シリアルポート設定", "Français": "Paramètres du port série", "Italiano": "Impostazioni della porta seriale", "Русский": "Настройка последовательного порта", "中文(繁體)": "串口設定", "한국어": "시리얼 포트 설정"},
    "COM Port": {"中文": "端口号", "English": "COM Port", "Deutsch": "COM-Port", "日本語": "COMポート", "Français": "Port COM", "Italiano": "Porta COM", "Русский": "Порт COM", "中文(繁體)": "端口号", "한국어": "COM 포트"},
    "Baudrate": {"中文": "波特率", "English": "Baudrate", "Deutsch": "Baudrate", "日本語": "ボーレート", "Français": "Taux de bauds", "Italiano": "Velocità di trasmissione", "Русский": "Скорость передачи данных", "中文(繁體)": "波特率", "한국어": "波特레이트"},
    "Data Bits": {"中文": "数据位", "English": "Data Bits", "Deutsch": "Datenbits", "日本語": "データビット", "Français": "Bits de données", "Italiano": "Bit di dati", "Русский": "Биты данных", "中文(繁體)": "數據位", "한국어": "데이터 비트"},
    "Parity": {"中文": "校验位", "English": "Parity", "Deutsch": "Parität", "日本語": "パリティ", "Français": "Parité", "Italiano": "Parità", "Русский": "Паритет", "中文(繁體)": "校驗位", "한국어": "파리티"},
    "Stop Bits": {"中文": "停止位", "English": "Stop Bits", "Deutsch": "Stoppbits", "日本語": "ストップビット", "Français": "Bits d'arrêt", "Italiano": "Bit di arresto", "Русский": "Биты остановки", "中文(繁體)": "停止位", "한국어": "스톱 비트"},
    "Flow Control": {"中文": "流控制", "English": "Flow Control", "Deutsch": "Flusskontrolle", "日本語": "フローコントロール", "Français": "Contrôle de flux", "Italiano": "Controllo di flusso", "Русский": "Контроль потока", "中文(繁體)": "流控制", "한국어": "흐름 제어"},
    "Display Format": {"中文": "显示格式", "English": "Display Format", "Deutsch": "Anzeigeformat", "日本語": "表示形式", "Français": "Format d'affichage", "Italiano": "Formato di visualizzazione", "Русский": "Формат отображения", "中文(繁體)": "顯示格式", "한국어": "표시 형식"},
    "ASCII": {"中文": "ASCII", "English": "ASCII", "Deutsch": "ASCII", "日本語": "ASCII", "Français": "ASCII", "Italiano": "ASCII", "Русский": "ASCII", "中文(繁體)": "ASCII", "한국어": "ASCII"},
    "HEX": {"中文": "HEX", "English": "HEX", "Deutsch": "HEX", "日本語": "HEX", "Français": "HEX", "Italiano": "HEX", "Русский": "HEX", "中文(繁體)": "HEX", "한국어": "HEX"},
    "Auto Line Feed": {"中文": "自动换行", "English": "Auto Line Feed", "Deutsch": "Automatische Zeilenumbruch", "日本語": "自動改行", "Français": "Saut de ligne automatique", "Italiano": "Riga automatica", "Русский": "Автоматический перевод строки", "中文(繁體)": "自動換行", "한국어": "자동 줄바꿈"},
    "Timestamp": {"中文": "时间戳", "English": "Timestamp", "Deutsch": "Zeitstempel", "日本語": "タイムスタンプ", "Français": "Horodatage", "Italiano": "Timestamp", "Русский": "Время", "中文(繁體)": "時間戳", "한국어": "타임스탬프"},
    "Receive Setting": {"中文": "接收设置", "English": "Receive Setting", "Deutsch": "Empfangseinstellungen", "日本語": "受信設定", "Français": "Paramètres de réception", "Italiano": "Impostazioni di ricezione", "Русский": "Настройки приёма", "中文(繁體)": "接收設定", "한국어": "수신 설정"},
    "Display Method": {"中文": "显示方式", "English": "Display Method", "Deutsch": "Anzeigemethode", "日本語": "表示方法", "Français": "Méthode d'affichage", "Italiano": "Metodo di visualizzazione", "Русский": "Способ отображения", "中文(繁體)": "顯示方式", "한국어": "표시 방법"},
    "Auto Feed Line": {"中文": "自动换行", "English": "Auto Feed Line", "Deutsch": "Automatische Zeilenumbruch", "日本語": "自動改行", "Français": "Saut de ligne automatique", "Italiano": "Riga automatica", "Русский": "Автоматический перевод строки", "中文(繁體)": "自動換行", "한국어": "자동 줄바꿈"},
    "Display Time": {"中文": "显示时间", "English": "Display Time", "Deutsch": "Anzeigezeit", "日本語": "表示時間", "Français": "Temps d'affichage", "Italiano": "Visualizza tempo", "Русский": "Время отображения", "中文(繁體)": "顯示時間", "한국어": "표시 시간"},
    "Send Panel": {"中文": "发送面板", "English": "Send Panel", "Deutsch": "Sendepanel", "日本語": "送信パネル", "Français": "Panneau d'envoi", "Italiano": "Pannello di invio", "Русский": "Панель отправки", "中文(繁體)": "發送面板", "한국어": "송신 패널"},
    "Clear": {"中文": "清除", "English": "Clear", "Deutsch": "Löschen", "日本語": "クリア", "Français": "Effacer", "Italiano": "Cancella", "Русский": "Очистить", "中文(繁體)": "清除", "한국어": "지우기"},
    "Save": {"中文": "保存", "English": "Save", "Deutsch": "Speichern", "日本語": "保存", "Français": "Enregistrer", "Italiano": "Salva", "Русский": "Сохранить", "中文(繁體)": "保存", "한국어": "저장"},
    "Serial Monitor": {"中文": "串口监视器", "English": "Serial Monitor", "Deutsch": "Serieller Monitor", "日本語": "シリアルモニター", "Français": "Moniteur série", "Italiano": "Monitor seriale", "Русский": "Серийный монитор", "中文(繁體)": "串口監視器", "한국어": "시리얼 모니터"},
    "Help": {"中文": "ℹ 帮助", "English": "ℹ Help", "Deutsch": "ℹ Hilfe", "日本語": "ℹ ヘルプ", "Français": "ℹ Aide", "Italiano": "ℹ Aiuto", "Русский": "ℹ Помощь", "中文(繁體)": "ℹ 幫助", "한국어": "ℹ 도움말"},
    "About": {"中文": "关于", "English": "About", "Deutsch": "Über", "日本語": "バージョン情報", "Français": "À propos", "Italiano": "Informazioni", "Русский": "О программе", "中文(繁體)": "關於", "한국어": "정보"},
}

在菜单创建页面增加语言选项:

view_menu = tk.Menu(menubar, tearoff=0)
        view_menu.add_radiobutton(label="中文", command=lambda: self.set_language("中文"))
        view_menu.add_radiobutton(label="English", command=lambda: self.set_language("English"))
        view_menu.add_radiobutton(label="Deutsch", command=lambda: self.set_language("Deutsch"))
        view_menu.add_radiobutton(label="日本語", command=lambda: self.set_language("日本語"))
        view_menu.add_radiobutton(label="Français", command=lambda: self.set_language("Français"))
        view_menu.add_radiobutton(label="Italiano", command=lambda: self.set_language("Italiano"))
        view_menu.add_radiobutton(label="Русский", command=lambda: self.set_language("Русский"))
        view_menu.add_radiobutton(label="中文(繁體)", command=lambda: self.set_language("中文(繁體)"))
        view_menu.add_radiobutton(label="한국어", command=lambda: self.set_language("한국어"))
        menubar.add_cascade(label="◉ View", menu=view_menu)

编写语言设置函数:

def set_language(self, lang):
        self.language = lang

        menubar = self.nametowidget(self["menu"])

        file_menu = menubar.winfo_children()[0]
        edit_menu = menubar.winfo_children()[1]
        view_menu = menubar.winfo_children()[2]
        tools_menu = menubar.winfo_children()[3]
        about_menu = menubar.winfo_children()[4]

        menubar.delete(0, 'end')

        menubar.add_cascade(label=self.translations["⇱ file"][lang], menu=file_menu)
        menubar.add_cascade(label=self.translations["✎ Edit"][lang], menu=edit_menu)
        menubar.add_cascade(label=self.translations["◉ View"][lang], menu=view_menu)
        menubar.add_cascade(label=self.translations["⚙ Tools"][lang], menu=tools_menu)
        menubar.add_cascade(label=self.translations["About"][lang], menu=about_menu)

        file_menu.entryconfig(0, label=self.translations["Open Configs"][lang])
        file_menu.entryconfig(1, label=self.translations["Save Configs"][lang])
        file_menu.entryconfig(3, label=self.translations["Save serial contents"][lang])

        edit_menu.entryconfig(0, label=self.translations["Start ▶"][lang])
        edit_menu.entryconfig(1, label=self.translations["Stop ||"][lang])
        edit_menu.entryconfig(2, label=self.translations["Reset ↺"][lang])

        if not self.tools:
            tools_menu.entryconfig(0, label=self.translations["(No tools found)"][lang])

        self.send_btn.config(text=self.translations["Send"][lang])
        self.send_entry_label.config(text=self.translations["Send:"][lang])

        
        self.config_frame.config(text=self.translations["Serial Port Setting"][lang])
        self.label_port.config(text=self.translations["COM Port"][lang])
        self.label_baud.config(text=self.translations["Baudrate"][lang])
        self.label_data.config(text=self.translations["Data Bits"][lang])
        self.label_parity.config(text=self.translations["Parity"][lang])
        self.label_stop.config(text=self.translations["Stop Bits"][lang])
        self.label_flow.config(text=self.translations["Flow Control"][lang])
        self.recv_frame.config(text=self.translations["Receive Setting"][lang])
        self.label_display_method.config(text=self.translations["Display Method"][lang])
        self.chk_autoline.config(text=self.translations["Auto Feed Line"][lang])
        self.chk_timestamp.config(text=self.translations["Display Time"][lang])

        self.config(menu=menubar)

运行显示窗口:

效果如下:

Linux设备运行结果(树莓派为例)

资源链接

一款功能强大的跨平台串口调试工具,支持Windows/Linux/macOS系统,它提供实时串口数据收发、参数配置、包括多语言界面、数据记录导出,以及可扩展的工具插件系统,界面直观、操作简便资源-CSDN文库

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值