融合SignalR的ModBus-TCP客户端

搞完OPC搞ModBus,最近是和自控系统杠上了,自己的业务系统要和一堆现场设备对接,各种协议都有,上次刚写了一篇关于融合SignalR的OPCClient,这次就换成ModBus了。解决思路基本类似,具体实现稍有不同,详见下文。


严谨的讲,OPC和ModBus完全不是一个层次的东西,并不存在可比性。OPC是纯粹软件层面的协议,而ModBus是硬件之间的通讯协议,使用范围不同。ModBus是常见的工业控制通信协议之一,其他的还有CANBus、ProfiBus等,而OPC协议正是为了解决多种协议并存时软件驱动混乱的问题而诞生的。ModBus更接近底层硬件,OPC更接近上层软件。
鉴于实际项目需要,我们开发的业务系统需要从现场设备读取数据,而设备由不同的施工单位负责,有的设备厂商在安装调试后提供了OPCServer,直接用之前开发的OPCClient连接就好了;有的设备厂商不提供OPCServer,就要自己想办法了。(不提供OPCServer的工控系统集成商不是好的施工单位)

解决方案

总体思路沿用之前开发OPCClient的方案,使用融合SignalR的服务端做数据转发。对于不提供OPCServer又需要读取数据的设备,有两种方案可以解决这个问题:
方案一:安装带ModBus驱动的OPCServer,连接设备,然后再用OPCClient连接OPCServer就行了。
方案二:开发基于ModBus-TCP协议的客户端,直接连接设备读取数据。
方案一和方案二的区别如下方左图和右图所示。
这里写图片描述

本次采用的是方案二,方案一后面会单独写文说明。方案二是基于ModBus协议直接从设备控制器中读取写入数据,而ModBus协议同时提供了TCP、RTU和ASCII三种编码格式,本次选用的是TCP编码格式。

ModBus-TCP编码格式

协议就是标准、规范,ModBus的TCP实现就是通过Socket发送接收字节流,按照ModBus的TCP编码格式发送消息就能得到特定格式的反馈。这种格式是Requset/Response形式的,即发送一个请求(Request)得到一个反馈(Response)。ModBus规定的两者格式如下:

序号01234567891011
请求------设备地址功能码起始地址起始地址读取长度读取长度
反馈------设备地址功能码数据长度数据数据

请求和反馈的第0-5位相对固定,第5位存放从第6位开始之后所有数据的长度值;第6位均为设备地址;第7位为功能码,标识要执行的操作或返回的数据类型,常见功能码有数字量输入DI、数字量输出DO、模拟量输入AI、模拟量输出AO等。对于请求,第7位的功能码直接决定了8、9位开始地址的取值范围。同时由于请求中10、11位的读取长度,决定了单次请求可以获取多个测点的数据,这点在反馈的8位也得到了体现。反馈的8位存放从9位开始的所有数据的长度值。
有了以上对ModBus-TCP编码格式的理解,下一步编写代码实现功能的思路就更清晰了。

服务端实现

为了保证数据的通用性,使用的仍是上篇文章中的服务端。

融合SignalR的OPCClient

ModBus客户端实现

数据库

数据库仍旧使用的是SQLite,用来存储测点信息。根据ModBus协议的格式,发送一次请求获得的一次反馈中可以包含多个测点的数据,所以数据库表结构与之前OPCClient的表结构略有不同。
这里写图片描述

连接/断开Socket

全局变量

        private string _controllerGuid; //控制器ID
        private string _controllerIp; //控制器地址
        private readonly int _controllerPort = 502; //控制器端口号
        private string _deviceGuid; //设备ID
        private string _deviceAddress; //设备地址
        private static readonly int _updateRate = int.Parse(ConfigurationManager.AppSettings["UpdateRate"]); //刷新频率
        private bool isConnect; //是否连接ModBus

        private DataTable _dtItems = new DataTable();
        private DataTable _dtDevices=new DataTable();

        private DispatherTimer _time;

连接、断开ModBus

        #region 连接ModBus

        private void btnStart_Click(object sender, RoutedEventArgs e)
        {
            byte[] data = new byte[1024];
            IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(_controllerIp), _controllerPort);
            _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            try
            {
                _socket.Connect(endPoint);
                isConnect = true;
                WriteToStatus("控制器" + _controllerIp + "已连接!");
                btnStart.IsEnabled = false;
                btnStop.IsEnabled = true;
            }
            catch (Exception ex)
            {
                WriteToStatus(ex.Message);
                return;
            }
            ThreadStart threadStart = ReceiveMessage;
            ThreadMessage = new Thread(threadStart);
            ThreadMessage.Start();

            _timer.Start();
        }



        #endregion

        #region 断开ModBus

        private void btnStop_Click(object sender, RoutedEventArgs e)
        {
            ThreadMessage.Abort();
            _timer.Stop();
            _socket.Close();
            isConnect = false;
            WriteToStatus("控制器" + _controllerIp + "已断开!");
        }

        #endregion

发送/接收数据

        #region 发送数据

        private void Send(byte[] dataBytes)
        {
            _socket.Send(dataBytes);
        }

        #endregion

        #region 接收数据
        private void ReceiveMessage()
        {
            while (true)
            {
                try
                {
                    byte[] data = new byte[512];
                    _socket.Receive(data);
                    Dispatcher.InvokeAsync(() =>
                    {
                        RebuildData(data);
                    });
                }
                catch (Exception e)
                {
                    WriteToStatus(e.Message);
                }
            }
        }

反馈数据解析

由于反馈的数据中包含了多个测点数据,所以要将这些数据分别解析出来。对于DI、DO、AI、AO的反馈数据格式是不同的,需要区分对待,放上DI、DO的解析方法。

        private void RebuildData(byte[] dataBytes)
        {
            byte deviceAddress = dataBytes[6];
            byte functionCode = dataBytes[7];
            int dataLength = Convert.ToInt32(dataBytes[8]);
            byte[] datas=new byte[dataLength];
            Buffer.BlockCopy(dataBytes,9,datas,0,dataLength);

            switch (functionCode)
            {
                case 0x01:
                    ConvertByteToDIO(ByteToStringHex(deviceAddress), ByteToStringHex(functionCode), dataLength,datas);
                    break;
                case 0x02:
                    ConvertByteToDIO(ByteToStringHex(deviceAddress), ByteToStringHex(functionCode), dataLength, datas);
                    break;
                case 0x03:
                    break;
                case 0x04:
                    break;
            }

            WriteToStatus(BitConverter.ToString(datas));
        }

        internal void ConvertByteToDIO(string deviceAddress,string functionCode,int dataLength,byte[] data)
        {
            string tmp = string.Empty;
            for (int i=0;i<dataLength;i++)
            {
                string s = ("00000000" + Convert.ToString(data[dataLength - i - 1], 2));
                tmp += s.Substring(s.Length - 8, 8);
            }
            int length = tmp.Length;
            string[] arry=new string[length];
            for (int i = 0; i < length; i++)
            {
                arry[i] = tmp.Substring(length - i - 1, 1);
            }
            DataRow[] drs = _dtItems.Select("DeviceAddress='"+ deviceAddress + "' and FunctionCode='"+ functionCode + "'");
            foreach (DataRow dr in drs)
            {
                int index = int.Parse(dr["ItemIndex"].ToString());
                OPCValueModel model = new OPCValueModel()
                {
                    ServerId = dr["ControllerGuid"].ToString(),
                    ServerName = dr["ControllerIP"].ToString(),
                    GroupName = dr["DeviceAddress"].ToString(),
                    ItemHandle = dr["ItemAddress"].ToString(),
                    ItemId = dr["ItemGuid"].ToString(),
                    ItemName = dr["ItemName"].ToString(),
                    ItemRemark = dr["ItemRemark"].ToString(),
                    AlarmType = dr["AlarmType"].ToString(),
                    AlarmLevel = dr["AlarmLevel"].ToString(),
                    MaxValue = dr["MaxValue"].ToString(),
                    MinValue = dr["MinValue"].ToString(),
                    ItemValue = arry[index]
                };
                if (isStart)
                {
                    Sender("RefreshOPCData", model);
                }
            }
        }

        internal string ByteToStringHex(byte hex)
        {
            string tmp = string.Empty;
            switch (hex)
            {
                case 0x01:
                    tmp = "01";
                    break;
                case 0x02:
                    tmp = "02";
                    break;
                case 0x03:
                    tmp = "03";
                    break;
                case 0x04:
                    tmp = "04";
                    break;
                case 0x05:
                    tmp = "05";
                    break;
            }
            return tmp;
        }

定时发送请求获取数据

因为只有发送请求才能获取反馈,为了保证数据的实时性,需要创建一个定时器不断发送请求获取数据。

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            _timer = new DispatcherTimer();
            _timer.Interval = new TimeSpan(0, 0, 0, _updateRate);
            _timer.Tick += TimeCycle;

     }
 //从数据库获取所有设备地址、功能码、起始地址和读取长度,并拼接成字节数组。  
          private void GetAllDevices()
        {
            OperationFun operation = new OperationFun();
            _dtDevices = operation.GetDevice(_controllerGuid);
        }

        private void TimeCycle(object sender, EventArgs e)
        {
            foreach (DataRow dr in _dtDevices.Rows)
            {
                byte[] dataBytes = new byte[] { 0x00, 0x01, 0x00, 0x00, 0x00, 0x06,Convert.ToByte(dr["DeviceAddress"]),Convert.ToByte(dr["FunctionCode"]),0x00, Convert.ToByte(dr["StartAddress"]) ,0x00, Convert.ToByte(dr["DataLength"]) };
                Send(dataBytes);
            }

}

实时数据传输

实时数据传输依然是利用的SignalR,将解析出来的数据转换成特定格式发送给服务端并由服务端转发至PC客户端、Web客户端。代码和上篇文章一模一样。

客户端

PC客户端和Web客户端的代码也都没变,代码还是看上一篇了。
写完这个ModBusClient之后,整个系统的结构就变成下面这样了:
这里写图片描述
OPCClient和ModBusClient相互独立,不会影响彼此运行,至此预期目的已达成。


条条大路通罗马,罗马虽已到,但选的这条路不一定是最快最便捷的,下次再试试其他的路。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值