本項目的孵化說來也是機緣巧合的事,本人於13年杭州某大學畢業後去了一家大型的國企工做,慢慢的走上了工業軟件,上位機軟件開發的道路。於14年正式開發基於windows的軟件,當時可選的技術棧就是MFC和C#的winform,後來就發現C#的更爲簡單一些,那就直接幹,先作再說。須要作一些界面相關的軟件,就直接採用了C#的winform,基礎不夠,百度來湊。後來領導就下達了一個任務,開發一個硫化機系統的上位機,對某個車間共計五六十臺硫化機進行監控和曲線查看。因爲沒有可參考的界面程序,開發起來就比較費勁,具體有什麼功能,都是幹嗎的,工藝須要什麼等等都是未知數,沒辦法,只有硬着頭皮和現場的工藝人員,電氣人員,來回溝通交流,加上一些我本身的理解,正式踏上了工業軟件開發的道路。linux
開始作項目的時候,硫化機設備是採用PLC做爲主控制器的,第一道攔路虎就是如何將三菱的PLC(邏輯控制器,一般做爲設備的核心控制單元)的數據給拿到個人軟件中來呢?這真是一個棘手的問題啊,首先就是百度,搜索到了MX component組件,初步試了試,真的比較麻煩,關鍵還沒弄通。而後就去看看有沒有其餘的方式實現,後來就在工廠的備件庫裏看到了三菱的以太網模塊QJ71E71-100,而後就搜索支持的通信說明,在三菱的官網上找到了通訊說明,打開一看,我去,這麼長篇大論。那也沒有辦法,按照邊測試邊開發,勉勉強強讀到了我想要讀的數據(固然,這時候的代碼基本都是寫死的),又開始解析數據成真實的數據,而後研究如何存入數據庫中去,再研究怎麼顯示曲線,到這裏爲止,這個項目的基本技術難題基本算是攻克了,持續的迭代,那是後話了。git
在接下來的兩三年裏,接觸並開發了好幾個相似的項目,發現一般工業軟件的需求是採集,分析,存數據庫,顯示。後來對通訊的理解深刻,由單機軟件發展成了CS架構的軟件,微軟的數據庫SQL Server原本就支持局域網訪問。後來在17年趁着換工做和考駕照的間隙,梳理了上份工做積累的經驗,和實際的需求,再加上本身的代碼水平也稍微進步了一點點,就整理成了HslCommunication,並將之開源出來,初步的功能是三菱PLC的數據讀寫,C#軟件之間的數據通訊。後來又集成了modbus協議,西門子,歐姆龍,ab plc,三菱串口等等,發現寫庫的要求和寫簡單程序的要求並非等同的,要寫成庫的話,須要保證功能靈活性,你寫的代碼基本符合大多數人的使用需求,而不是某種特定的狀況。也就是說,有些人可能簡單的使用而已,而有些人會深刻使用,壓榨性能。而後就是代碼了,全部寫代碼的標準的最終目的都是爲了讓代碼可讀性加強,可維護性加強,方便快速的理解,升級,查錯。這方便確實卻要經驗積累。程序員
作這個項目(HslCommunication)的目標和開源的初衷是方便廣大的像我這種的在工廠一線的軟件工程師,我一直以爲咱們不該該把本身看作是程序員,程序員的角色更像是碼農,主要工做就是敲代碼,而軟件工程師應該是更大的定義,設計軟件的總體架構和開發的。這幾年大多數工做都開始意識到工業軟件,上位機軟件,數據追述系統,SCADA軟件,MES軟件開發的重要性,因此像我這樣的有通訊需求的人應該不在少數,何況開源有助於別人來一塊兒改進,和代碼測試。因此在開源以後,在博客園就陸陸續續的寫了一些文章,好比如何使用C#和三菱PLC通信,C#和西門子通信等等。從博客園的點擊量來看,確實有大量的工廠的程序員有這方便的需求,而直接採用socket來開發,比較晦澀難懂,坑又比較多,事實上確實有不少人來報告了bug。幫助我修復了這個組件,提升了穩定性。再次感謝全部使用或是報告bug的萬千網友,沒有大家的支持就沒有本項目的今天。github
因爲我也是這個項目的使用者之一,實際上我本身在工做或是其餘方面的使用也是很頻繁的,在開發項目上就會站在使用者的角度出發,好比我想讀取三菱PLC的D100的數據,能不能有個組件一兩行代碼就能夠實現?僞代碼的邏輯就是redis
1. 實例化數據庫
2. 讀windows
這樣纔算是簡單的操做,本着這樣設計思想,最終有了如今的開源項目。數組
相比大多數人比較關心這個問題,綜合前言的介紹,這個組件主要是用於工業通訊的,也有兩個程序之間的通信,還有其餘雜七雜八的功能,更像是個人工具插件。各類小功能,擴展的小功能等等。直接上圖:服務器
這是這個開源項目的demo程序,基本上將80%的功能列舉出來了,固然還有一些小功能沒有列舉。大多數支持的設備都在上面進行顯示了,能夠方便的進行測試,看看是否是能夠實現讀寫的操做(對現場實際在生產的設備應當注意寫入不正確的數據會致使意外事故發生)。好比咱們來看看三菱的PLC的demo程序:網絡
其餘的截圖畫面就不一一舉例了,都是相似或是基本相似的。能夠方便的使用demo進行測試。
特別注意,本組件實現的全部的通信都是基於socket直接實現的,通訊部分不依賴任何第三方通信庫或是組件安裝,也就是說,你拿個dll能夠直接和PLC通信,這對於部署,開發調試,升級都是很是方便的。
當你須要進行PLC通訊時,能夠先用demo程序進行測試,若是demo程序能夠讀寫,那麼用本組件也就絕對能夠讀寫,有些PLC的參數若是不清楚,就須要聯繫電氣工程師進行確認。好比AB PLC的slot,不知道該寫什麼,就嘗試爲0,若是不行,就只能聯繫電氣工程師解決這個問題。
demo項目的意義:當我開發了三菱PLC的通信程序和西門子的通信程序以後,我發現若是我想要測試一個新的PLC通不通?或是簡單的經過代碼讀PLC的某個地址的程序的時候,就好費勁,須要常常建立一些小項目,這些小項目自己並無什麼實際的意義,就是簡單的讀個數據之類的。後來就想把這部份內容作成一個通用的測試,因而就有了demo項目,將本項目支持的各類設備都往界面上羅列,作成一個測試環境的demo程序,這樣當你們也有這樣的需求的時候,並不須要再新建一些無用的小項目了,本demo就基本上知足你們全部的需求了。
demo項目的彩蛋:在18年11月以後,demo項目實現了版本控制和自動升級,12月以後實現了統計全球的使用狀況,下圖就是demo項目 v5.6.2-最新 的2018年12月到2019年2月中旬的全球使用狀況(這是不徹底統計,舊版未統計,大量的舊版不支持自動更新,有些demo屏蔽了檢測,實際使用量應該遠超圖片所示)
整個框架的項目結構以下:
首先文件夾 TestProject 裏面的項目都是一些demo項目,固然最重要的就是 HslCommunicationDemo 項目了。就是最上面的demo項目的截圖,Hsl具體能幹什麼能夠參照這個。
本項目使用了三個框架的項目,也就是說,本項目提供dll文件包含了三個框架版本:
維護三份源代碼顯然是什麼痛苦的,因此我採用了維護一份源代碼,也就是 .Net 4.5的代碼,其餘兩個項目引用.net 4.5的代碼,若是有不一致的地方,就用預編譯指令進行區分。例如在modbusserver類中
而 HslCommunication_Net45.Test 項目是一個單元測試項目,包含了一些代碼類的測試,還有示例代碼的編寫。因此咱們的重點來看看 .net 4.5的項目便可,總體的結構以下圖:
BasicFramework 放些了一些基於的小工具的類,好比SoftBasic提供了大量小的靜態輔助方法,幫助你快速開發實現一些基礎的小功能的。
Core 裏放置了一些本項目的核心代碼,全部網絡通訊類的基礎類,基礎功能實現都在Core裏。
Enthernet 裏放置了一些高級程序語言之間的通訊,好比兩個exe間通訊,或是局域網兩臺電腦通訊,或是多個電腦程序通訊。
LogNet 是實現了本項目的日誌工具,能夠方便的存儲日誌信息。
ModBus 實現了基於網絡的modbus-tcp協議,modbus-rtu協議,modbus-server,modbus-ascii協議的通訊。
Profinet 實現了三菱,西門子,歐姆龍,松下,ab plc的數據通訊。
這個類爲何拿出來出來講呢?由於這個類貫穿了HSL整個項目,是本開源項目的思想之一。對這個類的理解,和對於本項目的理解相當重要。
左邊也便是這個類的位置,右邊是這個類的定義,在項目最初的開發階段,我遇到了一個問題,這也是軟件開發過程當中你們都會遇到的問題,好比我要實現一個讀取PLC一個數據的操做,讀取成功了天然皆大歡喜,若是讀取失敗了呢?
我如何將讀取失敗,或是寫入失敗,或是操做失敗的信息傳遞給調用者呢?除了失敗的信息以外,應該還要包含一個爲何失敗的信息,PLC自己的失敗會返回一個錯誤碼,那就也須要一個錯誤碼。因此就有了 OperateResult 的雛形:
/// <summary> /// 指示本次訪問是否成功 /// </summary> public bool IsSuccess { get; set; } /// <summary> /// 具體的錯誤描述 /// </summary> public string Message { get; set; } = StringResources.Language.UnknownError; /// <summary> /// 具體的錯誤代碼 /// </summary> public int ErrorCode { get; set; } = 10000;
因而就有了上面的三個屬性內容,可是這時候還有一點須要注意,返回的結果對象應該是能夠帶內容的,好比你讀取了一個int數據,應該帶一個int的結果,讀取了一個short的數據,就應該帶一個short類型的數據,若是須要這個結果對象支持多類型的內容的話,查了查書,發現有個泛型的功能恰好合適,可是以後又發現,萬一我想要帶2個不一樣類型的結果對象時,那怎麼辦?這時候就須要定義多個不一樣類型的 OperateResult 類型了。
此處定義多達十個的泛型對象,知足絕大多數的狀況請用。這個類型對象除了能返回帶有錯誤信息的結果對象以外,還容許進行結果路由,咱們來看看這個項目裏的一個方法:
/// <summary> /// 使用底層的數據報文來通信,傳入須要發送的消息,返回最終的數據結果,被拆分紅了頭子節和內容字節信息 /// </summary> /// <param name="socket">網絡套接字</param> /// <param name="send">發送的數據</param> /// <returns>結果對象</returns> /// <remarks> /// 當子類重寫InitializationOnConnect方法和ExtraOnDisconnect方法時,須要和設備進行數據交互後,必須用本方法來數據交互,由於本方法是無鎖的。 /// </remarks> protected OperateResult<byte[], byte[]> ReadFromCoreServerBase(Socket socket, byte[] send ) { LogNet?.WriteDebug( ToString( ), StringResources.Language.Send + " : " + BasicFramework.SoftBasic.ByteToHexString( send, ' ' ) ); TNetMessage netMsg = new TNetMessage { SendBytes = send }; // 發送數據信息 OperateResult sendResult = Send( socket, send ); if (!sendResult.IsSuccess) { socket?.Close( ); return OperateResult.CreateFailedResult<byte[], byte[]>( sendResult ); } // 接收超時時間大於0時才容許接收遠程的數據 if (receiveTimeOut >= 0) { // 接收數據信息 OperateResult<TNetMessage> resultReceive = ReceiveMessage(socket, receiveTimeOut, netMsg); if (!resultReceive.IsSuccess) { socket?.Close( ); return new OperateResult<byte[], byte[]>( StringResources.Language.ReceiveDataTimeout + receiveTimeOut ); } LogNet?.WriteDebug( ToString( ), StringResources.Language.Receive + " : " + BasicFramework.SoftBasic.ByteToHexString( BasicFramework.SoftBasic.SpliceTwoByteArray( resultReceive.Content.HeadBytes, resultReceive.Content.ContentBytes ), ' ' ) ); // Success return OperateResult.CreateSuccessResult( resultReceive.Content.HeadBytes, resultReceive.Content.ContentBytes ); } else { // Not need receive return OperateResult.CreateSuccessResult( new byte[0], new byte[0] ); } }
咱們看到,方法裏面的錯誤信息,能夠由結果路由進行層層上傳,最終拋給調用者,代碼裏須要作的就是發生錯誤的時候處理好後續的邏輯便可。這個類提供了幾個靜態方法快速的處理結果路由
講完告終果路由再來講說,整個網絡類的核心在於 NetworkBase類,在項目的開發過來中,尤爲是開發了幾個不一樣的PLC和C#程序之間的服務器客戶端通訊以後,發現有些底層代碼是有些重複的,因此通過不斷的提煉代碼造成了全部網絡的底層基類,這個類呢,只是提供了一個socket相關通用的操做邏輯,好比,建立並鏈接的socket對象,接收指定長度的數據,發送字節數據,關閉,接收流,發送流等等操做。
這個類實現了基礎的字節收發功能和鏈接斷開功能。接下來就是 NetworkDoubleBase 類的實現,實現了長短鏈接的操做,在咱們實際讀寫設備的過程當中,網絡情況每每是差異很大,因此本項目的初衷就是同時支持長鏈接和短鏈接。根據你們需求的不一樣,
所謂的短鏈接是讀取的時候再鏈接,讀取完成就關閉鏈接。缺點就是鏈接打開和關閉耗時,影響讀取速率,優勢就是對網絡情況反饋即便,讀取失敗了就說明網絡斷了,適合頻率較低的讀寫。
長鏈接就是讀取開始前鏈接一次,就再也不關閉,進行頻繁的讀取,最後再關閉,好處固然是高速了,缺點就是網絡情況不是那麼好的時候,效率比較低下,對網絡情況反應也不及時。
短鏈接就是直接的實例化,而後讀取寫入操做,每一次操做都是一次完整的通訊過程。
切換長鏈接有兩種辦法,效果是一致的,
1. 對象讀寫前調用ConnectServer();
2. 對象讀寫前調用SetPersistentConnection( );
這兩個方法都是雙模式類裏支持並實現的。全部的派生類都符合這個調用機制。
實現了長短的鏈接後,還要實現設備的BCL類型的讀寫,本質是基於byte數組和C#基礎類型的轉換,可是這裏有個問題,不一樣的PLC,modbus協議對於轉換的格式不是固定的,有多是同樣的,有可能不是同樣的,因此又抽象出來一個 IByteTransform 接口
這個接口集成到了下面的設備交互的基類 NetworkDeviceBase 裏,這個基類實現了一些基礎的類型的數據讀寫。
因此到這裏能夠看到,從NetworkDeviceBase類繼承出去的設備類(大部分的設備通訊協議都是從這個繼承出去的),其基本的讀寫代碼都是一致的,關於解析協議,通訊的底層都是封裝完畢,
先舉例說明三菱PLC的讀寫操做:
// 實例化對象,指定PLC的ip地址和端口號 MelsecMcNet melsecMc = new MelsecMcNet( "192.168.1.110", 6000 ); // 鏈接對象 OperateResult connect = melsecMc.ConnectServer( ); if (!connect.IsSuccess) { Console.WriteLine( "connect failed:" + connect.Message ); return; } // 舉例讀取D100的值 short D100 = melsecMc.ReadInt16( "D100" ).Content; melsecMc.ConnectClose( );
通過層層封裝後,讀寫的邏輯精簡爲,實例化,鏈接,讀寫,關閉。不管是三菱的PLC,仍是西門子的PLC,都是一致的,由於基類的模型都是一致的。
// 實例化對象,指定PLC的ip地址和端口號 SiemensS7Net siemens = new SiemensS7Net( SiemensPLCS.S1200, " 192.168.1.110" ); // 鏈接對象 OperateResult connect = siemens.ConnectServer( ); if (!connect.IsSuccess) { Console.WriteLine( "connect failed:" + connect.Message ); return; } // 舉例讀取M100的值 short M100 = siemens.ReadInt16( "M100" ).Content; siemens.ConnectClose( );
固然,支持大多數的C#類型數據讀寫
MelsecMcNet melsec_net = new MelsecMcNet( "192.168.0.100", 6000 ); // 此處以D寄存器做爲示例 short short_D1000 = melsec_net.ReadInt16( "D1000" ).Content; // 讀取D1000的short值 ushort ushort_D1000 = melsec_net.ReadUInt16( "D1000" ).Content; // 讀取D1000的ushort值 int int_D1000 = melsec_net.ReadInt32( "D1000" ).Content; // 讀取D1000-D1001組成的int數據 uint uint_D1000 = melsec_net.ReadUInt32( "D1000" ).Content; // 讀取D1000-D1001組成的uint數據 float float_D1000 = melsec_net.ReadFloat( "D1000" ).Content; // 讀取D1000-D1001組成的float數據 long long_D1000 = melsec_net.ReadInt64( "D1000" ).Content; // 讀取D1000-D1003組成的long數據 ulong ulong_D1000 = melsec_net.ReadUInt64( "D1000" ).Content; // 讀取D1000-D1003組成的long數據 double double_D1000 = melsec_net.ReadDouble( "D1000" ).Content; // 讀取D1000-D1003組成的double數據 string str_D1000 = melsec_net.ReadString( "D1000", 10 ).Content; // 讀取D1000-D1009組成的條碼數據 // 讀取數組 short[] short_D1000_array = melsec_net.ReadInt16( "D1000", 10 ).Content; // 讀取D1000的short值 ushort[] ushort_D1000_array = melsec_net.ReadUInt16( "D1000", 10 ).Content; // 讀取D1000的ushort值 int[] int_D1000_array = melsec_net.ReadInt32( "D1000", 10 ).Content; // 讀取D1000-D1001組成的int數據 uint[] uint_D1000_array = melsec_net.ReadUInt32( "D1000", 10 ).Content; // 讀取D1000-D1001組成的uint數據 float[] float_D1000_array = melsec_net.ReadFloat( "D1000", 10 ).Content; // 讀取D1000-D1001組成的float數據 long[] long_D1000_array = melsec_net.ReadInt64( "D1000", 10 ).Content; // 讀取D1000-D1003組成的long數據 ulong[] ulong_D1000_array = melsec_net.ReadUInt64( "D1000", 10 ).Content; // 讀取D1000-D1003組成的long數據 double[] double_D1000_array = melsec_net.ReadDouble( "D1000", 10 ).Content; // 讀取D1000-D1003組成的double數據
寫入的操做:
MelsecMcNet melsec_net = new MelsecMcNet( "192.168.0.100", 6000 ); // 此處以D寄存器做爲示例 melsec_net.Write( "D1000", (short)1234 ); // 寫入D1000 short值 ,W3C0,R3C0 效果是同樣的 melsec_net.Write( "D1000", (ushort)45678 ); // 寫入D1000 ushort值 melsec_net.Write( "D1000", 1234566 ); // 寫入D1000 int值 melsec_net.Write( "D1000", (uint)1234566 ); // 寫入D1000 uint值 melsec_net.Write( "D1000", 123.456f ); // 寫入D1000 float值 melsec_net.Write( "D1000", 123.456d ); // 寫入D1000 double值 melsec_net.Write( "D1000", 123456661235123534L ); // 寫入D1000 long值 melsec_net.Write( "D1000", 523456661235123534UL ); // 寫入D1000 ulong值 melsec_net.Write( "D1000", "K123456789" ); // 寫入D1000 string值 // 讀取數組 melsec_net.Write( "D1000", new short[] { 123, 3566, -123 } ); // 寫入D1000 short值 ,W3C0,R3C0 效果是同樣的 melsec_net.Write( "D1000", new ushort[] { 12242, 42321, 12323 } ); // 寫入D1000 ushort值 melsec_net.Write( "D1000", new int[] { 1234312312, 12312312, -1237213 } ); // 寫入D1000 int值 melsec_net.Write( "D1000", new uint[] { 523123212, 213,13123 } ); // 寫入D1000 uint值 melsec_net.Write( "D1000", new float[] { 123.456f, 35.3f, -675.2f } ); // 寫入D1000 float值 melsec_net.Write( "D1000", new double[] { 12343.542312d, 213123.123d, -231232.53432d } ); // 寫入D1000 double值 melsec_net.Write( "D1000", new long[] { 1231231242312,34312312323214,-1283862312631823 } ); // 寫入D1000 long值 melsec_net.Write( "D1000", new ulong[] { 1231231242312, 34312312323214, 9731283862312631823 } ); // 寫入D1000 ulong值
這裏舉例了三菱的PLC,實際上各類PLC的操做都是相似的。
除了上述的基本的設備通訊,還實現了redis數據庫讀寫操做,分了兩個類實現,下圖爲通常的通訊功能
同時demo中實現了一個瀏覽redis服務器的界面功能
本通訊庫實現了.net 3.5 和 .net 4.5的框架,還附帶了一些簡單的控件,此外還實現了.net standard版本,已在linux測試成功,因爲官方在.net core2.2中還未實現串口類,因此暫時沒有實現串口相關的。
將來的方向,但願繼續優化代碼,架構,集成實現更多設備通訊,方便廣大的網友直接開發測試。
開源地址:https://github.com/dathlin/HslCommunication
官網:http://www.hslcommunication.cn/
更多詳細的內容請查看源代碼的readme文件。