目录
- 项目背景与需求分析
- 开发环境搭建(Windows/Linux)
- Python 基础语法速成
- 串口数据通信原理与实现
- 多设备 CSV 文件管理系统
- 数据分析核心技术
- 可视化图表设计与实现
- 完整项目架构与代码
- 部署与运行指南
- 故障排除与优化
- 进阶学习路径
8. 完整项目架构与代码
8.1 项目整体架构
本项目采用模块化设计,各模块职责清晰,便于维护和扩展。完整架构如下:
plaintext
电机测试数据分析系统
├── 数据采集层
│ └── 串口通信模块 (serial_communication.py)
│ ├── 串口初始化与配置
│ ├── 数据接收与解析
│ └── 实时数据缓存管理
│
├── 数据存储层
│ └── CSV文件管理模块 (csv_management.py)
│ ├── 多设备文件组织
│ ├── 定时文件生成
│ ├── 数据写入与格式转换
│ └── 历史数据读取
│
├── 数据分析层
│ └── 数据分析模块 (data_analysis.py)
│ ├── 基本统计分析
│ ├── 时间序列分析
│ ├── 异常检测
│ ├── 状态转换分析
│ └── 多电机对比分析
│
├── 可视化层
│ └── 可视化模块 (visualization.py)
│ ├── 实时数据监控图表
│ ├── 历史趋势分析图表
│ ├── 统计数据图表
│ └── 多电机对比图表
│
└── 应用层
├── 主程序 (main.py)
├── 测试脚本
└── 用户配置
8.2 主程序实现
创建main.py作为项目的主入口:
电机测试数据分析系统主程序
V1
main.py
import time
import threading
import argparse
from datetime import datetime
import os
# 导入项目模块
from src.serial_communication import init_serial, read_serial_data, parse_data, MotorDataProcessor
from src.csv_management import CSVFileManager
from src.data_analysis import basic_statistics, detect_anomalies
from src.visualization import (plot_current_vs_time, plot_encoder_vs_time,
plot_multiple_parameters, plot_anomalies)
class MotorTestSystem:
"""电机测试数据分析系统主类"""
def __init__(self, config):
"""
初始化系统
参数:
config: 系统配置字典
"""
self.config = config
self.running = False
self.data_processor = MotorDataProcessor(buffer_size=config.get('buffer_size', 1000))
self.csv_manager = CSVFileManager(
base_dir=config.get('data_dir', 'data'),
interval_minutes=config.get('file_interval', 10),
max_files_per_motor=config.get('max_files', 100)
)
self.serial = None
self.stats = {} # 存储各电机的统计信息
self.recent_anomalies = {} # 存储最近的异常值
# 创建保存图表的目录
self.plot_dir = config.get('plot_dir', 'plots')
if not os.path.exists(self.plot_dir):
os.makedirs(self.plot_dir)
print("电机测试数据分析系统初始化完成")
def connect_serial(self):
"""连接到串口设备"""
try:
self.serial = init_serial(
port=self.config.get('serial_port', 'COM3'),
baudrate=self.config.get('baudrate', 115200)
)
return self.serial is not None
except Exception as e:
print(f"连接串口失败:{e}")
return False
def data_collector(self):
"""数据采集线程函数"""
print("数据采集线程启动")
while self.running and self.serial and self.serial.is_open:
try:
# 读取串口数据
data_str = read_serial_data(self.serial)
if data_str:
# 解析数据
data = parse_data(data_str)
if data:
motor_id = data['motor_id']
# 添加到数据处理器
self.data_processor.add_data(data)
# 写入CSV文件
# 转换datetime为字符串,便于CSV存储
data_for_csv = data.copy()
data_for_csv['timestamp'] = data_for_csv['timestamp'].strftime("%Y-%m-%d %H:%M:%S")
self.csv_manager.write_data(data_for_csv)
# 定期计算统计信息
current_time = time.time()
if (motor_id not in self.stats or
current_time - self.stats[motor_id].get('last_update', 0) >
self.config.get('stats_interval', 60)):
# 计算统计信息
stats = self.data_processor.get_statistics(motor_id)
if stats:
self.stats[motor_id] = {
'stats': stats,
'last_update': current_time
}
print(f"\n电机 {motor_id} 统计更新:")
print(f"平均电流:{stats['avg_current']:.2f}A,最大电流:{stats['max_current']:.2f}A")
print(f"总行程:{stats['total_distance']:.2f},样本数:{stats['sample_count']}")
# 检查异常值(简单检查)
if data['current'] > self.config.get('current_threshold', 4.0):
if motor_id not in self.recent_anomalies:
self.recent_anomalies[motor_id] = []
# 只保留最近的异常值
if len(self.recent_anomalies[motor_id]) >= 10:
self.recent_anomalies[motor_id].pop(0)
self.recent_anomalies[motor_id].append(data)
print(f"警告:电机 {motor_id} 电流过高 - {data['current']:.2f}A")
except Exception as e:
print(f"数据采集错误:{e}")
# 短暂延迟,避免出错时CPU占用过高
time.sleep(1)
# 短暂延迟,降低CPU占用
time.sleep(0.01)
print("数据采集线程结束")
def data_analyzer(self):
"""数据分析线程函数"""
print("数据分析线程启动")
while self.running:
# 等待一段时间再进行分析
time.sleep(self.config.get('analysis_interval', 300)) # 默认5分钟
if not self.running:
break
# 获取所有电机ID
motor_ids = self.data_processor.get_all_motor_ids()
for motor_id in motor_ids:
try:
print(f"\n开始分析电机 {motor_id} 的数据...")
# 从CSV加载该电机的所有数据
motor_df = self.csv_manager.load_motor_dataframe(motor_id)
if motor_df is not None and not motor_df.empty:
# 基本统计分析
stats = basic_statistics(motor_df)
# 异常检测
anomalies = detect_anomalies(motor_df, threshold=3.0)
# 生成图表
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# 电流随时间变化图
current_plot_path = os.path.join(
self.plot_dir,
f"motor_{motor_id}_current_{timestamp}.png"
)
plot_current_vs_time(motor_df, motor_id, save_path=current_plot_path)
# 编码器行程随时间变化图
encoder_plot_path = os.path.join(
self.plot_dir,
f"motor_{motor_id}_encoder_{timestamp}.png"
)
plot_encoder_vs_time(motor_df, motor_id, save_path=encoder_plot_path)
# 多参数对比图
multi_param_plot_path = os.path.join(
self.plot_dir,
f"motor_{motor_id}_multi_param_{timestamp}.png"
)
plot_multiple_parameters(motor_df, motor_id, save_path=multi_param_plot_path)
# 异常值图(如果有异常值)
if anomalies is not None and not anomalies.empty:
anomaly_plot_path = os.path.join(
self.plot_dir,
f"motor_{motor_id}_anomalies_{timestamp}.png"
)
plot_anomalies(motor_df, anomalies, motor_id, save_path=anomaly_plot_path)
except Exception as e:
print(f"分析电机 {motor_id} 时出错:{e}")
print("数据分析线程结束")
def start(self):
"""启动系统"""
if self.running:
print("系统已在运行中")
return
# 连接串口
if not self.connect_serial():
print("无法连接到串口,系统启动失败")
return
self.running = True
# 启动数据采集线程
self.collector_thread = threading.Thread(target=self.data_collector, daemon=True)
self.collector_thread.start()
# 启动数据分析线程
self.analyzer_thread = threading.Thread(target=self.data_analyzer, daemon=True)
self.analyzer_thread.start()
print("系统启动成功,开始采集和分析数据...")
try:
# 主线程等待用户输入退出命令
while self.running:
cmd = input("输入 'exit' 停止系统:")
if cmd.strip().lower() == 'exit':
self.stop()
break
time.sleep(1)
except KeyboardInterrupt:
print("\n用户中断,停止系统...")
self.stop()
def stop(self):
"""停止系统"""
if not self.running:
return
print("正在停止系统...")
self.running = False
# 等待子线程结束
if hasattr(self, 'collector_thread') and self.collector_thread.is_alive():
self.collector_thread.join(timeout=5)
if hasattr(self, 'analyzer_thread') and self.analyzer_thread.is_alive():
self.analyzer_thread.join(timeout=5)
# 关闭CSV文件
self.csv_manager.close_all_files()
# 关闭串口
if self.serial and self.serial.is_open:
self.serial.close()
print("串口已关闭")
print("系统已停止")
def main():
"""主函数"""
# 解析命令行参数
parser = argparse.ArgumentParser(description='电机测试数据分析系统')
parser.add_argument('--port', type=str, default='COM3', help='串口号,如COM3或/dev/ttyUSB0')
parser.add_argument('--baudrate', type=int, default=115200, help='波特率,默认115200')
parser.add_argument('--data-dir', type=str, default='data', help='数据存储目录')
parser.add_argument('--plot-dir', type=str, default='plots', help='图表存储目录')
parser.add_argument('--file-interval', type=int, default=10, help='生成新文件的时间间隔(分钟)')
parser.add_argument('--max-files', type=int, default=100, help='每个电机最多保留的文件数')
parser.add_argument('--buffer-size', type=int, default=1000, help='数据缓冲区大小')
parser.add_argument('--stats-interval', type=int, default=60, help='统计信息更新间隔(秒)')
parser.add_argument('--analysis-interval', type=int, default=300, help='数据分析间隔(秒)')
parser.add_argument('--current-threshold', type=float, default=4.0, help='电流异常阈值(A)')
args = parser.parse_args()
# 创建配置字典
config = {
'serial_port': args.port,
'baudrate': args.baudrate,
'data_dir': args.data_dir,
'plot_dir': args.plot_dir,
'file_interval': args.file_interval,
'max_files': args.max_files,
'buffer_size': args.buffer_size,
'stats_interval': args.stats_interval,
'analysis_interval': args.analysis_interval,
'current_threshold': args.current_threshold
}
# 创建并启动系统
system = MotorTestSystem(config)
system.start()
if __name__ == "__main__":
main()
8.3 各模块完整代码
8.3.1 串口通信模块(完整代码)
串口通信模块完整代码
V1
serial_communication.py
import serial
import time
import datetime
def init_serial(port='COM3', baudrate=115200):
"""
初始化串口通信
参数:
port: 串口号,Windows通常为'COMx',Linux为'/dev/ttyUSBx'
baudrate: 波特率
返回:
初始化成功的串口对象,失败则返回None
"""
try:
# 创建串口对象
ser = serial.Serial(
port=port,
baudrate=baudrate,
timeout=1,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS
)
# 检查串口是否成功打开
if ser.is_open:
print(f"串口 {port} 已打开,波特率:{baudrate}")
return ser
else:
print(f"无法打开串口 {port}")
return None
except Exception as e:
print(f"串口初始化错误:{e}")
return None
def read_serial_data(ser):
"""
从串口读取一行数据
参数:
ser: 已初始化的串口对象
返回:
读取到的字符串数据,失败则返回None
"""
try:
# 检查串口是否有效且已打开
if ser and ser.is_open:
# 读取一行数据,解码为字符串并去除首尾空白
data = ser.readline().decode('utf-8').strip()
return data
return None
except Exception as e:
print(f"读取串口数据错误:{e}")
return None
def parse_data(data_str):
"""
解析串口数据字符串
参数:
data_str: 从串口读取的字符串
返回:
解析后的字典数据,格式如下:
{
'motor_id': 电机序号(int),
'timestamp': 时间戳(datetime),
'encoder': 编码器行程(float),
'current': 母线电流(float),
'status': 状态(str)
}
解析失败则返回None
"""
try:
# 按逗号分割数据
parts = data_str.split(',')
# 验证数据格式是否正确(应该有5个字段)
if len(parts) != 5:
print(f"数据格式错误,预期5个字段,实际{len(parts)}个:{data_str}")
return None
# 提取各个字段
&n
Python实现无人机电机数据采集与分析

最低0.47元/天 解锁文章
590

被折叠的 条评论
为什么被折叠?



