本篇博文主要说说hslcommunication的结果链的知识,说一下前因后果,以及目前最新的功能扩充,(V9.5.0以上)
以前也写过一篇文章:https://www.cnblogs.com/dathlin/p/7865682.html 不看也没事,参考这篇新的文章就好了。
首先还是聊聊,为什么会诞生这个 OperateResult ,比如我有个方法,获取一些信息的,或是执行一些操作的,比如读取文件的内容。
public string ReadFileContent( string path ) { return System.IO.File.ReadAllText( path ); }
很简单吧,方法里面复杂也没有关系的,如果这个方法保证不会发生异常,或是失败,那就没有关系,这样写也挺好的,但是事实就是极容易发生异常,就拿这个例子来说,可能因为文件不存在,可能因为其他异常。如果我们需要返回的内容包含下面三大块,肯定包括 1. 是否成功 2.错误消息 3.内容 于是我加了一个错误码,就有了下面的类(以下是简写)
public class OperateResult { public bool IsSuccess { get; set; } public string Message { get; set; } public int ErrorCode { get; set; } }
然后可能携带各种不同类型的结果内容,又可能是多个的,所以有了泛型的派生类,这算是泛型的一个经典的例子,另一个例子就是List<T>数组了。
public class OperateResult<T> : OperateResult public class OperateResult<T1, T2> : OperateResult public class OperateResult<T1, T2, T3> : OperateResult public class OperateResult<T1, T2, T3, T4> : OperateResult public class OperateResult<T1, T2, T3, T4, T5> : OperateResult public class OperateResult<T1, T2, T3, T4, T5, T6> : OperateResult public class OperateResult<T1, T2, T3, T4, T5, T6, T7> : OperateResult public class OperateResult<T1, T2, T3, T4, T5, T6, T7, T8> : OperateResult public class OperateResult<T1, T2, T3, T4, T5, T6, T7, T8, T9> : OperateResult public class OperateResult<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10> : OperateResult
又定义了十个泛型类对象,最多可以携带10个不同类型的参数信息,当然,为了扩充一些转化信息,整个 OperateResult.cs 文件的源代码长达 3577 行源代码。
好了,所以上面的方法可以改写为:
public OperateResult<string> ReadFileContent( string path ) { try { return OperateResult.CreateSuccessResult( System.IO.File.ReadAllText( path ) ); } catch(Exception ex) { return new OperateResult<string>( ex.Message ); } }
这样我们就能把结果信息返回了,当然了,实际可能更加复杂一点,比如下面所示,在读取文件之前,还需要检查当前账户是否有权限。
public bool CheckPermission( ) { // 检查账户合法性,是否有权利下载 return true; } public OperateResult<string> ReadFileContent( string path ) { if (!CheckPermission( )) return new OperateResult<string>( "当前无权读取文件的内容" ); try { return OperateResult.CreateSuccessResult( System.IO.File.ReadAllText( path ) ); } catch(Exception ex) { return new OperateResult<string>( ex.Message ); } }
到这里,已经成型基本的意思了。我们再来说一下HslCommunication自身的经典应用,我们来看一个三菱PLC的数据读取示例,我们为了要读取一个地址的原始字节数据,会提供这样的方法,
public override OperateResult<byte[]> Read( string address, ushort length )
但是呢,实际上错误的原因是很多的,可能一开始地址输入错误了,可能网络发生了错误,可能PLC返回了一个错误码,然后进行解析得到正确的数据。那么底层这么实现
public override OperateResult<byte[]> Read( string address, ushort length ) { // 获取指令 var command = BuildReadCommand(address, length, false, PLCNumber); if (!command.IsSuccess) return OperateResult.CreateFailedResult<byte[]>(command); // 核心交互 var read = ReadFromCoreServer(command.Content); if (!read.IsSuccess) return OperateResult.CreateFailedResult<byte[]>(read); // 错误代码验证 OperateResult check = CheckResponseLegal( read.Content ); if (!check.IsSuccess) return OperateResult.CreateFailedResult<byte[]>( check ); // 数据解析,需要传入是否使用位的参数 return ExtractActualData(read.Content, false); }
我们再来看看这个核心交互是怎么实现的?
public OperateResult<byte[]> ReadFromCoreServer( byte[] send ) { var result = new OperateResult<byte[]>( ); OperateResult<Socket> resultSocket = null; InteractiveLock.Enter( ); try { // 获取有用的网络通道,如果没有,就建立新的连接 resultSocket = GetAvailableSocket( ); if (!resultSocket.IsSuccess) { IsSocketError = true; AlienSession?.Offline( ); InteractiveLock.Leave( ); result.CopyErrorFromOther( resultSocket ); return result; } OperateResult<byte[]> read = ReadFromCoreServer( resultSocket.Content, send ); if (read.IsSuccess) { IsSocketError = false; result.IsSuccess = read.IsSuccess; result.Content = read.Content; result.Message = StringResources.Language.SuccessText; } else { IsSocketError = true; AlienSession?.Offline( ); result.CopyErrorFromOther( read ); } ExtraAfterReadFromCoreServer( read ); InteractiveLock.Leave( ); } catch { InteractiveLock.Leave( ); throw; } if (!isPersistentConn) resultSocket?.Content?.Close( ); return result; }
我们可以看到,一旦中间的某个环节发生了错误或是异常,这个错误信息会一直向上传递,直到传递给最上层的调用者。以此形成上下的链条。
那么Convert,Check,Then是什么意思呢?主要是简化代码的。我们来看看下面的代码
public OperateResult<string> Write( ) { OperateResult write = siemens.Write( "M100", (short)12 ); if (!write.IsSuccess) return OperateResult.CreateFailedResult<string>( write ); return OperateResult.CreateSuccessResult( "M100写入成功" ); }
这个代码就可以简化为:
public OperateResult<string> Write( ) => siemens.Write( "M100", (short)12 ).Convert<string>( "M100写入成功" );
我们看到代码简化了很多,所以Convert意思就是,如果原来的结果对象失败,就直接返回,如果成功,就返回给定的结果内容。
我们再来看第二种情况:这种情况主要是对读取的内容进行一些判断操作。
public OperateResult Check( ) { OperateResult<short> read = siemens.ReadInt16( "M100" ); if (!read.IsSuccess) return OperateResult.CreateFailedResult<string>( read ); if (read.Content == 10) return OperateResult.CreateSuccessResult( ); else return new OperateResult( "设备的数据值不对" ); }
这个代码可以简化为:
public OperateResult Check( ) => siemens.ReadInt16( "M100" ).Check( m => m == 10, "设备的数据值不对" );
当然,如果我的检查的方法比较复杂,也可以这么写:
public OperateResult CheckStatus(short value ) { if (value == 1) return new OperateResult( "错误原因1" ); if (value == 2) return new OperateResult( "错误原因2" ); if (value == 3) return new OperateResult( "错误原因3" ); if (value == 4) return new OperateResult( "错误原因4" ); return OperateResult.CreateSuccessResult( ); } public OperateResult Check( ) => siemens.ReadInt16( "M100" ).Check( m => CheckStatus( m ) );
我们再来看看一种更复杂的情况。
public OperateResult StartPLC( ) { // 这是一个启动PLC的方法,逻辑就是,M100.0是启动PLC,但是在启动之前,需要向PLC的多个地址写入初始参数。 OperateResult write = siemens.Write( "M200", (short)123 ); if (!write.IsSuccess) return write; write = siemens.Write( "M202", 123f ); if (!write.IsSuccess) return write; write = siemens.Write( "M206", "123456" ); if (!write.IsSuccess) return write; return siemens.Write( "M100.0", true ); }
嗯,这时候,就需要使用Then方法了,可以简化为:
public OperateResult StartPLC( ) => siemens.Write( "M200", (short)123 ). Then( ( ) => siemens.Write( "M202", 123f ) ). Then( ( ) => siemens.Write( "M206", "123456" ) ). Then( ( ) => siemens.Write( "M100.0", true ) );
一旦发生失败,就会立即回传。现在我们来看个更复杂的综合例子,这是一个现场流程中间的一个小环节,当AGV车到达库位后,需要通知PLC进行连串的交互,以及读取条码信息:
string barcode = string.Empty; public OperateResult CheckSignalAfterAgvReach( ) { // 通知PLC信息,AGV已经到达 OperateResult write = siemens.Write( "DB101.3.1", true ); if (!write.IsSuccess) return write; // 等待PLC复位 允许AGV放胚信号 为false OperateResult wait = siemens.Wait( "DB101.3.2", false ); if (wait.IsSuccess) return wait; // 复位AGV放胚完成信号 write = siemens.Write( "DB101.3.1", false ); if (!write.IsSuccess) return write; // 等待允许读取条码信息 wait = siemens.Wait( "DB101.1.3", true ); if (wait.IsSuccess) return wait; // 读取条码的信息 var readBarCode = siemens.ReadString( "DB102.0" ); if (!readBarCode.IsSuccess) return readBarCode; // 条码用于其他用途 barcode = readBarCode.Content; // 将上料读取条码完成值true write = siemens.Write( "DB101.1.4", true ); if (!write.IsSuccess) return write; // 等待上料允许读取条码设置为false wait = siemens.Wait( "DB101.1.3", false ); if (wait.IsSuccess) return wait; // 复位上料条码读取完成信号 return siemens.Write( "DB101.1.4", false ); }
那么这部分的代码可以简写为:
public OperateResult CheckSignalAfterAgvReach2( ) => siemens.Write( "DB101.3.1", true ). Then( ( ) => siemens.Wait( "DB101.3.2", false ) ). Then( ( ) => siemens.Write( "DB101.3.1", false ) ). Then( ( ) => siemens.Wait( "DB101.1.3", true ) ). Then( ( ) => siemens.ReadString( "DB102.0" ) ). Then( m => { barcode = m; return siemens.Write( "DB101.1.4", true ); } ). Then( ( ) => siemens.Wait( "DB101.1.3", false ) ). Then( ( ) => siemens.Write( "DB101.1.4", false ) ); string barcode = string.Empty;
emmmm,好像写多了,代码是简化了,可读性并没有提升很多,也是给了一个方向。