Qt編寫物聯網管理平臺37-邏輯設計

一、前言

本系統的邏輯設計是個人認爲做過的系統中最好的,一個系統支持多個通信端口,每個通信端口都可選不同的通信協議,一個通信端口可以接255個控制器,相當於主設備,一個控制器可以接255個探測器,相當於子設備,這樣就打破了一條線路255個設備總數的限制,一下子提高到65000個,而且整個軟件支持多個線路,就是幾十萬個設備接入也可以,但是實際上不會有這麼多,真要有這麼多設備,那賣硬件也賺翻了,實際的應用場景一般在幾千個子設備,本系統親測幾千個設備毫無壓力。

在程序設計上面,根據端口表中有多少個端口就new多少個通信類,每個端口有哪些控制器地址,從控制器表中拿到,因爲控制器會選擇一個唯一的端口,然後通信類中根據控制器地址集合做輪詢訪問數據,至於每次需要問控制器要多少個探測器也就是子設備的數據,通過探測器信息表拿到範圍值,就是寄存器地址,所以每次問控制器要數據返回的都是多個探測器的數據,按照對應位置取出對應數據解析即可。網絡方式的一般是一個端口對應一個控制器,後期還會增加tcp服務器模式,軟件這邊作爲服務器監聽端口,下面的設備主動上報數據。

二、功能特點

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 DeviceServer::init()
{
    //如果開啓了網絡接收則以網絡接收的數據爲準
    if (AppConfig::UseNetSend) {
        connect(this, SIGNAL(receiveData(QString, quint8, QByteArray)),
                UdpSend::Instance(), SLOT(sendData(QString, quint8, QByteArray)));
        UdpSend::Instance()->start();
    } else if (AppConfig::UseNetReceive) {
        connect(UdpReceive::Instance(), SIGNAL(receiveData(QString, quint8, QByteArray)),
                this, SIGNAL(receiveData(QString, quint8, QByteArray)));
        connect(UdpReceive::Instance(), SIGNAL(receiveInfo(QString, quint8, QString)),
                this, SIGNAL(receiveInfo(QString, quint8, QString)));
        connect(UdpReceive::Instance(), SIGNAL(receiveError(QString, quint8, QString)),
                this, SIGNAL(receiveError(QString, quint8, QString)));
        connect(UdpReceive::Instance(), SIGNAL(receiveOnline(QString, quint8, bool)),
                this, SLOT(doReceiveOnline(QString, quint8, bool)));
        connect(UdpReceive::Instance(), SIGNAL(receiveValue(QString, quint8, QList<quint16>)),
                this, SLOT(doReceiveValue(QString, quint8, QList<quint16>)));
        UdpReceive::Instance()->setAddrs(DbData::NodeInfo_NodeAddr);
        UdpReceive::Instance()->start();
        return;
    }

    //判斷是從數據庫採集數據還是設備採集數據
    if (AppConfig::WorkMode == 1 || AppConfig::WorkMode == 2) {
        DbReceive::Instance()->start();
        return;
    }

    //從端口信息中找到所有端口,逐個設置對應的地址集合
    for (int i = 0; i < DbData::PortInfo_Count; ++i) {
        //取出端口名稱,從設備表中找到該端口對應的地址即可
        QList<quint8> addrs;
        QString portName = DbData::PortInfo_PortName.at(i);
        QString portType = DbData::PortInfo_PortType.at(i);
        QString comName = DbData::PortInfo_ComName.at(i);
        int baudRate = DbData::PortInfo_BaudRate.at(i);
        QString tcpIP = DbData::PortInfo_TcpIP.at(i);
        int tcpPort = DbData::PortInfo_TcpPort.at(i);
        //浮點數 這樣就可以支持小數點的比如 0.2 表示200毫秒
        float readInterval = DbData::PortInfo_ReadInterval.at(i);
        int readTimeout = DbData::PortInfo_ReadTimeout.at(i);
        int readMaxtime = DbData::PortInfo_ReadMaxtime.at(i);

        for (int j = 0; j < DbData::DeviceInfo_Count; j++) {
            if (DbData::DeviceInfo_PortName.at(j) == portName) {
                addrs << DbData::DeviceInfo_DeviceAddr.at(j);
            }
        }

        //如果設備地址存在則立即實例化通信對象
        if (addrs.count() > 0) {
            DeviceClient *deviceClient = new DeviceClient(this);
            connect(deviceClient, SIGNAL(sendData(QString, quint8, QByteArray)),
                    this, SIGNAL(sendData(QString, quint8, QByteArray)));
            connect(deviceClient, SIGNAL(receiveData(QString, quint8, QByteArray)),
                    this, SIGNAL(receiveData(QString, quint8, QByteArray)));
            connect(deviceClient, SIGNAL(receiveInfo(QString, quint8, QString)),
                    this, SIGNAL(receiveInfo(QString, quint8, QString)));
            connect(deviceClient, SIGNAL(receiveError(QString, quint8, QString)),
                    this, SIGNAL(receiveError(QString, quint8, QString)));
            connect(deviceClient, SIGNAL(receiveOnline(QString, quint8, bool)),
                    this, SLOT(doReceiveOnline(QString, quint8, bool)));

            //不同工作模式關聯到對應槽函數處理
            if (AppConfig::WorkMode == 0) {
                connect(deviceClient, SIGNAL(receiveValue(QString, quint8, QList<quint16>)),
                        this, SLOT(doReceiveValue(QString, quint8, QList<quint16>)));
            } else if (AppConfig::WorkMode == 3) {
                connect(deviceClient, SIGNAL(receiveValue(QString, quint8, QList<quint16>)),
                        this, SLOT(doReceiveValue2(QString, quint8, QList<quint16>)));
            }

            //設置對應的參數
            deviceClient->setPortName(portName);
            deviceClient->setPortType(portType);
            deviceClient->setComName(comName);
            deviceClient->setBaudRate(baudRate);
            deviceClient->setTcpIP(tcpIP);
            deviceClient->setTcpPort(tcpPort);
            deviceClient->setReadInterval(readInterval * 1000);
            deviceClient->setReadTimeout(readTimeout);
            deviceClient->setReadMaxtime(readMaxtime * 1000);
            deviceClient->setAddrs(addrs);
            deviceClient->init();
            deviceClients << deviceClient;
        }
    }

    //初始化報警聯動
    connect(AlarmLink::Instance(), SIGNAL(sendData(QString, quint8, QByteArray)),
            this, SIGNAL(sendData(QString, quint8, QByteArray)));
    connect(AlarmLink::Instance(), SIGNAL(receiveData(QString, quint8, QByteArray)),
            this, SIGNAL(receiveData(QString, quint8, QByteArray)));
    connect(AlarmLink::Instance(), SIGNAL(receiveInfo(QString, quint8, QString)),
            this, SIGNAL(receiveInfo(QString, quint8, QString)));
    connect(AlarmLink::Instance(), SIGNAL(receiveError(QString, quint8, QString)),
            this, SIGNAL(receiveError(QString, quint8, QString)));
    AlarmLink::Instance()->init();
}

void DeviceServer::start()
{
    foreach (DeviceClient *deviceClient, deviceClients) {
        deviceClient->start();
    }

    DbQuery::addUserLog("設備上報", "端口服務啓動");
    AlarmLink::Instance()->start();
}

void DeviceServer::stop()
{
    foreach (DeviceClient *deviceClient, deviceClients) {
        deviceClient->stop();
    }

    DbQuery::addUserLog("設備上報", "端口服務停止");
    AlarmLink::Instance()->stop();
}

void DeviceServer::readValueAll()
{
    foreach (DeviceClient *deviceClient, deviceClients) {
        deviceClient->readValueAll();
    }
}

void DeviceServer::readValue(const QString &portName, quint8 addr, bool all)
{
    //設備採集模式纔有此功能,如果沒有添加端口查不到端口號也不需要處理
    if (AppConfig::WorkMode == 1 || AppConfig::WorkMode == 2 || portName.isEmpty()) {
        return;
    }

    if (all) {
        getDeviceClient(portName)->readValueAll();
    } else {
        getDeviceClient(portName)->readValue(addr);
    }
}

void DeviceServer::writeData(const QString &portName, const QString &type, quint8 addr, const QByteArray &body)
{
    //這裏需要過濾下,在設備採集以外的模式可能調用此函數
    DeviceClient *client = getDeviceClient(portName);
    if (client != 0) {
        //改成了追加命令,這樣防止衝突,比如正在回覆數據的時候觸發報警聯動
        client->append(type, addr, body);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章