#include "mainwindow.h" #include "ui_mainwindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 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()<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; ireceiveTextEdit->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 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_.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(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(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(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)); }