基于 Python + PyModbus + Tkinter 的可视化串口 Modbus 数据采集系统,专门用于读取风向、风速传感器的 Modbus RTU 数据,完成原始值解析、实际值换算,并提供直观的 UI 界面展示、状态监控和历史记录功能,完全适配实训场景的 Modbus 通信参数和数据转换要求。

from pymodbus.client import ModbusSerialClient
from pymodbus.payload import BinaryPayloadDecoder
from pymodbus.constants import Endian
from pymodbus.exceptions import ModbusException
import tkinter as tk
from tkinter import ttk, messagebox
from datetime import datetime
import threading
import time
class WindSensorGUI:
def __init__(self, root):
self.root = root
self.root.title("风向风速采集与转换系统")
self.root.geometry("800×600")
self.root.resizable(False, False)
# 设备配置参数(与实训手册一致)
self.config = {
"port": "COM100",
"baudrate": 9600,
"slave_address": 2,
"start_reg": 0,
"read_count": 8,
# 传感器量程参数(来自附件任务四)
"wind_dir_min": 0, # 风向最小量程(度)
"wind_dir_max": 360, # 风向最大量程(度)
"wind_spd_min": 0, # 风速最小量程(m/s)
"wind_spd_max": 70 # 风速最大量程(m/s)
}
# 状态变量
self.client = None
self.is_running = False
self采集_thread = None
# 构建UI界面
self.setup_ui()
def setup_ui(self):
# 1. 配置区域(上半部分)
config_frame = ttk.LabelFrame(self.root, text="设备配置", padding=(10, 5))
config_frame.pack(fill=tk.X, padx=20, pady=10)
# 串口配置
ttk.Label(config_frame, text="串口号:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
self.port_var = tk.StringVar(value=self.config["port"])
ttk.Entry(config_frame, textvariable=self.port_var, width=10).grid(row=0, column=1, padx=5, pady=5)
ttk.Label(config_frame, text="波特率:").grid(row=0, column=2, padx=5, pady=5, sticky=tk.W)
self.baud_var = tk.StringVar(value=self.config["baudrate"])
ttk.Entry(config_frame, textvariable=self.baud_var, width=10).grid(row=0, column=3, padx=5, pady=5)
ttk.Label(config_frame, text="从站地址:").grid(row=0, column=4, padx=5, pady=5, sticky=tk.W)
self.slave_var = tk.StringVar(value=self.config["slave_address"])
ttk.Entry(config_frame, textvariable=self.slave_var, width=10).grid(row=0, column=5, padx=5, pady=5)
# 连接/断开按钮
self.connect_btn = ttk.Button(config_frame, text="连接设备", command=self.toggle_connection)
self.connect_btn.grid(row=0, column=6, padx=20, pady=5)
# 2. 数据显示区域(中间部分)
data_frame = ttk.LabelFrame(self.root, text="实时数据", padding=(10, 5))
data_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=10)
# 数据标签
self.wind_dir_raw = tk.StringVar(value="–") # 风向原始值(16进制)
self.wind_dir_real = tk.StringVar(value="–") # 风向实际值(度)
self.wind_spd_raw = tk.StringVar(value="–") # 风速原始值(16进制)
self.wind_spd_real = tk.StringVar(value="–") # 风速实际值(m/s)
self.status_var = tk.StringVar(value="未连接")
# 风向显示
ttk.Label(data_frame, text="风向传感器(Vin0口):", font=("Arial", 12)).grid(row=0, column=0, padx=20, pady=30, sticky=tk.W)
ttk.Label(data_frame, text="原始值(16进制):", font=("Arial", 10)).grid(row=0, column=1, padx=10, pady=30)
ttk.Label(data_frame, textvariable=self.wind_dir_raw, font=("Arial", 10, "bold"), foreground="blue").grid(row=0, column=2, padx=10, pady=30)
ttk.Label(data_frame, text="实际值:", font=("Arial", 10)).grid(row=0, column=3, padx=10, pady=30)
ttk.Label(data_frame, textvariable=self.wind_dir_real, font=("Arial", 12, "bold"), foreground="red").grid(row=0, column=4, padx=10, pady=30)
ttk.Label(data_frame, text="度", font=("Arial", 10)).grid(row=0, column=5, padx=5, pady=30)
# 风速显示
ttk.Label(data_frame, text="风速传感器(Vin1口):", font=("Arial", 12)).grid(row=1, column=0, padx=20, pady=30, sticky=tk.W)
ttk.Label(data_frame, text="原始值(16进制):", font=("Arial", 10)).grid(row=1, column=1, padx=10, pady=30)
ttk.Label(data_frame, textvariable=self.wind_spd_raw, font=("Arial", 10, "bold"), foreground="blue").grid(row=1, column=2, padx=10, pady=30)
ttk.Label(data_frame, text="实际值:", font=("Arial", 10)).grid(row=1, column=3, padx=10, pady=30)
ttk.Label(data_frame, textvariable=self.wind_spd_real, font=("Arial", 12, "bold"), foreground="red").grid(row=1, column=4, padx=10, pady=30)
ttk.Label(data_frame, text="m/s", font=("Arial", 10)).grid(row=1, column=5, padx=5, pady=30)
# 状态显示
ttk.Label(data_frame, text="设备状态:", font=("Arial", 10)).grid(row=2, column=0, padx=20, pady=10, sticky=tk.W)
ttk.Label(data_frame, textvariable=self.status_var, font=("Arial", 10, "bold"), foreground="orange").grid(row=2, column=1, padx=10, pady=10)
# 3. 历史记录区域(下半部分)
log_frame = ttk.LabelFrame(self.root, text="历史记录", padding=(10, 5))
log_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=10)
# 滚动条
scrollbar = ttk.Scrollbar(log_frame)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 列表框
self.log_list = tk.Listbox(log_frame, yscrollcommand=scrollbar.set, font=("Arial", 10), height=8)
self.log_list.pack(fill=tk.BOTH, expand=True)
scrollbar.config(command=self.log_list.yview)
# 4. 控制按钮区域
btn_frame = ttk.Frame(self.root)
btn_frame.pack(fill=tk.X, padx=20, pady=10)
self.start_btn = ttk.Button(btn_frame, text="开始采集", command=self.start_collection, state=tk.DISABLED)
self.start_btn.pack(side=tk.LEFT, padx=10)
self.stop_btn = ttk.Button(btn_frame, text="停止采集", command=self.stop_collection, state=tk.DISABLED)
self.stop_btn.pack(side=tk.LEFT, padx=10)
ttk.Button(btn_frame, text="清空记录", command=self.clear_log).pack(side=tk.LEFT, padx=10)
ttk.Button(btn_frame, text="退出程序", command=self.exit_program).pack(side=tk.RIGHT, padx=10)
def toggle_connection(self):
"""连接/断开设备"""
if self.client and self.client.connected:
# 断开连接
self.client.close()
self.client = None
self.connect_btn.config(text="连接设备")
self.status_var.set("未连接")
self.start_btn.config(state=tk.DISABLED)
self.stop_btn.config(state=tk.DISABLED)
self.add_log("设备已断开连接")
else:
# 连接设备
try:
self.client = ModbusSerialClient(
port=self.port_var.get(),
baudrate=int(self.baud_var.get()),
parity="N",
stopbits=1,
bytesize=8,
timeout=3,
retries=1
)
if self.client.connect():
self.connect_btn.config(text="断开设备")
self.status_var.set("已连接")
self.start_btn.config(state=tk.NORMAL)
self.add_log(f"设备连接成功({self.port_var.get()})")
else:
raise ConnectionError("串口物理连接失败")
except Exception as e:
messagebox.showerror("连接失败", f"错误原因:{str(e)}\\n请检查:1.COM口是否正确 2.设备是否供电 3.串口是否被占用 4.RS485接线")
self.status_var.set("连接失败")
def convert_value(self, raw_value, min_range, max_range):
"""根据实训公式转换实际值:(最大量程-最小量程)/65535*模拟量值+最小量程"""
try:
return round(((max_range – min_range) / 65535) * raw_value + min_range, 2)
except Exception as e:
self.add_log(f"数据转换错误:{str(e)}")
return None
def collect_data(self):
"""数据采集线程函数"""
while self.is_running:
try:
if not self.client or not self.client.connected:
self.add_log("采集失败:设备已断开")
self.stop_collection()
break
# 读取Modbus寄存器(与实训指令一致:02 03 00 00 00 08)
response = self.client.read_holding_registers(
address=self.config["start_reg"],
count=self.config["read_count"],
slave=int(self.slave_var.get())
)
if response.isError():
self.add_log(f"Modbus错误:{response}")
time.sleep(1)
continue
# 解码数据(Endian配置与实训一致)
decoder = BinaryPayloadDecoder.fromRegisters(
response.registers,
byteorder=Endian.BIG,
wordorder=Endian.BIG
)
# 提取Vin0(风向)和Vin1(风速)数据(对应实训第4-7位)
# 按顺序解码8个16位数据,Vin0对应第1组,Vin1对应第2组
data_list = [decoder.decode_16bit_uint() for _ in range(8)]
wind_dir_raw = data_list[0] # Vin0口原始值
wind_spd_raw = data_list[1] # Vin1口原始值
# 转换为实际值(使用实训公式)
wind_dir_real = self.convert_value(
wind_dir_raw,
self.config["wind_dir_min"],
self.config["wind_dir_max"]
)
wind_spd_real = self.convert_value(
wind_spd_raw,
self.config["wind_spd_min"],
self.config["wind_spd_max"]
)
# 更新UI(线程安全)
self.root.after(0, self.update_data_display, wind_dir_raw, wind_dir_real, wind_spd_raw, wind_spd_real)
# 记录日志
self.add_log(f"[{datetime.now().strftime('%H:%M:%S')}] "
f"风向:0x{wind_dir_raw:04X} → {wind_dir_real}度 | "
f"风速:0x{wind_spd_raw:04X} → {wind_spd_real}m/s")
time.sleep(1) # 采集间隔1秒
except ModbusException as e:
self.add_log(f"Modbus通信失败:{str(e)}")
time.sleep(1)
except Exception as e:
self.add_log(f"采集异常:{str(e)}")
time.sleep(1)
def update_data_display(self, wind_dir_raw, wind_dir_real, wind_spd_raw, wind_spd_real):
"""更新UI数据显示(线程安全)"""
self.wind_dir_raw.set(f"0x{wind_dir_raw:04X}")
self.wind_spd_raw.set(f"0x{wind_spd_raw:04X}")
self.wind_dir_real.set(f"{wind_dir_real}" if wind_dir_real is not None else "错误")
self.wind_spd_real.set(f"{wind_spd_real}" if wind_spd_real is not None else "错误")
def start_collection(self):
"""开始采集"""
if not self.client or not self.client.connected:
messagebox.showwarning("警告", "请先连接设备!")
return
self.is_running = True
self.采集_thread = threading.Thread(target=self.collect_data, daemon=True)
self.采集_thread.start()
self.start_btn.config(state=tk.DISABLED)
self.stop_btn.config(state=tk.NORMAL)
self.status_var.set("正在采集")
self.add_log("开始数据采集…")
def stop_collection(self):
"""停止采集"""
self.is_running = False
if self.采集_thread:
self.采集_thread.join(timeout=2)
self.start_btn.config(state=tk.NORMAL)
self.stop_btn.config(state=tk.DISABLED)
self.status_var.set("已连接(未采集)")
self.add_log("停止数据采集")
def add_log(self, content):
"""添加历史记录"""
self.log_list.insert(tk.END, content)
self.log_list.see(tk.END) # 自动滚动到最后一行
def clear_log(self):
"""清空历史记录"""
self.log_list.delete(0, tk.END)
self.add_log("历史记录已清空")
def exit_program(self):
"""退出程序"""
if self.is_running:
self.stop_collection()
if self.client and self.client.connected:
self.client.close()
self.root.destroy()
if __name__ == "__main__":
# 检查依赖包
try:
import pymodbus
except ImportError:
print("缺少pymodbus库,请先安装:pip install pymodbus")
exit(1)
root = tk.Tk()
app = WindSensorGUI(root)
root.mainloop()
风向风速采集与转换系统 代码详细解释:
一、整体架构与核心依赖
1. 核心库作用
- pymodbus:Modbus 通信核心库,实现Modbus Serial/RTU客户端功能,完成寄存器读取、数据解码、异常处理;
- tkinter/ttk:Python 内置 GUI 库,构建可视化界面(配置区、数据显示区、日志区、控制区);
- threading:实现后台数据采集线程,避免采集阻塞 UI 界面刷新;
- datetime/time:实现日志时间戳、采集间隔控制;
- 其他:数据类型转换、异常捕获、界面交互辅助。
2. 程序整体流程
plaintext
配置串口参数 → 连接Modbus从站设备 → 启动后台采集线程 → 读取保持寄存器 → 解码16位原始值 → 按实训公式换算实际值 → 实时更新UI → 记录采集日志 → 支持停止/重新采集/断开设备
二、类与核心变量说明
程序采用 ** 面向对象(OOP)** 设计,核心为WindSensorGUI类,所有功能(UI 构建、通信、采集、解析、展示)均封装在类中,保证代码模块化和可维护性。
1. 初始化方法 __init__
类的入口方法,完成界面初始化、参数配置、状态变量定义三大核心工作:
python
运行
def __init__(self, root):
self.root = root # 主窗口对象
# 窗口基础设置:标题、大小、禁止缩放
self.root.title("风向风速采集与转换系统")
self.root.geometry("800×600")
self.root.resizable(False, False)
# 设备配置参数(与实训手册完全一致,直接修改即可适配不同设备)
self.config = {
"port": "COM100", # 串口号
"baudrate": 9600, # 波特率
"slave_address": 2, # Modbus从站地址
"start_reg": 0, # 起始寄存器地址
"read_count": 8, # 读取寄存器数量
# 传感器量程(实训任务四附件参数,换算实际值的核心依据)
"wind_dir_min": 0, "wind_dir_max": 360, # 风向:0-360度
"wind_spd_min": 0, "wind_spd_max": 70 # 风速:0-70m/s
}
# 状态变量:控制通信和采集流程
self.client = None # Modbus串口客户端对象
self.is_running = False # 采集状态标记(True=正在采集,False=停止)
self.采集_thread = None # 后台采集线程对象
# 构建UI界面(调用独立方法,解耦初始化逻辑)
self.setup_ui()
2. 核心状态变量说明
| self.client | ModbusSerialClient | 唯一的 Modbus 通信客户端,连接成功后初始化,断开后置为 None |
| self.is_running | 布尔值 | 采集线程的 “开关”,通过修改该值控制线程循环(无需强制终止线程,更安全) |
| self.采集_thread | 线程对象 | 后台采集线程,守护线程(主程序退出时自动销毁,避免内存泄漏) |
三、UI 界面构建 setup_ui
将界面分为4 个功能区域,通过ttk.LabelFrame做区域隔离,布局清晰、层次分明,适配实训场景的操作习惯,代码中通过grid(网格布局)实现控件精准定位,pack实现区域整体排布。
1. 4 大功能区域详解
(1)设备配置区(上半部分)
包含串口号、波特率、从站地址输入框和「连接设备 / 断开设备」按钮,支持实时修改配置参数,无需重启程序,输入框默认值为self.config中的配置,与实训手册一致。
(2)实时数据区(中间核心区)
展示风向、风速传感器的原始值(16 进制)和实际值(物理单位),其中:
- 原始值:蓝色粗体,直接展示传感器采集的 16 位无符号整数(16 进制格式,如0x1234);
- 实际值:红色粗体,按实训公式换算后的物理值(风向:度,风速:m/s);
- 额外显示设备状态(未连接 / 已连接 / 正在采集 / 连接失败),橙色粗体提示,直观反馈系统运行状态。
(3)历史记录区(下半部分)
包含带滚动条的列表框,自动记录设备连接 / 断开、采集开始 / 停止、数据采集结果、异常信息,并添加时间戳,支持自动滚动到最新记录,方便实训过程中的问题排查。
(4)控制按钮区(最底部)
包含「开始采集、停止采集、清空记录、退出程序」按钮,按钮状态会根据系统状态自动禁用 / 启用(如未连接设备时,开始采集按钮禁用),避免误操作。
2. 界面变量说明
通过tk.StringVar定义动态绑定的界面变量,实现数据的 “实时刷新”,无需手动更新控件内容,只需修改变量值,界面会自动同步:
python
运行
# 风向原始值(16进制)、实际值(度)
self.wind_dir_raw = tk.StringVar(value="–")
self.wind_dir_real = tk.StringVar(value="–")
# 风速原始值(16进制)、实际值(m/s)
self.wind_spd_raw = tk.StringVar(value="–")
self.wind_spd_real = tk.StringVar(value="–")
# 设备状态
self.status_var = tk.StringVar(value="未连接")
四、Modbus 设备连接与断开 toggle_connection
核心通信控制方法,实现连接 / 断开的一键切换,包含完善的异常处理和用户提示,适配实训中常见的串口连接问题。
1. 核心逻辑
- 断开逻辑:如果客户端已连接,直接关闭连接,重置客户端对象,更新按钮文字、设备状态,禁用采集按钮,并记录日志;
- 连接逻辑:通过用户输入的参数初始化ModbusSerialClient,配置标准 Modbus RTU 参数(无奇偶校验、1 位停止位、8 位数据位,与实训一致),尝试建立连接,根据连接结果分情况处理。
2. 关键代码与参数说明
python
运行
def toggle_connection(self):
if self.client and self.client.connected:
# 断开连接流程
self.client.close()
self.client = None
self.connect_btn.config(text="连接设备")
self.status_var.set("未连接")
self.start_btn.config(state=tk.DISABLED)
self.stop_btn.config(state=tk.DISABLED)
self.add_log("设备已断开连接")
else:
# 连接设备流程
try:
# 初始化Modbus串口客户端,配置标准RTU参数
self.client = ModbusSerialClient(
port=self.port_var.get(), # 串口号(用户输入)
baudrate=int(self.baud_var.get()), # 波特率(转整数)
parity="N", # 无奇偶校验(Modbus RTU标准)
stopbits=1, # 1位停止位
bytesize=8, # 8位数据位
timeout=3, # 通信超时时间3秒
retries=1 # 通信失败重试1次
)
# 尝试建立连接
if self.client.connect():
# 连接成功:更新界面、启用采集按钮、记录日志
self.connect_btn.config(text="断开设备")
self.status_var.set("已连接")
self.start_btn.config(state=tk.NORMAL)
self.add_log(f"设备连接成功({self.port_var.get()})")
else:
raise ConnectionError("串口物理连接失败")
except Exception as e:
# 连接失败:弹窗提示详细错误,给出排查方向(实训中高频问题)
messagebox.showerror("连接失败", f"错误原因:{str(e)}\\n请检查:1.COM口是否正确 2.设备是否供电 3.串口是否被占用 4.RS485接线")
self.status_var.set("连接失败")
3. 异常处理亮点
连接失败时,弹窗直接给出4 个核心排查方向,完全适配实训场景的常见问题(如串口号输错、RS485 接线反、串口被其他软件占用、设备未供电),降低调试难度。
五、数据核心处理:解码与换算
1. 原始值解码 collect_data 中解码部分
传感器采集的原始数据为16 位无符号整数,存储在 Modbus 保持寄存器中,通过pymodbus的BinaryPayloadDecoder完成解码,解码参数与实训一致:
python
运行
# 解码数据(Endian配置与实训手册完全一致,保证解码正确性)
decoder = BinaryPayloadDecoder.fromRegisters(
response.registers, # 读取到的寄存器列表
byteorder=Endian.BIG, # 字节序:大端
wordorder=Endian.BIG # 字序:大端
)
# 按顺序解码8个16位无符号整数(与read_count=8对应)
data_list = [decoder.decode_16bit_uint() for _ in range(8)]
# 提取指定通道数据:Vin0(风向)=第1个值,Vin1(风速)=第2个值(实训指定)
wind_dir_raw = data_list[0] # 风向原始值(16位无符号整数)
wind_spd_raw = data_list[1] # 风速原始值(16位无符号整数)
- 解码关键:byteorder和wordorder必须与实训手册一致(均为大端Endian.BIG),否则会出现解码错误;
- 通道对应:风向传感器接 Vin0 口,对应解码后第 1 个值;风速传感器接 Vin1 口,对应第 2 个值,与实训硬件接线一致。
2. 实际值换算 convert_value
完全遵循实训指定公式:实际值 = (最大量程-最小量程)/65535 * 原始值 + 最小量程,实现原始 16 位整数(0-65535)到物理单位的线性换算,是整个程序的核心业务逻辑。
方法代码与说明
python
运行
def convert_value(self, raw_value, min_range, max_range):
"""
根据实训公式转换实际值
:param raw_value: 传感器原始16位无符号整数(0-65535)
:param min_range: 传感器量程最小值
:param max_range: 传感器量程最大值
:return: 换算后的实际值(保留2位小数),异常返回None
"""
try:
# 实训标准公式,线性换算
real_value = ((max_range – min_range) / 65535) * raw_value + min_range
return round(real_value, 2) # 保留2位小数,提升展示可读性
except Exception as e:
self.add_log(f"数据转换错误:{str(e)}")
return None
调用示例(风向 / 风速换算)
python
运行
# 风向换算:量程0-360度
wind_dir_real = self.convert_value(wind_dir_raw, self.config["wind_dir_min"], self.config["wind_dir_max"])
# 风速换算:量程0-70m/s
wind_spd_real = self.convert_value(wind_spd_raw, self.config["wind_spd_min"], self.config["wind_spd_max"])
- 公式依据:16 位 AD 采集的原始值范围是 0-65535,对应传感器的满量程,因此通过线性插值完成换算;
- 鲁棒性:包含异常捕获,避免换算过程中出现除零、类型错误等问题,异常时记录日志并返回 None,界面显示 “错误”,不影响程序整体运行。
六、后台数据采集:线程化设计
1. 为什么需要独立采集线程?
Tkinter 的 UI 界面运行在主线程中,如果直接在主线程中执行数据采集(包含time.sleep(1)的延时和 Modbus 通信的阻塞),会导致UI 界面卡死、无法响应操作(如点击按钮、关闭窗口),这是 GUI 编程的经典问题。
2. 采集线程实现 collect_data + start_collection
(1)采集线程函数 collect_data
后台执行的核心函数,通过while self.is_running实现循环采集,只有当self.is_running=False时,循环才会退出,线程正常结束:
python
运行
def collect_data(self):
"""数据采集线程函数(后台运行,不阻塞UI)"""
while self.is_running: # 采集开关:由self.is_running控制
try:
# 检查设备连接状态,断开则自动停止采集
if not self.client or not self.client.connected:
self.add_log("采集失败:设备已断开")
self.stop_collection()
break
# 读取Modbus保持寄存器(与实训指令一致:02 03 00 00 00 08)
# 02=从站地址,03=读保持寄存器,00 00=起始地址,00 08=读取数量
response = self.client.read_holding_registers(
address=self.config["start_reg"], # 起始地址0
count=self.config["read_count"], # 读取数量8
slave=int(self.slave_var.get()) # 从站地址2
)
# 检查Modbus响应是否错误
if response.isError():
self.add_log(f"Modbus错误:{response}")
time.sleep(1)
continue
# 解码→换算→更新UI→记录日志(核心流程)
… # 前文已详解,此处省略
time.sleep(1) # 采集间隔1秒,可根据实训要求修改
# 捕获Modbus通信专属异常
except ModbusException as e:
self.add_log(f"Modbus通信失败:{str(e)}")
time.sleep(1)
# 捕获其他所有异常,保证线程不崩溃
except Exception as e:
self.add_log(f"采集异常:{str(e)}")
time.sleep(1)
(2)启动采集 start_collection
初始化并启动守护线程,设置采集开关为True,更新界面按钮状态和设备状态:
python
运行
def start_collection(self):
if not self.client or not self.client.connected:
messagebox.showwarning("警告", "请先连接设备!")
return
self.is_running = True # 打开采集开关
# 创建守护线程:target=采集函数,daemon=True(主程序退出时自动销毁)
self.采集_thread = threading.Thread(target=self.collect_data, daemon=True)
self.采集_thread.start() # 启动线程
# 更新界面:禁用开始,启用停止,修改状态,记录日志
self.start_btn.config(state=tk.DISABLED)
self.stop_btn.config(state=tk.NORMAL)
self.status_var.set("正在采集")
self.add_log("开始数据采集…")
3. 停止采集 stop_collection
通过修改采集开关实现线程安全停止,无需强制终止线程,避免资源泄漏:
python
运行
def stop_collection(self):
self.is_running = False # 关闭采集开关,线程循环自然退出
if self.采集_thread:
self.采集_thread.join(timeout=2) # 等待线程结束,超时2秒
# 更新界面:启用开始,禁用停止,修改状态,记录日志
self.start_btn.config(state=tk.NORMAL)
self.stop_btn.config(state=tk.DISABLED)
self.status_var.set("已连接(未采集)")
self.add_log("停止数据采集")
4. 线程安全的 UI 更新 update_data_display
Tkinter 的 UI 控件只能在主线程中更新,如果直接在后台采集线程中修改StringVar变量,会导致程序崩溃或界面异常。
解决方案:使用 Tkinter 提供的 **after(0, 函数, 参数)方法,将 UI 更新操作提交到主线程的事件循环 ** 中执行,实现线程安全的 UI 刷新:
python
运行
def update_data_display(self, wind_dir_raw, wind_dir_real, wind_spd_raw, wind_spd_real):
"""更新UI数据显示(线程安全,仅在主线程执行)"""
self.wind_dir_raw.set(f"0x{wind_dir_raw:04X}") # 16进制格式化,补零到4位
self.wind_spd_raw.set(f"0x{wind_spd_raw:04X}")
self.wind_dir_real.set(f"{wind_dir_real}" if wind_dir_real is not None else "错误")
self.wind_spd_real.set(f"{wind_spd_real}" if wind_spd_real is not None else "错误")
# 采集线程中调用(提交到主线程)
self.root.after(0, self.update_data_display, wind_dir_raw, wind_dir_real, wind_spd_raw, wind_spd_real)
- after(0, …):0 表示 “立即” 将操作加入主线程事件循环,等待主线程空闲时执行;
- 16 进制格式化:0x{raw_value:04X} 保证原始值以 4 位 16 进制展示(如原始值 10→0x000A),格式统一,符合实训展示要求。
七、辅助功能方法
1. 日志管理:add_log + clear_log
- add_log:向历史记录列表框添加内容,自动滚动到最后一行,保证始终看到最新日志;
- clear_log:清空所有历史记录,并添加 “历史记录已清空” 的提示,方便实训过程中重新记录。
2. 程序退出:exit_program
实现优雅退出,避免强制关闭导致的资源泄漏:
python
运行
def exit_program(self):
if self.is_running:
self.stop_collection() # 先停止采集
if self.client and self.client.connected:
self.client.close() # 再断开设备连接
self.root.destroy() # 最后销毁主窗口,退出程序
八、程序入口与依赖检查
python
运行
if __name__ == "__main__":
# 检查核心依赖pymodbus是否安装,未安装则提示并退出
try:
import pymodbus
except ImportError:
print("缺少pymodbus库,请先安装:pip install pymodbus")
exit(1)
# 初始化Tkinter主窗口,创建应用对象,启动界面事件循环
root = tk.Tk()
app = WindSensorGUI(root)
root.mainloop()
- 依赖检查:避免因缺少pymodbus导致程序直接崩溃,给出明确的安装命令;
- 事件循环:root.mainloop() 是 Tkinter 的核心,启动后界面开始响应用户操作,程序进入运行状态。
九、实训适配与使用注意事项
1. 快速适配实训环境
只需修改__init__中的self.config配置,无需修改其他代码:
python
运行
self.config = {
"port": "COM3", # 改为你的实训设备串口号(如COM3、COM4)
"baudrate": 9600, # 实训设备波特率(一般为9600,按手册修改)
"slave_address": 2,# 实训Modbus从站地址(按手册修改)
# 传感器量程按实训任务四附件修改,默认0-360度(风向)、0-70m/s(风速)
}





