《边学边练:Python 从零开始:无人机直流电机测试数据采集与可视化全攻略 5》

Python实现无人机电机数据采集与分析

目录

  1. 项目背景与需求分析
  2. 开发环境搭建(Windows/Linux)
  3. Python 基础语法速成
  4. 串口数据通信原理与实现
  5. 多设备 CSV 文件管理系统
  6. 数据分析核心技术
  7. 可视化图表设计与实现
  8. 完整项目架构与代码
  9. 部署与运行指南
  10. 故障排除与优化
  11. 进阶学习路径

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值