Qt編寫物聯網管理平臺35-實時曲線

一、前言

設備採集到的數據,通過曲線展示也是一種非常好的方式,儘管之前已經有了表格數據展示、設備面板展示、設備地圖展示等,實時曲線也是一種不錯的方式,這個用戶自由選擇,反正通過端口已經拿到了所有要採集的數據,根據通信協議已經把所有設備的數據解析好了,至於何種展示形式,都可以很方便的把數據傳入進行處理。

目前做的實時曲線展示,採用的用戶自己選擇某個設備進行監聽,近期經過再次研究qcustomplot的功能,發現其實qcustomplot內置了一個控件中擺放多個曲線,每條曲線都可以獨立的座標軸系統,大概在2014年的時候開始用的qcustomplot控件,當時也有這個一個設備多條曲線的場景,如果座標軸範圍一樣,這還可以多條曲線公用座標軸,如果不同曲線座標軸範圍相差很大,多條曲線顯示在一塊會顯示的很不明確,就算不同的曲線有不同的座標軸,也很容易搞混。所以後面又做成了一條曲線對應一個qcustomplot控件,好處是很明確,缺點也很多,就是一下子增加了很多內存佔用,而且CPU佔用在數據展示很快的時候,非常大,每個曲線控件都在刷新。於是一個曲線控件同時包含多個曲線而且垂直分佈在佈局中,一次性刷新,這個大大提升性能,後面準備把這種展示形式引入,替換現有的方案,比如每次選中一個控制器,該控制器下面的所有設備的實時曲線一次性繪製到曲線控件中。

二、功能特點

2.1 軟件模塊

  1. 設備監控模塊,包括數據監控(表格形式展示)、設備面板(面板形式展示)、地圖監控(地圖形式展示)、曲線監控(曲線形式展示)。
  2. 數據查詢模塊,包括報警記錄、運行記錄、操作記錄。
  3. 系統設置模塊,包括基本設置、端口管理、控制器管理、探測器管理、報警聯動、類型設置等。
  4. 其他設置模塊,包括用戶管理、地圖管理、位置調整、組態設計、設備調試等。

2.2 基礎功能

  1. 設備數據採集,支持串口、網絡,串口可設置串口號、波特率,網絡可設置IP地址、通訊端口。
  2. 每個端口支持採集週期時間,默認1秒鐘一個設備。
  3. 支持設置通訊超時次數,默認3次。
  4. 支持最大重連時間,用於重新讀取離線的設備。
  5. 控制器信息,能夠添加控制器名稱,選擇控制器地址、控制器型號,設置該控制器下面的探測器數量。
  6. 探測器信息,能夠添加位號、探測器型號、氣體種類、氣體符號、高報值、低報值、緩衝值、清零值、是否啓用、報警聲音、背景地圖、存儲週期、數值換算小數點位數、報警延時時間、報警的類型(HH,LL,HL)等。
  7. 類型管理可配置控制器型號、探測器型號、氣體種類、氣體符號等。
  8. 地圖支持導入和刪除,所有的探測器在地圖上的位置可自由拖動保存。
  9. 端口信息、控制器信息、探測器信息、類型信息、用戶信息等,都支持導入、導出、導出到excel、打印。
  10. 運行記錄、報警記錄、操作記錄,都支持多條件組合查詢,比如時間段、控制器、探測器等,所有記錄支持導出到excel/pdf和打印。
  11. 運行記錄、報警記錄、操作記錄都可刪除指定時間範圍內的數據。
  12. 系統設置可選擇對應表最大保存記錄數,自動清理早期數據,留出足夠的空間存儲重要的數據。
  13. 報警短信轉發,支持多個接收手機號碼,可設定發送間隔,比如即時發送或者6個小時發送一次所有的報警信息,短信內容過長,自動拆分多條短信。
  14. 報警郵件轉發,支持多個接收郵箱,可設定發送間隔,比如即時發送或者6個小時發送一次所有的報警信息,支持附件發送。
  15. 設置軟件的中文標題、英文標題、logo路徑、版權所有等。
  16. 開關設置開機運行、報警聲音、自動登錄、記住密碼等。
  17. 報警聲音可設置播放次數,界面風格樣式提供18套皮膚文件選擇。
  18. 用戶管理,包括用戶權限配置,不同用戶可以有不同模塊的權限。
  19. 用戶登錄和用戶退出,可以記住密碼和自動登錄,超過三次報錯提示並關閉程序。
  20. 四種監控模式,設備面板監控、地圖監控、表格數據監控、曲線數據監控,可自由切換,四種模式下都實時展示採集到的數據,報警閃爍等。
  21. 報警繼電器聯動,一個位號可以跨串口聯動多個模塊和繼電器號,支持多對多。

2.3 特色功能

  1. 通信協議支持modbus_com、modbus_tcp_rtu,後期拓展mqtt等協議。
  2. 數據源除了真實的硬件設備採集,還可選數據庫採集,這樣用戶可以安排其他程序員比如java程序員將前端採集好的數據放到數據庫,本系統直接從數據庫採集即可。數據庫採集模式可以作爲通用的系統使用,更適合多人多系統協作。
  3. 智能跳過超時的設備,加快對在線設備的採集速度,當設備數量很多的時候尤其有用。
  4. 對智能跳過的超時的設備,在設定的重連時間自動採集一次,以便探測設備是否又重新上線。
  5. 每個探測器可控是否啓用,不啓用則不會採集,也不會在界面顯示,相當於運行階段臨時關閉。
  6. 探測器可設置緩衝值和報警延時時間,在該值附近波動產生的報警,不計入報警,只有持續處於報警值且超過報警延時時間纔算真正報警,這樣可以規避很多波動導致的誤報。
  7. 探測器可設置存儲週期,按照設定的時間來存儲一條運行記錄,可以按照重要程度對重要性高的設定存儲週期短一些,不重要的設定大一些,這樣可以節省不少的存儲空間,也保證了重要的數據及時存儲。
  8. 探測器可設置清零值,在一些高精度高靈敏的設備可能出廠的時候默認值未必是0,需要設定清零值來表示初始值。
  9. 探測器可設置小數點,用於計算後的真實數據控制小數點點位顯示,相當於除以10、除以100、除以1000,這樣大部分的探測器數據直接通過小數點位設置控制真實換算後的值,極個別的需要特殊轉換的可以在通信協議中約定。
  10. 探測器報警的類型支持多種,有些設備是高於某個值高報,低於某個值低報,而有些設備是在最小值最大值範圍內是高報,低於最小值低報,高於最大值正常。這樣可以分情況處理,涵蓋各種報警類型。
  11. 原創數據導入、導出、打印機制,跨平臺不依賴任何組件,瞬間導出數據。
  12. 導出到excel的記錄支持所有excel、wps等表格文件版本,不依賴excel等軟件。
  13. 高報顏色、低報顏色、正常顏色、默認值顏色等,都可以自由設置。
  14. 支持雲端數據同步,將本地採集到的數據實時同步到雲端。
  15. 支持網絡轉發和網絡接收,網絡接收開啓後,軟件從udp接收數據進行解析。網絡轉發支持多個目標IP,這樣就實現了本地採集的軟件,自由將數據轉到客戶端,隨時查看採集到的數據。
  16. 自動記住用戶最後停留的界面以及其他配置信息,重啓後自動應用。
  17. 報警自動切換到對應的地圖,探測器按鈕閃爍,表格數據對應顏色顯示。
  18. 雙擊探測器圖標,彈出對應探測器詳細信息,可以根據需要定製回控操作。
  19. 數據庫支持多種,包括sqlite、mysql、sqlserver、postgresql、oracle、人大金倉等。
  20. 本地設備採集到的數據實時上傳到雲端,以便手機APP或者web等其他方式提取。
  21. 自帶設備模擬工具,支持不同型號的多個設備數據模擬,同時還帶數據庫數據模擬,以便在沒有設備的時候測試數據。
  22. 標準modbus協議,各種控制器類型、探測器類型、種類、符號等全部自定義,非常靈活和強大,通信協議示例數據非常完整,通用各種modbus協議系統,適用於各種應用場景接入。
  23. 同時集成了串口通信、網絡通信、數據庫通信、數據導入導出打印、通信協議解析、界面UI、全局換膚等衆多組件和知識點,非常適合新手入門和進階。
  24. 支持xp、win7、win10、、win11、linux、mac、各種國產系統(UOS、中標麒麟、銀河麒麟等)、嵌入式linux等系統。
  25. 註釋完整,項目結構清晰,超級詳細完整的使用開發手冊,精確到每個代碼文件的功能說明,不斷持續迭代版本。

三、體驗地址

  1. 國內站點:https://gitee.com/feiyangqingyun
  2. 國際站點:https://github.com/feiyangqingyun
  3. 個人主頁:https://blog.csdn.net/feiyangqingyun
  4. 知乎主頁:https://www.zhihu.com/people/feiyangqingyun
  5. 產品主頁:https://blog.csdn.net/feiyangqingyun/article/details/97565652
  6. 在線文檔:https://feiyangqingyun.gitee.io/qwidgetdemo/iotsystem/
  7. 體驗地址:https://pan.baidu.com/s/1ZxG-oyUKe286LPMPxOrO2A 提取碼:o05q 文件名:bin_iotsystem.zip。
  8. 文章導航:https://qtchina.blog.csdn.net/article/details/121330922

四、效果圖

五、相關代碼

void frmMultiPlot::initPlot()
{
    //顏色集合
    QList<QColor> colors = CustomPlotHelper::getColorList();

    //拿到佈局對象指針
    QCPLayoutGrid *layout = plot->plotLayout();
    //把之前的佈局清除
    layout->clear();
    //設置行間距
    layout->setRowSpacing(0);
    layout->setColumnSpacing(0);

    //清空曲線圖
    plot->clearGraphs();
    this->clearPlot();

    //循環添加多個曲線圖
    int count = ui->cboxCount->currentText().toInt();
    for (int i = 0; i < count; ++i) {
        QCPAxisRect *axisRect = new QCPAxisRect(plot);
        //設置邊距不然默認邊距比較大,第一個和末一個不設置,留點空隙更好看
        if (i > 0 && i < count - 1) {
            axisRect->setMinimumMargins(QMargins(0, 0, 15, 0));
        }

        //設置最小尺寸,超過了則會自動拉伸圖表控件,在曲線很多的時候特別有用不然擠一塊
        axisRect->setMinimumSize(100, 100);

        //拿到XY座標軸對象
        QCPAxis *xAxis = axisRect->axis(QCPAxis::atBottom);
        QCPAxis *yAxis = axisRect->axis(QCPAxis::atLeft);
        yAxis->setLabel(QString("曲線 %1").arg(i + 1));

        //網格線不可見
        xAxis->grid()->setVisible(false);
        yAxis->grid()->setVisible(false);

        //爲了方便顯示設置底部X軸標識爲時間標識
        int type = ui->cboxType->currentIndex();
        if (type == 1 || type == 2) {
            //座標軸格式爲時間
            QSharedPointer<QCPAxisTickerTime> timeTicker(new QCPAxisTickerTime);
            timeTicker->setTimeFormat("%h:%m:%s");
            xAxis->setTicker(timeTicker);
            xAxis->setRange(0, maxX, Qt::AlignRight);
        } else if (type == 3) {
            //座標軸格式爲文本
            QSharedPointer<QCPAxisTickerText> textTicker(new QCPAxisTickerText);
            for (int i = 0; i < max2; ++i) {
                textTicker->addTick(i, STRTIME);
            }
            xAxis->setTicker(textTicker);
            xAxis->setRange(0, max2);
        } else {
            xAxis->setRange(0, maxX);
        }

        //設置座標軸記號標識數量
        xAxis->ticker()->setTickCount(10);
        yAxis->ticker()->setTickCount(3);

        //設置刻度原點
        yAxis->ticker()->setTickOrigin(1);
        //設置座標軸範圍,可以根據項目自行調整
        yAxis->setRange(0, maxY + 2);

        //將座標軸添加到佈局中
        layout->addElement(i, 0, axisRect);
        //添加對應的畫布,對應座標軸爲剛剛產生的座標軸,底部作爲X軸,左側作爲Y軸
        QCPGraph *graph = plot->addGraph(xAxis, yAxis);
        //開啓平滑算法
        graph->setSmooth(ui->cboxSmooth->currentIndex());
        //關閉抗鋸齒
        //graph->setAntialiased(false);

        //取隨機顏色設置顏色
        QColor color = colors.at(rand() % colors.count());
        graph->setPen(QPen(color, 1));
        if (ui->ckBg->isChecked()) {
            //背景顏色在顏色基礎上加個透明度更好看
            //也可以自行設置漸變顏色
            QColor c = color;
            c.setAlpha(100);
            graph->setBrush(c);
        }

        xAxis->setLabelColor(color);
        yAxis->setLabelColor(color);
        xAxis->setTickLabelColor(color);
        yAxis->setTickLabelColor(color);
        xAxis->setBasePen(color);
        yAxis->setBasePen(color);
        xAxis->setTickPen(color);
        yAxis->setTickPen(color);
        xAxis->setSubTickPen(color);
        yAxis->setSubTickPen(color);
        //xAxis->grid()->setPen(color);
        //yAxis->grid()->setPen(color);
        //xAxis->grid()->setZeroLinePen(color);
        //yAxis->grid()->setZeroLinePen(color);
    }

    //設置滾動條的最小尺寸爲佈局的最小尺寸,這樣會自動產生滾動條效果
    ui->scrollAreaWidgetContents->setMinimumSize(layout->minimumOuterSizeHint());
    //重新繪製
    plot->replot();
}

void frmMultiPlot::loadData()
{
    if (!isVisible()) {
        return;
    }

    //不同類型不同值
    int type = ui->cboxType->currentIndex();
    if (type == 1 || type == 2) {
        key = timeStart.msecsTo(QDateTime::currentDateTime()) / 1000.0;
    } else {
        key++;
    }

    int count = plot->plotLayout()->elementCount();
    for (int i = 0; i < count; ++i) {
        //拿到對應畫布的座標軸
        QCPAxisRect *axisRect = (QCPAxisRect *)plot->plotLayout()->elementAt(i);
        QCPAxis *xAxis = axisRect->axis(QCPAxis::atBottom);
        QCPGraph *graph = plot->graph(i);
        int dataCount = graph->dataCount() - 1;

        //把最前面那個數據移除掉並重新設置X軸範圍值
        if (type == 1 || type == 2) {
            graph->data().data()->removeBefore(key - 10);
            xAxis->setRange(key, 10, Qt::AlignRight);
        } else if (type == 3) {
            QCPAxisTickerText *ticker = (QCPAxisTickerText *)xAxis->ticker().data();
            ticker->addTick(key, STRTIME);
            if (dataCount >= max2) {
                //每次移除最前面那個,超過了也需要移除不然會內存一直漲
                ticker->ticks().remove(ticker->ticks().firstKey());
                graph->data().data()->removeBefore(key - max2);
                xAxis->setRange(key - max2, key);
            }
        } else {
            if (dataCount >= maxX) {
                graph->data().data()->removeBefore(key - maxX);
                xAxis->setRange(key - maxX, key);
            }
        }

        //隨機模擬數據添加到圖表
        //double value = qSin(key) * 5;
        double value = QUIHelper::getRandValue(1, maxY, true, true);
        graph->addData(key, value);
        //qDebug() << dataCount << key << value;

        //自動調整Y軸範圍範圍
        //graph->rescaleValueAxis();
    }

    //全部設置好以後再更新
    plot->replot();
}

void frmMultiPlot::clearPlot()
{
    //復位數據
    int index = ui->cboxType->currentIndex();
    if (index == 1) {
        timeStart = QDateTime::currentDateTime();
    } else if (index == 2) {
        QDateTime now = QDateTime::currentDateTime();
        timeStart = QDateTime::fromString(now.toString("yyyy-MM-dd 00:00:00"), "yyyy-MM-dd HH:mm:ss");
    } else {
        key = -1;
    }

    //清空數據及復位座標軸範圍
    int count = plot->graphCount();
    for (int i = 0; i < count; ++i) {
        plot->graph(i)->data().data()->clear();
        QCPAxisRect *axisRect = (QCPAxisRect *)plot->plotLayout()->elementAt(i);
        QCPAxis *xAxis = axisRect->axis(QCPAxis::atBottom);
        if (index == 1 || index == 2) {
            xAxis->setRange(0, maxX, Qt::AlignRight);
        } else if (index == 3) {
            xAxis->setRange(0, max2);
        } else {
            xAxis->setRange(0, maxX);
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章