好久没有动手去写博客了,近两年时间忙碌着工作,未曾回过头细数做过的路程,感觉有点思想颓废了,之前一直从事的水利工程类开发及实施工作,考虑各方面情况下,从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年的时候入行时候做的!现在偶然也会更新完善下这个项目软件部分的功能,关于更新的部分这里就不记录了,写这文章目的我想积累并记录下点滴经验,方便自己,方便他人!
————————————————
版权声明:本文为优快云博主「囧囧虾克」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/pan869823184/article/details/9156553