一、前言
本系統嚴格意義上說是一個直連硬件的客戶端軟件,下面接的modbus協議的設備直接通過網絡或者串口和軟件通信,軟件負責解析數據和存儲記錄。有時候客戶想要領導辦公室或者分管這一塊的部門經理辦公室,也安裝一套這樣的軟件,能夠查看到對應設備的數據,一種方式是將本地數據存儲或者轉發到雲端,其他需要拉數據或者訂閱數據的地方,往服務器訂閱就行,客戶端這邊只負責上傳數據就行。另一種做法就是udp數據轉發,可填寫多個要轉發的地址和端口,這邊採集到的數據會將原數據直接通過udp發出去,採用udp的好處是不用建立連接,速度非常快迅速。
在系統設置中設置好網絡轉發參數後,可以在接收的地方開啓網絡數據接收,這樣只需要接收數據解析反應到界面就行,相當於數據源不是硬件設備而是網絡轉發過來的數據,不需要直接接硬件設備。
網絡轉發端,單擊同步數據會把本地的端口信息、控制器信息、探測器信息發到遠端。網絡轉發模塊也可以作爲無限級聯使用,比如接收端還可以開啓轉發,繼續轉發給需要的地方,一個客戶端上設置的轉發也支持多個,用英文分號 ; 隔開,一對多關係,採用的無連接udp協議,幾乎不佔用系統資源。
二、功能特點
2.1 軟件模塊
- 設備監控模塊,包括數據監控(表格形式展示)、設備面板(面板形式展示)、地圖監控(地圖形式展示)、曲線監控(曲線形式展示)。
- 數據查詢模塊,包括報警記錄、運行記錄、操作記錄。
- 系統設置模塊,包括基本設置、端口管理、控制器管理、探測器管理、報警聯動、類型設置等。
- 其他設置模塊,包括用戶管理、地圖管理、位置調整、組態設計、設備調試等。
2.2 基礎功能
- 設備數據採集,支持串口、網絡,串口可設置串口號、波特率,網絡可設置IP地址、通訊端口。
- 每個端口支持採集週期時間,默認1秒鐘一個設備。
- 支持設置通訊超時次數,默認3次。
- 支持最大重連時間,用於重新讀取離線的設備。
- 控制器信息,能夠添加控制器名稱,選擇控制器地址、控制器型號,設置該控制器下面的探測器數量。
- 探測器信息,能夠添加位號、探測器型號、氣體種類、氣體符號、高報值、低報值、緩衝值、清零值、是否啓用、報警聲音、背景地圖、存儲週期、數值換算小數點位數、報警延時時間、報警的類型(HH,LL,HL)等。
- 類型管理可配置控制器型號、探測器型號、氣體種類、氣體符號等。
- 地圖支持導入和刪除,所有的探測器在地圖上的位置可自由拖動保存。
- 端口信息、控制器信息、探測器信息、類型信息、用戶信息等,都支持導入、導出、導出到excel、打印。
- 運行記錄、報警記錄、操作記錄,都支持多條件組合查詢,比如時間段、控制器、探測器等,所有記錄支持導出到excel/pdf和打印。
- 運行記錄、報警記錄、操作記錄都可刪除指定時間範圍內的數據。
- 系統設置可選擇對應表最大保存記錄數,自動清理早期數據,留出足夠的空間存儲重要的數據。
- 報警短信轉發,支持多個接收手機號碼,可設定發送間隔,比如即時發送或者6個小時發送一次所有的報警信息,短信內容過長,自動拆分多條短信。
- 報警郵件轉發,支持多個接收郵箱,可設定發送間隔,比如即時發送或者6個小時發送一次所有的報警信息,支持附件發送。
- 設置軟件的中文標題、英文標題、logo路徑、版權所有等。
- 開關設置開機運行、報警聲音、自動登錄、記住密碼等。
- 報警聲音可設置播放次數,界面風格樣式提供18套皮膚文件選擇。
- 用戶管理,包括用戶權限配置,不同用戶可以有不同模塊的權限。
- 用戶登錄和用戶退出,可以記住密碼和自動登錄,超過三次報錯提示並關閉程序。
- 四種監控模式,設備面板監控、地圖監控、表格數據監控、曲線數據監控,可自由切換,四種模式下都實時展示採集到的數據,報警閃爍等。
- 報警繼電器聯動,一個位號可以跨串口聯動多個模塊和繼電器號,支持多對多。
2.3 特色功能
- 通信協議支持modbus_com、modbus_tcp_rtu,後期拓展mqtt等協議。
- 數據源除了真實的硬件設備採集,還可選數據庫採集,這樣用戶可以安排其他程序員比如java程序員將前端採集好的數據放到數據庫,本系統直接從數據庫採集即可。數據庫採集模式可以作爲通用的系統使用,更適合多人多系統協作。
- 智能跳過超時的設備,加快對在線設備的採集速度,當設備數量很多的時候尤其有用。
- 對智能跳過的超時的設備,在設定的重連時間自動採集一次,以便探測設備是否又重新上線。
- 每個探測器可控是否啓用,不啓用則不會採集,也不會在界面顯示,相當於運行階段臨時關閉。
- 探測器可設置緩衝值和報警延時時間,在該值附近波動產生的報警,不計入報警,只有持續處於報警值且超過報警延時時間纔算真正報警,這樣可以規避很多波動導致的誤報。
- 探測器可設置存儲週期,按照設定的時間來存儲一條運行記錄,可以按照重要程度對重要性高的設定存儲週期短一些,不重要的設定大一些,這樣可以節省不少的存儲空間,也保證了重要的數據及時存儲。
- 探測器可設置清零值,在一些高精度高靈敏的設備可能出廠的時候默認值未必是0,需要設定清零值來表示初始值。
- 探測器可設置小數點,用於計算後的真實數據控制小數點點位顯示,相當於除以10、除以100、除以1000,這樣大部分的探測器數據直接通過小數點位設置控制真實換算後的值,極個別的需要特殊轉換的可以在通信協議中約定。
- 探測器報警的類型支持多種,有些設備是高於某個值高報,低於某個值低報,而有些設備是在最小值最大值範圍內是高報,低於最小值低報,高於最大值正常。這樣可以分情況處理,涵蓋各種報警類型。
- 原創數據導入、導出、打印機制,跨平臺不依賴任何組件,瞬間導出數據。
- 導出到excel的記錄支持所有excel、wps等表格文件版本,不依賴excel等軟件。
- 高報顏色、低報顏色、正常顏色、默認值顏色等,都可以自由設置。
- 支持雲端數據同步,將本地採集到的數據實時同步到雲端。
- 支持網絡轉發和網絡接收,網絡接收開啓後,軟件從udp接收數據進行解析。網絡轉發支持多個目標IP,這樣就實現了本地採集的軟件,自由將數據轉到客戶端,隨時查看採集到的數據。
- 自動記住用戶最後停留的界面以及其他配置信息,重啓後自動應用。
- 報警自動切換到對應的地圖,探測器按鈕閃爍,表格數據對應顏色顯示。
- 雙擊探測器圖標,彈出對應探測器詳細信息,可以根據需要定製回控操作。
- 數據庫支持多種,包括sqlite、mysql、sqlserver、postgresql、oracle、人大金倉等。
- 本地設備採集到的數據實時上傳到雲端,以便手機APP或者web等其他方式提取。
- 自帶設備模擬工具,支持不同型號的多個設備數據模擬,同時還帶數據庫數據模擬,以便在沒有設備的時候測試數據。
- 標準modbus協議,各種控制器類型、探測器類型、種類、符號等全部自定義,非常靈活和強大,通信協議示例數據非常完整,通用各種modbus協議系統,適用於各種應用場景接入。
- 同時集成了串口通信、網絡通信、數據庫通信、數據導入導出打印、通信協議解析、界面UI、全局換膚等衆多組件和知識點,非常適合新手入門和進階。
- 支持xp、win7、win10、、win11、linux、mac、各種國產系統(UOS、中標麒麟、銀河麒麟等)、嵌入式linux等系統。
- 註釋完整,項目結構清晰,超級詳細完整的使用開發手冊,精確到每個代碼文件的功能說明,不斷持續迭代版本。
三、體驗地址
- 國內站點:https://gitee.com/feiyangqingyun
- 國際站點:https://github.com/feiyangqingyun
- 個人主頁:https://blog.csdn.net/feiyangqingyun
- 知乎主頁:https://www.zhihu.com/people/feiyangqingyun
- 產品主頁:https://blog.csdn.net/feiyangqingyun/article/details/97565652
- 在線文檔:https://feiyangqingyun.gitee.io/qwidgetdemo/iotsystem/
- 體驗地址:https://pan.baidu.com/s/1ZxG-oyUKe286LPMPxOrO2A 提取碼:o05q 文件名:bin_iotsystem.zip。
- 文章導航:https://qtchina.blog.csdn.net/article/details/121330922
四、效果圖
五、相關代碼
void UdpSend::sendData(const QString &portName, quint8 addr, const QByteArray &data)
{
if (!AppConfig::UseNetSend) {
return;
}
//取出ip和端口
QList<QString> ips;
QList<int> ports;
QStringList list = AppConfig::NetSendInfo.split(";");
foreach (QString str, list) {
if (str.contains(":")) {
QStringList temp = str.split(":");
ips << temp.at(0);
ports << temp.at(1).toInt();
}
}
//逐個發送
for (int i = 0; i < ips.count(); ++i) {
if (data.size() > 0) {
QString buffer;
if (portName == "PortInfo" || portName == "DeviceInfo" || portName == "NodeInfo") {
buffer = QString("%1|%2").arg(portName).arg(QString(data));
} else {
buffer = QString("%1|%2").arg(portName).arg(QUIHelper::byteArrayToHexStr(data));
}
udpSocket->writeDatagram(buffer.toUtf8(), QHostAddress(ips.at(i)), ports.at(i));
udpSocket->flush();
}
}
}
//type: 0=端口信息 1=控制器信息 2=探測器信息
void UdpReceive::saveDataBase(const QString table, const QStringList &datas)
{
QString flag, sql;
if (table == "PortInfo") {
flag = "端口信息";
sql = "insert into PortInfo(PortID,PortName,PortType,ComName,BaudRate,TcpIP,TcpPort,ReadInterval,ReadTimeout,ReadMaxtime) values";
} else if (table == "DeviceInfo") {
flag = "控制器信息";
sql = "insert into DeviceInfo(DeviceID,PortName,DeviceName,DeviceAddr,DeviceType,NodeNumber) values";
} else if (table == "NodeInfo") {
flag = "探測器信息";
sql = "insert into NodeInfo(NodeID,PositionID,DeviceName,NodeName,NodeAddr,NodeType,NodeClass,NodeSign,NodeUpper,NodeLimit,NodeMax,"
"NodeMin,NodeRange,NodeEnable,NodeSound,NodeImage,SaveInterval,DotCount,AlarmDelay,AlarmType,NodeX,NodeY) values";
}
//清空原有數據
DbHelper::clearTable(table);
//開啓數據庫事務
QSqlDatabase::database().transaction();
//組成sql語句挨個執行
foreach (QString data, datas) {
QStringList list = data.split(",");
QStringList values;
int count = list.count();
for (int i = 0; i < count; ++i) {
values << list.at(i);
}
QString sqlx = QString("%1('%2')").arg(sql).arg(values.join("','"));
DbHelper::execSql(sqlx);
}
//提交數據庫事務
if (QSqlDatabase::database().commit()) {
QString msg = QString("批量插入%1成功").arg(flag);
DbQuery::addUserLog(msg);
emit receiveInfo(portName, 255, msg);
} else {
QSqlDatabase::database().rollback();
QString msg = QString("批量插入%1失敗").arg(flag);
DbQuery::addUserLog(msg);
emit receiveError(portName, 255, msg);
}
}
void UdpReceive::checkData(const QString &data)
{
//取出端口名稱+地址+數據
QStringList list = data.split("|");
if (list.count() != 2) {
return;
}
//根據不同的端口處理數據,表信息也是端口號字段發過來的
QString portName = list.at(0);
if (portName == "PortInfo" || portName == "DeviceInfo" || portName == "NodeInfo") {
saveDataBase(portName, list.at(1).split(";"));
if (portName == "NodeInfo") {
//探測器表是最後發過來的,收到後延時重啓應用新的數據
QTimer::singleShot(3000, this, SLOT(reboot()));
}
return;
}
//傳過來的就是採集到的源數據
QByteArray buffer = QUIHelper::hexStrToByteArray(list.at(1));
//至少要多少個字節,保證下面取數據不出錯
int size = buffer.size();
if (size < 5) {
return;
}
//01 03 08 00 00 00 00 00 00 00 00 95 D7
//01 03 08 00 14 03 12 00 00 00 00 79 E6
//取出首字節,判斷是否爲當前地址集合中的地址
quint8 addr = buffer.at(0);
quint8 cmd = buffer.at(1);
quint8 len = buffer.at(2);
//如果是錯誤碼則直接解析錯誤信息
QList<quint8> cmds;
cmds << 0x03 << 0x04 << 0x06;
if (!cmds.contains(cmd)) {
emit receiveError(portName, addr, QString("數據出錯: %1").arg(QUIHelper::byteArrayToHexStr(buffer)));
buffer.clear();
return;
}
//後面的數據長度必須大於等於長度數據位表示的長度
if ((cmd == 0x03 || cmd == 0x04 || cmd == 0x06) && size < len + 5) {
return;
}
//放在這裏發出去數據是準確的完整的
emit receiveData(portName, addr, buffer);
//來過消息的設備,立馬更新最後的消息時間,以及判斷設備上線
int index = addrs.indexOf(addr);
times[index] = QDateTime::currentDateTime();
if ((!lives.at(index) && onlines.at(index)) || !onlines.at(index)) {
onlines[index] = true;
lives[index] = true;
emit receiveOnline(portName, addr, true);
emit receiveInfo(portName, addr, "設備上線");
}
//根據不同的cmd+不同的命令類型 取出對應的數據內容
if (cmd == 0x03) {
QString info;
if (currentType == "查詢濃度值") {
QList<quint16> values;
for (int i = 3; i < size - 2; i = i + 2) {
values << (float)QUIHelper::byteToUShort(buffer.mid(i, 2));
}
QStringList list;
foreach (quint16 value, values) {
list << QString::number(value);
}
info = QString("%1返回: %2").arg(currentType).arg(list.join(" "));
emit receiveValue(portName, addr, values);
}
//發送對應的文字解析
if (!info.isEmpty()) {
emit receiveInfo(portName, addr, info);
}
} else if (cmd == 0x04) {
} else if (cmd == 0x06) {
}
}