前言
本文主要是演示一个例子,服务器后台程序从PLC采集数据,并推送给在线客户端显示,以及推送给web端进行实时的显示,还支持远程操作,支持安卓端的同步监视和远程操作,关于HslCommunication的相关资料如下
nuget地址:https://www.nuget.org/packages/HslCommunication/
github地址:https://github.com/dathlin/HslCommunication 如果喜欢可以star或是fork,还可以打赏支持。
联系作者及加群方式(激活码在群里发放):http://www.hslcommunication.cn/Cooperation
在Visual Studio 中的NuGet管理器中可以下载安装,也可以直接在NuGet控制台输入下面的指令安装:
Install-Package HslCommunication
NuGet安装教程 http://www.cnblogs.com/dathlin/p/7705014.html
本项目的源代码地址:https://github.com/dathlin/RemoteMonitor
下面放几张截图:
服务器端的界面:
winform客户端地址:
web端的界面
所有的界面
安卓端
设计逻辑:
服务器端
主要是由数据订阅器,后台循环读取线程,在线管理器,同步网络交互网络组成。如下大致说一下各自负责的功能块:
数据订阅器
/**************************************************************************************************************** * * 本模块主要负责进行数据的发布。只要客户端订阅了相关的数据,服务器端进行推送后,客户端就可以收到数据 * * 因为本订阅器目前只支持字符串的数据订阅,所以在这里需要将byts[]转化成base64编码的数据,相关的知识请自行百度,此处不再说明 * *****************************************************************************************************************/ private NetPushServer pushServer = null; // 订阅发布核心服务器 private void NetPushServerInitialization( ) { pushServer = new NetPushServer( ); pushServer.LogNet = LogNet; pushServer.ServerStart( 23467 ); }
当你想要推送数据的时候,按照下面的方法
pushServer.PushString( "A", "这是测试数据"); // 推送数据,关键字为A
推送的测试数据到时候换成实际的数据即可。
后台循环读取数据
/*************************************************************************************************************** * * 以下演示了西门子的读取类,对于三菱和欧姆龙,或是modbustcp来说,逻辑都是一样的,你也可以尝试着换成三菱的类,来加深理解 * *****************************************************************************************************************/ private SiemensS7Net siemensTcpNet; // 西门子的网络访问器 private bool isReadingPlc = false; // 是否启动的标志,可以用来暂停项目 private int failed = 0; // 连续失败此处,连续三次失败就报警 private Thread threadReadPlc = null; // 后台读取PLC的线程 private void SiemensTcpNetInitialization( ) { siemensTcpNet = new SiemensS7Net( SiemensPLCS.S1200 ); // 实例化西门子的对象 siemensTcpNet.IpAddress = "192.168.1.195"; // 设置IP地址 siemensTcpNet.LogNet = LogNet; // 设置统一的日志记录器 siemensTcpNet.ConnectTimeOut = 1000; // 超时时间为1秒 // 启动后台读取的线程 threadReadPlc = new Thread( new System.Threading.ThreadStart( ThreadBackgroundReadPlc ) ); threadReadPlc.IsBackground = true; threadReadPlc.Priority = ThreadPriority.AboveNormal; threadReadPlc.Start( ); } private Random random = new Random( ); private bool isReadRandom = false; private void ThreadBackgroundReadPlc( ) { // 此处假设我们读取的是西门子PLC的数据,其实三菱的数据读取原理是一样的,可以仿照西门子的开发 /************************************************************************************************** * * 假设一:M100,M101存储了一个温度值,举例,100.5℃数据为1005 * 假设二:M102存储了设备启停信号,0为停止,1为启动 * 假设三:M103-M106存储了一个产量值,举例:12345678 * **************************************************************************************************/ while (true) { if (isReadingPlc) { // 这里仅仅演示了西门子的数据读取 // 事实上你也可以改成三菱的,无非解析数据的方式不一致而已,其他数据推送代码都是一样的 HslCommunication.OperateResult<JObject> read = null; //siemensTcpNet.Read( "M100", 7 ); if (isReadRandom) { // 当没有测试的设备的时候,此处就演示读取随机数的情况 read = HslCommunication.OperateResult.CreateSuccessResult( new JObject( ) { {"temp",new JValue(random.Next(2000)/10d) }, {"enable",new JValue(random.Next(100)>10) }, {"product",new JValue(random.Next(10000)) } } ); } else { HslCommunication.OperateResult<byte[]> tmp = siemensTcpNet.Read( "M100", 7 ); if(tmp.IsSuccess) { double temp1 = siemensTcpNet.ByteTransform.TransInt16( tmp.Content, 0 ) / 10.0; bool machineEnable = tmp.Content[2] != 0x00; int product = siemensTcpNet.ByteTransform.TransInt32( tmp.Content, 3 ); read = HslCommunication.OperateResult.CreateSuccessResult( new JObject( ) { {"temp",new JValue(temp1) }, {"enable",new JValue(machineEnable) }, {"product",new JValue(product) } } ); } else { read = HslCommunication.OperateResult.CreateFailedResult<JObject>( tmp ); } } if (read.IsSuccess) { failed = 0; // 读取失败次数清空 pushServer.PushString( "A", read.Content.ToString() ); // 推送数据,关键字为A ShowReadContent( read.Content ); // 在主界面进行显示,此处仅仅是测试,实际项目中不建议在服务端显示数据信息 } else { failed++; ShowFailedMessage( failed ); // 显示出来读取失败的情况 } } Thread.Sleep( 500 ); // 两次读取的时间间隔 } } // 只是用来显示连接失败的错误信息 private void ShowFailedMessage(int failed) { if(InvokeRequired) { Invoke(new Action<int>(ShowFailedMessage), failed); return; } textBox1.AppendText(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff ") + "第" + failed + "次读取失败!" + Environment.NewLine); } // 读取成功时,显示结果数据 private void ShowReadContent(JObject content) { // 本方法是考虑了后台线程调用的情况 if(InvokeRequired) { // 如果是后台调用显示UI,那么就使用委托来切换到前台显示 Invoke(new Action<JObject>(ShowReadContent), content); return; } // 提取数据 double temp1 = content["temp"].ToObject<double>( ); bool machineEnable = content["enable"].ToObject<bool>( ); int product = content["product"].ToObject<int>( ); // 实际项目的时候应该在此处进行将数据存储到数据库,你可以选择MySql,SQL SERVER,ORACLE等等 // SaveDataSqlServer( temp1 ); // 此处演示写入了SQL 数据库的方式 // 开始显示 label2.Text = temp1.ToString(); label2.BackColor = temp1 > 100d ? Color.Tomato : Color.Transparent; // 如果温度超100℃就把背景改为红色 label3.Text = product.ToString(); // 添加到缓存数据 AddDataHistory( (float)temp1 ); label5.Text = machineEnable ? "运行中" : "未启动"; }
这里面包含了后台的循环读取并且显示的操作,还支持了在没有实际的设备的情况下,直接用模拟数据来测试的情况。
这里为什么直接使用json对象来传送呢,是为了包装成一个统一的模型对象,方便后续的客户端直接进行提取操作。
在线客户端管理模块
这部分的功能需要根据实际情况来确认,如果不需要,可以直接删除,这部分的功能主要是实现在服务器端对所有在线的winform程序的在线情况进行掌控,并可以在客户端刚上线的时候处理一些事情。
/***************************************************************************************************** * * 特别说明:在线网络的模块的代码主要是为了支持服务器对客户端在线的情况进行管理 * * 当客户端刚上线的时候,服务器也可以发送一些初始数据给客户端 * *****************************************************************************************************/ private NetComplexServer netComplex; // 在线网络管理核心 private void NetComplexInitialization( ) { netComplex = new NetComplexServer( ); // 实例化 netComplex.AcceptString += NetComplex_AcceptString; // 绑定字符串接收事件 netComplex.ClientOnline += NetComplex_ClientOnline; // 客户端上线的时候触发 netComplex.ClientOffline += NetComplex_ClientOffline; // 客户端下线的时候触发 netComplex.LogNet = LogNet; // 设置日志 netComplex.ServerStart( 23456 ); // 启动网络服务 } private void NetComplex_ClientOffline( AppSession session, string object2 ) { // 客户端下线的时候触发方法 RemoveOnLine( session.ClientUniqueID ); } private void NetComplex_ClientOnline( AppSession session ) { // 回发一条初始化数据的信息 netComplex.Send( session, 2, GetHistory( ) ); // 有客户端上限时触发方法 NetAccount account = new NetAccount( ) { Guid = session.ClientUniqueID, Ip = session.IpAddress, Name = session.LoginAlias, OnlineTime = DateTime.Now, }; AddOnLine( account ); } private void NetComplex_AcceptString( AppSession stateone, HslCommunication.NetHandle handle, string data ) { // 接收到客户端发来的数据时触发 }
本例子里还包含了在线的客户端账号信息,支持扩展额外的信息登录
private List<NetAccount> all_accounts = new List<NetAccount>( ); private object obj_lock = new object( ); // 新增一个用户账户到在线客户端 private void AddOnLine( NetAccount item ) { lock (obj_lock) { all_accounts.Add( item ); } UpdateOnlineClients( ); } // 移除在线账户并返回相应的在线信息 private void RemoveOnLine( string guid ) { lock (obj_lock) { for (int i = 0; i < all_accounts.Count; i++) { if (all_accounts[i].Guid == guid) { all_accounts.RemoveAt( i ); break; } } } UpdateOnlineClients( ); } /// <summary> /// 更新客户端在线信息 /// </summary> private void UpdateOnlineClients( ) { if (InvokeRequired && IsHandleCreated) { Invoke( new Action( UpdateOnlineClients ) ); return; } lock (obj_lock) { listBox1.DataSource = all_accounts.ToArray( ); }
账号的类为
/// <summary> /// 用于在线控制的网络类 /// </summary> public class NetAccount { /// <summary> /// 唯一ID /// </summary> public string Guid { get; set; } /// <summary> /// Ip地址 /// </summary> public string Ip { get; set; } /// <summary> /// 上线时间 /// </summary> public DateTime OnlineTime { get; set; } /// <summary> /// 名称 /// </summary> public string Name { get; set; } private string GetOnlineTime() { TimeSpan ts = DateTime.Now - OnlineTime; if (ts.TotalSeconds < 60) { return ts.Seconds + " 秒"; } else if(ts.TotalHours < 1) { return ts.Minutes + "分" + ts.Seconds + "秒"; } else if(ts.TotalDays < 1) { return ts.Hours + "时" + ts.Minutes + "分"; } else { return ts.Days + "天" + ts.Hours + "时"; } } /// <summary> /// 字符串标识形式 /// </summary> /// <returns></returns> public override string ToString( ) { return "[" + Ip + "] : 在线时间 " + GetOnlineTime( ); } }
同步的网络模块
这部分的功能相当于一个接口功能,为了支持远程的客户端或是web端进行对PLC进行读写操作实现的,事实上,当远程的程序点击了启动XX功能之后,远程的代码就通过同步网络将消息都发送给了服务器,服务器再去操作PLC,然后在回发给远程结果。
/***************************************************************************************************** * * 特别说明:同步网络模块,用来支持远程的写入操作,特点是支持是否成功的反馈,这个信号对客户端来说是至关重要的 * * 不仅仅支持客户端的操作,还支持web端的操作。 * *****************************************************************************************************/ private NetSimplifyServer netSimplify; // 同步网络访问的服务支持 private void NetSimplifyInitialization( ) { netSimplify = new NetSimplifyServer( ); // 实例化 netSimplify.ReceiveStringEvent += NetSimplify_ReceiveStringEvent; // 服务器接收字符串信息的时候,触发 netSimplify.LogNet = LogNet; // 设置日志 netSimplify.ServerStart( 23457 ); // 启动服务 } private void NetSimplify_ReceiveStringEvent( AppSession session, HslCommunication.NetHandle handle, string msg ) { if (handle == 1) { string tmp = StartPLC( ); LogNet?.WriteInfo( tmp ); // 远程启动设备 netSimplify.SendMessage( session, handle, tmp ); } else if (handle == 2) { string tmp = StopPLC( ); LogNet?.WriteInfo( tmp ); // 远程停止设备 netSimplify.SendMessage( session, handle, tmp ); } else { netSimplify.SendMessage( session, handle, msg ); } }
客户端实现,
web端的数据推送实现,需要SignalR支持,关于这方面的技术细节,可以参考SignalR官网:https://www.asp.net/signalr
本项目还支持了简单的图表显示,图标的支持来源于百度的echart项目,http://echarts.baidu.com/
安卓客户端,主要是订阅器的实现和同步客户端的实现