1209 lines
41 KiB
C++
1209 lines
41 KiB
C++
#include "mainwindow.h"
|
||
#include "ui_mainwindow.h"
|
||
#include <QMessageBox>
|
||
#include <QTextStream>
|
||
#include <QDebug>
|
||
#include <QTimer>
|
||
#include <QFileDialog>
|
||
#include <QFile>
|
||
#include <QDateTime>
|
||
#include <QSerialPort>
|
||
#include <QMap>
|
||
#include <QCloseEvent>
|
||
#include <QDesktopServices>
|
||
#include <QUrl>
|
||
#include <QFileInfo>
|
||
#include <QDir>
|
||
#include <QSettings>
|
||
|
||
|
||
MainWindow::MainWindow(QWidget *parent)
|
||
: QMainWindow(parent)
|
||
, ui(new Ui::MainWindow)
|
||
, serial(new QSerialPort(this))
|
||
, txCount(0)
|
||
, rxCount(0)
|
||
, logFile(nullptr)
|
||
{
|
||
ui->setupUi(this);
|
||
// 菜单栏:工具菜单:打开配置文件、打开实时保存目录
|
||
QMenu *menu = menuBar()->addMenu(tr("配置"));
|
||
QAction *openCfgAction = menu->addAction(tr("打开配置文件"));
|
||
connect(openCfgAction, &QAction::triggered, this, &MainWindow::openConfigDir);
|
||
|
||
QAction *openSaveAction = menu->addAction(tr("打开实时保存目录"));
|
||
connect(openSaveAction, &QAction::triggered, this, &MainWindow::openRealTimeSaveDir);
|
||
|
||
ui->receiveTextEdit->setLineWrapMode(QPlainTextEdit::NoWrap);
|
||
updatePortList();
|
||
updateBaudRateList();
|
||
// 接收区默认勾选16进制,置灰不可修改
|
||
ui->hexDisplayCheckBox->setChecked(true);
|
||
ui->hexDisplayCheckBox->setEnabled(false);
|
||
|
||
|
||
initializeRecordCounts();
|
||
initializeLogFile();
|
||
// 记录程序启动时间
|
||
QString startupTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
|
||
writeLog("Start: " + startupTime);
|
||
|
||
connect(serial, &QSerialPort::readyRead, this, &MainWindow::readData);
|
||
connect(ui->portComboBox, &ClickableComboBox::clicked, this, &MainWindow::updatePortList);
|
||
// connect(ui->saveButton, &QPushButton::clicked, this, &MainWindow::on_saveButton_clicked);
|
||
// connect(ui->realTimeSaveCheckBox, &QCheckBox::stateChanged, this, &MainWindow::on_realTimeSaveCheckBox_stateChanged);
|
||
|
||
// 初始化定时器
|
||
timer = new QTimer(this);
|
||
connect(timer, &QTimer::timeout, this, &MainWindow::timer_Event);
|
||
|
||
// 初始化状态栏标签
|
||
txLabel = new QLabel(this);
|
||
rxLabel = new QLabel(this);
|
||
ui->statusBar->addPermanentWidget(txLabel);
|
||
ui->statusBar->addPermanentWidget(rxLabel);
|
||
txLabel->setText("TX: 0");
|
||
rxLabel->setText("RX: 0");
|
||
|
||
// 初始化系统托盘图标
|
||
trayIcon = new QSystemTrayIcon(this);
|
||
trayIcon->setIcon(QIcon(":/icons/BeiDou.ico"));
|
||
trayIcon->setToolTip("BeiDou Tool");
|
||
|
||
// 可选:添加右键菜单
|
||
QMenu *trayMenu = new QMenu(this);
|
||
QAction *restoreAction = trayMenu->addAction("显示");
|
||
QAction *quitAction = trayMenu->addAction("退出");
|
||
|
||
connect(restoreAction, &QAction::triggered, this, &MainWindow::show);
|
||
connect(quitAction, &QAction::triggered, qApp, &QApplication::quit);
|
||
|
||
trayIcon->setContextMenu(trayMenu);
|
||
|
||
// 添加双击恢复功能
|
||
connect(trayIcon, &QSystemTrayIcon::activated, this, [this](QSystemTrayIcon::ActivationReason reason) {
|
||
if (reason == QSystemTrayIcon::DoubleClick) {
|
||
this->show(); // 显示主窗口
|
||
this->raise(); // 置顶
|
||
this->activateWindow(); // 激活窗口
|
||
}
|
||
});
|
||
|
||
|
||
readConfig(); //读配置文件
|
||
|
||
}
|
||
|
||
void MainWindow::initializeLogFile()
|
||
{
|
||
QString saveDirPath;
|
||
if (!logDirPath.isEmpty()) {
|
||
saveDirPath = logDirPath; // 优先使用配置的日志目录
|
||
} else if (!defaultFilePath.isEmpty()) {
|
||
saveDirPath = defaultFilePath; // 回退到实时保存目录
|
||
} else {
|
||
saveDirPath = QApplication::applicationDirPath();// 最后回退到程序目录
|
||
}
|
||
|
||
QDir dir(saveDirPath);
|
||
if (!dir.exists()) {
|
||
dir.mkpath(".");
|
||
}
|
||
|
||
// 创建日志文件名,格式:BeiDou_Log_YYYYMM.log(按月创建)
|
||
QString currentMonth = QDateTime::currentDateTime().toString("yyyyMM");
|
||
QString logFileName = "BeiDou_Log_" + currentMonth + ".log";
|
||
QString logFilePath = dir.filePath(logFileName);
|
||
|
||
logFile = new QFile(logFilePath, this);
|
||
if (!logFile->open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
|
||
qDebug() << "无法创建日志文件:" << logFile->errorString();
|
||
delete logFile;
|
||
logFile = nullptr;
|
||
}
|
||
}
|
||
|
||
void MainWindow::writeLog(const QString &message)
|
||
{
|
||
if (!logFile || !logFile->isOpen()) {
|
||
return;
|
||
}
|
||
|
||
QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
|
||
QString logEntry = QString("[%1] %2\n").arg(timestamp, message);
|
||
|
||
QTextStream out(logFile);
|
||
out << logEntry;
|
||
out.flush(); // 立即写入文件
|
||
}
|
||
|
||
void MainWindow::readConfig()
|
||
{
|
||
QSettings settings("config.ini", QSettings::IniFormat);
|
||
|
||
// 读取串口设置
|
||
QString port = settings.value("SerialPort/port").toString();
|
||
int baudRate = settings.value("SerialPort/baudRate", 115200).toInt();
|
||
bool autoOpen = settings.value("SerialPort/autoOpen", false).toBool();
|
||
|
||
// 读取默认文件保存路径
|
||
QString defaultPath = settings.value("FileSave/defaultPath").toString().trimmed();
|
||
if (!defaultPath.isEmpty()) {
|
||
this->defaultFilePath = QDir::fromNativeSeparators(defaultPath);
|
||
}
|
||
|
||
// 读取日志目录
|
||
logDirPath = settings.value("Log/defaultPath").toString().trimmed();
|
||
if (!logDirPath.isEmpty()) {
|
||
logDirPath = QDir::fromNativeSeparators(logDirPath);
|
||
}
|
||
|
||
// 读取是否自动启用实时保存
|
||
bool autoRealTimeSave = settings.value("FileSave/autoRealTimeSave", false).toBool();
|
||
ui->realTimeSaveCheckBox->setChecked(autoRealTimeSave);
|
||
|
||
// 如果 autoRealTimeSave 为 true,则触发状态变化函数
|
||
if (autoRealTimeSave) {
|
||
on_realTimeSaveCheckBox_stateChanged(Qt::Checked);
|
||
}
|
||
|
||
// 自动选择串口号和波特率
|
||
int portIndex = ui->portComboBox->findText(port);
|
||
if (portIndex != -1) {
|
||
ui->portComboBox->setCurrentIndex(portIndex);
|
||
}
|
||
|
||
int baudRateIndex = ui->baudRateComboBox->findText(QString::number(baudRate));
|
||
if (baudRateIndex != -1) {
|
||
ui->baudRateComboBox->setCurrentIndex(baudRateIndex);
|
||
}
|
||
|
||
// 如果 autoOpen 为 true,则自动打开串口
|
||
if (autoOpen) {
|
||
on_openButton_clicked();
|
||
}
|
||
}
|
||
|
||
void MainWindow::initializeRecordCounts()
|
||
{
|
||
QString saveDirPath;
|
||
if (!defaultFilePath.isEmpty()) {
|
||
saveDirPath = defaultFilePath;
|
||
} else {
|
||
saveDirPath = QApplication::applicationDirPath();
|
||
}
|
||
|
||
QDir dir(saveDirPath);
|
||
if (!dir.exists()) {
|
||
return;
|
||
}
|
||
|
||
// 查找所有BeiDou_Data_*.dat文件
|
||
QStringList filters;
|
||
filters << "BeiDou_Data_*.dat";
|
||
QFileInfoList fileList = dir.entryInfoList(filters, QDir::Files);
|
||
|
||
foreach (const QFileInfo &fileInfo, fileList) {
|
||
QString fileName = fileInfo.baseName();
|
||
// 从文件名提取ID,例如:BeiDou_Data_001.dat -> 001
|
||
QString id = fileName.mid(12); // "BeiDou_Data_"长度为12
|
||
|
||
if (id.isEmpty()) continue;
|
||
|
||
// 读取文件,找到最大的RECORD值
|
||
QFile file(fileInfo.absoluteFilePath());
|
||
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||
QTextStream in(&file);
|
||
int maxRecord = 0;
|
||
int lineCount = 0;
|
||
|
||
while (!in.atEnd()) {
|
||
QString line = in.readLine();
|
||
lineCount++;
|
||
|
||
// 跳过前4行表头
|
||
if (lineCount <= 4) continue;
|
||
|
||
QStringList fields = line.split(',');
|
||
if (fields.size() >= 2) {
|
||
bool ok;
|
||
int recordValue = fields[1].toInt(&ok); // RECORD字段在第2列
|
||
if (ok && recordValue > maxRecord) {
|
||
maxRecord = recordValue;
|
||
}
|
||
}
|
||
}
|
||
file.close();
|
||
|
||
// 设置该ID的起始计数值
|
||
recordCounts[id] = maxRecord;
|
||
}
|
||
}
|
||
}
|
||
|
||
void MainWindow::closeEvent(QCloseEvent *event)
|
||
{
|
||
if (!isVisible()) {
|
||
event->accept();
|
||
return;
|
||
}
|
||
|
||
QMessageBox msgBox(this);
|
||
msgBox.setWindowTitle("关闭");
|
||
msgBox.setText("关闭窗口?");
|
||
//msgBox.setInformativeText("Choose 'Minimize' to keep it running in the system tray.");
|
||
msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
|
||
msgBox.setButtonText(QMessageBox::Yes, "最小化");
|
||
msgBox.setButtonText(QMessageBox::No, "关闭");
|
||
|
||
int ret = msgBox.exec();
|
||
|
||
if (ret == QMessageBox::Yes) {
|
||
// 最小化到托盘
|
||
hide();
|
||
trayIcon->show();
|
||
event->ignore(); // 忽略默认关闭操作
|
||
} else {
|
||
// 完全退出
|
||
trayIcon->hide();
|
||
event->accept();
|
||
}
|
||
}
|
||
|
||
MainWindow::~MainWindow()
|
||
{
|
||
// 记录程序关闭时间
|
||
if (logFile && logFile->isOpen()) {
|
||
QString shutdownTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
|
||
writeLog("Closed: " + shutdownTime);
|
||
logFile->close();
|
||
}
|
||
|
||
delete ui;
|
||
}
|
||
|
||
|
||
void MainWindow::timer_Event()
|
||
{
|
||
on_sendButton_clicked(); // 定时器事件触发时调用发送数据函数
|
||
}
|
||
|
||
void MainWindow::on_timerButton_clicked()
|
||
{
|
||
bool ok;
|
||
int interval = ui->timerIntervalLineEdit->text().toInt(&ok);
|
||
if (!ok || interval <= 0) {
|
||
QMessageBox::warning(this, tr("无效输入"), tr("请输入有效的正整数"));
|
||
return;
|
||
}
|
||
|
||
if (timer->isActive()) {
|
||
timer->stop();
|
||
ui->timerButton->setText("周期发送");
|
||
} else {
|
||
timer->start(interval);
|
||
ui->timerButton->setText("关闭周期发送");
|
||
}
|
||
}
|
||
|
||
void MainWindow::on_saveButton_clicked()
|
||
{
|
||
QString currentTime = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss");
|
||
QString defaultFileName = "savedata_" + currentTime + ".dat";
|
||
|
||
QString dirPath = defaultFilePath.isEmpty() ? "." : defaultFilePath;
|
||
|
||
QDir dir(dirPath);
|
||
if (!dir.exists()) {
|
||
dir.mkpath(".");
|
||
}
|
||
|
||
QString filePath = dir.filePath(defaultFileName);
|
||
|
||
QString fileName = QFileDialog::getSaveFileName(this, tr("Save File"), filePath, tr("dat Files (*.dat);;All Files (*)"));
|
||
if (fileName.isEmpty())
|
||
return;
|
||
|
||
QFile file(fileName);
|
||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||
QMessageBox::warning(this, tr("打开文件失败"), file.errorString());
|
||
return;
|
||
}
|
||
|
||
QTextStream out(&file);
|
||
|
||
// 从解析数据的第一行提取ID(范围1-5),用于动态表头
|
||
QString text = ui->decimalTextEdit->toPlainText();
|
||
QStringList lines = text.split('\n', Qt::SkipEmptyParts);
|
||
int headerIdNum = 1;
|
||
if (!lines.isEmpty()) {
|
||
QString firstLine = lines.first().trimmed();
|
||
QStringList f = firstLine.split(',', Qt::KeepEmptyParts);
|
||
if (!f.isEmpty()) {
|
||
QString idField = f.last().trimmed();
|
||
bool okId = false;
|
||
int idNum = idField.toInt(&okId);
|
||
if (okId && idNum >= 1 && idNum <= 5) {
|
||
headerIdNum = idNum;
|
||
}
|
||
}
|
||
}
|
||
QString lica = QString("LICA%1").arg(headerIdNum, 2, 10, QChar('0'));
|
||
QString idPad = QString::number(headerIdNum).rightJustified(5, '0');
|
||
|
||
// 添加动态表头和单位(基于ID)
|
||
out << "\"TOA5\",\"" << lica << "\",\"CR300\",\"" << headerIdNum
|
||
<< "\",\"CR1000X.Std.08.01\",\"CPU:HYR2019128_SDL_AWSl_BYP_15391.CR1000X\",\""
|
||
<< idPad << "\",\"Min_30\"\n";
|
||
out << "\"TIMESTAMP\",\"RECORD\",\"AirTC_Avg\",\"RH_Avg\",\"BP_mbar_Avg\",\"WS_ms_Avg\",\"WindDir_Avg\",\"WindDir_StDev\",\"SW_IN_Avg\",\"PAR_Avg\",\"Rain_mm_Tot\",\"ID\"\n";
|
||
out << "\"TS\",\"RN\",\"Deg C\",\"%\",\"mbar\",\"m/s\",\"Deg\",\"Deg\",\"m-2\",\"mol m-2 s-1\",\"mm\",\"\"\n";
|
||
out << "\"\",\"\",\"Avg\",\"Avg\",\"Avg\",\"Avg\",\"Avg\",\"Smp\",\"Avg\",\"Avg\",\"Smp\",\"Smp\"\n";
|
||
|
||
// 写入解析数据
|
||
out << text;
|
||
|
||
file.close();
|
||
}
|
||
|
||
|
||
void MainWindow::updatePortList()
|
||
{
|
||
ui->portComboBox->clear();
|
||
foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts())
|
||
{
|
||
ui->portComboBox->addItem(info.portName());
|
||
}
|
||
}
|
||
|
||
void MainWindow::updateBaudRateList()
|
||
{
|
||
ui->baudRateComboBox->clear();
|
||
QList<qint32> baudRates = QSerialPortInfo::standardBaudRates();
|
||
foreach (qint32 baudRate, baudRates)
|
||
{
|
||
ui->baudRateComboBox->addItem(QString::number(baudRate));
|
||
}
|
||
}
|
||
|
||
void MainWindow::on_openButton_clicked()
|
||
{
|
||
serial->setPortName(ui->portComboBox->currentText());
|
||
serial->setBaudRate(ui->baudRateComboBox->currentText().toInt());
|
||
|
||
// 明确设置串口参数:8N1 + 关闭流控
|
||
serial->setDataBits(QSerialPort::Data8);
|
||
serial->setStopBits(QSerialPort::OneStop);
|
||
serial->setParity(QSerialPort::NoParity);
|
||
serial->setFlowControl(QSerialPort::NoFlowControl);
|
||
|
||
if (serial->open(QIODevice::ReadWrite))
|
||
{
|
||
ui->statusLabel->setText("状态: 打开");
|
||
ui->openButton->setEnabled(false);
|
||
ui->closeButton->setEnabled(true);
|
||
}
|
||
else
|
||
{
|
||
QMessageBox::critical(this, tr("Error"), serial->errorString());
|
||
}
|
||
}
|
||
|
||
void MainWindow::on_closeButton_clicked()
|
||
{
|
||
if (serial->isOpen())
|
||
{
|
||
serial->close();
|
||
ui->statusLabel->setText("状态: 关闭");
|
||
ui->openButton->setEnabled(true);
|
||
ui->closeButton->setEnabled(false);
|
||
}
|
||
}
|
||
|
||
void MainWindow::on_sendButton_clicked()
|
||
{
|
||
if (serial->isOpen())
|
||
{
|
||
QByteArray data;
|
||
if (ui->hexSendCheckBox->isChecked())
|
||
{
|
||
QString hexData = ui->sendTextEdit->toPlainText().remove(QRegExp("[\\s\\n]"));
|
||
data = QByteArray::fromHex(hexData.toUtf8());
|
||
}
|
||
else
|
||
{
|
||
data = ui->sendTextEdit->toPlainText().toUtf8();
|
||
}
|
||
|
||
serial->write(data);
|
||
|
||
txCount += data.size(); // 更新发送计数器
|
||
txLabel->setText(QString("S: %1").arg(txCount));
|
||
}
|
||
else
|
||
{
|
||
QMessageBox::warning(this, tr("警告"), tr("串口未打开!"));
|
||
}
|
||
}
|
||
|
||
void MainWindow::on_clearSendButton_clicked()
|
||
{
|
||
ui->sendTextEdit->clear();
|
||
}
|
||
|
||
void MainWindow::on_clearReceiveButton_clicked()
|
||
{
|
||
ui->receiveTextEdit->clear();
|
||
|
||
txCount = 0;
|
||
rxCount = 0;
|
||
txLabel->setText("TX: 0");
|
||
rxLabel->setText("RX: 0");
|
||
}
|
||
|
||
void MainWindow::on_clearDecimalButton_clicked()
|
||
{
|
||
// 二次确认
|
||
QMessageBox::StandardButton reply;
|
||
reply = QMessageBox::question(this, tr("清除数据"),
|
||
tr("确定清除数据?"),
|
||
QMessageBox::Yes|QMessageBox::No);
|
||
if (reply == QMessageBox::Yes) {
|
||
ui->decimalTextEdit->clear();
|
||
} else {
|
||
//否,不做任何操作
|
||
}
|
||
}
|
||
|
||
void MainWindow::readData()
|
||
{
|
||
QByteArray data = serial->readAll();
|
||
QString strReceiveData = "";
|
||
|
||
//qDebug()<<data;
|
||
|
||
rxCount += data.size();
|
||
rxLabel->setText(QString("R: %1").arg(rxCount));
|
||
|
||
// 原始字节保留,用于其它解析(如 $BDTXR)
|
||
receiveBuffer.append(data);
|
||
|
||
// 将本次字节转为大写HEX并按字节加空格,追加到十六进制解析缓冲
|
||
{
|
||
QString hexString = QString::fromLatin1(data.toHex().toUpper());
|
||
QString spaced;
|
||
spaced.reserve(hexString.size() + hexString.size() / 2);
|
||
for (int i = 0; i < hexString.length(); i += 2) {
|
||
spaced += hexString.mid(i, 2);
|
||
spaced += ' ';
|
||
}
|
||
hexParseBuffer += spaced; // 统一使用空格分隔
|
||
}
|
||
|
||
// 接收区显示(与之前行为一致)
|
||
if (ui->hexDisplayCheckBox->isChecked()) {
|
||
// 直接显示本次的HEX(已带空格的spaced)
|
||
ui->receiveTextEdit->insertPlainText(hexParseBuffer.right(data.size() * 3)); // 每字节“XX ”三字符
|
||
} else {
|
||
displayBuffer.append(data);
|
||
QString displayText = QString::fromLocal8Bit(displayBuffer);
|
||
ui->receiveTextEdit->insertPlainText(displayText);
|
||
displayBuffer.clear();
|
||
}
|
||
ui->receiveTextEdit->moveCursor(QTextCursor::End);
|
||
|
||
// 基于十六进制缓冲实时解析 $TXXX
|
||
processHexTxxxFrames();
|
||
|
||
processHexBdtxrFrames(); // 新增:解析 $BDTXR
|
||
|
||
if (ui->hexDisplayCheckBox->checkState() == false) {
|
||
ui->receiveTextEdit->insertPlainText(data);
|
||
}
|
||
else {
|
||
//16进制显示,并转大写
|
||
QByteArray hexData = data.toHex();
|
||
strReceiveData = hexData.toUpper();
|
||
|
||
for(int i=0; i<strReceiveData.size(); i+=2+1)
|
||
strReceiveData.insert(i, QLatin1String(" "));
|
||
strReceiveData.remove(0, 1);
|
||
ui->receiveTextEdit->insertPlainText(strReceiveData);
|
||
}
|
||
// 移动光标到文本结尾
|
||
ui->receiveTextEdit->moveCursor(QTextCursor::End);
|
||
|
||
// 保留实时解析逻辑(仅处理 $TXXX 明文;$BDTXR 由十六进制缓冲解析)
|
||
QString all = QString::fromLatin1(receiveBuffer);
|
||
|
||
// 2) 解析 $TXXX 明文(满足10个逗号且最后ID在1..5)
|
||
int lastConsumedTXXX = 0;
|
||
int txxxIncompleteStart = -1;
|
||
int start = all.indexOf("$TXXX");
|
||
while (start != -1) {
|
||
int contentStart = start + 5;
|
||
QVector<int> commaPos;
|
||
commaPos.reserve(10);
|
||
int iScan = contentStart;
|
||
while (iScan < all.size() && commaPos.size() < 10) {
|
||
if (all.at(iScan) == ',') {
|
||
commaPos.push_back(iScan);
|
||
}
|
||
++iScan;
|
||
}
|
||
if (commaPos.size() < 10) { txxxIncompleteStart = start; break; }
|
||
|
||
int idStart = commaPos.back() + 1;
|
||
if (idStart >= all.size()) { txxxIncompleteStart = start; break; }
|
||
|
||
int j = idStart;
|
||
QString idToken;
|
||
while (j < all.size() && all.at(j).isDigit()) { idToken.append(all.at(j)); ++j; }
|
||
if (idToken.isEmpty()) { txxxIncompleteStart = start; break; }
|
||
|
||
bool okId = false; int idVal = idToken.toInt(&okId);
|
||
if (!okId || idVal < 1 || idVal > 5) {
|
||
writeLog(QString("Parse TXXX skipped: ID not in 1..5, id=%1").arg(idToken));
|
||
lastConsumedTXXX = j;
|
||
start = all.indexOf("$TXXX", j);
|
||
continue;
|
||
}
|
||
|
||
QString payload = all.mid(contentStart, j - contentStart).trimmed();
|
||
parseTxxxData(payload);
|
||
lastConsumedTXXX = j;
|
||
start = all.indexOf("$TXXX", j);
|
||
}
|
||
|
||
int allowedConsume = 0;
|
||
if (txxxIncompleteStart != -1) {
|
||
allowedConsume = txxxIncompleteStart;
|
||
} else {
|
||
allowedConsume = lastConsumedTXXX;
|
||
}
|
||
if (allowedConsume > 0) {
|
||
receiveBuffer = all.mid(allowedConsume).toLatin1();
|
||
}
|
||
}
|
||
|
||
void MainWindow::saveRealTimeData(const QString &data)
|
||
{
|
||
// ... existing code ...
|
||
if (!ui->realTimeSaveCheckBox->isChecked()) return;
|
||
|
||
QStringList lines = data.split('\n');
|
||
foreach (const QString &line, lines) {
|
||
if (line.isEmpty()) continue;
|
||
QStringList fields = line.split(',');
|
||
if (fields.size() >= 11) {
|
||
QString id = fields.last().trimmed(); // 最后一列作为ID
|
||
|
||
// 生成文件名:BeiDou_Data_<ID>.dat
|
||
QString defaultFileName = "BeiDou_Data_" + id + ".dat";
|
||
QString saveDirPath = defaultFilePath.isEmpty()
|
||
? QApplication::applicationDirPath()
|
||
: defaultFilePath;
|
||
|
||
QDir dir(saveDirPath);
|
||
if (!dir.exists()) {
|
||
dir.mkpath(".");
|
||
}
|
||
QString filePath = dir.filePath(defaultFileName);
|
||
|
||
// 首次遇到该ID:初始化计数并写表头(若新文件)
|
||
if (!recordCounts.contains(id)) {
|
||
bool fileExists = QFile::exists(filePath);
|
||
if (fileExists) {
|
||
int lastRecord = getLastRecordFromFile(filePath);
|
||
if (!recordCounts.contains(id)) {
|
||
recordCounts[id] = lastRecord;
|
||
}
|
||
writeLog("Open: " + defaultFileName + ", Last RECORD=" + QString::number(lastRecord));
|
||
} else {
|
||
// 新文件:初始化计数器为0,并写入表头(包含ID)
|
||
recordCounts[id] = 0;
|
||
QFile headerFile(filePath);
|
||
if (!headerFile.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
|
||
QMessageBox::warning(this, tr("Open File Failed"), headerFile.errorString());
|
||
writeLog("Open Failed: " + filePath + " - " + headerFile.errorString());
|
||
continue;
|
||
}
|
||
QTextStream hout(&headerFile);
|
||
// 动态表头:根据ID范围1-5生成 LICAxx、通道号和设备编号 000xx
|
||
bool okIdNum = false;
|
||
int idNum = id.toInt(&okIdNum);
|
||
if (!okIdNum || idNum < 1 || idNum > 5) idNum = 1;
|
||
QString lica = QString("LICA%1").arg(idNum, 2, 10, QChar('0'));
|
||
QString idPad = QString::number(idNum).rightJustified(5, '0');
|
||
hout << "\"TOA5\",\"" << lica << "\",\"CR300\",\"" << idNum
|
||
<< "\",\"CR1000X.Std.08.01\",\"CPU:HYR2019128_SDL_AWSl_BYP_15391.CR1000X\",\""
|
||
<< idPad << "\",\"Min_30\"\n";
|
||
hout << "\"TIMESTAMP\",\"RECORD\",\"AirTC_Avg\",\"RH_Avg\",\"BP_mbar_Avg\",\"WS_ms_Avg\",\"WindDir_Avg\",\"WindDir_StDev\",\"SW_IN_Avg\",\"PAR_Avg\",\"Rain_mm_Tot\",\"ID\"\n";
|
||
hout << "\"TS\",\"RN\",\"Deg C\",\"%\",\"mbar\",\"m/s\",\"Deg\",\"Deg\",\"m-2\",\"mol m-2 s-1\",\"mm\",\"\"\n";
|
||
hout << "\"\",\"\",\"Avg\",\"Avg\",\"Avg\",\"Avg\",\"Avg\",\"Smp\",\"Avg\",\"Avg\",\"Smp\",\"Smp\"\n";
|
||
headerFile.close();
|
||
writeLog("Create new file: " + defaultFileName + ", The header is written");
|
||
}
|
||
}
|
||
|
||
// 每次写入都打开、写入、关闭,释放文件占用
|
||
QFile file(filePath);
|
||
if (!file.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
|
||
QMessageBox::warning(this, tr("Open File Failed"), file.errorString());
|
||
writeLog("Open Failed: " + filePath + " - " + file.errorString());
|
||
continue;
|
||
}
|
||
QTextStream out(&file);
|
||
out << line << "\n";
|
||
file.close();
|
||
|
||
// 记录日志
|
||
QString timestamp = fields[0];
|
||
writeLog("Write Data: " + defaultFileName + ", " + timestamp + ", ID: " + id);
|
||
}
|
||
}
|
||
// ... existing code ...
|
||
}
|
||
|
||
void MainWindow::on_realTimeSaveCheckBox_stateChanged(int state)
|
||
{
|
||
if (state == Qt::Checked) {
|
||
// Already handled in saveRealTimeData, no action needed here
|
||
} else {
|
||
// Close and remove all files from the map
|
||
for (auto it = realTimeFiles.begin(); it != realTimeFiles.end(); ++it) {
|
||
if (it.value()->isOpen()) {
|
||
it.value()->close();
|
||
}
|
||
delete it.value();
|
||
}
|
||
realTimeFiles.clear();
|
||
}
|
||
}
|
||
|
||
|
||
|
||
QString MainWindow::hexToAscii(const QString& hexStr)
|
||
{
|
||
QString asciiStr;
|
||
for (int i = 0; i < hexStr.length(); i += 2) {
|
||
QString hexByte = hexStr.mid(i, 2);
|
||
char byte = static_cast<char>(hexByte.toInt(nullptr, 16));
|
||
asciiStr.append(byte);
|
||
}
|
||
return asciiStr;
|
||
}
|
||
/*
|
||
//25/7/1;修改,"2025-06-18 19:30:00" 并添加双引号
|
||
void MainWindow::parseBeidouData(const QString& beidouData)
|
||
{
|
||
// 匹配常见时间格式:例如 "11/20/2024 12:30:00"
|
||
QRegExp timePattern("\\d{1,2}/\\d{1,2}/\\d{4} \\d{1,2}:\\d{2}:\\d{2}");
|
||
int pos = timePattern.indexIn(beidouData);
|
||
|
||
if (pos != -1) {
|
||
QString validData = beidouData.mid(pos);
|
||
QStringList fields = validData.split(',');
|
||
|
||
if (fields.size() >= 11) { // 确保至少有 11 个字段
|
||
QString timestamp = fields[0]; // 时间
|
||
|
||
// 标准化时间格式为 "2025-06-18 19:30:00" 并添加双引号
|
||
QDateTime dateTime = QDateTime::fromString(timestamp, "M/d/yyyy h:mm:ss");
|
||
if (dateTime.isValid()) {
|
||
timestamp = "\"" + dateTime.toString("yyyy-MM-dd hh:mm:ss") + "\"";
|
||
} else {
|
||
// 如果解析失败,仍然添加双引号
|
||
timestamp = "\"" + timestamp + "\"";
|
||
}
|
||
|
||
QString id = fields.last(); // ID
|
||
|
||
// 为当前ID递增记录计数
|
||
if (!recordCounts.contains(id)) {
|
||
recordCounts[id] = 0;
|
||
}
|
||
recordCounts[id]++;
|
||
QString recordField = QString::number(recordCounts[id]);
|
||
|
||
QStringList middleFields;
|
||
for (int i = 1; i < fields.size() - 1; ++i) {
|
||
bool ok;
|
||
double value = fields[i].toDouble(&ok);
|
||
if (ok) {
|
||
middleFields << QString::number(value / 10, 'f', 1); // 保留一位小数
|
||
} else {
|
||
middleFields << fields[i]; // 非数字保持原样
|
||
}
|
||
}
|
||
|
||
// 拼接处理后的数据,在时间戳后插入RECORD字段
|
||
QString output = timestamp + "," + recordField + "," + middleFields.join(",") + "," + id;
|
||
|
||
// 显示并保存
|
||
ui->decimalTextEdit->appendPlainText(output);
|
||
saveRealTimeData(output);
|
||
} else {
|
||
qDebug() << "字段数量不足";
|
||
}
|
||
}
|
||
else if (beidouData.length() > 7 && beidouData.contains("$BDTXR,") && beidouData.contains('*')) {
|
||
int startIndex = beidouData.indexOf("A4") + 2;
|
||
int endIndex = beidouData.indexOf('*', startIndex);
|
||
QString dataSegment = beidouData.mid(startIndex, endIndex - startIndex);
|
||
|
||
QString asciiData = hexToAscii(dataSegment);
|
||
QStringList fields = asciiData.split(',');
|
||
|
||
if (fields.size() >= 11) {
|
||
QString timestamp = fields[0];
|
||
|
||
// 标准化时间格式
|
||
QDateTime dateTime = QDateTime::fromString(timestamp, "M/d/yyyy h:mm:ss");
|
||
if (dateTime.isValid()) {
|
||
timestamp = dateTime.toString("MM/dd/yyyy hh:mm:ss");
|
||
}
|
||
QString id = fields.last();
|
||
|
||
QStringList middleFields;
|
||
for (int i = 1; i < fields.size() - 1; ++i) {
|
||
bool ok;
|
||
double value = fields[i].toDouble(&ok);
|
||
if (ok) {
|
||
middleFields << QString::number(value / 10, 'f', 1);
|
||
} else {
|
||
middleFields << fields[i];
|
||
}
|
||
}
|
||
|
||
QString output = timestamp + "," + middleFields.join(",") + "," + id;
|
||
|
||
ui->decimalTextEdit->appendPlainText(output);
|
||
saveRealTimeData(output);
|
||
} else {
|
||
qDebug() << "ASCII 数据字段数量不足";
|
||
}
|
||
} else {
|
||
qDebug() << "数据格式不匹配";
|
||
}
|
||
}
|
||
*/
|
||
|
||
|
||
void MainWindow::parseBeidouData(const QString& beidouData)
|
||
{
|
||
// 在输入中查找每条 $BDTXR 帧:$BDTXR,...*HH
|
||
QRegExp frameRx("\\$BDTXR[^\\r\\n]*\\*[0-9A-Fa-f]{2}");
|
||
int pos = 0;
|
||
QString input = beidouData;
|
||
|
||
while ((pos = frameRx.indexIn(input, pos)) != -1) {
|
||
QString frame = frameRx.cap(0).trimmed();
|
||
pos += frameRx.matchedLength();
|
||
|
||
int star = frame.lastIndexOf('*');
|
||
if (star < 0) continue;
|
||
|
||
// 去掉开头的 $ 和末尾的 *HH,得到核心数据
|
||
QString core = frame.mid(1, star - 1);
|
||
QString checksumStr = frame.mid(star + 1, 2).toUpper();
|
||
|
||
// 可选:按 NMEA 规则校验 XOR 校验和
|
||
quint8 cs = 0;
|
||
for (int i = 0; i < core.size(); ++i) {
|
||
cs ^= static_cast<uchar>(core.at(i).toLatin1());
|
||
}
|
||
QString calc = QString("%1").arg(cs, 2, 16, QChar('0')).toUpper();
|
||
bool checksumOk = (checksumStr == calc);
|
||
|
||
// 按 3-11TXR 字段切分:$BDTXR,信息类别,ID,电文形式,发信时间,通信电文内容
|
||
QStringList tok = core.split(',', Qt::KeepEmptyParts);
|
||
if (tok.size() < 6) {
|
||
continue;
|
||
}
|
||
|
||
QString infoType = tok[1].trimmed();
|
||
QString bdId = tok[2].trimmed(); // 来自报文的 ID,不再用于文件命名
|
||
QString textForm = tok[3].trimmed();
|
||
QString sendTime = tok[4].trimmed();
|
||
QString contentAll = QStringList(tok.mid(5)).join(",");
|
||
|
||
// 将内容按规则解码:A+hex 表示 ASCII 的十六进制编码
|
||
auto cleanHex = [](const QString &s) {
|
||
QString r; r.reserve(s.size());
|
||
for (QChar ch : s) {
|
||
if ((ch >= '0' && ch <= '9') ||
|
||
(ch >= 'a' && ch <= 'f') ||
|
||
(ch >= 'A' && ch <= 'F')) {
|
||
r.append(ch);
|
||
}
|
||
}
|
||
return r;
|
||
};
|
||
|
||
QString payload;
|
||
if (contentAll.startsWith("A", Qt::CaseInsensitive)) {
|
||
QString hex = cleanHex(contentAll.mid(1));
|
||
if (hex.size() % 2 == 1) {
|
||
// 对齐为偶数字节,容错处理
|
||
hex = hex.mid(1);
|
||
}
|
||
payload = hexToAscii(hex);
|
||
} else {
|
||
payload = contentAll;
|
||
}
|
||
|
||
// payload 示例:11/25/2025 15:30:00,-45,76,6395,49,459,102,4861,9320,0,1
|
||
QStringList fields = payload.split(',', Qt::KeepEmptyParts);
|
||
if (fields.size() < 2) {
|
||
continue;
|
||
}
|
||
|
||
// 标准化时间为 "yyyy-MM-dd hh:mm:ss"
|
||
QString tsRaw = fields[0].trimmed();
|
||
QDateTime dt = QDateTime::fromString(tsRaw, "M/d/yyyy h:mm:ss");
|
||
if (!dt.isValid()) dt = QDateTime::fromString(tsRaw, "MM/dd/yyyy hh:mm:ss");
|
||
QString tsOut = dt.isValid()
|
||
? "\"" + dt.toString("yyyy-MM-dd hh:mm:ss") + "\""
|
||
: "\"" + tsRaw + "\"";
|
||
|
||
// 数值处理:前面的测量值除以10,最后一项作为ID不除以10
|
||
QStringList values;
|
||
for (int i = 1; i <= fields.size() - 2; ++i) { // 到倒数第二项(例如雨量)
|
||
QString v = fields[i].trimmed();
|
||
bool okNum = false;
|
||
double num = v.toDouble(&okNum);
|
||
if (okNum) {
|
||
values << QString::number(num / 10.0, 'f', 1);
|
||
} else {
|
||
values << v;
|
||
}
|
||
}
|
||
// 最后一项作为 ID(不除以10)
|
||
QString idData = fields.last().trimmed();
|
||
|
||
// 按 ID 递增 RECORD(基于最后一项 ID)
|
||
if (!recordCounts.contains(idData)) {
|
||
recordCounts[idData] = 0;
|
||
}
|
||
recordCounts[idData] += 1;
|
||
QString recordStr = QString::number(recordCounts[idData]);
|
||
|
||
// 输出格式:TIMESTAMP,RECORD,<10个测量值>,ID
|
||
QString output = tsOut + "," + recordStr + "," + values.join(",") + "," + idData;
|
||
|
||
ui->decimalTextEdit->appendPlainText(output);
|
||
saveRealTimeData(output);
|
||
|
||
// 记录日志(可选)
|
||
if (logFile && logFile->isOpen()) {
|
||
writeLog(QString("Parse TXR: ID=%1, checksum=%2").arg(idData, checksumOk ? "OK" : "SKIP"));
|
||
}
|
||
}
|
||
}
|
||
|
||
int MainWindow::getLastRecordFromFile(const QString &filePath)
|
||
{
|
||
QFile file(filePath);
|
||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||
return 0; // 打不开文件则从 0 开始
|
||
}
|
||
|
||
QTextStream in(&file);
|
||
int maxRecord = 0;
|
||
int lineCount = 0;
|
||
|
||
while (!in.atEnd()) {
|
||
QString line = in.readLine();
|
||
if (line.isEmpty()) {
|
||
continue;
|
||
}
|
||
lineCount++;
|
||
|
||
// 跳过前 4 行表头
|
||
if (lineCount <= 4) {
|
||
continue;
|
||
}
|
||
|
||
// 解析第二列 RECORD
|
||
QStringList fields = line.split(',');
|
||
if (fields.size() >= 2) {
|
||
bool ok = false;
|
||
int recordValue = fields[1].toInt(&ok);
|
||
if (ok && recordValue > maxRecord) {
|
||
maxRecord = recordValue;
|
||
}
|
||
}
|
||
}
|
||
|
||
file.close();
|
||
return maxRecord;
|
||
}
|
||
|
||
void MainWindow::parseTxxxData(const QString& payload)
|
||
{
|
||
// 必须有10个逗号(总计11个字段)才解析
|
||
if (payload.count(',') < 10) {
|
||
// 不完整数据:不进入解析区、不保存;接收区已经显示原始数据
|
||
writeLog("Parse TXXX skipped: comma count < 10, payload=\"" + payload + "\"");
|
||
return;
|
||
}
|
||
|
||
QStringList fields = payload.split(',', Qt::KeepEmptyParts);
|
||
if (fields.size() != 11) {
|
||
// 字段数不等于11,同样视为不完整
|
||
writeLog("Parse TXXX skipped: fields.size()!=11, payload=\"" + payload + "\"");
|
||
return;
|
||
}
|
||
|
||
// 时间一律使用系统接收当前时间(补全策略)
|
||
QString tsOut = "\"" + QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss") + "\"";
|
||
|
||
// 第1个字段为时间占位,不使用;后面10个字段中最后一个为ID
|
||
QStringList valueTokens = fields.mid(1); // 长度应为10
|
||
QString id = valueTokens.last().trimmed();
|
||
|
||
// ID 为空则不解析不保存(只显示在接收区)
|
||
if (id.isEmpty()) {
|
||
writeLog("Parse TXXX skipped: missing ID, payload=\"" + payload + "\"");
|
||
return;
|
||
}
|
||
|
||
// 前9个测量值需要除以10
|
||
QStringList values;
|
||
for (int i = 0; i < valueTokens.size() - 1; ++i) {
|
||
QString v = valueTokens[i].trimmed();
|
||
bool ok = false;
|
||
double num = v.toDouble(&ok);
|
||
values << (ok ? QString::number(num / 10.0, 'f', 1) : v);
|
||
}
|
||
|
||
// 记录递增按最终ID单独计数
|
||
if (!recordCounts.contains(id)) {
|
||
recordCounts[id] = 0;
|
||
}
|
||
recordCounts[id] += 1;
|
||
QString recordStr = QString::number(recordCounts[id]);
|
||
|
||
// 输出到解析区,并保存
|
||
QString output = tsOut + "," + recordStr + "," + values.join(",") + "," + id;
|
||
ui->decimalTextEdit->appendPlainText(output);
|
||
saveRealTimeData(output);
|
||
|
||
writeLog(QString("Parse TXXX: OK, ID=%1").arg(id));
|
||
}
|
||
|
||
|
||
void MainWindow::processHexTxxxFrames()
|
||
{
|
||
// 解析十六进制缓冲中的 $TXXX 帧:头 24 54 58 58 58,至少10个 2C,尾部 ,(32~35)
|
||
if (hexParseBuffer.isEmpty()) return;
|
||
|
||
auto hexToByte = [](const QString &h, bool &ok) -> int {
|
||
int v = h.toInt(&ok, 16);
|
||
return ok ? v : 0;
|
||
};
|
||
|
||
QStringList tk = hexParseBuffer.split(' ', Qt::SkipEmptyParts);
|
||
bool progressed = false;
|
||
|
||
while (true) {
|
||
// 寻找头部 24 54 58 58 58($TXXX)
|
||
int hdr = -1;
|
||
for (int i = 0; i + 4 < tk.size(); ++i) {
|
||
if (tk[i] == "24" && tk[i+1] == "54" && tk[i+2] == "58" && tk[i+3] == "58" && tk[i+4] == "58") {
|
||
hdr = i;
|
||
break;
|
||
}
|
||
}
|
||
if (hdr < 0) break; // 没有头部,等待更多数据
|
||
|
||
// 统计逗号,并寻找尾部ID(0x31~0x35),逗号数>=10
|
||
int commaCount = 0;
|
||
int tailIdx = -1; // 指向ID字节的token索引
|
||
for (int j = hdr + 5; j < tk.size(); ++j) {
|
||
if (tk[j] == "2C") {
|
||
++commaCount;
|
||
if (j + 1 < tk.size()) {
|
||
const QString &idHex = tk[j + 1];
|
||
if ((idHex == "31" || idHex == "32" || idHex == "33" || idHex == "34" || idHex == "35") && commaCount >= 10) {
|
||
// 将尾部判定为 ,ID 后面不是继续出现可打印ASCII(一般是0x00/校验/下一个帧)
|
||
bool okNext = false;
|
||
int nextVal = (j + 2 < tk.size()) ? tk[j + 2].toInt(&okNext, 16) : -1;
|
||
if ((j + 2 >= tk.size()) || !okNext || !(nextVal >= 0x20 && nextVal <= 0x7E)) {
|
||
tailIdx = j + 1;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (tailIdx < 0) {
|
||
// 数据不足以形成完整帧;丢弃头之前的噪声,保留从头开始的数据
|
||
if (hdr > 0) {
|
||
tk.erase(tk.begin(), tk.begin() + hdr);
|
||
progressed = true;
|
||
}
|
||
break;
|
||
}
|
||
|
||
// 从头到尾提取负载的ASCII(白名单:数字/空格/斜杠/冒号/逗号/负号)
|
||
QByteArray asciiPayloadBytes;
|
||
for (int k = hdr + 5; k <= tailIdx; ++k) {
|
||
bool ok = false;
|
||
int v = hexToByte(tk[k], ok);
|
||
if (!ok) continue;
|
||
if ((v >= 0x30 && v <= 0x39) || v == 0x20 || v == 0x2F || v == 0x3A || v == 0x2C || v == 0x2D) {
|
||
asciiPayloadBytes.append(char(v));
|
||
}
|
||
}
|
||
QString asciiPayload = QString::fromLatin1(asciiPayloadBytes);
|
||
|
||
// 最少10个逗号校验(冗余校验)
|
||
if (asciiPayload.count(',') >= 10) {
|
||
// 使用系统接收时间进行补全的解析函数
|
||
parseTxxxData(asciiPayload);
|
||
}
|
||
|
||
// 消费本帧对应的token,避免重复解析;保留尾部后的校验/分隔字节在缓冲中
|
||
tk.erase(tk.begin(), tk.begin() + (tailIdx + 1));
|
||
progressed = true;
|
||
// 继续寻找下一帧
|
||
}
|
||
|
||
if (progressed) {
|
||
hexParseBuffer = tk.join(" ");
|
||
if (!hexParseBuffer.isEmpty())
|
||
hexParseBuffer.append(' ');
|
||
}
|
||
}
|
||
|
||
void MainWindow::processHexBdtxrFrames()
|
||
{
|
||
// 基于十六进制缓冲的 $BDTXR 分帧与解析:头 24 42 44 54 58 52($BDTXR),尾部 *(2A)+ 两位 ASCII 十六进制校验和
|
||
if (hexParseBuffer.isEmpty()) return;
|
||
|
||
QStringList tk = hexParseBuffer.split(' ', Qt::SkipEmptyParts);
|
||
bool progressed = false;
|
||
|
||
auto hexByte = [](const QString &h, bool &ok) -> int {
|
||
int v = h.toInt(&ok, 16);
|
||
return ok ? v : -1;
|
||
};
|
||
auto isAsciiHexDigit = [&](const QString &t) -> bool {
|
||
bool ok = false;
|
||
int v = hexByte(t, ok);
|
||
if (!ok || v < 0) return false;
|
||
QChar ch(static_cast<uchar>(v));
|
||
ch = ch.toUpper();
|
||
return (ch.isDigit() || (ch >= 'A' && ch <= 'F'));
|
||
};
|
||
|
||
while (true) {
|
||
// 寻找头部 24 42 44 54 58 52($BDTXR)
|
||
int hdr = -1;
|
||
for (int i = 0; i + 5 < tk.size(); ++i) {
|
||
if (tk[i] == "24" && tk[i+1] == "42" && tk[i+2] == "44" && tk[i+3] == "54" && tk[i+4] == "58" && tk[i+5] == "52") {
|
||
hdr = i;
|
||
break;
|
||
}
|
||
}
|
||
if (hdr < 0) break;
|
||
|
||
// 寻找尾部:'*' (2A) + 两位 ASCII 十六进制校验
|
||
int star = -1;
|
||
for (int j = hdr + 6; j < tk.size(); ++j) {
|
||
if (tk[j] == "2A") { star = j; break; }
|
||
}
|
||
if (star < 0) {
|
||
// 数据未完整,保留从头开始的内容,丢弃头前噪声
|
||
if (hdr > 0) {
|
||
tk.erase(tk.begin(), tk.begin() + hdr);
|
||
progressed = true;
|
||
}
|
||
break;
|
||
}
|
||
if (star + 2 >= tk.size()) {
|
||
// 尚未收到完整校验和
|
||
if (hdr > 0) {
|
||
tk.erase(tk.begin(), tk.begin() + hdr);
|
||
progressed = true;
|
||
}
|
||
break;
|
||
}
|
||
if (!isAsciiHexDigit(tk[star + 1]) || !isAsciiHexDigit(tk[star + 2])) {
|
||
// 格式异常,跳过该星号后继续
|
||
tk.erase(tk.begin(), tk.begin() + star + 1);
|
||
progressed = true;
|
||
continue;
|
||
}
|
||
|
||
int tail = star + 2; // 到校验和最后一个字符为止
|
||
// 可选消费 CRLF
|
||
int endIdx = tail;
|
||
if (endIdx + 2 < tk.size() && tk[endIdx + 1] == "0D" && tk[endIdx + 2] == "0A") {
|
||
endIdx = tail + 2;
|
||
}
|
||
|
||
// 将 hdr..tail 这段转回 ASCII 形成完整帧 "$BDTXR,...*HH"
|
||
QByteArray asciiBytes;
|
||
for (int k = hdr; k <= tail; ++k) {
|
||
bool ok = false;
|
||
int v = hexByte(tk[k], ok);
|
||
if (!ok || v < 0) continue;
|
||
asciiBytes.append(char(v));
|
||
}
|
||
QString asciiFrame = QString::fromLatin1(asciiBytes);
|
||
|
||
// 交给现有解析逻辑(含字段拆分与保存)
|
||
parseBeidouData(asciiFrame);
|
||
|
||
// 消费本帧及其后续 CRLF(若存在)
|
||
tk.erase(tk.begin(), tk.begin() + (endIdx + 1));
|
||
progressed = true;
|
||
}
|
||
|
||
if (progressed) {
|
||
hexParseBuffer = tk.join(" ");
|
||
if (!hexParseBuffer.isEmpty())
|
||
hexParseBuffer.append(' ');
|
||
}
|
||
}
|
||
|
||
void MainWindow::openConfigDir()
|
||
{
|
||
QString appDir = QApplication::applicationDirPath();
|
||
QString tryAppPath = QDir(appDir).filePath("config.ini");
|
||
QString filePath;
|
||
|
||
if (QFileInfo(tryAppPath).exists()) {
|
||
filePath = QFileInfo(tryAppPath).absoluteFilePath();
|
||
} else {
|
||
QFileInfo cfg("config.ini");
|
||
if (cfg.exists()) {
|
||
filePath = cfg.absoluteFilePath();
|
||
}
|
||
}
|
||
|
||
if (filePath.isEmpty()) {
|
||
QMessageBox::warning(this, tr("未找到配置文件"), tr("未找到配置文件:config.ini"));
|
||
return;
|
||
}
|
||
|
||
QDesktopServices::openUrl(QUrl::fromLocalFile(filePath));
|
||
}
|
||
|
||
void MainWindow::openRealTimeSaveDir()
|
||
{
|
||
// 实时保存目录:defaultFilePath(若为空则程序目录)
|
||
QString dirPath = defaultFilePath.isEmpty()
|
||
? QApplication::applicationDirPath()
|
||
: defaultFilePath;
|
||
|
||
QDir dir(dirPath);
|
||
if (!dir.exists()) {
|
||
dir.mkpath(".");
|
||
}
|
||
|
||
QDesktopServices::openUrl(QUrl::fromLocalFile(dirPath));
|
||
} |