前言
本文將使用一個NuGet公開的組件技術來實現一個ModBus TCP的服務器端數據引擎,方便的實現接收來自各種設備的數據。並且該服務器模擬真實的設備,包含了數據池功能,可以接受來自任何支持Modbus tcp的客戶端進行讀寫數據。C#實現的客戶端類請參考下面這篇文章:http://www.cnblogs.com/dathlin/p/7885368.html 可以進行一些客戶端服務器的聯合調試。
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
下載地址
此處提供一個服務器的Demo軟件,下載解壓就可以直接運行,這個Demo的源代碼也在上面的示例,界面如下:后續新的版本可能會有點小區別
https://github.com/dathlin/HslCommunication/raw/master/Download/ModbusTcpServer.zip 這個地址的服務器軟件永遠都是最新的,會不停的更新。
如果您需要一個測試的客戶端,包括了tcp和rtu的都可以,下載下面的Demo即可,支持各種配置信息
隨便聊聊
使用本組件可以快速搭建一個高性能的MODBUS TCP總站,當我們一個上位機需要讀取100台西門子PLC設備(此處只是舉個例子,凡是都是使用Modbus tcp的都是一樣的)的時候,你采用服務器主動去請求100台設備的機制對性能來說是個極大的考驗,如果開100個線程去輪詢100台設備,那么性能損失將是非常大的,更不用說再增加設備,如果搭建Modbus tcp服務器,就可以完美的解決性能問題,因為連接的壓力將會平均分攤給每一台PLC,服務器端只要新增一個時間戳就可以知道客戶端有沒有連接上。
我們在100台PLC里都增加發送Modbus tcp方法,將數據發送到服務器的ip和端口上去,服務器根據站號來區分設備。這樣就可以搭建一個高性能總站。
關於數據池
本服務器端擁有兩個數據池,線圈數據池和寄存器數據池,任何客戶端包括服務器本身都可以對數據池進行讀寫數據,比如PLC1將所有的數據發送到寄存器地址0-99上,PLC2將數據發送到100-199上。
- 線圈數據池 模擬了真實的數據讀寫,自動解析指令並返回數據
- 離散輸入數據池 服務器端允許讀寫,對客戶端來說僅僅支持讀,功能碼02
- 寄存器數據池 模擬了真實的數據讀寫
- 輸入寄存器數據池 服務器端允許讀寫,對客戶端來說僅僅支持讀,功能碼04
四個數據池的地址范圍都是0-65535,起始地址是從0開始
Reference
ModBus組件所有的功能類都在 HslCommunication.ModBus命名空間,所以再使用之前先添加
using HslCommunication.ModBus;
How to Use
如果想快速的搭建一個Modbus-Tcp的服務器,只要2行代碼即可,實例化,啟動,而下面的例子稍微復雜了一點,額外配置了日志記錄器,綁定了一個數據接收的方法,每當客戶端進行數據交互,就會觸發,可用於實現自定義功能的Modbus-Tcp服務器
private HslCommunication.ModBus.ModbusTcpServer busTcpServer; private void button1_Click( object sender, EventArgs e ) { if(!int.TryParse(textBox2.Text,out int port)) { MessageBox.Show( "端口輸入不正確!" ); return; } try { busTcpServer = new HslCommunication.ModBus.ModbusTcpServer( ); busTcpServer.LogNet = new HslCommunication.LogNet.LogNetSingle( "logs.txt" ); busTcpServer.LogNet.BeforeSaveToFile += LogNet_BeforeSaveToFile; busTcpServer.OnDataReceived += BusTcpServer_OnDataReceived; busTcpServer.ServerStart( port ); button1.Enabled = false; panel2.Enabled = true; } catch (Exception ex) { MessageBox.Show( ex.Message ); } } private void BusTcpServer_OnDataReceived( byte[] modbus ) { if (InvokeRequired) { BeginInvoke( new Action<byte[]>( BusTcpServer_OnDataReceived ), modbus ); return; } textBox1.AppendText( "接收數據:" + HslCommunication.BasicFramework.SoftBasic.ByteToHexString(modbus) + Environment.NewLine ); } /// <summary> /// 當有日志記錄的時候,觸發,將日志信息也在主界面進行輸出 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void LogNet_BeforeSaveToFile( object sender, HslCommunication.LogNet.HslEventArgs e ) { if(InvokeRequired) { BeginInvoke( new Action<object, HslCommunication.LogNet.HslEventArgs>( LogNet_BeforeSaveToFile ), sender, e ); return; } textBox1.AppendText( e.HslMessage.ToString( ) + Environment.NewLine ); }
RTU支持
當客戶端進行連接了一個串口線后,也可以方便的讓服務器的支持同時支持串口訪問,
busTcpServer.StartSerialPort( "Com3" );
默認的串口參數是9600波特率。8位數據位,無奇偶校驗,1位停止位,當然也可以自行指定波特率
busTcpServer.StartSerialPort( "Com3",9600 );
還可以傳入一個初始化的委托方法,可以實現任意的支持。
創建數據訂閱
當客戶端進行發送讀寫指定時,如果你使用了OnDataReceived事件,都會觸發,當然可能這並不是你需要的,如果我們相對一個地址上的數據進行監視,也就當有客戶端寫入的時候觸發,或者是當這個數據更改了的時候觸發,那么我們可以創建自己的數據訂閱器:
private void button2_Click( object sender, EventArgs e ) { // 點擊數據監視 ModBusMonitorAddress monitorAddress = new ModBusMonitorAddress( ); monitorAddress.Address = ushort.Parse( textBox6.Text ); monitorAddress.OnChange += MonitorAddress_OnChange; monitorAddress.OnWrite += MonitorAddress_OnWrite; busTcpServer.AddSubcription( monitorAddress ); button2.Enabled = false; } private void MonitorAddress_OnWrite( ModBusMonitorAddress monitor, short value ) { // 當有客戶端寫入時就觸發 } private void MonitorAddress_OnChange( ModBusMonitorAddress monitor, short befor, short after ) { // 當該地址的值更改的時候觸發 if(InvokeRequired) { BeginInvoke( new Action<ModBusMonitorAddress, short, short>( MonitorAddress_OnChange ), monitor, befor, after ); return; } textBox9.Text = after.ToString( ); label11.Text = "寫入時間:" + DateTime.Now.ToString( ) + " 修改前:" + befor + " 修改后:" + after; }
當然,你可以只使用其中的一種數據訂閱,比如上述的操作,一旦有客戶端寫入地址0x01的數據(無論是寫入一個寄存器還是批量),那么馬上會觸發如下的方法。注意,從服務器自身寫入的數據不會觸發,所有的訂閱都可以關聯同一個方法,根據地址的不同來區分。
特別說明:
服務器只負責接受Modbus TCP協議的數據,無論客戶端發了讀寫指令,都會觸發接收事件。在服務器端有一個數據池,存儲了線圈數據和寄存器數據,模擬了一個真實的設備,允許客戶端使用Modbus tcp協議對服務器進行數據讀寫,並返回真實的數據,如果設備往這個服務器的寄存器地址寫了100,那么服務器端或者其他客戶端去讀取寄存器100地址的值的時候,那么也是100.
下面演示了一些簡單的數據讀寫,用於服務器端進行操作的。
bool Coil100 = busTcpServer.ReadCoil( "100" ); // 讀線圈100的值 bool[] Coil100_109 = busTcpServer.ReadCoil( "100", 10 ); // 讀線圈數組 short Short100 = busTcpServer.ReadInt16( "100" ); // 讀取寄存器值 ushort UShort100 = busTcpServer.ReadUInt16( "100" ); // 讀取寄存器ushort值 int Int100 = busTcpServer.ReadInt32( "100" ); // 讀取寄存器int值 uint UInt100 = busTcpServer.ReadUInt32( "100" ); // 讀取寄存器uint值 float Float100 = busTcpServer.ReadFloat( "100" ); // 讀取寄存器Float值 long Long100 = busTcpServer.ReadInt64( "100" ); // 讀取寄存器long值 ulong ULong100 = busTcpServer.ReadUInt64( "100" ); // 讀取寄存器ulong值 double Double100 = busTcpServer.ReadDouble( "100" ); // 讀取寄存器double值 busTcpServer.WriteCoil( "100", true ); // 寫線圈的通斷 busTcpServer.Write( "100", (short)5 ); // 寫入short值 busTcpServer.Write( "100", (ushort)45678 ); // 寫入ushort值 busTcpServer.Write( "100", 12345667 ); // 寫入int值 busTcpServer.Write( "100", (uint)12312312 );// 寫入uint值 busTcpServer.Write( "100", 123.456f ); // 寫入float值 busTcpServer.Write( "100", 1231231231233L );// 寫入long值 busTcpServer.Write( "100", 1212312313UL ); // 寫入ulong值 busTcpServer.Write( "100", 123.456d ); // 寫入double值
讀寫離散量:
bool value_100 = busTcpServer.ReadDiscrete( "100" ); // 讀取地址100的離散量 bool[] value_100_109 = busTcpServer.ReadDiscrete( "100", 10 ); // 讀取數據 busTcpServer.WriteDiscrete( "100", true); // 地址100為true busTcpServer.WriteDiscrete( "100", new bool[]{true,true}); // 地址100-101為true
讀寫輸入寄存器:
輸入寄存器的讀寫方式和寄存器的是一致的,只是地址改一下就好了,也即使用富地址的方式,舉個例子,寫輸入寄存器地址100為123
busTcpServer.Write( "x=4;100", (short)123 );
之前寫100,現在改成"x=4;123" x=4也即是使用功能碼04,那么之前寄存器的地址100等效於"x=3;100"
其他格式的數據參照這個即可。
過濾客戶端:
支持對客戶端的IP地址過濾,禁止不信任的Ip登錄
modbusTcpServer.SetTrustedIpAddress( new List<string>( "192.168.0.100", "192.168.0.101") );
如果要移除限制,重新恢復所有的客戶端登錄,那么按照如下操作
modbusTcpServer.SetTrustedIpAddress( null );
主動連接客戶端:
本服務器支持主動連接客戶端,然后再進行數據交互,這種情況在有些工業上應用還是很廣泛的
OperateResult connect = busTcpServer.ConnectHslAlientClient( "192.168.0.111", "10000", "12345678901" ); if (connect.IsSuccess) { MessageBox.Show( "連接成功!" ); } else { MessageBox.Show( "連接失敗!原因:" + connect.Message ); } }
需要制定對方的IP地址,端口號,以及唯一識別碼,目前使用的注冊包協議為HslAlien協議,過幾天專門寫一篇這個協議的博文,如果需要連接其他定制的客戶端,請聯系作者進行定制。
注意:
數據串的第7個字節為Modbus的站號信息,如下界面是FF,也即255,可以以此來區分不同的設備發來的數據信息,所以此處一個服務器實例掛的最大客戶端數為256台設備,前兩個字節為消息的頭序列,如果設備可以固定消息頭,用這個來標識設備的話,就可以區分65536台設備。
創作不易,感謝打賞:
參考鏈接:
如果你對MODBUS TCP不熟悉,那么請參照如下地址,我就是參照該地址的博客開發的代碼: