【原创】c#如何实现RTU远程数据采集功能及RTU在水利工程中的运用

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/pan869823184/article/details/9156553


好久没有动手去写博客了,近两年时间忙碌着工作,未曾回过头细数做过的路程,感觉有点思想颓废了,之前一直从事的水利工程类开发及实施工作,考虑各方面情况下,从11年开始转行投入到电力智能电网的软件开发工作,一转行就到了如今,虽行业上跨度自我感觉挺大的,不过对于一个从事软件开发工作者来说,从了解客户需求、工程实施,甚至是编码开发到后期维护都是大同小异的.

讲了这么多的废话,现在开始今天的主题了,在水利工程中,C#如何实现RTU对现场设备数据进行采集及控制功能。如果您对RTU不了解的话,先可以看下相关资料http://baike.baidu.com/view/430166.htm 。

如果您是现场实施人员,你需要了解RTU硬件接口、基本通讯协议、布线安装、调试维护等信息,一般购买RTU时,厂家会提供资料。

如果您是开发人员,你需要了解RTU支持哪些通讯协议(Modbus/TCP/485/232/串口...),及开发时所需的资料(开发包,调试工具),这部分资料厂家也会在购买时提供。

RTU设备

设备是我朋友公司自己做的,跟南瑞、基康RTU产品一样,呵呵.反正都差不多啦!

工程名称

谷城县潭口水库水雨情遥测系统

       【湖北一个地方性水库的项目,这里RTU主要作用是RTU通过458连接现场所有测点传感器、雨量计、水位计、闸门等设备,通过RTU的GPRS模块,将数据实时转发到互联网的指定IP的指定端口上,接收端通过以太网(TCP/IP协议)将数据传输到监控室,上位机软件接收数据及根据数据实现各种业务】

关于RTU通讯协议

RTU的通讯协议基于MODBUS协议,至于协议说明在后面的博客里面会提及到,我这里只讲一点数据帧格式,因为后面的接收数据的部分解析会用到。

  具体的帧格式如下:
modbus RTU  地址域 功能码 数据 差错校验
modbus TCP  目的地址 协议id 长度 单元号 功能码 数据
简单的说 tcp是由RTU加工而来的,而RTU则是另外一种概念,不包含在modbus协议内,是工控行业对监控设备的简称。

下面开始着重讲C#实现数据采集通讯模块的编码了。在上位机获取指定IP的指定端口数据之前,需要先了解Socket协议,基于TCP/IP的通讯方式,本文就不提了,以后有时间会在专栏中详细说道。实时获取数据,采用多线程非阻塞形式,一个连接连接对应一个线程,数据获取后,实时显示到指定的控件上及输出到监控窗口位置,最后立即释放该子线程资源,并等待下次连接。

程序采用MVC三层来写的,主要是考虑后期功能扩展及可移植性、通用性等情况。下面开始编码:

1、编写客户端操作类:该类主要实现连接断开服务器,收发数据及数据业务逻辑处理等。

using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Net;

namespace RTU
{
    public class Client : IDisposable
    {
        #region DataMember & Ctor
        private TcpClient tcpclient;
        private NetworkStream netstream;
        public string IP;
        public string Port;
        private byte[] readBytes;
        private object tcpClientLock = new object();//锁TcpClient;Dispose之后就不允许EndRead,远程连接断开以后,就不允许再调用Dispose
        private bool closed = false;//包括本地主动断开和远程断开

        //在事务处理结束后才触发下列事件
        public event DlgNoParam ConnectFailEvent;
        public event DlgOneParam<string> NewClientEvent;
        public event DlgOneParam<string> RecvMsgEvent;
        public event DlgOneParam<string> RemoteDisconnectEvent;
        public event DlgNoParam LocalDisconnectEvent;

        /// <summary>
        /// 服务的client的构造函数
        /// </summary>
        /// <param name="tcpclient"></param>
        public Client(TcpClient tcpclient)
        {
            this.tcpclient = tcpclient;
            readBytes = new byte[tcpclient.ReceiveBufferSize];//接收数据的缓冲区大小,如果太小一段数据会多次接收完成
            netstream = tcpclient.GetStream();//如果远程客户端断开一样可以获得netstream
        }

        /// <summary>
        /// 客户端client的构造函数
        /// </summary>
        public Client()
        {
        }
        #endregion

        #region 数据收发

        /// <summary>
        /// 在建立连接的情况下发送消息
        /// </summary>
        /// <param name="msg"></param>
        /// <returns></returns>
        public bool SendMsg(string msg)
        {
            bool result = false;
            try
            {
                if (!string.IsNullOrEmpty(msg))
                {
                    byte[] buff = Encoding.Default.GetBytes(msg);
                    netstream.Write(buff, 0, buff.Length);
                    result = true;
                }
            }
            catch
            {
                throw;
            }
            return result;
        }

        /// <summary>
        /// 服务的接收到客户端连接后最先做的操作之一,客户端接收数据线程起始
        /// </summary>
        /// <returns></returns>
        public bool BeginRead()
        {
            bool result = false;
            try
            {
                IP = (tcpclient.Client.RemoteEndPoint as IPEndPoint).Address.ToString();
                Port = (tcpclient.Client.RemoteEndPoint as IPEndPoint).Port.ToString();
                if (NewClientEvent != null)
                {
                    NewClientEvent(IP + " " + Port);
                }
                netstream.BeginRead(readBytes, 0, readBytes.Length, EndRead, null);//如果远程客户端断开这句话一样可以执行
                result = true;
            }
            catch
            {
                throw;
            }
            return result;
        }

        /// <summary>
        /// 有互斥资源
        /// 接收数据,远程连接断开,远程程序关闭,本地连接断开,都会按顺序调用进来;因为连接关闭后不再调用BeginRead
        /// 服务器listener.stop时不会进入这个函数,客户端照样通讯,服务端只是不能接收新连接而已
        /// </summary>
        /// <param name="ar"></param>
        private void EndRead(IAsyncResult ar)
        {
            lock (tcpClientLock)
            {
                if (!closed)//如果本地主动断开就不会进入
                {
                    try
                    {
                        int count = netstream.EndRead(ar);
                        if (count > 0)
                        {
                             //string recvStr = Encoding.Default.GetString(readBytes, 0, count);
                            string recvStr="";
                            List<String> list_str = new List<String>();
                            //把接受的数据转换成16进制字符串
                            for (int i = 0; i < count; i++)
                            {
                                String strtemp = Convert.ToString(readBytes[i], 16);
                                while (strtemp.Length < 2)
                                {
                                    strtemp = "0" + strtemp;
                                }
                                if (String.IsNullOrEmpty(recvStr))
                                {
                                    recvStr += strtemp;
                                }
                                else
                                {
                                    recvStr += " " + strtemp;
                                }
                                list_str.Add(strtemp);
                            }
                            recvStr = DateTime.Now.ToString("HH:mm:ss") + " [" + IP + " " + Port + "] :\r\n" + recvStr + "\r\n\r\n";

                            
                           


                            if (RecvMsgEvent != null)
                            {
                                RecvMsgEvent(recvStr);
                            }
                            readBytes = new byte[tcpclient.ReceiveBufferSize];
                            netstream.BeginRead(readBytes, 0, readBytes.Length, EndRead, null);
                        }
                        else//远程客户端主动断开
                        {
                            LocalClientClose();
                        }
                    }
                    catch (Exception ex)
                    {
                        if (ex.Message.Contains("无法从传输连接中读取数据: 远程主机强迫关闭了一个现有的连接"))
                        {
                            LocalClientClose();
                        }
                        else
                        {
                            throw;
                        }
                    }
                }
            }
        }

        #endregion

        #region 客户端连接和关闭

        /// <summary>
        /// 客户端的client连接服务器
        /// </summary>
        /// <param name="ip"></param>
        /// <param name="port"></param>
        /// <returns></returns>
        public bool Connect(string ip, string port)
        {
            bool result = false;
            try
            {
                ip = ip.Trim();
                port = port.Trim();
                if (!string.IsNullOrEmpty(ip) && !string.IsNullOrEmpty(port))
                {
                    IPAddress ipAddress = IPAddress.Parse(ip);
                    IPEndPoint point = new IPEndPoint(ipAddress, int.Parse(port));
                    tcpclient = new TcpClient(AddressFamily.InterNetwork);
                    readBytes = new byte[tcpclient.ReceiveBufferSize];
                    tcpclient.Connect(point);
                    netstream = tcpclient.GetStream();
                    closed = false;
                    result = true;
                }
            }
            catch (Exception ex)
            {
                if (ex.Message.Contains("由于目标机器积极拒绝,无法连接"))
                {
                    if (ConnectFailEvent != null)
                    {
                        ConnectFailEvent();
                    }
                }
                else
                {
                }
            }
            return result;
        }

        /// <summary>
        /// 远程连接断开后(点关闭断开,程序退出断开)本地连接处理
        /// </summary>
        private void LocalClientClose()
        {
            closed = true;
            DisposeEx();
            if (RemoteDisconnectEvent != null)
            {
                string param = IP + " " + Port;
                if (RemoteDisconnectEvent != null)
                {
                    RemoteDisconnectEvent(param);
                }
            }
        }

        /// <summary>
        /// 有互斥资源
        /// 在已连接条件下关闭本地连接和资源释放时调用
        /// </summary>
        public void Dispose()
        {
            lock (tcpClientLock)
            {
                if (!closed)
                {
                    closed = true;
                    DisposeEx();
                    if (LocalDisconnectEvent != null)
                    {
                        LocalDisconnectEvent();
                    }
                }
            }
        }

        /// <summary>
        /// 由Dispose和LocalClientClose调用
        /// </summary>
        private void DisposeEx()
        {
            if (netstream != null)
            {
                netstream.Dispose();
                netstream = null;
            }
            if (tcpclient != null)
            {
                tcpclient.Close();
                tcpclient = null;
            }
        }

        #endregion
    }
}

2、实现服务器操作类:服务器连接接收及监听端口数据、客户端操作。

using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Threading;
using System.Net;

namespace RTU
{
    public class Server : IDisposable
    {
        #region DataMember
        private TcpListener listener;
        private Client currentClient;

        private object listenerLock = new object();//锁TcpListener;在BeginAcceptClient,EndAcceptClient,ServerClose这3函数使用;ServerClose之后不能再调用BeginAcceptClient,EndAcceptClient
        private bool serverStart = false;

        private EventWaitHandle eventWaitHdl = new EventWaitHandle(false, EventResetMode.ManualReset);

        //在事务处理结束后才触发下列事件
        public event DlgNoParam ServerStartEvent;
        public event DlgNoParam ServerCloseEvent;
        public event DlgOneParam<string> NewClientEvent;
        public event DlgOneParam<string> RecvMsgEvent;
        public event DlgNoParam LocalDisconnectEvent;
        public event DlgOneParam<string> RemoteDisconnectEvent;

        #endregion

        #region 服务器监听启动关闭和连接接收
        public bool Start(string ip, int port)
        {
            bool result = false;
            try
            {
                if (!string.IsNullOrEmpty(ip) && port > 1024)
                {
                    IPAddress ipAddr = IPAddress.Parse(ip);
                    IPEndPoint point = new IPEndPoint(ipAddr, port);
                    listener = new TcpListener(point);
                    listener.Start();
                    result = true;
                    serverStart = true;
                    if (ServerStartEvent != null)
                    {
                        ServerStartEvent();
                    }
                }
            }
            catch
            {
                throw;
            }
            return result;
        }

        /// <summary>
        /// 有互斥资源
        /// 接收连接主入口,监听启动后调用和每次接收到连接后调用
        /// </summary>
        public void BeginAcceptClient()
        {
            try
            {
                while (true)
                {
                    eventWaitHdl.Reset();
                    lock (listenerLock)
                    {
                        if (serverStart)
                        {
                            listener.BeginAcceptTcpClient(EndAcceptClient, null);
                        }
                        else
                        {
                            return;
                        }
                    }
                    eventWaitHdl.WaitOne();
                }
            }
            catch
            {
                throw;
            }
        }

        /// <summary>
        /// 有互斥资源
        /// 接收到客户端连接时调用到,关闭listener时也调用到
        /// </summary>
        /// <param name="ar"></param>
        private void EndAcceptClient(IAsyncResult ar)
        {
            try
            {
                TcpClient tcpclient = null;
                lock (listenerLock)
                {
                    if (serverStart)
                    {
                        tcpclient = listener.EndAcceptTcpClient(ar);//在这句话之前或者client.BeginRead之前断掉远程客户端都没事,tcpclient都不为null
                    }
                }
                eventWaitHdl.Set();
                if (tcpclient != null)//listener.stop后tcpclient == null,不会进入下面代码
                {
                    InitClient(tcpclient);
                }
            }
            catch
            {
                throw;
            }
        }

        /// <summary>
        /// 有互斥资源
        /// 在listener监听状态下关闭listener时调用,资源释放时调用
        /// </summary>
        /// <returns></returns>
        public bool ServerClose()
        {
            bool result = false;
            try
            {
                lock (listenerLock)
                {
                    if (serverStart)
                    {
                        serverStart = false;
                        listener.Stop();
                        result = true;
                        if (ServerCloseEvent != null)
                        {
                            ServerCloseEvent();
                        }
                    }
                }
            }
            catch
            {
                throw;
            }
            return result;
        }
        #endregion

        #region Client相关操作
        private void InitClient(TcpClient tcpclient)
        {
            currentClient = new Client(tcpclient);
            ClientDlgSubscribe(true);
            currentClient.BeginRead();//即使在这之前服务器断开,该函数返回值也等于1
        }

        /// <summary>
        /// InitClient,CurrentClient_LocalDisconnectEvent,Client_DisconnectEvent调用到
        /// 在事务处理结束后调用到
        /// </summary>
        /// <param name="add"></param>
        public void ClientDlgSubscribe(bool add)
        {
            if (add)
            {
                currentClient.NewClientEvent += new DlgOneParam<string>(Client_NewClientEvent);
                currentClient.RecvMsgEvent += new DlgOneParam<string>(Client_RecvMsgEvent);
                currentClient.RemoteDisconnectEvent += new DlgOneParam<string>(Client_DisconnectEvent);
                currentClient.LocalDisconnectEvent += new DlgNoParam(CurrentClient_LocalDisconnectEvent);
            }
            else
            {
                currentClient.NewClientEvent -= new DlgOneParam<string>(Client_NewClientEvent);
                currentClient.RecvMsgEvent -= new DlgOneParam<string>(Client_RecvMsgEvent);
                currentClient.RemoteDisconnectEvent -= new DlgOneParam<string>(Client_DisconnectEvent);
                currentClient.LocalDisconnectEvent -= new DlgNoParam(CurrentClient_LocalDisconnectEvent);
            }
        }

        public bool SendMsg(string msg)
        {
            bool result = false;
            try
            {
                if (currentClient.SendMsg(msg))
                {
                    result = true;
                }
            }
            catch
            {
                throw;
            }
            return result;
        }

        /// <summary>
        /// 客户端连接情况下断开连接时调用,释放资源时调用
        /// </summary>
        /// <returns></returns>
        public bool DisconnectClient()
        {
            bool result = false;
            try
            {
                if (currentClient != null)
                {
                    currentClient.Dispose();
                    result = true;
                }
            }
            catch
            {
                throw;
            }
            return result;
        }

        public void Client_NewClientEvent(string param)
        {
            if (NewClientEvent != null)
            {
                NewClientEvent(param);
            }
        }

        public void Client_RecvMsgEvent(string param)
        {
            if (RecvMsgEvent != null)
            {
                RecvMsgEvent(param);
            }
        }

        public void Client_DisconnectEvent(string param)
        {
            ClientDlgSubscribe(false);
            if (RemoteDisconnectEvent != null)
            {
                RemoteDisconnectEvent(param);
            }
        }

        public void CurrentClient_LocalDisconnectEvent()
        {
            ClientDlgSubscribe(false);
            if (LocalDisconnectEvent != null)
            {
                LocalDisconnectEvent();
            }
        }
        #endregion

        #region 资源释放
        public void Dispose()
        {
            ServerClose();
            DisconnectClient();
        }
        #endregion
    }
}

3、客户端及服务器操作类已经完成了,现在开始实现基本的业务处理模块了。软件的实时监控界面如下:



【启动服务器】开启或者关闭服务器端口监听及数据处理功能,获取到的测站数据按照报文格式解析,分别显示到界面的各个Textbox里面,并在监控输出地方打印相关信息,这部分有专门的函数做业务处理,

 void fun_startorstopser()
        {
          try
           {
            if (button1.Text.Trim().Equals("停止服务器"))
            {
                trueorfalst = false; try
                {
                    socketclient.Shutdown(SocketShutdown.Both);
                }
                catch (Exception ex)
                {
                    RTU.SyslogErr.WriteErr(ex);
                }
                socketclient.Close();
                socketclient = null;
                threadcon.Abort();
                button1.Text = "启动服务器";
                isopen = false;
                textPRO.Enabled = true;
            }
            else
            {
                trueorfalst = true;
                string ip = GetLocalIp();
                IPAddress ip0 = IPAddress.Parse(ip);
                IPEndPoint endPoint = new IPEndPoint(ip0, int.Parse(textPRO.Text.Trim()));
                socketclient = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

       
                    RTU.SyslogErr.WriteErr("----------操作记录:启动服务器");
                    //将服务端与端口绑定
                    socketclient.Bind(endPoint);
                    //设定允许的排队队列大小
                    socketclient.Listen(300);

                    //线程列表中加入包含Accept的线程
                    //ThreadPool.QueueUserWorkItem(new WaitCallback(wacthprot));
                    //int a = 0, b = 0;
                    //ThreadPool.GetMaxThreads(out a, out b);
                    //MessageBox.Show(a.ToString() + "//" + b.ToString());
                   
                    threadcon = new Thread(new ThreadStart(wacthprot));
                    threadcon.IsBackground = true;
                    threadcon.Start();
                    textPRO.Enabled = false;
                    button1.Text = "停止服务器";
                    //信息输出类
                    RTU.SyslogErr.WriteErr("----------服务器名:" + System.Net.Dns.GetHostName());
                    RTU.SyslogErr.WriteErr("----------服务器IP:" + ip);
                    RTU.SyslogErr.WriteErr("----------在" + textPRO.Text.Trim() + "端口启动服务 等待客户端连接!");

                    isopen = true;
               }
           }
          catch (Exception ex)
          {
              txt_ljinfo.AppendText("异常:端口启动或关闭异常-" + ex.Message + "\n");
              RTU.SyslogErr.WriteErr(ex);
          }
        }


RTU数据接收方式是定时上报模式,采用五分钟上传一次数据作为实时监控数据显示,整点上传数据做业务数据处理、比如日雨量数据显示、水位数据显示、报表输出及数据存储、数据查询与转发等功能。


数据报文格式是:
7E FF 01 0E 3F 12 08 25 11 00 00 F0 00 08 00 14 00 00 00 00 00 00 00 00 00 00 00 00 01 7A 01 7A 01 7A 01 7A 01 7A 01 7A 01 7A 01 7A 01 7A 01 7A 01 7A 01 7A 14 33 00 00 00 00 00 00 00 00 00 00 00 03 DC FA

关于这段报文格式的说明,我还是把它截图下来具体说明下:



关于RTU远程数据采集功能的说明到这一步已经完成了,现在简单说明下雨水情监控系统相关说明,一套完整项目的雨水情监控系统,包含现场硬件设备之前,软件部分主要是包括雨水情监控系统跟WEB信息发布系统及LED实时屏显示软件部分。下面分开简单说明下各个系统的基本功能模块:

雨水情监控系统:

1、数据测控模块:

包含RTU数据采集及RTU控制功能功能,数据采集中包含数据解析、数据存储、数据显示等;

2、数据查询模块

包含日雨量数据、时段雨量数据、日水位数据、时段水位数据、RTU供电电压数据、水位对应溢洪道数据、水位对应库容数据、水文参数数据等;

3、数据曲线模块:

包含日/旬/月/年雨量、日/旬/月/年水位、及库容变化曲线等;

4、数据报表模块:

包含日/旬/月/年雨量数据报告导出、日/旬/月/年水位数据报告导出、支持自定义表格字段及自定义数据条件报表数据导出等;

5、RTU通讯检测模块:

包含RTU数据报文分析、网络通讯异常提醒用户、端口定时自检功能、故障记录日志数据记录等功能;

6、其他模块:

包含系统配置管理、用户管理、系统运行日志管理、权限管理、挂机锁等等;

关于数据传输:

采用GPRS无线网络传输方案:GPRS无线网络为主信道,利用现有固定IP的服务器.通过中国移动公网GPRS无线网络上传水位数据.雨量数据在本地服务器数据内,可直接读取。GSM短信为备用信道,在服务器上加装GSM短信模块。在信号较弱的时候,数据采集仪自动切换到GSM短信备用信道上传数据。预警及警报发布直接采用该信道下发预警、警报等信息。


WEB信息发布系统:

该系统的功能模块包含雨水情监测系统的模块功能,比如:数据查询、数据曲线、数据报表等等,同时也具备如下功能模块:

1、预警预报模块:

针对小型水库点多、面广、分布分散的特点,在地理信息系统平台上建设一个具有地图浏览、图上查询、报表输出、预警预报等基本功能的信息管理系统,在满足防汛、预警等基本功能的基础上构建信息查询统计、抗洪能力预估、防汛警报发布于一体的小型水库自动监控、预警平台。

2、抗洪能力预估模块:

根据工程现场降雨径流关系及当前影响雨量,自动计算各水库的可拦截水量,评估水库可承受的降雨量,对抗洪能力进行预估.做到雨前防汛心中有数。

LED实时屏显示软件:

该软件主要实时显示各个测点水位、雨量、库容、流量等等实时数据,并且具备自动快速响应预警功能,界面如图:




关于雨水情项目的一个实施基本情况说明已经完成了,如果您对这个项目感兴趣,欢迎留言或者QQ探讨交流!这个项目是11年的时候入行时候做的!现在偶然也会更新完善下这个项目软件部分的功能,关于更新的部分这里就不记录了,写这文章目的我想积累并记录下点滴经验,方便自己,方便他人!


展开阅读全文

没有更多推荐了,返回首页