基于PLC-Recorder数据转发功能的WebSocket客户端设计(高级语言及HTML,通讯内容为Json格式)[通俗易懂]

基于PLC-Recorder数据转发功能的WebSocket客户端设计(高级语言及HTML,通讯内容为Json格式)[通俗易懂]最近PLC-Recoder推出了V1.6.0版本,其最大的变化是增加了数据转发功能,所有能采集到的数据,都可以通过WebSocket和Json转发出去,为不熟悉PLC底层技术,且需要数据采集的朋友们提供了一个统一的接口。本文将介绍几种客户端创建方法。

最近PLC-Recoder推出了V1.6.0版本,其最大的变化是增加了数据转发功能,所有能采集到的数据,都可以通过WebSocket和Json转发出去,为不熟悉PLC底层技术,且需要数据采集的朋友们提供了一个统一的接口。下面将介绍几种客户端创建方法。

1、服务器端设置

1)配置好通道(需要采集数据的设备)和所有需要采集的变量,开启录波测试。如果没有实际的设备,也可以启动录波仿真功能,模拟录波,为所有的变量提供数据。
参考文章:
欧姆龙、松下、基恩士PLC进行连续数据采集、时序和故障追踪的方法》、
西门子PLC进行连续数据采集、时序和故障追踪的方法》、
三菱PLC进行连续数据采集、时序和故障追踪的方法
2)配置服务器参数,启动服务器:
通过菜单“转发”->“配置…”,打开配置窗口设置端口号和服务器识别码,点击“应用退出”。然后通过“启动服务器”和“停止服务器”来切换服务器的状态。启动后,软件标题中将出现“[转发中]”的字符。

2、用C#标准库实现客户端

界面设计:在这里插入图片描述
设置有连接、关闭、变量查询、订阅等按钮,下面介绍重要实现:
1)主要引用:WebSocket4Net、Newtonsoft.Json及其依赖项;
2)连接命令

private void btConnect_Click(object sender, EventArgs e)
        { 
   
            string address ="ws://"+ tbIP.Text + ":" + tbPort.Text;
            try
            { 
   
                wsClient = new WebSocket(address);
                
                wsClient.MessageReceived += WebSocket_OnWebSocketMessageReceived;
                wsClient.Opened += WebSocket_OnClientConnected;
                wsClient.Closed += WsClient_Closed;
                wsClient.Open();
                btClose.Enabled = true;
                btConnect.Enabled = false;//避免多次创建连接
            }
            catch (Exception ex)
            { 
   
                MessageBox.Show("Start Failed : " + ex.Message);
            }
        }
只听到从架构师办公室传来架构君的声音:
晓随天仗入,暮惹御香归。有谁来对上联或下联?

关闭程序:

此代码由Java架构师必看网-架构君整理
private void btClose_Click(object sender, EventArgs e) { wsClient.Close(); btClose.Enabled = false; btConnect.Enabled = true; }

变量查询程序:

private void btGetInfo_Click(object sender, EventArgs e)
        { 
   
            if (!connected) { 
    return; }
            QUERY qe = new QUERY();
            qe.ID = tbID.Text; ;
            string payload = JsonConvert.SerializeObject(qe);
            wsClient.Send(payload);
            tbMessage.AppendText("[DEALED] " + DateTime.Now.ToString() + " " + "发送查询信息:" + payload + Environment.NewLine);
        }

订阅变量程序

此代码由Java架构师必看网-架构君整理
private void btBook_Click(object sender, EventArgs e) { if (!connected) { return; } BOOK book = new BOOK(); if (!double.TryParse(tbCycle.Text, out book.CYCLE)) { MessageBox.Show("更新周期需要是数字!"); return; } book.ID = tbID.Text; foreach (chanel c in listChanels) { foreach (tag t in c.tags) { if (t.selected) { tagInfoForBook tag = new tagInfoForBook(); tag.TNAME = t.name; tag.CID = t.chanelid; book.listTIB.Add(tag); } } } book.COUNT = book.listTIB.Count; string payload = JsonConvert.SerializeObject(book); wsClient.Send(payload); tbMessage.AppendText("[DEALED] " + DateTime.Now.ToString() + " " + "发送订阅信息:" + payload + Environment.NewLine); }

连接建立和电文处理事件:

private void WebSocket_OnWebSocketMessageReceived(object sender,MessageReceivedEventArgs  message)
        { 
   
            try
            { 
   
                Invoke(new Action(() =>
                { 
   
                    string msg = message.Message;
                    /// <summary>
                    /// 客户端信息的Json对象
                    /// </summary>
                    JObject payloadGetJobjectNow;
                    if (infoUpdateEnable) { 
    tbMessage.AppendText("[RAW] "+DateTime.Now.ToString() + " " + msg + Environment.NewLine); }
                        
                    payloadGetJobjectNow =(JObject) JsonConvert.DeserializeObject(msg);
                    FS = getFSFromPayload(payloadGetJobjectNow);
                    switch (FS)
                    { 
   

                        case 10://验证结果
                            if (payloadGetJobjectNow["RESULT"].ToString() == "1")
                            { 
   
                                tbMessage.AppendText("[DEALED] " + DateTime.Now.ToString() + " " + "验证成功!" + Environment.NewLine);
                            }
                            else
                            { 
   
                                tbMessage.AppendText("[DEALED] " + DateTime.Now.ToString() + " " + "验证失败! " + payloadGetJobjectNow["REASON"].ToString()+ Environment.NewLine);
                            }
                            break;
                        case 20://读取设备信息
                            if (payloadGetJobjectNow.ContainsKey("listChanel") && payloadGetJobjectNow.ContainsKey("listTagInfo"))
                            { 
   
                                JArray jaChanel = (JArray)payloadGetJobjectNow["listChanel"];
                                JArray jaTag = (JArray)payloadGetJobjectNow["listTagInfo"];
                                JObject jobect;
                                int CIDn = 0;
                                chanel c;
                                if (jaChanel.Count > 0)
                                { 
   
                                    List<chanel> listChanelTemp = new List<chanel>();
                                    for(int i = 0; i < jaChanel.Count; i++)
                                    { 
   
                                        c = new chanel();
                                        listChanelTemp.Add(c);
                                    }
                                    for (int i = 0; i < jaChanel.Count; i++)
                                    { 
   
                                        jobect =(JObject) jaChanel[i];
                                        CIDn = 0;
                                        if (jobect.ContainsKey("CID"))
                                        { 
   
                                           if( int.TryParse(jobect["CID"].ToString(),out CIDn))
                                            { 
   
                                                chanel cTemp = listChanelTemp[CIDn];
                                                cTemp.TNAME = jobect["TNAME"].ToString();
                                                cTemp.BIGTYPE = jobect["BIGTYPE"].ToString();
                                                cTemp.DEVICETYPE = jobect["DEVICETYPE"].ToString();
                                                double.TryParse(jobect["CYCLE"].ToString(), out cTemp.CYCLE);
                                            }
                                        }
                                    }
                                    for (int i = 0; i < jaTag.Count; i++)
                                    { 
   
                                        CIDn = 0;
                                        jobect = (JObject)jaTag[i];
                                        if (jobect.ContainsKey("CID"))
                                        { 
   
                                            if (int.TryParse(jobect["CID"].ToString(), out CIDn))
                                            { 
   
                                                c = listChanelTemp[CIDn];
                                                tag t = new tag();
                                                c.tags.Add(t);
                                                t.chanelid = CIDn;
                                                t.name = jobect["TNAME"].ToString();
                                                t.type= jobect["TYPE"].ToString();
                                                t.comment= jobect["COMMENT"].ToString();
                                            }
                                        }
                                    }
                                    listChanels = listChanelTemp;
                                    dgvUpdate();
                                    valueUpdateEnable = false;
                                }
                            }
                            break;
                        case 30://全新订阅
                        case 31://增量订阅
                            if (payloadGetJobjectNow["RESULT"].ToString() == "1")
                            { 
   
                                tbMessage.AppendText("[DEALED] " + DateTime.Now.ToString() + " " + "订阅成功!" + Environment.NewLine);
                                valueUpdateEnable = true;
                            }
                            else if (payloadGetJobjectNow["RESULT"].ToString() == "3")
                            { 
   
                                tbMessage.AppendText("[DEALED] " + DateTime.Now.ToString() + " " + "服务器请求重新订阅!" + Environment.NewLine);
                                btBook_Click(null, null);
                            }
                            else
                            { 
   
                                tbMessage.AppendText("[DEALED] " + DateTime.Now.ToString() + " " + "订阅失败! " + payloadGetJobjectNow["REASON"].ToString() + Environment.NewLine);
                            }
                            break;
                        case 40://单次更新,更新所有的值
                        case 41://仅更新变化的变量
                            if (valueUpdateEnable)
                            { 
   
                                JArray ja = (JArray)payloadGetJobjectNow["listTV"];
                                int CID = 0;
                                String tagName = "";
                                tag tagTemp = null;
                                foreach (JObject jt in ja)
                                { 
   
                                    if (int.TryParse(jt["CID"].ToString(), out CID))
                                    { 
   
                                        tagName = jt["TNAME"].ToString();
                                        foreach (tag t in listChanels[CID].tags)
                                        { 
   
                                            if (t.name == tagName)
                                            { 
   
                                                tagTemp = t;
                                                if (double.TryParse(jt["VALUE"].ToString(), out tagTemp.value))
                                                { 
    }
                                                break;
                                            }
                                        }
                                    }
                                }
                                dgvValueUpdate();
                            }
                            break;
                        default:
                            tbMessage.AppendText("[DEALED] " + DateTime.Now.ToString() + " " + msg + Environment.NewLine);
                            break;
                    }
                }));
            }
            catch { 
    }
        }
        /// <summary>
        /// 建立连接后,马上依据现有变量配置进行订阅,并开始数值的更新。
        /// </summary>
        private void WebSocket_OnClientConnected(object  sender,EventArgs e)
        { 
   
            try
            { 
   
                Invoke(new Action(() =>
                { 
   
                    tbMessage.AppendText("[DEALED] " + DateTime.Now.ToString() + " " + "连接成功!" + Environment.NewLine);
                    connected = true;
                    btBook_Click(null, null);
                }));
            }
            catch { 
    }
        }

完整源码及协议格式下载链接

3、用HSL库的WebSocket组件构建客户端

利用商业组件库HSL(HslCommunication),可以更加方便地客户端构建(本服务器就是用HSL库来实现),引用该组件后,就不需要再引用WebSocket4Net。主要代码与第2章相同,但是HSL的客户端有一个优势:在服务器异常、网络中断后,都会自动尝试重连,用户不需要考虑重连的问题。
比如,服务器重启后,客户端会自动恢复连接,然后服务器发出需要重新订阅变量的通知,客户端只需要进行响应,重发订阅信息(见下图),就可以完成所有的工作,继续进行数据刷新了。
在这里插入图片描述

4、HTML客户端源代码

HTML天生支持WebSocket和Json,因此,可以方便地实现客户端:
在这里插入图片描述

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>SocketClientDemoWeb(www.hiddenmap.cn提供)</title>

<script>
var _hmt = _hmt || [];
    var wsIsOpened=false;
    var ws; function Book(){ 
   //订阅变量 if (wsIsOpened)
			{ 
   
				var JsonBook={ 
   "FC":30,"COUNT":4,"CYCLE":300,"ID":"abc123","listTIB":[{ 
   "CID":0,"TNAME":"tag0"},{ 
   "CID":0,"TNAME":"tag1"},{ 
   "CID":1,"TNAME":"tag0"},{ 
   "CID":1,"TNAME":"tag1"}]};//
				var JsonBookString=JSON.stringify(JsonBook);
				ws.send(JsonBookString);
			}
   }
</script>
	
</head>

<body style="margin: 0px; font-style: normal; font-family: '微软雅黑';" > <script> window.οnlοad=function bodyloaded(){ 
    
        if ("WebSocket" in window){ 
   
				ws= new WebSocket("ws://127.0.0.1:1883");//服务器地址 ws.onopen = function(){ 
   
                wsIsOpened=true;                
            }
            ws.onmessage = function (evt){ 
   
                var received_msg = evt.data;
                var count1=0;
                var count2=0; if(received_msg.length>0)
                { 
   
					document.getElementById("message_in").innerHTML=received_msg;//完整显示收到的信息内容。
					var jObject=JSON.parse(received_msg);//解析为JSON对象。
					var FS=jObject["FS"];//查询信息携带的功能码。 if(FS==40 || FS==41)
					{ 
   
						var tagCount=jObject["COUNT"];
						var listTV=jObject["listTV"];//变量数组
						for(i=0;i<listTV.length;i++){ 
   
							var chanelID=listTV[i]["CID"];
							var tagName=listTV[i]["TNAME"];
							var tagType=listTV[i]["TYPE"];
							var tagValue=listTV[i]["VALUE"]; if(tagType=="Bool"){ 
   
								if(tagValue==1){ 
   
								    tagValue="true";}
									else{ 
   
									tagValue="false";}
							}//根据数据类型进行数据的呈现。 //根据自己的变量进行解析,放在不同的位置进行呈现。 if(chanelID==0){ 
   
								if(tagName=="tag0"){ 
   
									document.getElementById("C0tag0").innerHTML=tagValue;
								}else{ 
   
								document.getElementById("C0tag1").innerHTML=tagValue;}
							}else{ 
   
								if(tagName=="tag0"){ 
   
									document.getElementById("C1tag0").innerHTML=tagValue;
								}else{ 
   
									document.getElementById("C1tag1").innerHTML=tagValue;
								}
							}
						}
					}
                }
            }          
        }      
    }
    </script>

<div align="left">

  <span style="font-weight: bold; font-size: 24px; font-family: '微软雅黑';">&nbsp;操作&nbsp;&nbsp;<button class="btn-style" οnclick="Book()">订阅</button></span>
	<br>&nbsp;&nbsp;来自于服务器的信息:
	<br>&nbsp;&nbsp;<span style="font-family: '微软雅黑'; font-size: small"><label id="message_in" >-</label></span>
</div>
	<hr>
<div align="left" font-family= "微软雅黑"> 
  <span style="font-weight: bold; font-size: 24px; font-family: '微软雅黑';">&nbsp;变量信息</span>
    <ol>
    <div align="center">
    <table width="700px" border="1" cellspacing="0">
      <tbody>
    <tr>
      <th width="10%" scope="row">&nbsp;序号</th>
      <th width="30%">&nbsp;通道</th>
	  <th width="30%">&nbsp;变量名</th>
      <th >&nbsp;值</th>
    </tr>
    <tr> <th scope="row">&nbsp;1</th>
		<td>&nbsp;0</td>
        <td>&nbsp;tag0</td> 
		<td>&nbsp;<label id="C0tag0" >??</label></td> 		
		</tr>
   <tr> <th scope="row">&nbsp;2</th>
		<td>&nbsp;0</td>
        <td>&nbsp;tag1</td> 
		<td>&nbsp;<label id="C0tag1" >??</label></td> 		
		</tr>
    <tr> <th scope="row">&nbsp;3</th>
		<td>&nbsp;1</td>
        <td>&nbsp;tag0</td> 
		<td>&nbsp;<label id="C1tag0" >??</label></td> 		
		</tr>
   <tr> <th scope="row">&nbsp;4</th>
		<td>&nbsp;1</td>
        <td>&nbsp;tag1</td> 
		<td>&nbsp;<label id="C1tag1" >??</label></td> 		
		</tr>
   </tbody>
  </table> </div></ol>
</div>
</body>
</html>

5、小结

PLC-Recorder的转发功能也是应很多网友的要求开发,配合已经实现的后台功能(关闭界面,缩小为右下角图标),希望能为大家的后续开发工作提供便利。

					2020年7月8日

录波软件、客户端完整源码及协议格式下载链接

架构君码字不易,如需转载,请注明出处:https://javajgs.com/archives/212124
0
   

发表评论