From 44c37ec76901cd8ddab57ffbf9d298d4a4324bb0 Mon Sep 17 00:00:00 2001 From: zhoujie <929834232@qq.com> Date: Fri, 20 Feb 2026 23:45:58 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(gps):=20=E6=96=B0=E5=A2=9EGPS?= =?UTF-8?q?=E6=B5=B7=E6=8B=94=E6=95=B0=E6=8D=AE=E6=94=AF=E6=8C=81=E5=B9=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=B3=BB=E7=BB=9F=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在数据包结构中新增gps_altitude字段以支持海拔数据存储 - 更新PackData和PackCorrectedData系列函数,增加altitude参数 - 移除timestamp字段以精简数据包结构,提高传输效率 - 优化GPS数据处理逻辑,取消GPS有效性检查,直接使用原始GPS数据 - 将调试输出和监控保存间隔统一调整为30秒,降低系统负载 - 将数据存储文件最大大小从20MB提升至100MB,支持更长时间数据采集 - 将GPS数据超时时间从2秒延长至10秒,提高在弱信号环境下的稳定性 🔧 chore(spi): 调整SPI配置以优化通信稳定性 - 将所有SPI接口的时钟相位从SPI_PHASE_1EDGE调整为SPI_PHASE_2EDGE - 将SPI1的波特率预分频器从4调整为8,降低通信速率以提高稳定性 - 更新STM32CubeMX配置文件(.ioc)以反映SPI配置变更 📝 docs(script): 新增高性能数据分析工具脚本 - 创建atem_parse.py脚本,提供数据解析和可视化功能 - 支持V1/V2数据格式解析,V2版本包含GPS经纬度和海拔数据 - 实现串口实时数据接收和GPS动态模拟输出功能 - 提供波形图、轨迹图和海拔曲线等多标签可视化界面 - 包含数据表格展示和CSV导出功能,支持高性能大数据处理 --- Core/Src/main.c | 30 +- Core/Src/spi.c | 8 +- STM_ATEM_F405.ioc | 13 +- Scripts/atem_parse.py | 686 ++++++++++++++++++++++++++++++++++++++++++ User/data_packet.c | 15 +- User/data_packet.h | 15 +- User/data_storage.h | 2 +- User/gps_driver.h | 2 +- 8 files changed, 737 insertions(+), 34 deletions(-) create mode 100644 Scripts/atem_parse.py diff --git a/Core/Src/main.c b/Core/Src/main.c index 74d789d..f2eb815 100644 --- a/Core/Src/main.c +++ b/Core/Src/main.c @@ -50,8 +50,8 @@ /* USER CODE BEGIN PD */ // 监控功能宏开关(统一控制串口输出和文件存储) #define ENABLE_SYSTEM_MONITOR 1 // 系统监控开关 -#define DEBUG_OUTPUT_INTERVAL_MS 1000 // 调试输出间隔(毫秒) -#define MONITOR_SAVE_INTERVAL_MS 10000 // 监控状态保存间隔(毫秒) - 10秒 +#define DEBUG_OUTPUT_INTERVAL_MS 30000 // 调试输出间隔(毫秒) +#define MONITOR_SAVE_INTERVAL_MS 30000 // 监控状态保存间隔(毫秒) - 30秒 // 数据输出模式选择(运行时配置,从SD卡加载) // 注意:DATA_OUTPUT_MODE_UART 和 DATA_OUTPUT_MODE_STORAGE 已改为运行时配置 @@ -185,8 +185,12 @@ static void ProcessAdcData(void) &correction_result, &g_correction_params) == HAL_OK) { // 4a. 打包校正后的数据(带GPS关键信息:仅经纬度) - float lat = gps_valid ? (float)current_gps_data.position.latitude : 0.0f; - float lon = gps_valid ? (float)current_gps_data.position.longitude : 0.0f; + // float lat = gps_valid ? (float)current_gps_data.position.latitude : 0.0f; + // float lon = gps_valid ? (float)current_gps_data.position.longitude : 0.0f; + // float alt = gps_valid ? (float)current_gps_data.position.altitude : 0.0f; + float lat = (float)current_gps_data.position.latitude; + float lon = (float)current_gps_data.position.longitude; + float alt = (float)current_gps_data.position.altitude; uint32_t gps_time = gps_valid ? (current_gps_data.time.hour * 10000 + current_gps_data.time.minute * 100 + current_gps_data.time.second) : 0; @@ -197,7 +201,8 @@ static void ProcessAdcData(void) correction_result.corrected_z, gps_time, lat, - lon); + lon, + alt); correction_applied = 1; @@ -209,13 +214,17 @@ static void ProcessAdcData(void) } else { // 4b. 校正失败或未启用,使用原始数据 - float lat = gps_valid ? (float)current_gps_data.position.latitude : 0.0f; - float lon = gps_valid ? (float)current_gps_data.position.longitude : 0.0f; + // float lat = gps_valid ? (float)current_gps_data.position.latitude : 0.0f; + // float lon = gps_valid ? (float)current_gps_data.position.longitude : 0.0f; + // float alt = gps_valid ? (float)current_gps_data.position.altitude : 0.0f; + float lat = (float)current_gps_data.position.latitude; + float lon = (float)current_gps_data.position.longitude; + float alt = (float)current_gps_data.position.altitude; uint32_t gps_time = gps_valid ? (current_gps_data.time.hour * 10000 + current_gps_data.time.minute * 100 + current_gps_data.time.second) : 0; - PackData(&g_data_packet, raw_adc[0], raw_adc[1], raw_adc[2], gps_time, lat, lon); + PackData(&g_data_packet, raw_adc[0], raw_adc[1], raw_adc[2], gps_time, lat, lon, alt); // 发送原始数据包到串口(运行时配置) if (Config_IsUartOutputEnabled()) { @@ -698,10 +707,9 @@ void SystemClock_Config(void) void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { static uint32_t cnt = 0; - if(LTC2508_IsInited() == 0) return; - + if(LTC2508_IsInited() == 0) return; cnt ++; - // if(cnt % 5 == 0) + // if(cnt % 2 == 0) { // HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET); if (GPIO_Pin == ADC_DRY_Pin) { diff --git a/Core/Src/spi.c b/Core/Src/spi.c index a91f639..af0b6bd 100644 --- a/Core/Src/spi.c +++ b/Core/Src/spi.c @@ -48,9 +48,9 @@ void MX_SPI1_Init(void) hspi1.Init.Direction = SPI_DIRECTION_2LINES; hspi1.Init.DataSize = SPI_DATASIZE_16BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; - hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; + hspi1.Init.CLKPhase = SPI_PHASE_2EDGE; hspi1.Init.NSS = SPI_NSS_SOFT; - hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; + hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; hspi1.Init.TIMode = SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; @@ -80,7 +80,7 @@ void MX_SPI2_Init(void) hspi2.Init.Direction = SPI_DIRECTION_2LINES_RXONLY; hspi2.Init.DataSize = SPI_DATASIZE_16BIT; hspi2.Init.CLKPolarity = SPI_POLARITY_LOW; - hspi2.Init.CLKPhase = SPI_PHASE_1EDGE; + hspi2.Init.CLKPhase = SPI_PHASE_2EDGE; hspi2.Init.NSS = SPI_NSS_SOFT; hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB; hspi2.Init.TIMode = SPI_TIMODE_DISABLE; @@ -111,7 +111,7 @@ void MX_SPI3_Init(void) hspi3.Init.Direction = SPI_DIRECTION_2LINES_RXONLY; hspi3.Init.DataSize = SPI_DATASIZE_16BIT; hspi3.Init.CLKPolarity = SPI_POLARITY_LOW; - hspi3.Init.CLKPhase = SPI_PHASE_1EDGE; + hspi3.Init.CLKPhase = SPI_PHASE_2EDGE; hspi3.Init.NSS = SPI_NSS_SOFT; hspi3.Init.FirstBit = SPI_FIRSTBIT_MSB; hspi3.Init.TIMode = SPI_TIMODE_DISABLE; diff --git a/STM_ATEM_F405.ioc b/STM_ATEM_F405.ioc index 76b060d..de973af 100644 --- a/STM_ATEM_F405.ioc +++ b/STM_ATEM_F405.ioc @@ -337,21 +337,24 @@ SDIO.HardwareFlowControl=SDIO_HARDWARE_FLOW_CONTROL_DISABLE SDIO.IPParameters=ClockDiv,HardwareFlowControl SH.GPXTI1.0=GPIO_EXTI1 SH.GPXTI1.ConfNb=1 -SPI1.BaudRatePrescaler=SPI_BAUDRATEPRESCALER_4 -SPI1.CalculateBaudRate=21.0 MBits/s +SPI1.BaudRatePrescaler=SPI_BAUDRATEPRESCALER_8 +SPI1.CLKPhase=SPI_PHASE_2EDGE +SPI1.CalculateBaudRate=10.5 MBits/s SPI1.DataSize=SPI_DATASIZE_16BIT SPI1.Direction=SPI_DIRECTION_2LINES -SPI1.IPParameters=VirtualType,Mode,Direction,CalculateBaudRate,BaudRatePrescaler,DataSize +SPI1.IPParameters=VirtualType,Mode,Direction,CalculateBaudRate,BaudRatePrescaler,DataSize,CLKPhase SPI1.Mode=SPI_MODE_MASTER SPI1.VirtualType=VM_MASTER +SPI2.CLKPhase=SPI_PHASE_2EDGE SPI2.DataSize=SPI_DATASIZE_16BIT SPI2.Direction=SPI_DIRECTION_2LINES_RXONLY -SPI2.IPParameters=VirtualType,Mode,Direction,DataSize +SPI2.IPParameters=VirtualType,Mode,Direction,DataSize,CLKPhase SPI2.Mode=SPI_MODE_SLAVE SPI2.VirtualType=VM_SLAVE +SPI3.CLKPhase=SPI_PHASE_2EDGE SPI3.DataSize=SPI_DATASIZE_16BIT SPI3.Direction=SPI_DIRECTION_2LINES_RXONLY -SPI3.IPParameters=VirtualType,Mode,Direction,DataSize +SPI3.IPParameters=VirtualType,Mode,Direction,DataSize,CLKPhase SPI3.Mode=SPI_MODE_SLAVE SPI3.VirtualType=VM_SLAVE TIM2.IPParameters=Prescaler,Period diff --git a/Scripts/atem_parse.py b/Scripts/atem_parse.py new file mode 100644 index 0000000..fef4d88 --- /dev/null +++ b/Scripts/atem_parse.py @@ -0,0 +1,686 @@ +import sys +import time +import numpy as np +import pandas as pd +import serial +import serial.tools.list_ports +from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, + QHBoxLayout, QPushButton, QFileDialog, QTableView, + QLabel, QRadioButton, QButtonGroup, QTabWidget, + QMessageBox, QHeaderView, QCheckBox, QComboBox, QSplitter) +from PyQt6.QtCore import Qt, QAbstractTableModel, QThread, pyqtSignal, QTimer +import pyqtgraph as pg + +# ========================================== +# 1. 数据结构定义 (移除了 timestamp) +# ========================================== +RAW_DTYPE_V1 = np.dtype([ + ('start_byte', ' 0: + data = self.serial_port.read(self.serial_port.in_waiting) + buffer.extend(data) + + packets = [] + while True: + idx = buffer.find(start_marker) + if idx == -1: + buffer = buffer[-3:] if len(buffer) >= 3 else buffer + break + + if len(buffer) >= idx + packet_size: + packet_bytes = buffer[idx : idx + packet_size] + packets.append(packet_bytes) + buffer = buffer[idx + packet_size :] + else: + buffer = buffer[idx:] + break + + if packets: + combined_bytes = b''.join(packets) + parsed_arr = np.frombuffer(combined_bytes, dtype=self.dtype) + self.data_received.emit(parsed_arr) + + except Exception as e: + if self.is_running: + self.error_occurred.emit(str(e)) + finally: + self.stop() + + def stop(self): + self.is_running = False + if self.serial_port: + try: + self.serial_port.cancel_read() + except Exception: + pass + try: + if self.serial_port.is_open: + self.serial_port.close() + except Exception: + pass + finally: + self.serial_port = None + +# ========================================== +# 4. 后台线程:GPS 动态模拟输出 (10Hz) +# ========================================== +class GPSSimulatorThread(QThread): + error_occurred = pyqtSignal(str) + + def __init__(self, port, baudrate): + super().__init__() + self.port = port + self.baudrate = baudrate + self.is_running = False + self.serial_port = None + + def get_nmea_checksum(self, sentence): + calc_cksum = 0 + for char in sentence: + calc_cksum ^= ord(char) + return f"{calc_cksum:02X}" + + def run(self): + self.is_running = True + try: + self.serial_port = serial.Serial(self.port, self.baudrate, timeout=1) + + current_lat = 39.9042 + current_lon = 116.3972 + current_alt = 43.0 + + lat_step = 0.00001 + lon_step = 0.00001 + alt_step = 0.05 + + while self.is_running: + start_time = time.perf_counter() + + now = time.time() + gm_time = time.gmtime(now) + ms = int((now % 1) * 1000) + time_str = time.strftime("%H%M%S", gm_time) + f".{ms:03d}" + date_str = time.strftime("%d%m%y", gm_time) + + current_lat += lat_step + current_lon += lon_step + current_alt += alt_step + + lat_deg = int(abs(current_lat)) + lat_min = (abs(current_lat) - lat_deg) * 60 + lat_str = f"{lat_deg:02d}{lat_min:07.4f}" + lat_dir = "N" if current_lat >= 0 else "S" + + lon_deg = int(abs(current_lon)) + lon_min = (abs(current_lon) - lon_deg) * 60 + lon_str = f"{lon_deg:03d}{lon_min:07.4f}" + lon_dir = "E" if current_lon >= 0 else "W" + + speed_knots = "19.4" + course_true = "45.0" + + gga_core = f"GPGGA,{time_str},{lat_str},{lat_dir},{lon_str},{lon_dir},1,08,1.0,{current_alt:.1f},M,0.0,M,," + rmc_core = f"GPRMC,{time_str},A,{lat_str},{lat_dir},{lon_str},{lon_dir},{speed_knots},{course_true},{date_str},0.0,E" + + gga_sentence = f"${gga_core}*{self.get_nmea_checksum(gga_core)}\r\n" + rmc_sentence = f"${rmc_core}*{self.get_nmea_checksum(rmc_core)}\r\n" + + if self.serial_port and self.serial_port.is_open: + self.serial_port.write(gga_sentence.encode('ascii')) + self.serial_port.write(rmc_sentence.encode('ascii')) + + elapsed = time.perf_counter() - start_time + sleep_time = 0.1 - elapsed + if sleep_time > 0: + time.sleep(sleep_time) + + except Exception as e: + if self.is_running: + self.error_occurred.emit(str(e)) + finally: + self.stop() + + def stop(self): + self.is_running = False + if self.serial_port: + try: + self.serial_port.cancel_write() + except Exception: + pass + try: + if self.serial_port.is_open: + self.serial_port.close() + except Exception: + pass + finally: + self.serial_port = None + +# ========================================== +# 5. 主窗口 +# ========================================== +class DataAnalyzerUI(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("🚀 高性能数据分析工具 (含经纬度地图与海拔曲线)") + self.resize(1300, 900) + + self.df = pd.DataFrame() + self.live_data_list = [] + self.is_live_mode = False + self.curves = [] + + self.last_fps_time = 0 + self.frame_count = 0 + self.last_packet_count = 0 + self.current_packet_count = 0 + + self.is_sim_running = False + self.sim_thread = None + + pg.setConfigOptions(antialias=False) + self.init_ui() + + self.plot_timer = QTimer() + self.plot_timer.timeout.connect(self.update_live_plot) + + def init_ui(self): + main_widget = QWidget() + self.setCentralWidget(main_widget) + layout = QVBoxLayout(main_widget) + + # --- 第一排工具栏 --- + top_bar1 = QHBoxLayout() + self.ver_group = QButtonGroup(self) + self.rb_v2 = QRadioButton("V2 (带GPS与海拔)") + self.rb_v2.setChecked(True) + self.ver_group.addButton(self.rb_v2) + + self.mode_group = QButtonGroup(self) + self.rb_raw = QRadioButton("原始数据 (Raw)") + self.rb_corr = QRadioButton("校正数据 (Corr)") + self.rb_raw.setChecked(True) + self.mode_group.addButton(self.rb_raw) + self.mode_group.addButton(self.rb_corr) + + btn_load = QPushButton("📂 打开文件") + btn_load.clicked.connect(self.load_file) + btn_export = QPushButton("💾 导出CSV") + btn_export.clicked.connect(self.export_csv) + + self.chk_mouse_mode = QCheckBox("🔍 鼠标框选放大") + self.chk_mouse_mode.stateChanged.connect(self.toggle_mouse_mode) + + btn_autoscale = QPushButton("⟲ 复位视图") + btn_autoscale.clicked.connect(self.reset_view) + + top_bar1.addWidget(QLabel("版本:")) + top_bar1.addWidget(self.rb_v2) + top_bar1.addSpacing(15) + top_bar1.addWidget(QLabel("模式:")) + top_bar1.addWidget(self.rb_raw) + top_bar1.addWidget(self.rb_corr) + top_bar1.addSpacing(15) + top_bar1.addWidget(btn_load) + top_bar1.addWidget(btn_export) + top_bar1.addStretch() + top_bar1.addWidget(self.chk_mouse_mode) + top_bar1.addWidget(btn_autoscale) + + # --- 第二排工具栏 --- + top_bar2 = QHBoxLayout() + self.cb_ports = QComboBox() + self.cb_baudrate = QComboBox() + self.cb_baudrate.addItems(["9600", "115200", "230400", "460800", "921600", "2000000"]) + self.cb_baudrate.setCurrentText("115200") + btn_refresh_ports = QPushButton("🔄 刷新端口") + btn_refresh_ports.clicked.connect(self.refresh_ports) + self.btn_toggle_serial = QPushButton("▶ 打开接收串口") + self.btn_toggle_serial.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;") + self.btn_toggle_serial.clicked.connect(self.toggle_serial) + + top_bar2.addWidget(QLabel("🔌 接收串口:")) + top_bar2.addWidget(self.cb_ports) + top_bar2.addWidget(btn_refresh_ports) + top_bar2.addWidget(QLabel("波特率:")) + top_bar2.addWidget(self.cb_baudrate) + top_bar2.addWidget(self.btn_toggle_serial) + top_bar2.addStretch() + + # --- 第三排工具栏 (GPS 模拟器) --- + top_bar3 = QHBoxLayout() + self.cb_sim_ports = QComboBox() + self.cb_sim_baudrate = QComboBox() + self.cb_sim_baudrate.addItems(["9600", "115200", "230400", "460800", "921600"]) + self.cb_sim_baudrate.setCurrentText("115200") + self.btn_toggle_sim = QPushButton("🛰 开启GPS动态模拟 (10Hz)") + self.btn_toggle_sim.setStyleSheet("background-color: #FF9800; color: white; font-weight: bold;") + self.btn_toggle_sim.clicked.connect(self.toggle_gps_sim) + + top_bar3.addWidget(QLabel("📡 输出串口:")) + top_bar3.addWidget(self.cb_sim_ports) + top_bar3.addWidget(QLabel("波特率:")) + top_bar3.addWidget(self.cb_sim_baudrate) + top_bar3.addWidget(self.btn_toggle_sim) + top_bar3.addStretch() + + layout.addLayout(top_bar1) + layout.addLayout(top_bar2) + layout.addLayout(top_bar3) + self.refresh_ports() + + # --- 监控栏 --- + info_layout = QHBoxLayout() + self.lbl_info = QLabel("请加载文件或打开串口接收数据...") + self.lbl_info.setStyleSheet("color: blue; font-weight: bold;") + self.lbl_fps = QLabel("📈 绘图帧率: -- FPS | 📥 接收率: -- 包/秒") + self.lbl_fps.setStyleSheet("color: #E91E63; font-weight: bold;") + info_layout.addWidget(self.lbl_info) + info_layout.addStretch() + info_layout.addWidget(self.lbl_fps) + layout.addLayout(info_layout) + + # ========================================== + # 多标签内容区域设置 + # ========================================== + self.tabs = QTabWidget() + layout.addWidget(self.tabs) + + # [Tab 1] 主波形图 + self.plot_widget = pg.PlotWidget() + self.plot_widget.setBackground('w') + self.plot_widget.showGrid(x=True, y=True, alpha=0.3) + self.plot_widget.addLegend() + self.plot_widget.setLabel('bottom', 'Data Points (Index)') # 更新了标签 + self.plot_widget.setLabel('left', 'Value') + self.vb = self.plot_widget.plotItem.vb + self.tabs.addTab(self.plot_widget, "📈 波形图 (Plot)") + + # [Tab 2] GPS 轨迹与海拔视图 + traj_container = QWidget() + traj_layout = QVBoxLayout(traj_container) + traj_splitter = QSplitter(Qt.Orientation.Vertical) + + self.traj_plot = pg.PlotWidget(title="🗺️ 实时轨迹 (经度 vs 纬度)") + self.traj_plot.setBackground('w') + self.traj_plot.showGrid(x=True, y=True, alpha=0.5) + self.traj_plot.setLabel('bottom', 'Longitude (经度)') + self.traj_plot.setLabel('left', 'Latitude (纬度)') + self.traj_curve = self.traj_plot.plot(pen=pg.mkPen('b', width=2), symbol='o', symbolSize=3, symbolBrush='b') + + self.alt_plot = pg.PlotWidget(title="⛰️ 海拔高度 (Altitude)") + self.alt_plot.setBackground('w') + self.alt_plot.showGrid(x=True, y=True, alpha=0.5) + self.alt_plot.setLabel('bottom', 'Data Points (Index)') # 更新了标签 + self.alt_plot.setLabel('left', 'Altitude (m)') + self.alt_curve = self.alt_plot.plot(pen=pg.mkPen('g', width=2)) + + traj_splitter.addWidget(self.traj_plot) + traj_splitter.addWidget(self.alt_plot) + traj_layout.addWidget(traj_splitter) + self.tabs.addTab(traj_container, "🗺️ 轨迹与海拔 (Trajectory)") + + # [Tab 3] 数据表格 + self.table_view = QTableView() + self.table_view.setAlternatingRowColors(True) + self.table_view.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive) + self.tabs.addTab(self.table_view, "🔢 数据表 (Table)") + + def get_current_dtype(self): + is_v2 = self.rb_v2.isChecked() + is_raw = self.rb_raw.isChecked() + if is_v2: + return RAW_DTYPE_V2 if is_raw else CORRECTED_DTYPE_V2 + return RAW_DTYPE_V1 if is_raw else CORRECTED_DTYPE_V1 + + def get_column_config(self): + is_v2 = self.rb_v2.isChecked() + is_raw = self.rb_raw.isChecked() + + # 这里移除了 timestamp + if is_raw: + base_cols = ['adc1', 'adc2', 'adc3'] + labels = ['ADC 1', 'ADC 2', 'ADC 3'] + data_cols = ['adc1', 'adc2', 'adc3'] + else: + base_cols = ['corr_x', 'corr_y', 'corr_z'] + labels = ['X Axis', 'Y Axis', 'Z Axis'] + data_cols = ['corr_x', 'corr_y', 'corr_z'] + + cols = base_cols + ['gps_time', 'gps_latitude', 'gps_longitude', 'gps_altitude'] if is_v2 else base_cols + ['checksum'] + return cols, data_cols, labels + + def load_file(self): + if self.is_live_mode: + QMessageBox.warning(self, "警告", "请先关闭串口后再加载文件。") + return + + file_name, _ = QFileDialog.getOpenFileName(self, "选择文件", "", "Data (*.dat);;All (*)") + if not file_name: return + + try: + dtype = self.get_current_dtype() + raw_data = np.fromfile(file_name, dtype=dtype) + if len(raw_data) == 0: return + + self.df = pd.DataFrame(raw_data) + cols, data_cols, labels = self.get_column_config() + + self.lbl_info.setText(f"文件加载成功 | {len(self.df)} 行 | 版本: {'V2' if self.rb_v2.isChecked() else 'V1'}") + self.lbl_fps.setText("📈 绘图帧率: -- FPS | 📥 接收率: -- 包/秒") + self.refresh_table(cols) + self.plot_static_data(data_cols, labels) + + except Exception as e: + QMessageBox.critical(self, "解析错误", str(e)) + + def refresh_table(self, cols): + display_df = self.df[cols] if not self.df.empty else pd.DataFrame(columns=cols) + self.model = BigDataModel(display_df) + self.table_view.setModel(self.model) + self.table_view.resizeColumnsToContents() + + def plot_static_data(self, data_cols, labels): + # 1. 主波形图静态渲染 + self.plot_widget.clear() + self.curves.clear() + colors = ['#FF0000', '#00AA00', '#0000FF'] + + # 移除了 timestamp,改为使用数据点索引 + x_data = np.arange(len(self.df)) + + for i, col in enumerate(data_cols): + y_data = self.df[col].values + curve = pg.PlotCurveItem(x=x_data, y=y_data, pen=pg.mkPen(color=colors[i], width=1.5), + name=labels[i], skipFiniteCheck=True, autoDownsample=True, clipToView=True) + self.plot_widget.addItem(curve) + + self.plot_widget.setLabel('bottom', 'Data Points (Index)') + self.reset_view() + + # 2. 轨迹和海拔静态渲染 + if self.rb_v2.isChecked() and 'gps_longitude' in self.df.columns: + lons = self.df['gps_longitude'].values + lats = self.df['gps_latitude'].values + alts = self.df['gps_altitude'].values if 'gps_altitude' in self.df.columns else np.zeros_like(lons) + + valid_idx = (lons != 0.0) & (lats != 0.0) + if np.any(valid_idx): + self.traj_curve.setData(lons[valid_idx], lats[valid_idx]) + else: + self.traj_curve.setData(lons, lats) + + self.alt_curve.setData(alts) + self.traj_plot.autoRange() + self.alt_plot.autoRange() + else: + self.traj_curve.setData([], []) + self.alt_curve.setData([]) + + def refresh_ports(self): + self.cb_ports.clear() + self.cb_sim_ports.clear() + ports = serial.tools.list_ports.comports() + for p in ports: + port_name = f"{p.device} - {p.description}" + self.cb_ports.addItem(port_name, p.device) + self.cb_sim_ports.addItem(port_name, p.device) + + def toggle_serial(self): + if not self.is_live_mode: + port = self.cb_ports.currentData() + if not port: + QMessageBox.warning(self, "提示", "未找到有效串口") + return + + baud = int(self.cb_baudrate.currentText()) + dtype = self.get_current_dtype() + + self.df = pd.DataFrame() + self.live_data_list = [] + + self.plot_widget.clear() + self.curves.clear() + self.traj_curve.setData([], []) + self.alt_curve.setData([]) + + self.plot_widget.setLabel('bottom', 'Latest Points (Index)') + + colors = ['#FF0000', '#00AA00', '#0000FF'] + _, _, labels = self.get_column_config() + for i in range(len(labels)): + curve = pg.PlotCurveItem(pen=pg.mkPen(color=colors[i], width=1.5), name=labels[i]) + self.plot_widget.addItem(curve) + self.curves.append(curve) + + self.current_packet_count = 0 + self.last_packet_count = 0 + self.frame_count = 0 + self.last_fps_time = time.perf_counter() + self.lbl_fps.setText("📈 绘图帧率: 计算中... | 📥 接收率: 计算中...") + + self.serial_thread = SerialReaderThread(port, baud, dtype) + self.serial_thread.data_received.connect(self.on_live_data_received) + self.serial_thread.error_occurred.connect(self.on_serial_error) + self.serial_thread.start() + + self.plot_timer.start(50) + + self.is_live_mode = True + self.btn_toggle_serial.setText("⏹ 关闭串口停止接收") + self.btn_toggle_serial.setStyleSheet("background-color: #f44336; color: white; font-weight: bold;") + + self.rb_v2.setEnabled(False) + self.rb_raw.setEnabled(False) + self.rb_corr.setEnabled(False) + + else: + self.serial_thread.stop() + self.serial_thread.wait() + self.plot_timer.stop() + + self.is_live_mode = False + self.btn_toggle_serial.setText("▶ 打开接收串口") + self.btn_toggle_serial.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;") + self.lbl_fps.setText("📈 绘图帧率: -- FPS | 📥 接收率: -- 包/秒") + + self.rb_v2.setEnabled(True) + self.rb_raw.setEnabled(True) + self.rb_corr.setEnabled(True) + + if self.live_data_list: + full_array = np.concatenate(self.live_data_list) + self.df = pd.DataFrame(full_array) + cols, _, _ = self.get_column_config() + self.refresh_table(cols) + self.lbl_info.setText(f"串口采集完毕。总计收集 {len(self.df)} 行数据。") + + def on_live_data_received(self, data_array): + self.live_data_list.append(data_array) + self.current_packet_count += len(data_array) + self.lbl_info.setText(f"🟢 正在接收数据... 已接收: {self.current_packet_count} 帧") + + def update_live_plot(self): + self.frame_count += 1 + current_time = time.perf_counter() + elapsed = current_time - self.last_fps_time + + if elapsed >= 1.0: + fps = self.frame_count / elapsed + pps = (self.current_packet_count - self.last_packet_count) / elapsed + self.lbl_fps.setText(f"📈 绘图帧率: {fps:.1f} FPS | 📥 接收率: {pps:.0f} 包/秒") + + self.last_fps_time = current_time + self.frame_count = 0 + self.last_packet_count = self.current_packet_count + + if not self.live_data_list: + return + + MAX_POINTS = 5000 + recent_chunks = [] + point_count = 0 + for arr in reversed(self.live_data_list): + recent_chunks.append(arr) + point_count += len(arr) + if point_count >= MAX_POINTS: + break + + recent_data = np.concatenate(recent_chunks[::-1]) + if len(recent_data) > MAX_POINTS: + recent_data = recent_data[-MAX_POINTS:] + + # 1. 更新主波形图 (自动以接收点索引作为 X 轴) + _, data_cols, _ = self.get_column_config() + for i, col in enumerate(data_cols): + y_data = recent_data[col]# / 429496729.0 + self.curves[i].setData(y_data) + + # 2. 更新轨迹和海拔 + if self.rb_v2.isChecked() and 'gps_longitude' in recent_data.dtype.names: + lons = recent_data['gps_longitude'] + lats = recent_data['gps_latitude'] + alts = recent_data['gps_altitude'] if 'gps_altitude' in recent_data.dtype.names else np.zeros_like(lons) + + valid_idx = (lons != 0.0) & (lats != 0.0) + if np.any(valid_idx): + self.traj_curve.setData(lons[valid_idx], lats[valid_idx]) + else: + self.traj_curve.setData(lons, lats) + + self.alt_curve.setData(alts) + + def on_serial_error(self, err_msg): + self.toggle_serial() + QMessageBox.critical(self, "接收串口错误", f"发生错误:\n{err_msg}") + + def toggle_gps_sim(self): + if not self.is_sim_running: + port = self.cb_sim_ports.currentData() + if not port: + QMessageBox.warning(self, "提示", "未找到有效的输出串口") + return + + if self.is_live_mode and port == self.cb_ports.currentData(): + reply = QMessageBox.question(self, "警告", + "模拟输出端口与当前接收端口相同,可能会导致端口冲突。确定要继续吗?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) + if reply == QMessageBox.StandardButton.No: return + + baud = int(self.cb_sim_baudrate.currentText()) + self.sim_thread = GPSSimulatorThread(port, baud) + self.sim_thread.error_occurred.connect(self.on_sim_error) + self.sim_thread.start() + + self.is_sim_running = True + self.btn_toggle_sim.setText("⏹ 关闭GPS动态模拟") + self.btn_toggle_sim.setStyleSheet("background-color: #f44336; color: white; font-weight: bold;") + else: + self.sim_thread.stop() + self.sim_thread.wait() + + self.is_sim_running = False + self.btn_toggle_sim.setText("🛰 开启GPS动态模拟 (10Hz)") + self.btn_toggle_sim.setStyleSheet("background-color: #FF9800; color: white; font-weight: bold;") + + def on_sim_error(self, err_msg): + self.toggle_gps_sim() + QMessageBox.critical(self, "输出串口错误", f"模拟器串口发生错误:\n{err_msg}") + + def toggle_mouse_mode(self, state): + mode = self.vb.RectMode if state == 2 else self.vb.PanMode + self.vb.setMouseMode(mode) + self.traj_plot.plotItem.vb.setMouseMode(mode) + self.alt_plot.plotItem.vb.setMouseMode(mode) + + def reset_view(self): + self.plot_widget.autoRange() + self.traj_plot.autoRange() + self.alt_plot.autoRange() + + def export_csv(self): + if self.df.empty: return + path, _ = QFileDialog.getSaveFileName(self, "保存", "export.csv", "CSV (*.csv)") + if path: + self.df.to_csv(path, index=False) + QMessageBox.information(self, "完成", "导出成功") + +if __name__ == "__main__": + app = QApplication(sys.argv) + w = DataAnalyzerUI() + w.show() + sys.exit(app.exec()) \ No newline at end of file diff --git a/User/data_packet.c b/User/data_packet.c index ddd4a46..985348b 100644 --- a/User/data_packet.c +++ b/User/data_packet.c @@ -19,7 +19,7 @@ uint16_t Calculate_CRC16(const uint8_t *data, uint16_t len) { } void PackData(DataPacket_t *packet, int32_t adc1, int32_t adc2, int32_t adc3, - uint32_t gps_time, float latitude, float longitude) + uint32_t gps_time, float latitude, float longitude, float altitude) { if (packet == NULL) return; @@ -27,7 +27,7 @@ void PackData(DataPacket_t *packet, int32_t adc1, int32_t adc2, int32_t adc3, packet->start_byte = PACKET_START_BYTE; // 设置时间戳 - packet->timestamp = HAL_GetTick(); +// packet->timestamp = HAL_GetTick(); // 设置ADC数据 packet->adc_data1 = adc1; @@ -38,6 +38,7 @@ void PackData(DataPacket_t *packet, int32_t adc1, int32_t adc2, int32_t adc3, packet->gps_time = gps_time; packet->gps_latitude = latitude; packet->gps_longitude = longitude; + packet->gps_altitude = altitude; } uint8_t ValidatePacket(const DataPacket_t *packet) @@ -63,7 +64,7 @@ uint8_t ValidateCorrectedPacket(const CorrectedDataPacket_t *packet) } void PackCorrectedData(CorrectedDataPacket_t *packet, float x, float y, float z, - uint32_t gps_time, float latitude, float longitude) + uint32_t gps_time, float latitude, float longitude, float altitude) { if (packet == NULL) return; @@ -71,7 +72,7 @@ void PackCorrectedData(CorrectedDataPacket_t *packet, float x, float y, float z, packet->start_byte = PACKET_START_BYTE; // 设置时间戳 - packet->timestamp = HAL_GetTick(); + // packet->timestamp = HAL_GetTick(); // 设置校正后数据 packet->corrected_x = x; @@ -82,10 +83,11 @@ void PackCorrectedData(CorrectedDataPacket_t *packet, float x, float y, float z, packet->gps_time = gps_time; packet->gps_latitude = latitude; packet->gps_longitude = longitude; + packet->gps_altitude = altitude; } void PackCorrectedDataWithGPS(CorrectedDataPacketWithGPS_t *packet, float x, float y, float z, - uint32_t gps_time, float latitude, float longitude) + uint32_t gps_time, float latitude, float longitude, float altitude) { if (packet == NULL) return; @@ -93,7 +95,7 @@ void PackCorrectedDataWithGPS(CorrectedDataPacketWithGPS_t *packet, float x, flo packet->start_byte = PACKET_START_BYTE; // 设置时间戳 - packet->timestamp = HAL_GetTick(); + // packet->timestamp = HAL_GetTick(); // 设置校正后数据 packet->corrected_x = x; @@ -104,6 +106,7 @@ void PackCorrectedDataWithGPS(CorrectedDataPacketWithGPS_t *packet, float x, flo packet->gps_time = gps_time; packet->gps_latitude = latitude; packet->gps_longitude = longitude; + packet->gps_altitude = altitude; } uint8_t ValidateCorrectedPacketWithGPS(const CorrectedDataPacketWithGPS_t *packet) diff --git a/User/data_packet.h b/User/data_packet.h index cc78748..250f1fc 100644 --- a/User/data_packet.h +++ b/User/data_packet.h @@ -9,47 +9,50 @@ // 数据包结构(精简版 - 有包头无校验和,含GPS) typedef struct __attribute__((packed)) { uint32_t start_byte; // 包头 (4字节) = 0xFFFFFFFF - uint32_t timestamp; // 系统时间戳 (4字节) + // uint32_t timestamp; // 系统时间戳 (4字节) int32_t adc_data1; // ADC1 数据 (4字节) int32_t adc_data2; // ADC2 数据 (4字节) int32_t adc_data3; // ADC3 数据 (4字节) uint32_t gps_time; // GPS时间戳 (4字节) HHMMSS格式 float gps_latitude; // GPS纬度 (4字节) float gps_longitude; // GPS经度 (4字节) + float gps_altitude; // GPS海拔 (4字节) } DataPacket_t; // 校正后数据包结构(精简版 - 有包头无校验和,含GPS) typedef struct __attribute__((packed)) { uint32_t start_byte; // 包头 (4字节) = 0xFFFFFFFF - uint32_t timestamp; // 系统时间戳 (4字节) + // uint32_t timestamp; // 系统时间戳 (4字节) float corrected_x; // 校正后X轴数据 (4字节) float corrected_y; // 校正后Y轴数据 (4字节) float corrected_z; // 校正后Z轴数据 (4字节) uint32_t gps_time; // GPS时间戳 (4字节) HHMMSS格式 float gps_latitude; // GPS纬度 (4字节) float gps_longitude; // GPS经度 (4字节) + float gps_altitude; // GPS海拔 (4字节) } CorrectedDataPacket_t; // 带GPS信息的校正数据包结构(精简版 - 有包头无校验和) typedef struct __attribute__((packed)) { uint32_t start_byte; // 包头 (4字节) = 0xFFFFFFFF - uint32_t timestamp; // 系统时间戳 (4字节) + // uint32_t timestamp; // 系统时间戳 (4字节) float corrected_x; // 校正后X轴数据 (4字节) float corrected_y; // 校正后Y轴数据 (4字节) float corrected_z; // 校正后Z轴数据 (4字节) uint32_t gps_time; // GPS时间戳 (4字节) HHMMSS格式 float gps_latitude; // GPS纬度 (4字节) float gps_longitude; // GPS经度 (4字节) + float gps_altitude; // GPS海拔 (4字节) } CorrectedDataPacketWithGPS_t; // 函数声明 uint16_t Calculate_CRC16(const uint8_t *data, uint16_t len); void PackData(DataPacket_t *packet, int32_t adc1, int32_t adc2, int32_t adc3, - uint32_t gps_time, float latitude, float longitude); + uint32_t gps_time, float latitude, float longitude, float altitude); void PackCorrectedData(CorrectedDataPacket_t *packet, float x, float y, float z, - uint32_t gps_time, float latitude, float longitude); + uint32_t gps_time, float latitude, float longitude, float altitude); void PackCorrectedDataWithGPS(CorrectedDataPacketWithGPS_t *packet, float x, float y, float z, - uint32_t gps_time, float latitude, float longitude); + uint32_t gps_time, float latitude, float , float altitude); uint8_t ValidatePacket(const DataPacket_t *packet); uint8_t ValidateCorrectedPacket(const CorrectedDataPacket_t *packet); uint8_t ValidateCorrectedPacketWithGPS(const CorrectedDataPacketWithGPS_t *packet); diff --git a/User/data_storage.h b/User/data_storage.h index a1419dd..c4c95f1 100644 --- a/User/data_storage.h +++ b/User/data_storage.h @@ -10,7 +10,7 @@ // 数据存储配置 #define DATA_STORAGE_BUFFER_SIZE 32768 // 缓冲区大小(字节) -#define DATA_STORAGE_FILE_MAX_SIZE (20*1024*1024) // 单个文件最大20MB +#define DATA_STORAGE_FILE_MAX_SIZE (100*1024*1024) // 单个文件最大100MB #define DATA_STORAGE_BASE_PATH "0:/DATA" // 数据存储基础路径 #define DATA_STORAGE_FILE_PREFIX "/ADC_DATA_" // 文件名前缀 #define DATA_STORAGE_FOLDER_PREFIX "SESSION_" // 文件夹名前缀 diff --git a/User/gps_driver.h b/User/gps_driver.h index afe67d5..d5cb01b 100644 --- a/User/gps_driver.h +++ b/User/gps_driver.h @@ -82,7 +82,7 @@ typedef struct { #define GPS_UART_HANDLE huart3 // GPS使用的UART句柄 #define GPS_RX_BUFFER_SIZE 512 // 接收缓冲区大小 #define GPS_NMEA_MAX_LENGTH 128 // NMEA语句最大长度 -#define GPS_DATA_TIMEOUT_MS 2000 // 数据超时时间(ms) +#define GPS_DATA_TIMEOUT_MS 10000 // 数据超时时间(ms) /* Exported macro ------------------------------------------------------------*/