最近PLC-Recoder推出了V1.6.0版本,其最大的变化是增加了数据转发功能,所有能采集到的数据,都可以通过WebSocket和Json转发出去,为不熟悉PLC底层技术,且需要数据采集的朋友们提供了一个统一的接口。下面将介绍几种客户端创建方法。
1)配置好通道(需要采集数据的设备)和所有需要采集的变量,开启录波测试。如果没有实际的设备,也可以启动录波仿真功能,模拟录波,为所有的变量提供数据。
参考文章:
《欧姆龙、松下、基恩士PLC进行连续数据采集、时序和故障追踪的方法》、
《西门子PLC进行连续数据采集、时序和故障追踪的方法》、
《三菱PLC进行连续数据采集、时序和故障追踪的方法》
2)配置服务器参数,启动服务器:
通过菜单“转发”->“配置…”,打开配置窗口设置端口号和服务器识别码,点击“应用退出”。然后通过“启动服务器”和“停止服务器”来切换服务器的状态。启动后,软件标题中将出现“[转发中]”的字符。
界面设计:
设置有连接、关闭、变量查询、订阅等按钮,下面介绍重要实现:
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);
}
}
关闭程序:
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);
}
订阅变量程序
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;
///
/// 客户端信息的Json对象
///
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 { }
}
///
/// 建立连接后,马上依据现有变量配置进行订阅,并开始数值的更新。
///
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 { }
}
完整源码及协议格式下载链接。
利用商业组件库HSL(HslCommunication),可以更加方便地客户端构建(本服务器就是用HSL库来实现),引用该组件后,就不需要再引用WebSocket4Net。主要代码与第2章相同,但是HSL的客户端有一个优势:在服务器异常、网络中断后,都会自动尝试重连,用户不需要考虑重连的问题。
比如,服务器重启后,客户端会自动恢复连接,然后服务器发出需要重新订阅变量的通知,客户端只需要进行响应,重发订阅信息(见下图),就可以完成所有的工作,继续进行数据刷新了。
HTML天生支持WebSocket和Json,因此,可以方便地实现客户端:
"utf-8">
<span class="token function">SocketClientDemoWeb</span><span class="token punctuation">(</span>www.hiddenmap.cn提供<span class="token punctuation">)</span>
"margin: 0px; font-style: normal; font-family: '微软雅黑';" >
"left">
"font-weight: bold; font-size: 24px; font-family: '微软雅黑';"> ;操作 ; ;
; ;来自于服务器的信息:
; ;"font-family: '微软雅黑'; font-size: small">
"left" font-family= "微软雅黑">
"font-weight: bold; font-size: 24px; font-family: '微软雅黑';"> ;变量信息
"center">
"700px" border="1" cellspacing="0">
"10%" scope="row"> ;序号
"30%"> ;通道
"30%"> ;变量名
;值
"row"> ;1
;0
;tag0
;
"row"> ;2
;0
;tag1
;
"row"> ;3
;1
;tag0
;
"row"> ;4
;1
;tag1
;
PLC-Recorder的转发功能也是应很多网友的要求开发,配合已经实现的后台功能(关闭界面,缩小为右下角图标),希望能为大家的后续开发工作提供便利。
2020年7月8日
录波软件、客户端完整源码及协议格式下载链接。