本项目的孵化说来也是机缘巧合的事,本人于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文件。