using HslCommunication.BasicFramework; using HslCommunication.Core; using HslCommunication.Core.Address; using HslCommunication.Core.IMessage; using HslCommunication.Core.Net; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace HslCommunication.Profinet.Melsec { /// /// 三菱PLC通讯类,采用Qna兼容3E帧协议实现,需要在PLC侧先的以太网模块先进行配置,必须为ASCII通讯格式 /// /// /// 地址的输入的格式说明如下: /// /// /// 地址名称 /// 地址代号 /// 示例 /// 地址进制 /// 字操作 /// 位操作 /// 备注 /// /// /// 内部继电器 /// M /// M100,M200 /// 10 /// /// /// /// /// /// 输入继电器 /// X /// X100,X1A0 /// 16 /// /// /// /// /// /// 输出继电器 /// Y /// Y100,Y1A0 /// 16 /// /// /// /// /// /// 锁存继电器 /// L /// L100,L200 /// 10 /// /// /// /// /// /// 报警器 /// F /// F100,F200 /// 10 /// /// /// /// /// /// 边沿继电器 /// V /// V100,V200 /// 10 /// /// /// /// /// /// 链接继电器 /// B /// B100,B1A0 /// 16 /// /// /// /// /// /// 步进继电器 /// S /// S100,S200 /// 10 /// /// /// /// /// /// 数据寄存器 /// D /// D1000,D2000 /// 10 /// /// × /// /// /// /// 链接寄存器 /// W /// W100,W1A0 /// 16 /// /// × /// /// /// /// 文件寄存器 /// R /// R100,R200 /// 10 /// /// × /// /// /// /// ZR文件寄存器 /// ZR /// ZR100,ZR2A0 /// 16 /// /// × /// /// /// /// 变址寄存器 /// Z /// Z100,Z200 /// 10 /// /// × /// /// /// /// 定时器的触点 /// TS /// TS100,TS200 /// 10 /// /// /// /// /// /// 定时器的线圈 /// TC /// TC100,TC200 /// 10 /// /// /// /// /// /// 定时器的当前值 /// TN /// TN100,TN200 /// 10 /// /// × /// /// /// /// 累计定时器的触点 /// SS /// SS100,SS200 /// 10 /// /// /// /// /// /// 累计定时器的线圈 /// SC /// SC100,SC200 /// 10 /// /// /// /// /// /// 累计定时器的当前值 /// SN /// SN100,SN200 /// 10 /// /// × /// /// /// /// 计数器的触点 /// CS /// CS100,CS200 /// 10 /// /// /// /// /// /// 计数器的线圈 /// CC /// CC100,CC200 /// 10 /// /// /// /// /// /// 计数器的当前值 /// CN /// CN100,CN200 /// 10 /// /// × /// /// /// /// /// /// /// /// public class MelsecMcAsciiNet : NetworkDeviceBase { #region Constructor /// /// 实例化三菱的Qna兼容3E帧协议的通讯对象 /// public MelsecMcAsciiNet( ) { WordLength = 1; } /// /// 实例化一个三菱的Qna兼容3E帧协议的通讯对象 /// /// PLC的Ip地址 /// PLC的端口 public MelsecMcAsciiNet( string ipAddress, int port ) { WordLength = 1; IpAddress = ipAddress; Port = port; } #endregion #region Public Member /// /// 网络号 /// public byte NetworkNumber { get; set; } = 0x00; /// /// 网络站号 /// public byte NetworkStationNumber { get; set; } = 0x00; #endregion #region Address Analysis /// /// 分析地址的方法,允许派生类里进行重写操作 /// /// 地址信息 /// 数据长度 /// 解析后的数据信息 protected virtual OperateResult McAnalysisAddress( string address, ushort length ) { return McAddressData.ParseMelsecFrom( address, length ); } #endregion #region Read Write Override /// /// 从三菱PLC中读取想要的数据,返回读取结果,读取的单位为字 /// /// 读取地址,格式为"M100","D100","W1A0" /// 读取的数据长度,字最大值960,位最大值7168 /// 带成功标志的结果数据对象 /// /// 地址支持的列表参考 的备注说明 /// /// /// 假设起始地址为D100,D100存储了温度,100.6℃值为1006,D101存储了压力,1.23Mpa值为123,D102,D103存储了产量计数,读取如下: /// /// 以下是读取不同类型数据的示例 /// /// public override OperateResult Read( string address, ushort length ) { // 分析地址 OperateResult addressResult = McAnalysisAddress( address, length ); if (!addressResult.IsSuccess) return OperateResult.CreateFailedResult( addressResult ); List bytesContent = new List( ); ushort alreadyFinished = 0; while (alreadyFinished < length) { ushort readLength = (ushort)Math.Min( length - alreadyFinished, 450 ); addressResult.Content.Length = readLength; OperateResult read = ReadAddressData( addressResult.Content ); if (!read.IsSuccess) return read; bytesContent.AddRange( read.Content ); alreadyFinished += readLength; // 字的话就是正常的偏移位置,如果是位的话,就转到位的数据 if (addressResult.Content.McDataType.DataType == 0) addressResult.Content.AddressStart += readLength; else addressResult.Content.AddressStart += readLength * 16; } return OperateResult.CreateSuccessResult( bytesContent.ToArray( ) ); } private OperateResult ReadAddressData( McAddressData addressData ) { // 地址分析 byte[] coreResult = MelsecHelper.BuildAsciiReadMcCoreCommand( addressData, false ); // 核心交互 var read = ReadFromCoreServer( PackMcCommand( coreResult, NetworkNumber, NetworkStationNumber ) ); if (!read.IsSuccess) return OperateResult.CreateFailedResult( read ); // 错误代码验证 ushort errorCode = Convert.ToUInt16( Encoding.ASCII.GetString( read.Content, 18, 4 ), 16 ); if (errorCode != 0) return new OperateResult( errorCode, StringResources.Language.MelsecPleaseReferToManulDocument ); // 数据解析,需要传入是否使用位的参数 return ExtractActualData( read.Content, false ); } /// /// 向PLC写入数据,数据格式为原始的字节类型 /// /// 初始地址 /// 原始的字节数据 /// /// 假设起始地址为D100,D100存储了温度,100.6℃值为1006,D101存储了压力,1.23Mpa值为123,D102,D103存储了产量计数,写入如下: /// /// 以下是读取不同类型数据的示例 /// /// /// 结果 public override OperateResult Write( string address, byte[] value ) { // 分析地址 OperateResult addressResult = McAnalysisAddress( address, 0 ); if (!addressResult.IsSuccess) return OperateResult.CreateFailedResult( addressResult ); // 地址分析 byte[] coreResult = MelsecHelper.BuildAsciiWriteWordCoreCommand( addressResult.Content, value ); // 核心交互 OperateResult read = ReadFromCoreServer( PackMcCommand( coreResult, NetworkNumber, NetworkStationNumber ) ); if (!read.IsSuccess) return read; // 错误码验证 ushort errorCode = Convert.ToUInt16( Encoding.ASCII.GetString( read.Content, 18, 4 ), 16 ); if (errorCode != 0) return new OperateResult( errorCode, StringResources.Language.MelsecPleaseReferToManulDocument ); // 写入成功 return OperateResult.CreateSuccessResult( ); } #endregion #region Bool Operate Support /// /// 从三菱PLC中批量读取位软元件,返回读取结果 /// /// 起始地址 /// 读取的长度 /// 带成功标志的结果数据对象 /// /// 地址支持的列表参考 的备注说明 /// /// /// /// public virtual OperateResult ReadBool( string address, ushort length ) { // 分析地址 OperateResult addressResult = McAnalysisAddress( address, length ); if (!addressResult.IsSuccess) return OperateResult.CreateFailedResult( addressResult ); // 地址分析 byte[] coreResult = MelsecHelper.BuildAsciiReadMcCoreCommand( addressResult.Content, true ); // 核心交互 var read = ReadFromCoreServer( PackMcCommand( coreResult, NetworkNumber, NetworkStationNumber ) ); if (!read.IsSuccess) return OperateResult.CreateFailedResult( read ); // 错误代码验证 ushort errorCode = Convert.ToUInt16( Encoding.ASCII.GetString( read.Content, 18, 4 ), 16 ); if (errorCode != 0) return new OperateResult( errorCode, StringResources.Language.MelsecPleaseReferToManulDocument ); // 数据解析,需要传入是否使用位的参数 var extract = ExtractActualData( read.Content, true ); if(!extract.IsSuccess) return OperateResult.CreateFailedResult( extract ); // 转化bool数组 return OperateResult.CreateSuccessResult( extract.Content.Select( m => m == 0x01 ).Take( length ).ToArray( ) ); } /// /// 从三菱PLC中批量读取位软元件,返回读取结果 /// /// 起始地址 /// 带成功标志的结果数据对象 /// 参照 方法 public OperateResult ReadBool( string address ) { OperateResult read = ReadBool( address, 1 ); if (!read.IsSuccess) return OperateResult.CreateFailedResult( read ); return OperateResult.CreateSuccessResult( read.Content[0] ); } /// /// 向PLC中位软元件写入bool数组,返回值说明,比如你写入M100,values[0]对应M100 /// /// 要写入的数据地址 /// 要写入的实际数据,长度为8的倍数 /// /// 详细请查看方法的示例 /// /// 返回写入结果 public OperateResult Write( string address, bool value ) { return Write( address, new bool[] { value } ); } /// /// 向PLC中位软元件写入bool数组,返回值说明,比如你写入M100,values[0]对应M100 /// /// 要写入的数据地址 /// 要写入的实际数据,可以指定任意的长度 /// /// /// /// 返回写入结果 public virtual OperateResult Write( string address, bool[] values ) { // 分析地址 OperateResult addressResult = McAnalysisAddress( address, 0 ); if (!addressResult.IsSuccess) return addressResult; // 解析指令 byte[] coreResult = MelsecHelper.BuildAsciiWriteBitCoreCommand( addressResult.Content, values ); // 核心交互 OperateResult read = ReadFromCoreServer( PackMcCommand( coreResult, NetworkNumber, NetworkStationNumber ) ); if (!read.IsSuccess) return read; // 错误码验证 ushort errorCode = Convert.ToUInt16( Encoding.ASCII.GetString( read.Content, 18, 4 ), 16 ); if (errorCode != 0) return new OperateResult( errorCode, StringResources.Language.MelsecPleaseReferToManulDocument ); // 写入成功 return OperateResult.CreateSuccessResult( ); } #endregion #region Remote Operate /// /// 远程Run操作 /// /// 是否成功 public OperateResult RemoteRun() { // 核心交互 OperateResult read = ReadFromCoreServer( PackMcCommand( Encoding.ASCII.GetBytes("1001000000010000"), NetworkNumber, NetworkStationNumber ) ); if (!read.IsSuccess) return read; // 错误码校验 ushort errorCode = Convert.ToUInt16( Encoding.ASCII.GetString( read.Content, 18, 4 ), 16 ); if (errorCode != 0) return new OperateResult( errorCode, StringResources.Language.MelsecPleaseReferToManulDocument ); // 成功 return OperateResult.CreateSuccessResult( ); } /// /// 远程Stop操作 /// /// 是否成功 public OperateResult RemoteStop() { // 核心交互 OperateResult read = ReadFromCoreServer( PackMcCommand( Encoding.ASCII.GetBytes( "100200000001" ), NetworkNumber, NetworkStationNumber ) ); if (!read.IsSuccess) return read; // 错误码校验 ushort errorCode = Convert.ToUInt16( Encoding.ASCII.GetString( read.Content, 18, 4 ), 16 ); if (errorCode != 0) return new OperateResult( errorCode, StringResources.Language.MelsecPleaseReferToManulDocument ); // 成功 return OperateResult.CreateSuccessResult( ); } /// /// 读取PLC的型号信息 /// /// 返回型号的结果对象 public OperateResult ReadPlcType() { // 核心交互 OperateResult read = ReadFromCoreServer( PackMcCommand( Encoding.ASCII.GetBytes( "01010000" ), NetworkNumber, NetworkStationNumber ) ); if (!read.IsSuccess) return OperateResult.CreateFailedResult( read ); // 错误码校验 ushort errorCode = Convert.ToUInt16( Encoding.ASCII.GetString( read.Content, 18, 4 ), 16 ); if (errorCode != 0) return new OperateResult( errorCode, StringResources.Language.MelsecPleaseReferToManulDocument ); // 成功 return OperateResult.CreateSuccessResult( Encoding.ASCII.GetString( read.Content, 22, 16 ).TrimEnd( ) ); } #endregion #region Object Override /// /// 获取当前对象的字符串标识形式 /// /// 字符串信息 public override string ToString( ) { return $"MelsecMcAsciiNet[{IpAddress}:{Port}]"; } #endregion #region Static Method Helper /// /// 将MC协议的核心报文打包成一个可以直接对PLC进行发送的原始报文 /// /// MC协议的核心报文 /// 网络号 /// 网络站号 /// 原始报文信息 public static byte[] PackMcCommand( byte[] mcCore, byte networkNumber = 0, byte networkStationNumber = 0 ) { byte[] plcCommand = new byte[22 + mcCore.Length]; plcCommand[ 0] = 0x35; // 副标题 plcCommand[ 1] = 0x30; plcCommand[ 2] = 0x30; plcCommand[ 3] = 0x30; plcCommand[ 4] = SoftBasic.BuildAsciiBytesFrom( networkNumber )[0]; // 网络号 plcCommand[ 5] = SoftBasic.BuildAsciiBytesFrom( networkNumber )[1]; plcCommand[ 6] = 0x46; // PLC编号 plcCommand[ 7] = 0x46; plcCommand[ 8] = 0x30; // 目标模块IO编号 plcCommand[ 9] = 0x33; plcCommand[10] = 0x46; plcCommand[11] = 0x46; plcCommand[12] = SoftBasic.BuildAsciiBytesFrom( networkStationNumber )[0]; // 目标模块站号 plcCommand[13] = SoftBasic.BuildAsciiBytesFrom( networkStationNumber )[1]; plcCommand[14] = SoftBasic.BuildAsciiBytesFrom( (ushort)(plcCommand.Length - 18) )[0]; // 请求数据长度 plcCommand[15] = SoftBasic.BuildAsciiBytesFrom( (ushort)(plcCommand.Length - 18) )[1]; plcCommand[16] = SoftBasic.BuildAsciiBytesFrom( (ushort)(plcCommand.Length - 18) )[2]; plcCommand[17] = SoftBasic.BuildAsciiBytesFrom( (ushort)(plcCommand.Length - 18) )[3]; plcCommand[18] = 0x30; // CPU监视定时器 plcCommand[19] = 0x30; plcCommand[20] = 0x31; plcCommand[21] = 0x30; mcCore.CopyTo( plcCommand, 22 ); return plcCommand; } /// /// 从PLC反馈的数据中提取出实际的数据内容,需要传入反馈数据,是否位读取 /// /// 反馈的数据内容 /// 是否位读取 /// 解析后的结果对象 public static OperateResult ExtractActualData( byte[] response, bool isBit ) { if (isBit) { // 位读取 byte[] Content = new byte[response.Length - 22]; for (int i = 22; i < response.Length; i++) { Content[i - 22] = response[i] == 0x30 ? (byte)0x00 : (byte)0x01; } return OperateResult.CreateSuccessResult( Content ); } else { // 字读取 byte[] Content = new byte[(response.Length - 22) / 2]; for (int i = 0; i < Content.Length / 2; i++) { ushort tmp = Convert.ToUInt16( Encoding.ASCII.GetString( response, i * 4 + 22, 4 ), 16 ); BitConverter.GetBytes( tmp ).CopyTo( Content, i * 2 ); } return OperateResult.CreateSuccessResult( Content ); } } #endregion } }