在Qt中使用MQTT(二)

在Qt中使用MQTT(二)

在上一篇中,我们简单使用了MQTT,现在我们做一个更复杂一点的程序。

一、配置项目

1.创建项目DeviceMonitor,并且配置CMakeList.txt,如下图:

项目中使用了Charts模块和Mqtt模块,需要加载进来。

二、使用MQTT

项目代码如下:

MainWindow.h

c++ 复制代码
#ifndef DEVICEMONITOR_H
#define DEVICEMONITOR_H

#include <QMainWindow>
#include <QMqttClient>
#include <QMap>
#include <QDateTime>
#include <QMqttSubscription>
#include <QMqttTopicFilter>
#include <QValueAxis>

QT_BEGIN_NAMESPACE
class QTableWidget;
class QPushButton;
class QTextEdit;
class QLineEdit;
class QLabel;
class QProgressBar;
class QTimer;
class QChart;
class QChartView;
class QLineSeries;
QT_END_NAMESPACE

// 设备数据结构
struct DeviceInfo {
    QString id;           // 设备ID
    QString name;         // 设备名称
    QString status;       // 在线/离线/运行中/故障
    double temperature;   // 温度
    double humidity;      // 湿度
    double voltage;       // 电压
    QDateTime lastSeen;   // 最后通信时间
    int alertCount;       // 告警次数
};

class DeviceMonitor : public QMainWindow {
    Q_OBJECT
public:
    DeviceMonitor(QWidget *parent = nullptr);
    ~DeviceMonitor();

private slots:
    void onConnectClicked();
    void onDisconnectClicked();
    void onMqttConnected();
    void onMqttDisconnected();
    void onMessageReceived(const QByteArray &message, const QMqttTopicName &topic);
    void onCheckDevicesTimeout();      // 定时检查设备离线
    void onClearAlertsClicked();        // 清除告警
    void onExportLogClicked();          // 导出日志

private:
    void setupUI();
    void setupStyle();
    void parseDeviceData(const QString &deviceId, const QJsonObject &json);
    void checkThresholds(const QString &deviceId, const DeviceInfo &device);
    void addAlert(const QString &deviceId, const QString &alertType, const QString &message);
    void updateDeviceTable();
    void updateStatusBar();
    void updateChart(double temperature, double humidity);
    void autoScaleYAxis();
    QString getStatusColor(const QString &status);

    // MQTT
    QMqttClient *m_client;
    bool m_connected = false;

    // 设备数据
    QMap<QString, DeviceInfo> m_devices;
    QStringList m_alertHistory;

    // UI 控件
    QLineEdit *m_hostEdit;
    QLineEdit *m_portEdit;
    QPushButton *m_connectBtn;
    QPushButton *m_disconnectBtn;
    QLabel *m_statusLabel;

    // 设备表格
    QTableWidget *m_deviceTable;

    // 实时曲线
    QChart *m_chart;
    QChartView *m_chartView;
    QLineSeries *m_tempSeries;
    QLineSeries *m_humiditySeries;

    // 告警区域
    QTextEdit *m_alertLog;
    QPushButton *m_clearAlertBtn;
    QPushButton *m_exportBtn;
    QLabel *m_alertCountLabel;

    // 统计标签
    QLabel *m_totalDeviceLabel;
    QLabel *m_onlineLabel;
    QLabel *m_offlineLabel;
    QLabel *m_faultLabel;

    // 定时器
    QTimer *m_checkTimer;

    // 曲线数据管理
    static int m_timeCounter;           // 时间计数器(静态,跨消息保持)
    static const int MAX_POINTS = 100;  // 最大保留点数
    QValueAxis *m_axisX = nullptr;
    QValueAxis *m_axisY = nullptr;
};

#endif

MainWindow.cpp

C++ 复制代码
#include "mainwindow.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
#include <QTableWidget>
#include <QTableWidgetItem>
#include <QPushButton>
#include <QLineEdit>
#include <QLabel>
#include <QProgressBar>
#include <QTextEdit>
#include <QGroupBox>
#include <QHeaderView>
#include <QMessageBox>
#include <QFileDialog>
#include <QFile>
#include <QTextStream>
#include <QTimer>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QDateTime>
#include <QGraphicsDropShadowEffect>
#include <QtCharts/QChart>
#include <QtCharts/QChartView>
#include <QtCharts/QLineSeries>
#include <QtCharts/QValueAxis>
#include <QApplication>

int DeviceMonitor::m_timeCounter = 0;
const int DeviceMonitor::MAX_POINTS;

DeviceMonitor::DeviceMonitor(QWidget *parent)
    : QMainWindow(parent)
    , m_client(new QMqttClient(this))
{
    setupUI();
    setupStyle();

    // MQTT 信号连接
    connect(m_client, &QMqttClient::connected, this, &DeviceMonitor::onMqttConnected);
    connect(m_client, &QMqttClient::disconnected, this, &DeviceMonitor::onMqttDisconnected);
    connect(m_client, &QMqttClient::messageReceived, this, &DeviceMonitor::onMessageReceived);

    // 定时检查设备离线(每30秒)
    m_checkTimer = new QTimer(this);
    connect(m_checkTimer, &QTimer::timeout, this, &DeviceMonitor::onCheckDevicesTimeout);
}

DeviceMonitor::~DeviceMonitor() = default;

void DeviceMonitor::setupUI() {
    auto *central = new QWidget(this);
    auto *mainLayout = new QVBoxLayout(central);
    mainLayout->setSpacing(16);
    mainLayout->setContentsMargins(20, 20, 20, 20);

    // ==================== 顶部连接栏 ====================
    auto *connGroup = new QGroupBox("MQTT 服务器连接", this);
    auto *connLayout = new QHBoxLayout(connGroup);
    m_hostEdit = new QLineEdit("broker.hivemq.com", this);
    m_portEdit = new QLineEdit("1883", this);
    m_portEdit->setMaximumWidth(80);
    m_connectBtn = new QPushButton("🔌 连接", this);
    m_disconnectBtn = new QPushButton("⏹ 断开", this);
    m_disconnectBtn->setEnabled(false);
    m_statusLabel = new QLabel("● 未连接", this);
    m_statusLabel->setStyleSheet("color: #b2bec3; font-weight: bold;");

    connLayout->addWidget(new QLabel("服务器:"));
    connLayout->addWidget(m_hostEdit);
    connLayout->addWidget(new QLabel("端口:"));
    connLayout->addWidget(m_portEdit);
    connLayout->addWidget(m_connectBtn);
    connLayout->addWidget(m_disconnectBtn);
    connLayout->addStretch();
    connLayout->addWidget(m_statusLabel);

    // ==================== 统计卡片 ====================
    auto *statsLayout = new QHBoxLayout();
    auto createStatCard = [&](const QString &title, const QString &color) -> QLabel* {
        auto *card = new QWidget(this);
        card->setStyleSheet(QString(R"(
            QWidget {
                background-color: #ffffff;
                border-radius: 10px;
                border-left: 4px solid %1;
            }
        )").arg(color));
        auto *effect = new QGraphicsDropShadowEffect(card);
        effect->setBlurRadius(15);
        effect->setColor(QColor(0,0,0,25));
        effect->setOffset(0, 3);
        card->setGraphicsEffect(effect);

        auto *layout = new QVBoxLayout(card);
        auto *titleLabel = new QLabel(title, this);
        titleLabel->setStyleSheet("color: #636e72; font-size: 11px;");
        auto *valueLabel = new QLabel("0", this);
        valueLabel->setStyleSheet("color: #2d3436; font-size: 24px; font-weight: bold;");
        layout->addWidget(titleLabel);
        layout->addWidget(valueLabel);
        layout->setContentsMargins(16, 12, 16, 12);

        statsLayout->addWidget(card);
        return valueLabel;
    };

    m_totalDeviceLabel = createStatCard("设备总数", "#0984e3");
    m_onlineLabel = createStatCard("在线", "#00b894");
    m_offlineLabel = createStatCard("离线", "#fdcb6e");
    m_faultLabel = createStatCard("故障", "#d63031");

    // ==================== 中部:设备表格 + 实时曲线 ====================
    auto *midLayout = new QHBoxLayout();

    // 左侧设备表格
    auto *tableGroup = new QGroupBox("设备状态监控", this);
    auto *tableLayout = new QVBoxLayout(tableGroup);
    m_deviceTable = new QTableWidget(this);
    m_deviceTable->setColumnCount(7);
    m_deviceTable->setHorizontalHeaderLabels(
        QStringList() << "设备ID" << "名称" << "状态" << "温度(°C)" << "湿度(%)" << "电压(V)" << "最后通信");
    m_deviceTable->horizontalHeader()->setStretchLastSection(true);
    m_deviceTable->setSelectionBehavior(QAbstractItemView::SelectRows);
    m_deviceTable->setAlternatingRowColors(true);
    m_deviceTable->setMinimumWidth(600);
    tableLayout->addWidget(m_deviceTable);

    // 右侧实时曲线
    auto *chartGroup = new QGroupBox("实时数据曲线", this);
    auto *chartLayout = new QVBoxLayout(chartGroup);

    m_chart = new QChart();
    m_chart->setTitle("温度 / 湿度 趋势");
    m_chart->legend()->setVisible(true);

    m_tempSeries = new QLineSeries();
    m_tempSeries->setName("温度 °C");
    m_humiditySeries = new QLineSeries();
    m_humiditySeries->setName("湿度 %");

    m_chart->addSeries(m_tempSeries);
    m_chart->addSeries(m_humiditySeries);

    m_axisX = new QValueAxis();
    m_axisX->setTitleText("时间");
    m_axisX->setLabelFormat("%d");
    m_axisX->setRange(0, MAX_POINTS);
    m_axisX->setTickCount(11);

    m_axisY = new QValueAxis();
    m_axisY->setTitleText("数值");
    m_axisY->setRange(0, 100);
    m_axisY->setTickCount(11);

    m_chart->addAxis(m_axisX, Qt::AlignBottom);
    m_chart->addAxis(m_axisY, Qt::AlignLeft);

    m_tempSeries->attachAxis(m_axisX);
    m_tempSeries->attachAxis(m_axisY);
    m_humiditySeries->attachAxis(m_axisX);
    m_humiditySeries->attachAxis(m_axisY);

    m_chartView = new QChartView(m_chart);
    m_chartView->setRenderHint(QPainter::Antialiasing);
    m_chartView->setMinimumWidth(400);
    chartLayout->addWidget(m_chartView);

    midLayout->addWidget(tableGroup, 2);
    midLayout->addWidget(chartGroup, 1);




    // ==================== 底部告警区域 ====================
    auto *alertGroup = new QGroupBox("故障预警中心", this);
    auto *alertLayout = new QVBoxLayout(alertGroup);

    auto *alertBtnLayout = new QHBoxLayout();
    m_alertCountLabel = new QLabel("当前告警: 0", this);
    m_alertCountLabel->setStyleSheet("color: #d63031; font-weight: bold;");
    m_clearAlertBtn = new QPushButton("🗑 清除告警", this);
    m_exportBtn = new QPushButton("📄 导出日志", this);
    alertBtnLayout->addWidget(m_alertCountLabel);
    alertBtnLayout->addStretch();
    alertBtnLayout->addWidget(m_clearAlertBtn);
    alertBtnLayout->addWidget(m_exportBtn);

    m_alertLog = new QTextEdit(this);
    m_alertLog->setReadOnly(true);
    m_alertLog->setMaximumHeight(150);

    alertLayout->addLayout(alertBtnLayout);
    alertLayout->addWidget(m_alertLog);

    // 组装主布局
    mainLayout->addWidget(connGroup);
    mainLayout->addLayout(statsLayout);
    mainLayout->addLayout(midLayout, 1);
    mainLayout->addWidget(alertGroup);

    setCentralWidget(central);
    setWindowTitle("🔧 工业设备监控与故障预警系统");
    resize(1200, 800);

    // 信号连接
    connect(m_connectBtn, &QPushButton::clicked, this, &DeviceMonitor::onConnectClicked);
    connect(m_disconnectBtn, &QPushButton::clicked, this, &DeviceMonitor::onDisconnectClicked);
    connect(m_clearAlertBtn, &QPushButton::clicked, this, &DeviceMonitor::onClearAlertsClicked);
    connect(m_exportBtn, &QPushButton::clicked, this, &DeviceMonitor::onExportLogClicked);
}

void DeviceMonitor::setupStyle()
{
    QString style = R"(
        QWidget {
            font-family: "Microsoft YaHei UI";
            font-size: 9pt;
        }
        QMainWindow {
            background-color: #f5f6fa;
        }
        QGroupBox {
            font-weight: bold;
            border: 1px solid #dcdde1;
            border-radius: 8px;
            margin-top: 10px;
            padding-top: 10px;
        }
        QGroupBox::title {
            subcontrol-origin: margin;
            left: 10px;
            padding: 0 5px;
            color: #2d3436;
        }
        QPushButton {
            background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
                stop:0 #667eea, stop:1 #764ba2);
            color: white;
            border: none;
            border-radius: 6px;
            padding: 8px 16px;
            font-weight: bold;
            min-width: 80px;
        }
        QPushButton:hover {
            background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
                stop:0 #5a6fd6, stop:1 #6a4190);
        }
        QPushButton:pressed {
            padding: 9px 16px 7px 16px;
        }
        QPushButton:disabled {
            background: #b2bec3;
        }
        QLineEdit {
            background: #ffffff;
            border: 1px solid #dcdde1;
            border-radius: 6px;
            padding: 6px 10px;
        }
        QLineEdit:focus {
            border: 2px solid #667eea;
        }
        QTableWidget {
            background: #ffffff;
            border: 1px solid #dcdde1;
            border-radius: 8px;
            gridline-color: #f1f2f6;
        }
        QTableWidget::item:selected {
            background: #74b9ff;
            color: white;
        }
        QHeaderView::section {
            background: #f1f2f6;
            padding: 8px;
            border: none;
            font-weight: bold;
            color: #2d3436;
        }
        QTextEdit {
            background: #ffffff;
            border: 1px solid #dcdde1;
            border-radius: 8px;
            padding: 8px;
        }
        QLabel {
            color: #2d3436;
        }
    )";

    qApp->setStyleSheet(style);
}

// ==================== MQTT 连接 ====================
void DeviceMonitor::onConnectClicked() {
    m_client->setHostname(m_hostEdit->text());
    m_client->setPort(m_portEdit->text().toInt());
    m_client->connectToHost();
    m_statusLabel->setText("● 连接中...");
    m_statusLabel->setStyleSheet("color: #fdcb6e; font-weight: bold;");
}

void DeviceMonitor::onDisconnectClicked() {
    m_client->disconnectFromHost();
}

void DeviceMonitor::onMqttConnected() {
    m_connected = true;
    m_statusLabel->setText("● 已连接");
    m_statusLabel->setStyleSheet("color: #00b894; font-weight: bold;");
    m_connectBtn->setEnabled(false);
    m_disconnectBtn->setEnabled(true);

    auto sub1 = m_client->subscribe(QMqttTopicFilter("factory/device/+/data"));
    auto sub2 = m_client->subscribe(QMqttTopicFilter("factory/device/+/status"));

    qDebug() << "订阅 data 结果:" << (sub1 ? "成功" : "失败");
    qDebug() << "订阅 status 结果:" << (sub2 ? "成功" : "失败");

    m_alertLog->append(QString("[%1] ✅ MQTT 连接成功,开始监听设备...")
                           .arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")));


    m_checkTimer->start(30000);
}

void DeviceMonitor::onMqttDisconnected() {
    m_connected = false;
    m_statusLabel->setText("● 未连接");
    m_statusLabel->setStyleSheet("color: #d63031; font-weight: bold;");
    m_connectBtn->setEnabled(true);
    m_disconnectBtn->setEnabled(false);
    m_checkTimer->stop();
}


void DeviceMonitor::onMessageReceived(const QByteArray &message, const QMqttTopicName &topic)
{
    QString topicStr = topic.name();
    QString payload = QString::fromUtf8(message);

    // 显示到日志
    QString timestamp = QDateTime::currentDateTime().toString("hh:mm:ss");
    m_alertLog->append(QString("[%1] 📩 [%2]").arg(timestamp, topicStr));

    // 解析 JSON
    QJsonDocument doc = QJsonDocument::fromJson(message);
    if (!doc.isObject()) {
        m_alertLog->append("❌ JSON 解析失败");
        return;
    }

    QJsonObject json = doc.object();

    // 提取设备ID(兼容多种 topic 格式)
    QString deviceId = "unknown";
    QStringList parts = topicStr.split('/', Qt::SkipEmptyParts);
    if (parts.size() >= 3) {
        deviceId = parts[2];  // factory/device/D001/data → D001
    }

    // 创建设备信息(统一处理,不区分 status/data)
    DeviceInfo device;
    device.id = deviceId;
    device.name = json.value("name").toString(deviceId);
    device.status = json.value("status").toString("unknown");
    device.temperature = json.value("temperature").toDouble(0);
    device.humidity = json.value("humidity").toDouble(0);
    device.voltage = json.value("voltage").toDouble(0);
    device.lastSeen = QDateTime::currentDateTime();
    device.alertCount = m_devices.contains(deviceId) ? m_devices[deviceId].alertCount : 0;

    // 保存到设备列表
    m_devices[deviceId] = device;

    // 更新界面
    updateChart(device.temperature, device.humidity);
    updateDeviceTable();
    updateStatusBar();
    checkThresholds(deviceId, device);

    // 更新曲线
    static int timeCounter = 0;
    timeCounter++;
    m_tempSeries->append(timeCounter, device.temperature);
    m_humiditySeries->append(timeCounter, device.humidity);

    if (m_tempSeries->count() > 50) {
        m_tempSeries->remove(0);
        m_humiditySeries->remove(0);
    }

    if (m_chart->axisX()) {
        m_chart->axisX()->setRange(qMax(0, timeCounter - 50), timeCounter);
    }
}

// ==================== 阈值检查(故障预警) ====================
void DeviceMonitor::checkThresholds(const QString &deviceId, const DeviceInfo &device) {
    QStringList alerts;

    // 温度阈值: > 80°C 高温告警, > 100°C 严重故障
    if (device.temperature > 100) {
        alerts << QString("🔥 【严重】设备 %1 温度异常: %2°C(超过100°C)")
                      .arg(device.name).arg(device.temperature);
    } else if (device.temperature > 80) {
        alerts << QString("⚠️ 【警告】设备 %1 温度过高: %2°C")
                      .arg(device.name).arg(device.temperature);
    }

    // 湿度阈值: > 90% 高湿告警
    if (device.humidity > 90) {
        alerts << QString("💧 【警告】设备 %1 湿度过高: %2%")
                      .arg(device.name).arg(device.humidity);
    }

    // 电压阈值: < 200V 欠压, > 250V 过压
    if (device.voltage < 200 && device.voltage > 0) {
        alerts << QString("🔋 【警告】设备 %1 电压过低: %2V")
                      .arg(device.name).arg(device.voltage);
    } else if (device.voltage > 250) {
        alerts << QString("⚡ 【警告】设备 %1 电压过高: %2V")
                      .arg(device.name).arg(device.voltage);
    }

    // 状态故障
    if (device.status == "fault" || device.status == "error") {
        alerts << QString("❌ 【故障】设备 %1 状态异常: %2")
                      .arg(device.name).arg(device.status);
    }

    for (const QString &alert : alerts) {
        addAlert(deviceId, "threshold", alert);
    }
}

void DeviceMonitor::addAlert(const QString &deviceId, const QString &alertType, const QString &message) {
    QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
    QString fullMsg = QString("[%1] %2").arg(timestamp, message);

    // 避免重复告警(同一设备同一类型5分钟内只报一次)
    static QMap<QString, QDateTime> lastAlertTime;
    QString key = deviceId + "_" + alertType;
    if (lastAlertTime.contains(key) &&
        lastAlertTime[key].secsTo(QDateTime::currentDateTime()) < 300) {
        return;
    }
    lastAlertTime[key] = QDateTime::currentDateTime();

    m_alertHistory.append(fullMsg);

    // 红色显示告警
    m_alertLog->append(QString("<span style='color:#d63031; font-weight:bold;'>%1</span>").arg(fullMsg));

    // 更新告警计数
    if (m_devices.contains(deviceId)) {
        m_devices[deviceId].alertCount++;
    }
    m_alertCountLabel->setText(QString("当前告警: %1").arg(m_alertHistory.size()));

    // 严重告警弹窗
    if (alertType == "threshold" && message.contains("【严重】")) {
        QMessageBox::critical(this, "🚨 严重故障告警", message);
    }
}

// ==================== 设备离线检查 ====================
void DeviceMonitor::onCheckDevicesTimeout() {
    QDateTime now = QDateTime::currentDateTime();
    for (auto it = m_devices.begin(); it != m_devices.end(); ++it) {
        if (it.value().lastSeen.secsTo(now) > 60) { // 60秒无数据视为离线
            if (it.value().status != "offline") {
                it.value().status = "offline";
                addAlert(it.key(), "offline",
                         QString("📡 设备 %1 已离线(超过60秒无通信)").arg(it.value().name));
            }
        }
    }
    updateDeviceTable();
    updateStatusBar();
}

// ==================== UI 更新 ====================
void DeviceMonitor::updateDeviceTable() {
    m_deviceTable->setRowCount(m_devices.size());
    int row = 0;
    for (const auto &device : m_devices) {
        m_deviceTable->setItem(row, 0, new QTableWidgetItem(device.id));
        m_deviceTable->setItem(row, 1, new QTableWidgetItem(device.name));

        auto *statusItem = new QTableWidgetItem(device.status.toUpper());
        statusItem->setForeground(QColor(getStatusColor(device.status)));
        statusItem->setFont(QFont("Microsoft YaHei UI", 9, QFont::Bold));
        m_deviceTable->setItem(row, 2, statusItem);

        m_deviceTable->setItem(row, 3, new QTableWidgetItem(QString::number(device.temperature, 'f', 1)));
        m_deviceTable->setItem(row, 4, new QTableWidgetItem(QString::number(device.humidity, 'f', 1)));
        m_deviceTable->setItem(row, 5, new QTableWidgetItem(QString::number(device.voltage, 'f', 1)));
        m_deviceTable->setItem(row, 6, new QTableWidgetItem(device.lastSeen.toString("hh:mm:ss")));

        row++;
    }
}

void DeviceMonitor::updateStatusBar() {
    int total = m_devices.size();
    int online = 0, offline = 0, fault = 0;

    for (const auto &device : m_devices) {
        if (device.status == "online" || device.status == "running") online++;
        else if (device.status == "offline") offline++;
        else if (device.status == "fault" || device.status == "error") fault++;
    }

    m_totalDeviceLabel->setText(QString::number(total));
    m_onlineLabel->setText(QString::number(online));
    m_offlineLabel->setText(QString::number(offline));
    m_faultLabel->setText(QString::number(fault));
}

QString DeviceMonitor::getStatusColor(const QString &status) {
    if (status == "online" || status == "running") return "#00b894";
    if (status == "offline") return "#fdcb6e";
    if (status == "fault" || status == "error") return "#d63031";
    return "#636e72";
}

// ==================== 按钮功能 ====================
void DeviceMonitor::onClearAlertsClicked() {
    m_alertHistory.clear();
    m_alertLog->clear();
    m_alertCountLabel->setText("当前告警: 0");
    for (auto &device : m_devices) {
        device.alertCount = 0;
    }
}

void DeviceMonitor::onExportLogClicked() {
    QString fileName = QFileDialog::getSaveFileName(this, "导出告警日志",
                                                    QString("alert_log_%1.txt").arg(QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss")),
                                                    "文本文件 (*.txt)");
    if (fileName.isEmpty()) return;

    QFile file(fileName);
    if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
        QTextStream stream(&file);
        stream << "===== 设备监控告警日志 =====\n";
        stream << "导出时间: " << QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss") << "\n";
        stream << "设备总数: " << m_devices.size() << "\n\n";
        for (const QString &log : m_alertHistory) {
            stream << log << "\n";
        }
        file.close();
        QMessageBox::information(this, "导出成功", "告警日志已保存到:\n" + fileName);
    }
}
void DeviceMonitor::updateChart(double temperature, double humidity) {
    m_timeCounter++;

    // 添加新数据点
    m_tempSeries->append(m_timeCounter, temperature);
    m_humiditySeries->append(m_timeCounter, humidity);

    // 限制最大点数,移除旧数据(滑动窗口效果)
    if (m_tempSeries->count() > MAX_POINTS) {
        m_tempSeries->remove(0);
        m_humiditySeries->remove(0);

        // 移除后需要重新设置 X 轴范围,保持滑动效果
        int startX = m_timeCounter - MAX_POINTS + 1;
        m_axisX->setRange(startX, m_timeCounter);
    } else {
        // 数据点未满时,X 轴跟随数据增长
        m_axisX->setRange(0, qMax(MAX_POINTS, m_timeCounter));
    }

    // 动态调整 Y 轴范围(自适应)
    autoScaleYAxis();
}

void DeviceMonitor::autoScaleYAxis() {
    if (m_tempSeries->count() == 0) return;

    // 找出所有数据点的最小/最大值
    double minY = 9999, maxY = -9999;

    for (int i = 0; i < m_tempSeries->count(); i++) {
        double temp = m_tempSeries->at(i).y();
        double hum = m_humiditySeries->at(i).y();
        minY = qMin(minY, qMin(temp, hum));
        maxY = qMax(maxY, qMax(temp, hum));
    }

    // 增加 10% 的边距,避免数据贴边
    double margin = (maxY - minY) * 0.1;
    if (margin < 5) margin = 5;  // 最小边距 5

    double lower = qMax(0.0, minY - margin);
    double upper = maxY + margin;

    // 平滑过渡动画(可选)
    m_axisY->setRange(lower, upper);
}

三、项目代码说明

我们使用免费公共 MQTT 测试服务器broker.hivemq.com ,端口号是1883,相似的网站如下:

常用公共 MQTT Broker 汇总

服务商 地址 TCP 端口 TLS 端口 WebSocket 特点 推荐度
EMQX (全球) broker.emqx.io 1883 8883 8083/8084 国内访问较快,支持 MQTT 5.0/3.1.1,多区域集群 ⭐⭐⭐⭐⭐
EMQX (国内) broker-cn.emqx.io 1883 8883 8083/8084 腾讯云上海节点,国内延迟最低 (~164ms) ⭐⭐⭐⭐⭐
Eclipse Mosquitto mqtt.eclipseprojects.io 1883 8883 80/443 老牌 Eclipse 项目,稳定性好 ⭐⭐⭐⭐
Mosquitto 社区 test.mosquitto.org 1883 8883/8884 80/443 端口选择多,但延迟较高 (~378ms) ⭐⭐⭐
HiveMQ broker.hivemq.com 1883 --- 8000 你正在用的,德国节点,延迟中等 (~252ms) ⭐⭐⭐⭐
Bevywise broker.bevywise.com 1883 --- 10443 支持 MQTT 5.0/3.1.1 ⭐⭐⭐
Coreflux iot.coreflux.cloud 1883 8883 5000/443 无需注册,快速测试 ⭐⭐⭐
MaQiaTTo maqiatto.com 1883 --- --- 完全免费

然后我们使用MQTTX软件来测试,MQTTX下载地址如下:

MQTTX

项目是由AI生成的,涉及比较多的模块,后面我也会慢慢学习。

| 端口选择多,但延迟较高 (~378ms) | ⭐⭐⭐ |

| HiveMQ | broker.hivemq.com | 1883 | --- | 8000 | 你正在用的,德国节点,延迟中等 (~252ms) | ⭐⭐⭐⭐ |

| Bevywise | broker.bevywise.com | 1883 | --- | 10443 | 支持 MQTT 5.0/3.1.1 | ⭐⭐⭐ |

| Coreflux | iot.coreflux.cloud | 1883 | 8883 | 5000/443 | 无需注册,快速测试 | ⭐⭐⭐ |

| MaQiaTTo | maqiatto.com | 1883 | --- | --- | 完全免费 | |

然后我们使用MQTTX软件来测试,MQTTX下载地址如下:

MQTTX

具体测试效果如下:

外链图片转存中...(img-L4LTvXQr-1782564681823)

项目是由AI生成的,涉及比较多的模块,后面我也会慢慢学习。