【.NET6+Modbus】Modbus TCP協議解析、仿真環境以及基于.NET實現基礎通信

接下來的內容,我會以從頭開發一個簡單的基于modbus tcp通信的案例,來實現一個基礎的通信功能。

有關環境:

開發環境:VS 2022企業版

運行環境:Win 10 專業版

.NET 環境版本:.NET 6

【備注】 源碼在文末?

1、新建一個基于.NET 6帶控制器的webapi項目,以及一個類庫項目。如下圖所示,新建以后的項目目錄結構。

?68588d6988e9f87ed5f4eec4028c606b.png

2、由于modbus tcp通信實際上就是一個socket通信,所以在類庫項目下,先創建了一個Modbus服務類,并且提供一個基于socket通信連接的方法。socket連接以后,需要返回socket實例拿來使用。

?08eacc90cfedce1b7b9007be4f093b85.png

3、為了方便一點,再新增一個通用的返回信息類,用于存儲一些返回信息使用。

?5546a0ef6e06739a26b4efc7ea6525a5.png

4、基于以上的返回信息類,咱對連接方法進行稍微改造一下,讓它看起來更方便一點。這樣可以用來驗證連接是否正常,以及返回對應的異常信息,好做進一步處理。

?098bc0edc3508261194b43309148aca0.png

5、Modbus TCP請求的報文規則,一些解析信息如下:

站地址:默認0x01, 除非PLC告訴我們其他站地址。

功能碼:代表讀寫數據時候指定的讀寫方法等。例如讀取線圈的功能碼是0x01。

地址和讀取長度:地址目前個人在施耐德物理的PLC環境上,不能超過30000。同時,單次讀寫長度不能超過248個byte,否則PLC可能會飄。當然,也可能將來一些PLC可以支持更長的批量數據讀寫,目前在施耐德PLC環境下不支持(具體型號忘記了,有點久了,當前身邊沒得PLC了,等下會使用仿真工具來做環境)。

頭部校驗(消息唯一識別碼):0~65535,用于PLC服務端進行區分不同的客戶端而使用的一組數據標識,不同的客戶端必須保證標識碼不重合。例如多個客戶端同時存在時候,發起的通信請求,必須保持不一樣的識別碼,否則Modbus服務端有可能會因為不知是哪個客戶端發起的請求而導致信息亂了。

無(協議標識):默認0,代表是Modbus協議。

數據長度:發送的報文的長度,剛好是6位,所以可以寫成固定值0x06。(寫入的規則不一樣,此處固定值只當作讀取時候使用)

?f64662d7d6df759219a7df759cd3c71f.png

6、根據協議的一些具體內容,寫一個存儲功能碼和異常返回碼的數據類,用于后期做通信時候傳參和通信數據驗證使用。有關協議具體內容,如下代碼所示。

?d858fea1b1b3788f0700bce7007c77cf.png

7、由于異常碼是byte數據,直接驗證可能會麻煩一點,為了可以直觀一些,此處再新增一個用于解析Modbus返回的異常信息的方法,用于備用。

?46d272f08707868d5cc0c5d6091be66a.png

8、根據協議規則,提供一些參數,并先搭建一個簡單的方法框架,用來可以進行讀取線圈的功能。包含簡單的報文數據拆分以及報文發送和接收。由于發送報文長度不能超過248byte(1 bool大小 == 1 byte,如果是其他類型,需要做其他長度換算),所以當長度超過時候,做個簡單的算法進行拆分再發送,防止發生不必要的異常。以下做一個讀取線圈(Bool類型數據)的簡單方法。

?6c135fdd09f3edfac2554d9a50b9f14b.png

9、根據上方提供的協議報文組裝規則,進行開發一個通用的報文組織方法。有高低位之分,所以對于占用2byte的數據,需要進行"倒裝"。

?814b024e3a03493653428ae9e51532cf.png

10、發送報文以后,返回的報文含有校驗信息:發送的數據報文的第7位的數據,加上?0x80?以后,跟返回的報文的第7位byte數據如果一致,則代表當前通信上可能有異常。異常碼在接收的響應報文的第8位。

所以可以繼續寫一個驗證是否成功的校驗方法:

?156efa11cf23195e23cf17f92374657a.png

11、由于返回的數據也都是byte數據,以上讀取的線圈值(布爾值),就需要提供一個數據類型轉換的功能用于把byte數組轉換為bool數組。

?ada7de925f5c1a515d440fc09380ce49.png

12、對讀取線圈的最開始的方法,進行一些完善以后的代碼如下。響應報文長度是 發送數據長度*2+9 。

?b5585b0f79c08e473719cb7f084f8a7f.png

13、接下來做一個簡單的測試。準備一下仿真環境,進行本地的測試,看看是否可以連通。先準備兩個工具,一個是 modbus poll,另一個是modbus slave。一個用來模擬服務端環境,另一個可用來模擬數據收發驗證。

?6b63dc23e40591aaf3385d73aea15f33.png

14、兩邊都設置為讀寫單個線圈的功能,用于測試以上線圈讀取的代碼的功能。

?c930a895ebf37481a05089a7b0b3275d.png

15、兩邊都設置為modbus tcp連接方式。Slave站點啟動以后,默認為本地,poll工具上的IP地址選擇本地即可。如果是真實PLC環境,則填寫真實PLC地址。

?6cc63d6b22f781779bede7f0260169e2.png

16、測試兩邊是否通信上。給任意一個地址寫入一個true,可以看到另一邊也同步更新,說明通信是通的了。

【注意】modbus工具,poll和slave工具默認占用了消息唯一標識碼,大概是1~5左右的固定值,所以使用該工具期間,建議程序上的唯一消息識別碼設置為5以上,以防止通信干擾。

?073e32f61c543583d337c245a33d6083.png

17、接下來就可以繼續完善代碼進行驗證了。先新增ModbusService的接口IModbusService,用于實現依賴注入。然后在program.cs文件里面進行服務注冊。

?69c62bb13cd8404929da34d040681067.png

18、新建一個控制器,用來進行模擬實驗。有關代碼和注釋如圖所示。

?d449f4e80b7e71f9c2e37437f9ed6fa3.png

19、進行讀取一個長度試試效果。結果是數據不支持,說明報文有問題。

?113d3eff8a85bb908cd57e079ab7bb02.png

20、通過斷點,找到問題所在,上面的代碼里面,length經過簡單算法計算以后等于0,此處需要用的應該是newLength變量的值。

?0ca26cf0426c5c6b1794dd13c3362c63.png

21、再次測試,地址從1開始,讀取兩個地址,結果符合預期。

?bf9411bf70225b5725ff4e3b93dd4e3f.png

22、再測試一下,從0開始讀取30個,并隨即設置若干個是True的值。

?75a961cb4eefc0b61446b68ae2e14a15.png

23、其他的寫入、以及其他類型讀寫,基本類似。由于篇幅有限,就不繼續進行一步一步操作的截圖了。讀取的,選好類型,報文格式都是一樣的,唯一有差別的是寫入的報文。下面是寫入單個線圈值的報文。線圈當前僅支持一個一個寫入。

?c69f2d35974ad69cf41d0694d8fb7c18.png

24、寫入寄存器的規則會有些偏差,協議規則如下圖。

?88828cf896425a1af00557543299a22f.png

【備注】以上圖的標題,我寫錯了,應該是 “寫入寄存器”報文協議,懶得換圖了,大佬們看的時候自己辨別哈~

?讀取線圈當作引導,其他類型也都異曲同工,大佬們可以自行嘗試。

?另外說點,如果是生產環境下使用,建議把客戶端連接做成【長連接】,不然重復創建連接比較耗費資源,耗時也會因為新建連接而占用一大半。同時,如果是多線程訪問,使用同一個客戶端連接,必須加鎖,否則會干擾數據;如果是多線程,不同客戶端,就要保證每個消息識別碼必須不同,如果存在同一個識別碼,很容易發生數據異常等情況。

有關源碼:

ModbusService源碼:

66f5117f35d1af52a42b3f331e0ff585.jpeg

public class ModbusService: IModbusService{        public ResultInformation<Socket> ConnectModbusTcpService( IPAddress ip, int port){ResultInformation<Socket> client = new();            try{client.Result = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);client.Result.Connect(new IPEndPoint(ip, port));client.IsSucceed = true;}            catch (Exception ex){client.IsSucceed=false;client.Message = ex.Message;}            return client;}        /// <summary>/// 讀取線圈值(Bool)        /// </summary>/// <param name="client">客戶端</param>/// <param name="headCode">頭部標識</param>/// <param name="station">站地址</param>/// <param name="address">地址</param>/// <param name="length">長度</param>/// <returns></returns>public ResultInformation<bool[]> ReadCoils(Socket client,ushort headCode,byte station, ushort address, ushort length){ResultInformation<bool[]> result = new();            int resultIndex = 0;            ushort newLength = 0;            ushort realLength = length;  // 存儲實際長度try{List<byte> byteResult = new List<byte>(); // 存儲實際讀取到的所有有效的byte數據while (length > 0){                    if (length > 248)  // 長度限制,不能超過248                    {length = (ushort)(length - 248);newLength = 248;}                    else{newLength = length;length = 0;}resultIndex += newLength;                    byte[] sendBuffers = BindByteData(headCode,station,FunctionCode.ReadCoil,address,newLength); // 組裝報文                    client.Send(sendBuffers);                    byte[] receiveBuffers = new byte[newLength * 2 + 9]; int count = client.Receive(receiveBuffers); // 等待接收報文var checkResult = CheckReceiveBuffer(sendBuffers, receiveBuffers); // 驗證消息發送成功與否if (checkResult.IsSucceed){                        // 成功,如果長度超出單次讀取長度,進行繼續讀取,然后對數據進行拼接List<byte> byteList = new List<byte>(receiveBuffers);byteList.RemoveRange(0, 9); // 去除前面9個非數據位byteResult.AddRange(byteList); // 讀取到的數據進行添加進集合address += newLength; // 下一個起始地址                    }                    else{                        throw new Exception(checkResult.Message);}}result.IsSucceed = true;result.Result = ByteToBoolean(byteResult.ToArray(), realLength);}            catch (Exception ex){result.IsSucceed = false;result.Result = new bool[0];result.Message = ex.Message;}            return result;}        private bool[] ByteToBoolean(byte[] data,int length){            if (data == null){                return new bool[0];}            if (length > data.Length * 8) length = data.Length * 8;            bool[] result = new bool[length];            for (int i = 0; i < length; i++){                int index = i / 8;                int offect = i % 8;                byte temp = 0;                switch (offect){                    case 0: temp = 0x01; break;                    case 1: temp = 0x02; break;                    case 2: temp = 0x04; break;                    case 3: temp = 0x08; break;                    case 4: temp = 0x10; break;                    case 5: temp = 0x20; break;                    case 6: temp = 0x40; break;                    case 7: temp = 0x80; break;                    default: break;}                if ((data[index] & temp) == temp){result[i] = true;}}            return result;}        private byte[] BindByteData(ushort headCode,byte station,byte functionCode,ushort address, ushort length){            byte[] head = new byte[6];head[0] = station; // 站地址head[1] = functionCode; // 功能碼head[2] = BitConverter.GetBytes(address)[1]; // 起始地址head[3] = BitConverter.GetBytes(address)[0];head[4] = BitConverter.GetBytes(length)[1]; // 長度head[5] = BitConverter.GetBytes(length)[0];            return GetSocketBytes(headCode,head);}        private byte[] GetSocketBytes(ushort headCode,byte[] head){            byte[] buffers = new byte[head.Length+6]; buffers[0] = BitConverter.GetBytes(headCode)[1];buffers[1] = BitConverter.GetBytes(headCode)[0];            // 2 和 3位置默認,所以不需要賦值buffers[4] = BitConverter.GetBytes(head.Length)[1];buffers[5] = BitConverter.GetBytes(head.Length)[0];head.CopyTo(buffers, 6);            return buffers;}        private ResultInformation<string> CheckReceiveBuffer(byte[] send,byte[] receive){ResultInformation<string> result = new();            if ((send[7] + 0x80) == receive[7]){                var str = FunctionCode.GetDescriptionByErrorCode(receive[8]);result.IsSucceed = false;result.Message = str;}            else{result.IsSucceed = true;}            return result;}}

340f7e8600ff2cd17c5b1992fb4f79cd.jpeg

控制器源碼:

e47ee2dce8608a2dbc07b88151e52f85.jpeg

[Route("api/[controller]/[action]")][ApiController]    public class TestModbusController : ControllerBase{IModbusService _service;        public TestModbusController(IModbusService modbusService){_service = modbusService;}[HttpPost]        public IActionResult ReadCoil(ushort address, ushort length){            var ip = IPAddress.Parse("127.0.0.1"); // ip地址int port = 502; // modbus tcp通信,默認端口byte station = (byte)((short)1); // 站地址為1var connectResult = _service.ConnectModbusTcpService(ip,port);            if (connectResult.IsSucceed){                // socket連接創建成功var readResult = _service.ReadCoils(connectResult.Result,6,station,address,length);  // 唯一消息碼設為6(大于5,且不重復即可)if (readResult.IsSucceed){                    if (readResult.Result.Any()){StringBuilder sb = new StringBuilder();                        for(int i = 0; i < readResult.Result.Length; i++){sb.AppendLine($"[{i}]:{readResult.Result[i]}");}                        return Ok(sb.ToString());}}                else{                    return Ok(readResult.Message);}}            else{                return Ok(connectResult.Message);}            return Ok();}}

b4d5a644f9a1d1c1e3b04d3425b5658a.jpeg

功能碼和異常碼:

5451ed8fe0032a15ba61dab1325876ab.jpeg

public class FunctionCode{        #region 功能碼        public const byte ReadCoil = 0x01; // 讀取線圈狀態  寄存器PLC地址 00001 - 09999public const byte ReadInputDiscrete = 0x02; // 讀取 可輸入的離散量  寄存器PLC地址 10001 - 19999public const byte ReadRegister = 0x03; // 讀取 保持寄存器  40001 - 49999public const byte ReadInputRegister = 0x04; // 讀取 可輸入寄存器  30001 - 39999public const byte WriteSingleCoil = 0x05; // 寫單個 線圈  00001 - 09999public const byte WriteSingleRegister = 0x06; // 寫單個 保持寄存器  40001 - 49999public const byte WriteMultiCoil = 0x0F;  // 寫多個 線圈  00001 - 09999public const byte WriteMultiRegister = 0x10; // 寫多個 保持寄存器  40001 - 49999public const byte SelectSlave = 0x11; //  查詢從站狀態信息  (串口通信使用)#endregion#region 異常碼        public const byte FunctionCodeNotSupport = 0x01;// 非法功能碼public const byte DataAddressNotSupport = 0x02;// 非法數據地址public const byte DataValueNotSupport = 0x03;// 非法數據值public const byte DeviceNotWork = 0x04;// 從站設備異常public const byte LongTimeResponse = 0x05;// 請求需要更長時間才能進行處理請求public const byte DeviceBusy = 0x06;// 設備繁忙public const byte OddEvenError = 0x08;// 奇偶性錯誤public const byte GatewayNotSupport = 0x0A;// 網關錯誤public const byte GatewayDeviceResponseTimeout = 0x0B;// 網關設備響應失敗#endregionpublic static string GetDescriptionByErrorCode(byte code){            switch (code){                case FunctionCodeNotSupport:                    return "FunctionCodeNotSupport";                case DataAddressNotSupport:                    return "DataAddressNotSupport";                case DataValueNotSupport:                    return "DataValueNotSupport";                case DeviceNotWork:                    return "DeviceNotWork";                case LongTimeResponse:                    return "LongTimeResponse";                case DeviceBusy:                    return "DeviceBusy";                case OddEvenError:                    return "OddEvenError";                case GatewayNotSupport:                    return "GatewayNotSupport";                case GatewayDeviceResponseTimeout:                    return "GatewayDeviceResponseTimeout";                default:                    return "UnknownError";}}}

72e134e4654c4524186df96d6da1a2e0.jpeg

好了,以上就是該文章的全部內容。如果覺得有幫助,歡迎轉發、在看和點贊。也歡迎關注我的公眾號:Dotnet Dancer

謝謝大家~

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/283981.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/283981.shtml
英文地址,請注明出處:http://en.pswp.cn/news/283981.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

源碼深度剖析Eureka與Ribbon服務發現原理

本文基于 spring cloud dalston&#xff0c;同時文章較長&#xff0c;請選擇舒服姿勢進行閱讀。 Eureka 與 Ribbon 是什么&#xff1f;和服務發現什么關系&#xff1f; Eureka 與 Ribbon 都是 Netflix 提供的微服務組件&#xff0c;分別用于服務注冊與發現、負載均衡。同時&a…

3月6日云棲精選夜讀:如何實現32.5萬筆/秒的交易峰值?阿里交易系統TMF2.0技術揭秘...

交易平臺遇到的挑戰 2017雙11&#xff0c;交易峰值達到了32.5萬筆/秒&#xff0c;這給整個交易系統帶來了非常大的挑戰。 一方面&#xff0c;系統需要支撐全集團幾十個事業部的所有交易類需求&#xff1a;要考慮如何能更快響應需求、加快發布周期&#xff1b;如何能為新小業務提…

std的find和reverse_iterator聯合使用

上代碼&#xff1a; // test2013.cpp : 定義控制臺應用程序的入口點。 //#include "stdafx.h" #include <stdlib.h> #include <stdio.h> #include<iostream> #include<vector> #include<map> #include<string> using namespace …

論如何提升學習的能力

為啥要學習如果有一件事情是能改變你自己的&#xff0c;我想這件事情必然就是學習&#xff0c;我的人生重要的轉折點也是從學習這件事情始發的&#xff0c;那么&#xff0c;我們就從這里開始。學習不僅僅是為了找到答案&#xff0c;而是為了找到方法&#xff0c;找到一個可以找…

linux下svn常用指令

windows下的TortoiseSVN是資源管理器的一個插件&#xff0c;以覆蓋圖標表示文件狀態&#xff0c;幾乎所以命令都有圖形界面支持&#xff0c;比較好用&#xff0c;這里就不多說。主要說說linux下svn的使用&#xff0c;因為linux下大部分的操作都是通過命令行來進行&#xff0c;所…

CSS布局解決方案(終結版)

前端布局非常重要的一環就是頁面框架的搭建&#xff0c;也是最基礎的一環。在頁面框架的搭建之中&#xff0c;又有居中布局、多列布局以及全局布局&#xff0c;今天我們就來總結總結前端干貨中的CSS布局。 居中布局 水平居中 1&#xff09;使用inline-blocktext-align&#xff…

基于ABP和Magicodes實現Excel導出操作

前端使用的vue-element-admin框架&#xff0c;后端使用ABP框架&#xff0c;Excel導出使用的Magicodes.IE.Excel.Abp庫。Excel導入和導出操作幾乎一樣&#xff0c;不再介紹。文本主要介紹Excel導出操作和過程中遇到的坑&#xff0c;主要是Excel文件導出后無法打開的問題。一.Mag…

消息模式在實際開發應用中的優勢

曾經.NET面試過程中經常問的一個問題是&#xff0c;如果程序集A&#xff0c;引用B &#xff0c;B 引用C&#xff0c;那么C怎么去訪問A中的方法呢。 這個問題初學.net可能一時想不出該咋處理&#xff0c;這涉及到循環引用問題。但有點經驗的可能就簡單了&#xff0c;通過委托的方…

微服務:注冊中心ZooKeeper、Eureka、Consul 、Nacos對比

前言 服務注冊中心本質上是為了解耦服務提供者和服務消費者。對于任何一個微服務&#xff0c;原則上都應存在或者支持多個提供者&#xff0c;這是由微服務的分布式屬性決定的。更進一步&#xff0c;為了支持彈性擴縮容特性&#xff0c;一個微服務的提供者的數量和分布往往是動…

MyBatis總結七:動態sql和sql片段

開發中&#xff0c;sql拼接很常見&#xff0c;所以說一下動態sql&#xff1a; 1if2chose,when,otherwise3where,set4foreach用法解析(現有一張users表 內有id username age 三個字段)&#xff1a; <!--查詢所有用戶&#xff0c;傳遞參數type&#xff0c;如果值為0&#xff0…

iOS - OC Copy 拷貝

前言 copy&#xff1a;需要先實現 NSCopying 協議&#xff0c;創建的是不可變副本。mutableCopy&#xff1a;需要實現 NSMutableCopying 協議&#xff0c;創建的是可變副本。淺拷貝&#xff1a;指針拷貝&#xff0c;源對象和副本指向的是同一個對象。對象的引用計數器 &#xf…

三.選擇結構(一)

1.if結構: if(條件){ 代碼塊 } 2.隨機產生數: int randon (int)(Math.random()*10); 3.多重if選擇結構: if(條件1){ 代碼塊1 }else if (條件2){ 代碼塊2 }else{ 代碼塊3 } 4.嵌套if選擇結構: if(條件1){ if(條件2){ 代碼塊1 }else{ 代碼塊2 } }else{ 代碼塊3 } 轉載于:https://…

為了高性能、超大規模的模型訓練,這個組合“出道”了

點擊上方藍字關注我們&#xff08;本文閱讀時間&#xff1a;3分鐘)近年來&#xff0c;在大量數據上訓練的基于 transformer 的大規模深度學習模型在多項認知任務中取得了很好的成果&#xff0c;并且被使用到一些新產品和功能背后&#xff0c;進一步增強了人類的能力。在過去五年…

SVN就是這么簡單

什么是SVN SVN全稱&#xff1a;Subversion&#xff0c;是一個開放源代碼的版本控制系統 Svn是一種集中式文件版本管理系統。集中式代碼管理的核心是服務器&#xff0c;所有開發者在開始新一天的工作之前必須從服務器獲取代碼&#xff0c;然后開發&#xff0c;最后解決沖突&…

SpringCloud必會知識點大全

為什么要學習Spring Cloud 在項目開發中隨著業務越來越多&#xff0c;導致功能之間耦合性高、開發效率低、系統運行緩慢難以維護、不穩定。微服務 架構可以解決這些問題&#xff0c;而Spring Cloud是微服務架構最流行的實現. 1.微服務 微服務架構是使用一套小服務來開發單個應用…

thinkphp3.22 多項目配置

1.index.php if(version_compare(PHP_VERSION,5.3.0,<)) die(require PHP > 5.3.0 !); // 開啟調試模式 建議開發階段開啟 部署階段注釋或者設為false define(APP_DEBUG,true); // 創建 //define(BIND_MODULE,Login); define(erp,true); // 定義應用目錄 define(APP_PAT…

30分鐘掌握 C#7

1. out 變量&#xff08;out variables&#xff09; 以前我們使用out變量必須在使用前進行聲明&#xff0c;C# 7.0 給我們提供了一種更簡潔的語法 “使用時進行內聯聲明” 。如下所示&#xff1a; 1 var input ReadLine(); 2 if (int.TryParse(input, out var result)) 3 …

在 C# 中如何檢查參數是否為 null

前言前不久&#xff0c;微軟宣布從 C# 11 中移除參數空值檢查功能&#xff0c;該功能允許在方法開始執行之前&#xff0c;在參數名稱的末尾提供參數空值檢查&#xff08;!!操作符&#xff09;。那么&#xff0c;在 C# 中如何檢查參數是否為 null 呢&#xff1f;1. null這個可能…

什么是Maven快照(SNAPSHOT)

本文來說下Maven的SNAPSHOT版本有什么作用 文章目錄 問題解決 正式版本"abc-1.0"快照版本"abc-1.0-SNAPSHOT"本文小結問題 在使用maven進行依賴管理時&#xff0c;有的版本號后面會帶有"-SNAPSHOT"&#xff0c;有什么作用呢&#xff1f; <dep…

帶你剖析WebGis的世界奧秘----Geojson數據加載(高級)

前言&#xff1a;前兩周我帶你們分析了WebGis中關鍵步驟瓦片加載點擊事件&#xff08;具體的看前兩篇文章&#xff09;&#xff0c;下面呢&#xff0c;我帶大家來看看Geojson的加載及其點擊事件 Geojson數據解析 GeoJSON是一種對各種地理數據結構進行編碼的格式。GeoJSON對象可…