You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

630 lines
25 KiB
C#

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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
{
/// <summary>
/// 三菱PLC通讯类采用Qna兼容3E帧协议实现需要在PLC侧先的以太网模块先进行配置必须为ASCII通讯格式
/// </summary>
/// <remarks>
/// 地址的输入的格式说明如下:
/// <list type="table">
/// <listheader>
/// <term>地址名称</term>
/// <term>地址代号</term>
/// <term>示例</term>
/// <term>地址进制</term>
/// <term>字操作</term>
/// <term>位操作</term>
/// <term>备注</term>
/// </listheader>
/// <item>
/// <term>内部继电器</term>
/// <term>M</term>
/// <term>M100,M200</term>
/// <term>10</term>
/// <term>√</term>
/// <term>√</term>
/// <term></term>
/// </item>
/// <item>
/// <term>输入继电器</term>
/// <term>X</term>
/// <term>X100,X1A0</term>
/// <term>16</term>
/// <term>√</term>
/// <term>√</term>
/// <term></term>
/// </item>
/// <item>
/// <term>输出继电器</term>
/// <term>Y</term>
/// <term>Y100,Y1A0</term>
/// <term>16</term>
/// <term>√</term>
/// <term>√</term>
/// <term></term>
/// </item>
/// <item>
/// <term>锁存继电器</term>
/// <term>L</term>
/// <term>L100,L200</term>
/// <term>10</term>
/// <term>√</term>
/// <term>√</term>
/// <term></term>
/// </item>
/// <item>
/// <term>报警器</term>
/// <term>F</term>
/// <term>F100,F200</term>
/// <term>10</term>
/// <term>√</term>
/// <term>√</term>
/// <term></term>
/// </item>
/// <item>
/// <term>边沿继电器</term>
/// <term>V</term>
/// <term>V100,V200</term>
/// <term>10</term>
/// <term>√</term>
/// <term>√</term>
/// <term></term>
/// </item>
/// <item>
/// <term>链接继电器</term>
/// <term>B</term>
/// <term>B100,B1A0</term>
/// <term>16</term>
/// <term>√</term>
/// <term>√</term>
/// <term></term>
/// </item>
/// <item>
/// <term>步进继电器</term>
/// <term>S</term>
/// <term>S100,S200</term>
/// <term>10</term>
/// <term>√</term>
/// <term>√</term>
/// <term></term>
/// </item>
/// <item>
/// <term>数据寄存器</term>
/// <term>D</term>
/// <term>D1000,D2000</term>
/// <term>10</term>
/// <term>√</term>
/// <term>×</term>
/// <term></term>
/// </item>
/// <item>
/// <term>链接寄存器</term>
/// <term>W</term>
/// <term>W100,W1A0</term>
/// <term>16</term>
/// <term>√</term>
/// <term>×</term>
/// <term></term>
/// </item>
/// <item>
/// <term>文件寄存器</term>
/// <term>R</term>
/// <term>R100,R200</term>
/// <term>10</term>
/// <term>√</term>
/// <term>×</term>
/// <term></term>
/// </item>
/// <item>
/// <term>ZR文件寄存器</term>
/// <term>ZR</term>
/// <term>ZR100,ZR2A0</term>
/// <term>16</term>
/// <term>√</term>
/// <term>×</term>
/// <term></term>
/// </item>
/// <item>
/// <term>变址寄存器</term>
/// <term>Z</term>
/// <term>Z100,Z200</term>
/// <term>10</term>
/// <term>√</term>
/// <term>×</term>
/// <term></term>
/// </item>
/// <item>
/// <term>定时器的触点</term>
/// <term>TS</term>
/// <term>TS100,TS200</term>
/// <term>10</term>
/// <term>√</term>
/// <term>√</term>
/// <term></term>
/// </item>
/// <item>
/// <term>定时器的线圈</term>
/// <term>TC</term>
/// <term>TC100,TC200</term>
/// <term>10</term>
/// <term>√</term>
/// <term>√</term>
/// <term></term>
/// </item>
/// <item>
/// <term>定时器的当前值</term>
/// <term>TN</term>
/// <term>TN100,TN200</term>
/// <term>10</term>
/// <term>√</term>
/// <term>×</term>
/// <term></term>
/// </item>
/// <item>
/// <term>累计定时器的触点</term>
/// <term>SS</term>
/// <term>SS100,SS200</term>
/// <term>10</term>
/// <term>√</term>
/// <term>√</term>
/// <term></term>
/// </item>
/// <item>
/// <term>累计定时器的线圈</term>
/// <term>SC</term>
/// <term>SC100,SC200</term>
/// <term>10</term>
/// <term>√</term>
/// <term>√</term>
/// <term></term>
/// </item>
/// <item>
/// <term>累计定时器的当前值</term>
/// <term>SN</term>
/// <term>SN100,SN200</term>
/// <term>10</term>
/// <term>√</term>
/// <term>×</term>
/// <term></term>
/// </item>
/// <item>
/// <term>计数器的触点</term>
/// <term>CS</term>
/// <term>CS100,CS200</term>
/// <term>10</term>
/// <term>√</term>
/// <term>√</term>
/// <term></term>
/// </item>
/// <item>
/// <term>计数器的线圈</term>
/// <term>CC</term>
/// <term>CC100,CC200</term>
/// <term>10</term>
/// <term>√</term>
/// <term>√</term>
/// <term></term>
/// </item>
/// <item>
/// <term>计数器的当前值</term>
/// <term>CN</term>
/// <term>CN100,CN200</term>
/// <term>10</term>
/// <term>√</term>
/// <term>×</term>
/// <term></term>
/// </item>
/// </list>
/// </remarks>
/// <example>
/// <code lang="cs" source="HslCommunication_Net45.Test\Documentation\Samples\Profinet\MelsecAscii.cs" region="Usage" title="简单的短连接使用" />
/// <code lang="cs" source="HslCommunication_Net45.Test\Documentation\Samples\Profinet\MelsecAscii.cs" region="Usage2" title="简单的长连接使用" />
/// </example>
public class MelsecMcAsciiNet : NetworkDeviceBase<MelsecQnA3EAsciiMessage, RegularByteTransform>
{
#region Constructor
/// <summary>
/// 实例化三菱的Qna兼容3E帧协议的通讯对象
/// </summary>
public MelsecMcAsciiNet( )
{
WordLength = 1;
}
/// <summary>
/// 实例化一个三菱的Qna兼容3E帧协议的通讯对象
/// </summary>
/// <param name="ipAddress">PLC的Ip地址</param>
/// <param name="port">PLC的端口</param>
public MelsecMcAsciiNet( string ipAddress, int port )
{
WordLength = 1;
IpAddress = ipAddress;
Port = port;
}
#endregion
#region Public Member
/// <summary>
/// 网络号
/// </summary>
public byte NetworkNumber { get; set; } = 0x00;
/// <summary>
/// 网络站号
/// </summary>
public byte NetworkStationNumber { get; set; } = 0x00;
#endregion
#region Address Analysis
/// <summary>
/// 分析地址的方法,允许派生类里进行重写操作
/// </summary>
/// <param name="address">地址信息</param>
/// <param name="length">数据长度</param>
/// <returns>解析后的数据信息</returns>
protected virtual OperateResult<McAddressData> McAnalysisAddress( string address, ushort length )
{
return McAddressData.ParseMelsecFrom( address, length );
}
#endregion
#region Read Write Override
/// <summary>
/// 从三菱PLC中读取想要的数据返回读取结果读取的单位为字
/// </summary>
/// <param name="address">读取地址,格式为"M100","D100","W1A0"</param>
/// <param name="length">读取的数据长度字最大值960位最大值7168</param>
/// <returns>带成功标志的结果数据对象</returns>
/// <remarks>
/// 地址支持的列表参考 <seealso cref="MelsecMcAsciiNet"/> 的备注说明
/// </remarks>
/// <example>
/// 假设起始地址为D100D100存储了温度100.6℃值为1006D101存储了压力1.23Mpa值为123D102D103存储了产量计数读取如下
/// <code lang="cs" source="HslCommunication_Net45.Test\Documentation\Samples\Profinet\MelsecAscii.cs" region="ReadExample2" title="Read示例" />
/// 以下是读取不同类型数据的示例
/// <code lang="cs" source="HslCommunication_Net45.Test\Documentation\Samples\Profinet\MelsecAscii.cs" region="ReadExample1" title="Read示例" />
/// </example>
public override OperateResult<byte[]> Read( string address, ushort length )
{
// 分析地址
OperateResult<McAddressData> addressResult = McAnalysisAddress( address, length );
if (!addressResult.IsSuccess) return OperateResult.CreateFailedResult<byte[]>( addressResult );
List<byte> bytesContent = new List<byte>( );
ushort alreadyFinished = 0;
while (alreadyFinished < length)
{
ushort readLength = (ushort)Math.Min( length - alreadyFinished, 450 );
addressResult.Content.Length = readLength;
OperateResult<byte[]> 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<byte[]> ReadAddressData( McAddressData addressData )
{
// 地址分析
byte[] coreResult = MelsecHelper.BuildAsciiReadMcCoreCommand( addressData, false );
// 核心交互
var read = ReadFromCoreServer( PackMcCommand( coreResult, NetworkNumber, NetworkStationNumber ) );
if (!read.IsSuccess) return OperateResult.CreateFailedResult<byte[]>( read );
// 错误代码验证
ushort errorCode = Convert.ToUInt16( Encoding.ASCII.GetString( read.Content, 18, 4 ), 16 );
if (errorCode != 0) return new OperateResult<byte[]>( errorCode, StringResources.Language.MelsecPleaseReferToManulDocument );
// 数据解析,需要传入是否使用位的参数
return ExtractActualData( read.Content, false );
}
/// <summary>
/// 向PLC写入数据数据格式为原始的字节类型
/// </summary>
/// <param name="address">初始地址</param>
/// <param name="value">原始的字节数据</param>
/// <example>
/// 假设起始地址为D100D100存储了温度100.6℃值为1006D101存储了压力1.23Mpa值为123D102D103存储了产量计数写入如下
/// <code lang="cs" source="HslCommunication_Net45.Test\Documentation\Samples\Profinet\MelsecAscii.cs" region="WriteExample2" title="Write示例" />
/// 以下是读取不同类型数据的示例
/// <code lang="cs" source="HslCommunication_Net45.Test\Documentation\Samples\Profinet\MelsecAscii.cs" region="WriteExample1" title="Write示例" />
/// </example>
/// <returns>结果</returns>
public override OperateResult Write( string address, byte[] value )
{
// 分析地址
OperateResult<McAddressData> addressResult = McAnalysisAddress( address, 0 );
if (!addressResult.IsSuccess) return OperateResult.CreateFailedResult<byte[]>( addressResult );
// 地址分析
byte[] coreResult = MelsecHelper.BuildAsciiWriteWordCoreCommand( addressResult.Content, value );
// 核心交互
OperateResult<byte[]> 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<byte[]>( errorCode, StringResources.Language.MelsecPleaseReferToManulDocument );
// 写入成功
return OperateResult.CreateSuccessResult( );
}
#endregion
#region Bool Operate Support
/// <summary>
/// 从三菱PLC中批量读取位软元件返回读取结果
/// </summary>
/// <param name="address">起始地址</param>
/// <param name="length">读取的长度</param>
/// <returns>带成功标志的结果数据对象</returns>
/// <remarks>
/// 地址支持的列表参考 <seealso cref="MelsecMcAsciiNet"/> 的备注说明
/// </remarks>
/// <example>
/// <code lang="cs" source="HslCommunication_Net45.Test\Documentation\Samples\Profinet\MelsecAscii.cs" region="ReadBool" title="Bool类型示例" />
/// </example>
public virtual OperateResult<bool[]> ReadBool( string address, ushort length )
{
// 分析地址
OperateResult<McAddressData> addressResult = McAnalysisAddress( address, length );
if (!addressResult.IsSuccess) return OperateResult.CreateFailedResult<bool[]>( addressResult );
// 地址分析
byte[] coreResult = MelsecHelper.BuildAsciiReadMcCoreCommand( addressResult.Content, true );
// 核心交互
var read = ReadFromCoreServer( PackMcCommand( coreResult, NetworkNumber, NetworkStationNumber ) );
if (!read.IsSuccess) return OperateResult.CreateFailedResult<bool[]>( read );
// 错误代码验证
ushort errorCode = Convert.ToUInt16( Encoding.ASCII.GetString( read.Content, 18, 4 ), 16 );
if (errorCode != 0) return new OperateResult<bool[]>( errorCode, StringResources.Language.MelsecPleaseReferToManulDocument );
// 数据解析,需要传入是否使用位的参数
var extract = ExtractActualData( read.Content, true );
if(!extract.IsSuccess) return OperateResult.CreateFailedResult<bool[]>( extract );
// 转化bool数组
return OperateResult.CreateSuccessResult( extract.Content.Select( m => m == 0x01 ).Take( length ).ToArray( ) );
}
/// <summary>
/// 从三菱PLC中批量读取位软元件返回读取结果
/// </summary>
/// <param name="address">起始地址</param>
/// <returns>带成功标志的结果数据对象</returns>
/// <example>参照 <see cref="ReadBool(string, ushort)"/> 方法 </example>
public OperateResult<bool> ReadBool( string address )
{
OperateResult<bool[]> read = ReadBool( address, 1 );
if (!read.IsSuccess) return OperateResult.CreateFailedResult<bool>( read );
return OperateResult.CreateSuccessResult<bool>( read.Content[0] );
}
/// <summary>
/// 向PLC中位软元件写入bool数组返回值说明比如你写入M100,values[0]对应M100
/// </summary>
/// <param name="address">要写入的数据地址</param>
/// <param name="value">要写入的实际数据长度为8的倍数</param>
/// <example>
/// 详细请查看<see cref="Write(string, bool[])"/>方法的示例
/// </example>
/// <returns>返回写入结果</returns>
public OperateResult Write( string address, bool value )
{
return Write( address, new bool[] { value } );
}
/// <summary>
/// 向PLC中位软元件写入bool数组返回值说明比如你写入M100,values[0]对应M100
/// </summary>
/// <param name="address">要写入的数据地址</param>
/// <param name="values">要写入的实际数据,可以指定任意的长度</param>
/// <example>
/// <code lang="cs" source="HslCommunication_Net45.Test\Documentation\Samples\Profinet\MelsecAscii.cs" region="WriteBool" title="Write示例" />
/// </example>
/// <returns>返回写入结果</returns>
public virtual OperateResult Write( string address, bool[] values )
{
// 分析地址
OperateResult<McAddressData> addressResult = McAnalysisAddress( address, 0 );
if (!addressResult.IsSuccess) return addressResult;
// 解析指令
byte[] coreResult = MelsecHelper.BuildAsciiWriteBitCoreCommand( addressResult.Content, values );
// 核心交互
OperateResult<byte[]> 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<byte[]>( errorCode, StringResources.Language.MelsecPleaseReferToManulDocument );
// 写入成功
return OperateResult.CreateSuccessResult( );
}
#endregion
#region Remote Operate
/// <summary>
/// 远程Run操作
/// </summary>
/// <returns>是否成功</returns>
public OperateResult RemoteRun()
{
// 核心交互
OperateResult<byte[]> 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( );
}
/// <summary>
/// 远程Stop操作
/// </summary>
/// <returns>是否成功</returns>
public OperateResult RemoteStop()
{
// 核心交互
OperateResult<byte[]> 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( );
}
/// <summary>
/// 读取PLC的型号信息
/// </summary>
/// <returns>返回型号的结果对象</returns>
public OperateResult<string> ReadPlcType()
{
// 核心交互
OperateResult<byte[]> read = ReadFromCoreServer( PackMcCommand( Encoding.ASCII.GetBytes( "01010000" ), NetworkNumber, NetworkStationNumber ) );
if (!read.IsSuccess) return OperateResult.CreateFailedResult<string>( read );
// 错误码校验
ushort errorCode = Convert.ToUInt16( Encoding.ASCII.GetString( read.Content, 18, 4 ), 16 );
if (errorCode != 0) return new OperateResult<string>( errorCode, StringResources.Language.MelsecPleaseReferToManulDocument );
// 成功
return OperateResult.CreateSuccessResult( Encoding.ASCII.GetString( read.Content, 22, 16 ).TrimEnd( ) );
}
#endregion
#region Object Override
/// <summary>
/// 获取当前对象的字符串标识形式
/// </summary>
/// <returns>字符串信息</returns>
public override string ToString( )
{
return $"MelsecMcAsciiNet[{IpAddress}:{Port}]";
}
#endregion
#region Static Method Helper
/// <summary>
/// 将MC协议的核心报文打包成一个可以直接对PLC进行发送的原始报文
/// </summary>
/// <param name="mcCore">MC协议的核心报文</param>
/// <param name="networkNumber">网络号</param>
/// <param name="networkStationNumber">网络站号</param>
/// <returns>原始报文信息</returns>
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;
}
/// <summary>
/// 从PLC反馈的数据中提取出实际的数据内容需要传入反馈数据是否位读取
/// </summary>
/// <param name="response">反馈的数据内容</param>
/// <param name="isBit">是否位读取</param>
/// <returns>解析后的结果对象</returns>
public static OperateResult<byte[]> 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
}
}