✨ feat(gps): 新增GPS海拔数据支持并优化系统配置
- 在数据包结构中新增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导出功能,支持高性能大数据处理
This commit is contained in:
parent
5b245f89c1
commit
44c37ec769
@ -50,8 +50,8 @@
|
|||||||
/* USER CODE BEGIN PD */
|
/* USER CODE BEGIN PD */
|
||||||
// 监控功能宏开关(统一控制串口输出和文件存储)
|
// 监控功能宏开关(统一控制串口输出和文件存储)
|
||||||
#define ENABLE_SYSTEM_MONITOR 1 // 系统监控开关
|
#define ENABLE_SYSTEM_MONITOR 1 // 系统监控开关
|
||||||
#define DEBUG_OUTPUT_INTERVAL_MS 1000 // 调试输出间隔(毫秒)
|
#define DEBUG_OUTPUT_INTERVAL_MS 30000 // 调试输出间隔(毫秒)
|
||||||
#define MONITOR_SAVE_INTERVAL_MS 10000 // 监控状态保存间隔(毫秒) - 10秒
|
#define MONITOR_SAVE_INTERVAL_MS 30000 // 监控状态保存间隔(毫秒) - 30秒
|
||||||
|
|
||||||
// 数据输出模式选择(运行时配置,从SD卡加载)
|
// 数据输出模式选择(运行时配置,从SD卡加载)
|
||||||
// 注意:DATA_OUTPUT_MODE_UART 和 DATA_OUTPUT_MODE_STORAGE 已改为运行时配置
|
// 注意:DATA_OUTPUT_MODE_UART 和 DATA_OUTPUT_MODE_STORAGE 已改为运行时配置
|
||||||
@ -185,8 +185,12 @@ static void ProcessAdcData(void)
|
|||||||
&correction_result, &g_correction_params) == HAL_OK) {
|
&correction_result, &g_correction_params) == HAL_OK) {
|
||||||
|
|
||||||
// 4a. 打包校正后的数据(带GPS关键信息:仅经纬度)
|
// 4a. 打包校正后的数据(带GPS关键信息:仅经纬度)
|
||||||
float lat = gps_valid ? (float)current_gps_data.position.latitude : 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 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 +
|
uint32_t gps_time = gps_valid ? (current_gps_data.time.hour * 10000 +
|
||||||
current_gps_data.time.minute * 100 +
|
current_gps_data.time.minute * 100 +
|
||||||
current_gps_data.time.second) : 0;
|
current_gps_data.time.second) : 0;
|
||||||
@ -197,7 +201,8 @@ static void ProcessAdcData(void)
|
|||||||
correction_result.corrected_z,
|
correction_result.corrected_z,
|
||||||
gps_time,
|
gps_time,
|
||||||
lat,
|
lat,
|
||||||
lon);
|
lon,
|
||||||
|
alt);
|
||||||
|
|
||||||
correction_applied = 1;
|
correction_applied = 1;
|
||||||
|
|
||||||
@ -209,13 +214,17 @@ static void ProcessAdcData(void)
|
|||||||
} else {
|
} else {
|
||||||
|
|
||||||
// 4b. 校正失败或未启用,使用原始数据
|
// 4b. 校正失败或未启用,使用原始数据
|
||||||
float lat = gps_valid ? (float)current_gps_data.position.latitude : 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 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 +
|
uint32_t gps_time = gps_valid ? (current_gps_data.time.hour * 10000 +
|
||||||
current_gps_data.time.minute * 100 +
|
current_gps_data.time.minute * 100 +
|
||||||
current_gps_data.time.second) : 0;
|
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()) {
|
if (Config_IsUartOutputEnabled()) {
|
||||||
@ -698,10 +707,9 @@ void SystemClock_Config(void)
|
|||||||
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
|
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
|
||||||
{
|
{
|
||||||
static uint32_t cnt = 0;
|
static uint32_t cnt = 0;
|
||||||
if(LTC2508_IsInited() == 0) return;
|
if(LTC2508_IsInited() == 0) return;
|
||||||
|
|
||||||
cnt ++;
|
cnt ++;
|
||||||
// if(cnt % 5 == 0)
|
// if(cnt % 2 == 0)
|
||||||
{
|
{
|
||||||
// HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET);
|
// HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET);
|
||||||
if (GPIO_Pin == ADC_DRY_Pin) {
|
if (GPIO_Pin == ADC_DRY_Pin) {
|
||||||
|
|||||||
@ -48,9 +48,9 @@ void MX_SPI1_Init(void)
|
|||||||
hspi1.Init.Direction = SPI_DIRECTION_2LINES;
|
hspi1.Init.Direction = SPI_DIRECTION_2LINES;
|
||||||
hspi1.Init.DataSize = SPI_DATASIZE_16BIT;
|
hspi1.Init.DataSize = SPI_DATASIZE_16BIT;
|
||||||
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
|
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.NSS = SPI_NSS_SOFT;
|
||||||
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4;
|
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8;
|
||||||
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
|
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
|
||||||
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
|
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
|
||||||
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_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.Direction = SPI_DIRECTION_2LINES_RXONLY;
|
||||||
hspi2.Init.DataSize = SPI_DATASIZE_16BIT;
|
hspi2.Init.DataSize = SPI_DATASIZE_16BIT;
|
||||||
hspi2.Init.CLKPolarity = SPI_POLARITY_LOW;
|
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.NSS = SPI_NSS_SOFT;
|
||||||
hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB;
|
hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB;
|
||||||
hspi2.Init.TIMode = SPI_TIMODE_DISABLE;
|
hspi2.Init.TIMode = SPI_TIMODE_DISABLE;
|
||||||
@ -111,7 +111,7 @@ void MX_SPI3_Init(void)
|
|||||||
hspi3.Init.Direction = SPI_DIRECTION_2LINES_RXONLY;
|
hspi3.Init.Direction = SPI_DIRECTION_2LINES_RXONLY;
|
||||||
hspi3.Init.DataSize = SPI_DATASIZE_16BIT;
|
hspi3.Init.DataSize = SPI_DATASIZE_16BIT;
|
||||||
hspi3.Init.CLKPolarity = SPI_POLARITY_LOW;
|
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.NSS = SPI_NSS_SOFT;
|
||||||
hspi3.Init.FirstBit = SPI_FIRSTBIT_MSB;
|
hspi3.Init.FirstBit = SPI_FIRSTBIT_MSB;
|
||||||
hspi3.Init.TIMode = SPI_TIMODE_DISABLE;
|
hspi3.Init.TIMode = SPI_TIMODE_DISABLE;
|
||||||
|
|||||||
@ -337,21 +337,24 @@ SDIO.HardwareFlowControl=SDIO_HARDWARE_FLOW_CONTROL_DISABLE
|
|||||||
SDIO.IPParameters=ClockDiv,HardwareFlowControl
|
SDIO.IPParameters=ClockDiv,HardwareFlowControl
|
||||||
SH.GPXTI1.0=GPIO_EXTI1
|
SH.GPXTI1.0=GPIO_EXTI1
|
||||||
SH.GPXTI1.ConfNb=1
|
SH.GPXTI1.ConfNb=1
|
||||||
SPI1.BaudRatePrescaler=SPI_BAUDRATEPRESCALER_4
|
SPI1.BaudRatePrescaler=SPI_BAUDRATEPRESCALER_8
|
||||||
SPI1.CalculateBaudRate=21.0 MBits/s
|
SPI1.CLKPhase=SPI_PHASE_2EDGE
|
||||||
|
SPI1.CalculateBaudRate=10.5 MBits/s
|
||||||
SPI1.DataSize=SPI_DATASIZE_16BIT
|
SPI1.DataSize=SPI_DATASIZE_16BIT
|
||||||
SPI1.Direction=SPI_DIRECTION_2LINES
|
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.Mode=SPI_MODE_MASTER
|
||||||
SPI1.VirtualType=VM_MASTER
|
SPI1.VirtualType=VM_MASTER
|
||||||
|
SPI2.CLKPhase=SPI_PHASE_2EDGE
|
||||||
SPI2.DataSize=SPI_DATASIZE_16BIT
|
SPI2.DataSize=SPI_DATASIZE_16BIT
|
||||||
SPI2.Direction=SPI_DIRECTION_2LINES_RXONLY
|
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.Mode=SPI_MODE_SLAVE
|
||||||
SPI2.VirtualType=VM_SLAVE
|
SPI2.VirtualType=VM_SLAVE
|
||||||
|
SPI3.CLKPhase=SPI_PHASE_2EDGE
|
||||||
SPI3.DataSize=SPI_DATASIZE_16BIT
|
SPI3.DataSize=SPI_DATASIZE_16BIT
|
||||||
SPI3.Direction=SPI_DIRECTION_2LINES_RXONLY
|
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.Mode=SPI_MODE_SLAVE
|
||||||
SPI3.VirtualType=VM_SLAVE
|
SPI3.VirtualType=VM_SLAVE
|
||||||
TIM2.IPParameters=Prescaler,Period
|
TIM2.IPParameters=Prescaler,Period
|
||||||
|
|||||||
686
Scripts/atem_parse.py
Normal file
686
Scripts/atem_parse.py
Normal file
@ -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', '<u4'),
|
||||||
|
('adc1', '<i4'), ('adc2', '<i4'), ('adc3', '<i4'),
|
||||||
|
('checksum', '<u2'), ('end_byte', '<u2')
|
||||||
|
])
|
||||||
|
CORRECTED_DTYPE_V1 = np.dtype([
|
||||||
|
('start_byte', '<u4'),
|
||||||
|
('corr_x', '<f4'), ('corr_y', '<f4'), ('corr_z', '<f4'),
|
||||||
|
('checksum', '<u2'), ('end_byte', '<u2')
|
||||||
|
])
|
||||||
|
|
||||||
|
# ⚠️ 注意: 移除了 timestamp,保留了 gps_altitude
|
||||||
|
RAW_DTYPE_V2 = np.dtype([
|
||||||
|
('start_byte', '<u4'),
|
||||||
|
('adc1', '<i4'), ('adc2', '<i4'), ('adc3', '<i4'),
|
||||||
|
('gps_time', '<u4'), ('gps_latitude', '<f4'), ('gps_longitude', '<f4'),
|
||||||
|
('gps_altitude', '<f4')
|
||||||
|
])
|
||||||
|
CORRECTED_DTYPE_V2 = np.dtype([
|
||||||
|
('start_byte', '<u4'),
|
||||||
|
('corr_x', '<f4'), ('corr_y', '<f4'), ('corr_z', '<f4'),
|
||||||
|
('gps_time', '<u4'), ('gps_latitude', '<f4'), ('gps_longitude', '<f4'),
|
||||||
|
('gps_altitude', '<f4')
|
||||||
|
])
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# 2. 高性能表格模型
|
||||||
|
# ==========================================
|
||||||
|
class BigDataModel(QAbstractTableModel):
|
||||||
|
def __init__(self, data):
|
||||||
|
super(BigDataModel, self).__init__()
|
||||||
|
self._data = data
|
||||||
|
|
||||||
|
def rowCount(self, parent=None):
|
||||||
|
return self._data.shape[0]
|
||||||
|
|
||||||
|
def columnCount(self, parent=None):
|
||||||
|
return self._data.shape[1]
|
||||||
|
|
||||||
|
def data(self, index, role=Qt.ItemDataRole.DisplayRole):
|
||||||
|
if index.isValid() and role == Qt.ItemDataRole.DisplayRole:
|
||||||
|
val = self._data.iloc[index.row(), index.column()]
|
||||||
|
if isinstance(val, (float, np.floating)):
|
||||||
|
return f"{val:.6f}"
|
||||||
|
return str(val)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def headerData(self, col, orientation, role):
|
||||||
|
if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
|
||||||
|
return self._data.columns[col]
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# 3. 后台线程:串口数据接收 (带异常安全关闭)
|
||||||
|
# ==========================================
|
||||||
|
class SerialReaderThread(QThread):
|
||||||
|
data_received = pyqtSignal(np.ndarray)
|
||||||
|
error_occurred = pyqtSignal(str)
|
||||||
|
|
||||||
|
def __init__(self, port, baudrate, dtype):
|
||||||
|
super().__init__()
|
||||||
|
self.port = port
|
||||||
|
self.baudrate = baudrate
|
||||||
|
self.dtype = dtype
|
||||||
|
self.is_running = False
|
||||||
|
self.serial_port = None
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.is_running = True
|
||||||
|
buffer = bytearray()
|
||||||
|
packet_size = self.dtype.itemsize
|
||||||
|
start_marker = b'\xff\xff\xff\xff'
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.serial_port = serial.Serial(self.port, self.baudrate, timeout=1)
|
||||||
|
|
||||||
|
while self.is_running:
|
||||||
|
if self.serial_port and self.serial_port.in_waiting > 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())
|
||||||
@ -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,
|
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;
|
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->start_byte = PACKET_START_BYTE;
|
||||||
|
|
||||||
// 设置时间戳
|
// 设置时间戳
|
||||||
packet->timestamp = HAL_GetTick();
|
// packet->timestamp = HAL_GetTick();
|
||||||
|
|
||||||
// 设置ADC数据
|
// 设置ADC数据
|
||||||
packet->adc_data1 = adc1;
|
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_time = gps_time;
|
||||||
packet->gps_latitude = latitude;
|
packet->gps_latitude = latitude;
|
||||||
packet->gps_longitude = longitude;
|
packet->gps_longitude = longitude;
|
||||||
|
packet->gps_altitude = altitude;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t ValidatePacket(const DataPacket_t *packet)
|
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,
|
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;
|
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->start_byte = PACKET_START_BYTE;
|
||||||
|
|
||||||
// 设置时间戳
|
// 设置时间戳
|
||||||
packet->timestamp = HAL_GetTick();
|
// packet->timestamp = HAL_GetTick();
|
||||||
|
|
||||||
// 设置校正后数据
|
// 设置校正后数据
|
||||||
packet->corrected_x = x;
|
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_time = gps_time;
|
||||||
packet->gps_latitude = latitude;
|
packet->gps_latitude = latitude;
|
||||||
packet->gps_longitude = longitude;
|
packet->gps_longitude = longitude;
|
||||||
|
packet->gps_altitude = altitude;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PackCorrectedDataWithGPS(CorrectedDataPacketWithGPS_t *packet, float x, float y, float z,
|
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;
|
if (packet == NULL) return;
|
||||||
|
|
||||||
@ -93,7 +95,7 @@ void PackCorrectedDataWithGPS(CorrectedDataPacketWithGPS_t *packet, float x, flo
|
|||||||
packet->start_byte = PACKET_START_BYTE;
|
packet->start_byte = PACKET_START_BYTE;
|
||||||
|
|
||||||
// 设置时间戳
|
// 设置时间戳
|
||||||
packet->timestamp = HAL_GetTick();
|
// packet->timestamp = HAL_GetTick();
|
||||||
|
|
||||||
// 设置校正后数据
|
// 设置校正后数据
|
||||||
packet->corrected_x = x;
|
packet->corrected_x = x;
|
||||||
@ -104,6 +106,7 @@ void PackCorrectedDataWithGPS(CorrectedDataPacketWithGPS_t *packet, float x, flo
|
|||||||
packet->gps_time = gps_time;
|
packet->gps_time = gps_time;
|
||||||
packet->gps_latitude = latitude;
|
packet->gps_latitude = latitude;
|
||||||
packet->gps_longitude = longitude;
|
packet->gps_longitude = longitude;
|
||||||
|
packet->gps_altitude = altitude;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t ValidateCorrectedPacketWithGPS(const CorrectedDataPacketWithGPS_t *packet)
|
uint8_t ValidateCorrectedPacketWithGPS(const CorrectedDataPacketWithGPS_t *packet)
|
||||||
|
|||||||
@ -9,47 +9,50 @@
|
|||||||
// 数据包结构(精简版 - 有包头无校验和,含GPS)
|
// 数据包结构(精简版 - 有包头无校验和,含GPS)
|
||||||
typedef struct __attribute__((packed)) {
|
typedef struct __attribute__((packed)) {
|
||||||
uint32_t start_byte; // 包头 (4字节) = 0xFFFFFFFF
|
uint32_t start_byte; // 包头 (4字节) = 0xFFFFFFFF
|
||||||
uint32_t timestamp; // 系统时间戳 (4字节)
|
// uint32_t timestamp; // 系统时间戳 (4字节)
|
||||||
int32_t adc_data1; // ADC1 数据 (4字节)
|
int32_t adc_data1; // ADC1 数据 (4字节)
|
||||||
int32_t adc_data2; // ADC2 数据 (4字节)
|
int32_t adc_data2; // ADC2 数据 (4字节)
|
||||||
int32_t adc_data3; // ADC3 数据 (4字节)
|
int32_t adc_data3; // ADC3 数据 (4字节)
|
||||||
uint32_t gps_time; // GPS时间戳 (4字节) HHMMSS格式
|
uint32_t gps_time; // GPS时间戳 (4字节) HHMMSS格式
|
||||||
float gps_latitude; // GPS纬度 (4字节)
|
float gps_latitude; // GPS纬度 (4字节)
|
||||||
float gps_longitude; // GPS经度 (4字节)
|
float gps_longitude; // GPS经度 (4字节)
|
||||||
|
float gps_altitude; // GPS海拔 (4字节)
|
||||||
} DataPacket_t;
|
} DataPacket_t;
|
||||||
|
|
||||||
// 校正后数据包结构(精简版 - 有包头无校验和,含GPS)
|
// 校正后数据包结构(精简版 - 有包头无校验和,含GPS)
|
||||||
typedef struct __attribute__((packed)) {
|
typedef struct __attribute__((packed)) {
|
||||||
uint32_t start_byte; // 包头 (4字节) = 0xFFFFFFFF
|
uint32_t start_byte; // 包头 (4字节) = 0xFFFFFFFF
|
||||||
uint32_t timestamp; // 系统时间戳 (4字节)
|
// uint32_t timestamp; // 系统时间戳 (4字节)
|
||||||
float corrected_x; // 校正后X轴数据 (4字节)
|
float corrected_x; // 校正后X轴数据 (4字节)
|
||||||
float corrected_y; // 校正后Y轴数据 (4字节)
|
float corrected_y; // 校正后Y轴数据 (4字节)
|
||||||
float corrected_z; // 校正后Z轴数据 (4字节)
|
float corrected_z; // 校正后Z轴数据 (4字节)
|
||||||
uint32_t gps_time; // GPS时间戳 (4字节) HHMMSS格式
|
uint32_t gps_time; // GPS时间戳 (4字节) HHMMSS格式
|
||||||
float gps_latitude; // GPS纬度 (4字节)
|
float gps_latitude; // GPS纬度 (4字节)
|
||||||
float gps_longitude; // GPS经度 (4字节)
|
float gps_longitude; // GPS经度 (4字节)
|
||||||
|
float gps_altitude; // GPS海拔 (4字节)
|
||||||
} CorrectedDataPacket_t;
|
} CorrectedDataPacket_t;
|
||||||
|
|
||||||
// 带GPS信息的校正数据包结构(精简版 - 有包头无校验和)
|
// 带GPS信息的校正数据包结构(精简版 - 有包头无校验和)
|
||||||
typedef struct __attribute__((packed)) {
|
typedef struct __attribute__((packed)) {
|
||||||
uint32_t start_byte; // 包头 (4字节) = 0xFFFFFFFF
|
uint32_t start_byte; // 包头 (4字节) = 0xFFFFFFFF
|
||||||
uint32_t timestamp; // 系统时间戳 (4字节)
|
// uint32_t timestamp; // 系统时间戳 (4字节)
|
||||||
float corrected_x; // 校正后X轴数据 (4字节)
|
float corrected_x; // 校正后X轴数据 (4字节)
|
||||||
float corrected_y; // 校正后Y轴数据 (4字节)
|
float corrected_y; // 校正后Y轴数据 (4字节)
|
||||||
float corrected_z; // 校正后Z轴数据 (4字节)
|
float corrected_z; // 校正后Z轴数据 (4字节)
|
||||||
uint32_t gps_time; // GPS时间戳 (4字节) HHMMSS格式
|
uint32_t gps_time; // GPS时间戳 (4字节) HHMMSS格式
|
||||||
float gps_latitude; // GPS纬度 (4字节)
|
float gps_latitude; // GPS纬度 (4字节)
|
||||||
float gps_longitude; // GPS经度 (4字节)
|
float gps_longitude; // GPS经度 (4字节)
|
||||||
|
float gps_altitude; // GPS海拔 (4字节)
|
||||||
} CorrectedDataPacketWithGPS_t;
|
} CorrectedDataPacketWithGPS_t;
|
||||||
|
|
||||||
// 函数声明
|
// 函数声明
|
||||||
uint16_t Calculate_CRC16(const uint8_t *data, uint16_t len);
|
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,
|
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,
|
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,
|
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 ValidatePacket(const DataPacket_t *packet);
|
||||||
uint8_t ValidateCorrectedPacket(const CorrectedDataPacket_t *packet);
|
uint8_t ValidateCorrectedPacket(const CorrectedDataPacket_t *packet);
|
||||||
uint8_t ValidateCorrectedPacketWithGPS(const CorrectedDataPacketWithGPS_t *packet);
|
uint8_t ValidateCorrectedPacketWithGPS(const CorrectedDataPacketWithGPS_t *packet);
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
// 数据存储配置
|
// 数据存储配置
|
||||||
#define DATA_STORAGE_BUFFER_SIZE 32768 // 缓冲区大小(字节)
|
#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_BASE_PATH "0:/DATA" // 数据存储基础路径
|
||||||
#define DATA_STORAGE_FILE_PREFIX "/ADC_DATA_" // 文件名前缀
|
#define DATA_STORAGE_FILE_PREFIX "/ADC_DATA_" // 文件名前缀
|
||||||
#define DATA_STORAGE_FOLDER_PREFIX "SESSION_" // 文件夹名前缀
|
#define DATA_STORAGE_FOLDER_PREFIX "SESSION_" // 文件夹名前缀
|
||||||
|
|||||||
@ -82,7 +82,7 @@ typedef struct {
|
|||||||
#define GPS_UART_HANDLE huart3 // GPS使用的UART句柄
|
#define GPS_UART_HANDLE huart3 // GPS使用的UART句柄
|
||||||
#define GPS_RX_BUFFER_SIZE 512 // 接收缓冲区大小
|
#define GPS_RX_BUFFER_SIZE 512 // 接收缓冲区大小
|
||||||
#define GPS_NMEA_MAX_LENGTH 128 // NMEA语句最大长度
|
#define GPS_NMEA_MAX_LENGTH 128 // NMEA语句最大长度
|
||||||
#define GPS_DATA_TIMEOUT_MS 2000 // 数据超时时间(ms)
|
#define GPS_DATA_TIMEOUT_MS 10000 // 数据超时时间(ms)
|
||||||
|
|
||||||
/* Exported macro ------------------------------------------------------------*/
|
/* Exported macro ------------------------------------------------------------*/
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user