Files
BeiDou/mainwindow.cpp
2026-04-23 11:45:32 +08:00

1209 lines
41 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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; // 没有头部,等待更多数据
// 统计逗号并寻找尾部ID0x31~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));
}