PLC通讯实现 -C#访问OpcUa实现读写PLC(十)
背景
概念
特点
依赖
配置OpcUA Server
关键代码
代码下载
背景
由于工厂设备种类多、分阶段建设,工控程序开发通常面临对接多种PLC厂商设备和不同系列与型号。因此出现了一种专门与不同PLC通讯的软件协议-OPC(OLE for Process Control),而各厂家在OPC基础上进行了不同程度的扩展,为了应对标准化和跨平台的趋势,和了更好的推广OPC,OPC基金会近些年在之前OPC成功应用的基础上推出了一个新的OPC标准-OPC UA。处于通讯效率上的考虑,很多厂家生产了OPCUA设备模块,内置处理器,性价比不错。不过这不是本文关注的重点。
概念
OPC UA(OPC Unified Architecture)是指OPC统一体系架构,是一种基于服务的、跨越平台的解决方案。
特点
扩展了OPC的应用平台。传统的基于COM/DCOM 的OPC技术只能基于Windows操作系统,OPC UA支持拓展到Linux和Unix平台。这使得基于OPC UA的标准产品可以更好地实现工厂级的数据采集和管理;
不再基于DCOM通讯,不需要进行DCOM安全设置;
OPC UA定义了统一数据和服务模型,使数据组织更为灵活,可以实现报警与事件、数据存取、历史数据存取、控制命令、复杂数据的交互通信;
OPC UA比OPC DA更安全。OPC UA传递的数据是可以加密的,并对通信连接和数据本身都可以实现安全控制。新的安全模型保证了数据从原始设备到MES,ERP系统,从本地到远程的各级自动化和信息化系统的可靠传递;
OPC UA可以穿越防火墙,实现Internet 通讯。
依赖
我们通常不会从头写,可以基于OpcUa.core.dll库和OpcUa.Client.dll库,而且附上这2个库的源代码。
配置OpcUA Server
您可以安装任何一款支持OPCUA的服务端软件进行以下配置(此为示例配置,您可根据你的实际情况进行配置)
1、OpcUa Server Url:opc.tcp://192.168.100.1:4840。
2、OpcUa EndPoint:[UaServer@cMT-EAB9] [None] [#None] [opc.tcp://192.168.100.1:4840/G01]
3、PLC Device Name:Siemens S7-1200/S7-1500
4、Account:user1
5、Password:自己设置
6、在PLC中开了2个数据块,分别为DB4长度110个字、DB5长度122个字。
7、对应第4块创建标签,第一个名称为DB4.0-99,地址为DB4DBW0.100,数据类型为Short,长度100,即定义长度最长为100的Short数组。第二个名称为DB4.100-109,地址为DB4DBW100.10,数据类型为Short,方便快速读取。
5、对应第5块创建3个标签,第一个名称为DB5.0-99,地址为DB5DBW0.100,数据类型为Short,第二个名称为DB5.100-121, 地址为DB5DBW100.22,数据类型为Short,即定义长度最长为100的Short数组。方便快速读取。第三个标签名称为DB5DBW64,地址为DB5DBW64,数据类型为Short。
具体如下图:
关键代码
using System ;
using System. Collections. Generic ;
using System. Linq ; using Opc. Ua. Helper ;
using Mesnac. Equips ; namespace Mesnac. Equip. OPC. OpcUa. OPCUA
{ public class Equip : BaseEquip { #region 字段定义 private bool _isOpen = false ; private bool _isClosing = false ; private OPCUAClass myOpcHelper; private Dictionary< string , string > dicTags = null ; private Dictionary< string , object > readResult = null ; private int stepLen = 250 ; private string groupNamePrefix = "DB" ; private string childTagFlag = "~" ; private System. Threading. Thread innerReadThread = null ; private int innerReadRate = 1000 ; #endregion #region 属性定义 public string OpcUaServerUrl{ get { return "opc.tcp://192.168.1.102:4840" ; } } public string OpcUaServiceName{ get { return "[UaServer@cMT-9F1F] [None] [#None] [opc.tcp://192.168.1.102:4840/G01]" ; } } public string PLCName{ get { return "Siemens_192.168.2.1" ; } } public string Account{ get { return "user1" ; } } public string Password{ get { return "1" ; } } #endregion #region BaseEquip成员实现 public override bool Open ( ) { lock ( this ) { this . _isClosing = false ; if ( this . _isOpen == true && this . myOpcHelper != null ) { return true ; } this . State = false ; this . myOpcHelper = new OPCUAClass ( ) ; this . dicTags = this . myOpcHelper. ConnectOPCUA ( this . OpcUaServerUrl, this . Account, this . Password, this . OpcUaServiceName, this . PLCName) ; if ( this . dicTags == null || this . dicTags. Count == 0 ) { this . myOpcHelper = null ; Console. WriteLine ( "OPC连接失败!" ) ; this . State = false ; return false ; } else { this . State = true ; this . _isOpen = true ; #region 初始化读取结果 this . readResult = new Dictionary< string , object > ( ) ; foreach ( Equips. BaseInfo. Group group in this . Group. Values) { if ( ! group . IsAutoRead) { continue ; } int groupMinStart = group . Start; int groupMaxEnd = group . Start + group . Len; int groupMaxLen = group . Len; foreach ( Equips. BaseInfo. Group g in this . Group. Values) { if ( ! g. IsAutoRead) { continue ; } if ( g. Block == group . Block) { if ( g. Start < group . Start) { groupMinStart = g. Start; } if ( g. Start + g. Len > groupMaxEnd) { groupMaxEnd = g. Start + g. Len; } } } groupMaxLen = groupMaxEnd - groupMinStart; int tagCount = groupMaxLen % this . stepLen == 0 ? groupMaxLen / this . stepLen : groupMaxLen / this . stepLen + 1 ; int currLen = 0 ; for ( int i = 0 ; i < tagCount; i++ ) { string tagName = String. Empty; if ( tagCount == 1 ) { tagName = String. Format ( "{0}-{1}" , groupMinStart, groupMinStart + groupMaxLen - 1 ) ; currLen = groupMaxLen; } else if ( i == tagCount - 1 ) { tagName = String. Format ( "{0}-{1}" , groupMinStart + ( i * this . stepLen) , groupMinStart + ( i * this . stepLen) + ( groupMaxLen % this . stepLen == 0 ? this . stepLen : groupMaxLen % this . stepLen) - 1 ) ; currLen = groupMaxLen % this . stepLen; } else { tagName = String. Format ( "{0}-{1}" , groupMinStart + ( i * this . stepLen) , groupMinStart + ( i * this . stepLen) + this . stepLen - 1 ) ; currLen = this . stepLen; } string tagFullName = String. Format ( "{0}{1}.{2}" , groupNamePrefix, group . Block, tagName) ; if ( ! this . readResult. ContainsKey ( tagFullName) ) { bool exists = false ; #region 判断读取结果标签组的范围是否包括了此标签 比如tagFullName DB5.220-299,在readResult中存在 DB5.200-299,则认为已存在,不需要再添加 string [ ] beginend = null ; int begin = 0 ; int end = 0 ; string [ ] startstop = tagFullName. Replace ( String. Format ( "{0}{1}." , groupNamePrefix, group . Block) , String. Empty) . Split ( new char [ ] { '-' } ) ; int start = 0 ; int stop = 0 ; bool parseResult = false ; if ( startstop. Length == 2 ) { parseResult = int . TryParse ( startstop[ 0 ] , out start) ; if ( parseResult) { parseResult = int . TryParse ( startstop[ 1 ] , out stop) ; } } if ( parseResult) { int existsMinBegin = 0 ; int existsMaxEnd = 0 ; bool isContinue = true ; string [ ] existsTags = this . readResult. Keys. ToArray < string > ( ) ; foreach ( string tag in existsTags) { if ( tag. StartsWith ( String. Format ( "{0}{1}." , groupNamePrefix, group . Block) ) && tag. Contains ( "." ) && tag. Contains ( "-" ) ) { string [ ] tagname = tag. Split ( new char [ ] { '.' } ) ; if ( tagname. Length == 2 ) { beginend = tagname[ 1 ] . Split ( new char [ ] { '-' } ) ; if ( beginend. Length == 2 ) { parseResult = int . TryParse ( beginend[ 0 ] , out begin) ; if ( parseResult) { parseResult = int . TryParse ( beginend[ 1 ] , out end) ; } #region 计算最小开始索引和最大结束索引 if ( begin < existsMinBegin) { existsMinBegin = begin; #region 判断标签值是否连续 if ( existsMaxEnd != 0 && begin != existsMaxEnd + 1 ) { isContinue = false ; } #endregion } if ( end > existsMaxEnd) { existsMaxEnd = end; } #endregion } } if ( parseResult) { if ( start >= begin && stop <= end) { exists = true ; break ; } if ( isContinue) { if ( start >= existsMinBegin && stop <= existsMaxEnd) { exists = true ; break ; } } } } } } #endregion if ( ! exists) { ushort [ ] groupData = new ushort [ currLen] ; this . readResult[ tagFullName] = groupData; Console. WriteLine ( tagFullName) ; } } } } #endregion #region 开启内部定时读取 if ( this . innerReadThread == null ) { this . innerReadRate = this . Main. ReadHz / 2 ; this . innerReadThread = new System. Threading. Thread ( this . InnerAutoRead) ; this . innerReadThread. Start ( ) ; } #endregion } return this . State; } } public override bool Read ( string block, int start, int len, out object [ ] buff) { lock ( this ) { buff = null ; if ( this . _isClosing) { return false ; } string readstrflag = String. Format ( "{0}{1}.{2}-{3}" , this . groupNamePrefix, block, start, start + len - 1 ) ; System. Text. StringBuilder sbtaglength = new System. Text. StringBuilder ( ) ; string startTag = String. Empty; string groupName = String. Format ( "{0}{1}" , this . groupNamePrefix, block) ; List< ushort > groupData = new List< ushort > ( ) ; List< string > groupTagNames = new List< string > ( ) ; int startIndex = 0 ; try { if ( ! Open ( ) ) { return false ; } string [ ] keys = this . readResult. Keys. ToArray < string > ( ) ; foreach ( string key in keys) { if ( key. StartsWith ( groupName) && key. Replace ( String. Format ( "{0}." , groupName) , String. Empty) . Contains ( "-" ) ) { groupTagNames. Add ( key) ; } } groupTagNames. Sort ( ) ; foreach ( string key in groupTagNames) { if ( String. IsNullOrEmpty ( startTag) ) { startTag = key. Replace ( String. Format ( "{0}." , groupName) , String. Empty) ; } ushort [ ] values; if ( this . readResult[ key] is ushort [ ] ) { values = this . readResult[ key] as ushort [ ] ; } else { values = new ushort [ ] { ( ushort ) this . readResult[ key] } ; } sbtaglength. Append ( String. Format ( "tagName={0}, buff length = {1}" , key, values. Length) ) ; groupData. AddRange ( values) ; } buff = new object [ len] ; if ( ! String. IsNullOrEmpty ( startTag) ) { string strStartIndex = startTag. Substring ( 0 , startTag. IndexOf ( "-" ) ) ; int . TryParse ( strStartIndex, out startIndex) ; startIndex = start - startIndex; Array. Copy ( groupData. ToArray ( ) , startIndex, buff, 0 , buff. Length) ; } else { } return true ; } catch ( Exception ex) { Console. WriteLine ( String. Join ( ";" , groupTagNames. ToArray < string > ( ) ) ) ; Console. WriteLine ( "data length = " + groupData. Count) ; Console. WriteLine ( this . Name + "读取失败[" + readstrflag + "]:" + ex. Message) ; Console. WriteLine ( sbtaglength. ToString ( ) ) ; this . State = false ; return false ; } } } public override bool Write ( int block, int start, object [ ] buff) { bool result = true ; lock ( this ) { try { if ( this . _isClosing) { return false ; } if ( ! Open ( ) ) { return false ; } bool isWrite = false ; #region 按标签变量写入 string itemId = "" ; foreach ( Equips. BaseInfo. Group group in this . Group. Values) { if ( group . Block == block. ToString ( ) ) { foreach ( Equips. BaseInfo. Data data in group . Data. Values) { if ( group . Start + data. Start == start && data. Len == buff. Length) { if ( this . dicTags. ContainsKey ( data. Name) ) { itemId = this . dicTags[ data. Name] ; } break ; } } } } if ( ! String. IsNullOrEmpty ( itemId) ) { UInt16[ ] intBuff = new UInt16 [ buff. Length] ; for ( int i = 0 ; i < intBuff. Length; i++ ) { intBuff[ i] = 0 ; if ( ! UInt16. TryParse ( buff[ i] . ToString ( ) , out intBuff[ i] ) ) { Console. WriteLine ( "在写入OPCUA标签时把buff中的元素转为UInt16类型失败!" ) ; } } result = this . myOpcHelper. WriteUInt16 ( itemId, intBuff) ; if ( ! result) { Console. WriteLine ( String. Format ( "标签变量[{0}]写入失败!" , itemId) ) ; return false ; } else { Console. WriteLine ( "按标签变量写入..." + itemId) ; isWrite = true ; } } if ( isWrite) { return true ; } #endregion #region 按块写入 #region 先读取相应标签数数据 string startTag = String. Empty; string groupName = String. Format ( "{0}{1}" , this . groupNamePrefix, block) ; List< ushort > groupData = new List< ushort > ( ) ; string [ ] keys = readResult. Keys. Where ( o => o. StartsWith ( groupName) && o. Contains ( "-" ) ) . OrderBy ( c => c) . ToArray < string > ( ) ; foreach ( string key in keys) { if ( String. IsNullOrEmpty ( startTag) ) { startTag = key. Replace ( String. Format ( "{0}." , groupName) , String. Empty) ; } string [ ] beginEnd = key. Replace ( String. Format ( "{0}." , groupName) , String. Empty) . Split ( new char [ ] { '-' } ) ; if ( beginEnd. Length != 2 ) { Console. WriteLine ( String. Format ( "标签变量[{0}]未按约定方式命名,请按[DB块号].[起始字-结束字]方式标签变量进行命名!" , String. Format ( "{0}.{1}" , key) ) ) ; return false ; } int begin = 0 ; int end = 0 ; int . TryParse ( beginEnd[ 0 ] , out begin) ; int . TryParse ( beginEnd[ 1 ] , out end) ; #region 写入之前,先读取一下PLC的值 if ( ( start >= begin && start <= end) || ( ( start + buff. Length - 1 ) >= begin && ( start + buff. Length - 1 ) <= end) || ( start < begin && ( start + buff. Length - 1 ) > end) ) { this . ReadTag ( key) ; if ( this . readResult. ContainsKey ( key) && this . readResult[ key] is Array ) { Console. WriteLine ( "read = " + key) ; groupData. AddRange ( this . readResult[ key] as ushort [ ] ) ; } else { Console. WriteLine ( String. Format ( "读取结果中不包含标签变量[{0}]的值!" , String. Format ( "{0}" , key) ) ) ; } } else { if ( this . readResult. ContainsKey ( key) && this . readResult[ key] is Array ) { Console. WriteLine ( "no read = " + key) ; groupData. AddRange ( this . readResult[ key] as ushort [ ] ) ; } } #endregion } #endregion if ( String. IsNullOrEmpty ( startTag) ) { Console. WriteLine ( "写入失败,未在OPCUAserver中找到对应的标签,block = {0}, start = {1}, len = {2}" , block, start, buff. Length) ; return false ; } #region 更新标签中对应的数据后,再写回OPCServer int startIndex = 0 ; string strStartIndex = startTag. Substring ( 0 , startTag. IndexOf ( "-" ) ) ; int . TryParse ( strStartIndex, out startIndex) ; startIndex = start - startIndex; ushort [ ] newDataBuffer = groupData. ToArray ( ) ; for ( int i = 0 ; i < buff. Length; i++ ) { ushort svalue = 0 ; ushort . TryParse ( buff[ i] . ToString ( ) , out svalue) ; newDataBuffer[ startIndex + i] = svalue; } int index = 0 ; string [ ] keys2 = readResult. Keys. Where ( o => o. StartsWith ( groupName) && o. Contains ( "-" ) ) . OrderBy ( c => c) . ToArray < string > ( ) ; foreach ( string key2 in keys2) { string [ ] beginEnd = key2. Replace ( String. Format ( "{0}." , groupName) , String. Empty) . Split ( new char [ ] { '-' } ) ; if ( beginEnd. Length != 2 ) { Console. WriteLine ( String. Format ( "标签变量[{0}]未按约定方式命名,请按[DB块号].[起始字-结束字]方式标签变量进行命名!" , String. Format ( "{0}" , key2) ) ) ; return false ; } int begin = 0 ; int end = 0 ; int . TryParse ( beginEnd[ 0 ] , out begin) ; int . TryParse ( beginEnd[ 1 ] , out end) ; if ( ( start >= begin && start <= end) || ( ( start + buff. Length - 1 ) >= begin && ( start + buff. Length - 1 ) <= end) || ( start < begin && ( start + buff. Length - 1 ) > end) ) { if ( ! this . dicTags. ContainsKey ( key2) ) { Console. WriteLine ( String. Format ( "写入失败:标签变量[{0}]在OpcUA Server中未定义!" , String. Format ( "{0}" , key2) ) ) ; return false ; } int len = ( this . readResult[ key2] as ushort [ ] ) . Length; ushort [ ] tagDataBuff = new ushort [ len] ; int existsMinBegin = this . GetExistsMinBeginByBlock ( block. ToString ( ) ) ; Array. Copy ( newDataBuffer, begin - existsMinBegin, tagDataBuff, 0 , tagDataBuff. Length) ; index += tagDataBuff. Length; result = this . myOpcHelper. WriteUInt16 ( this . dicTags[ key2] , tagDataBuff) ; if ( ! result) { Console. WriteLine ( String. Format ( "向标签变量[{0}]中写入值失败!" , String. Format ( "{0}" , key2) ) ) ; return false ; } else { this . ReadTag ( key2) ; Console. WriteLine ( "写入..." ) ; } } } #endregion #endregion return result; } catch ( Exception ex) { Console. WriteLine ( this . Name + "写入失败:" + ex. Message) ; return false ; } } } public override void Close ( ) { try { this . _isClosing = true ; System. Threading. Thread. Sleep ( this . Main. ReadHz) ; if ( this . innerReadThread != null ) { this . innerReadThread. Abort ( ) ; this . innerReadThread = null ; } } catch ( Exception ex) { Console. WriteLine ( "关闭内部读取OPCUA线程异常:" + ex. Message) ; } try { if ( this . myOpcHelper != null ) { this . myOpcHelper. Close ( ) ; this . myOpcHelper = null ; this . State = false ; this . _isOpen = false ; } } catch ( Exception ex) { Console. WriteLine ( "关于与OPCUA服务连接异常:" + ex. Message) ; } } #endregion #region 辅助方法 private int GetExistsMinBeginByBlock ( string block) { int existsMinBegin = 99999 ; int existsMaxEnd = 0 ; bool isContinue = true ; string [ ] existsTags = this . readResult. Keys. ToArray < string > ( ) ; string [ ] beginend = null ; bool parseResult = false ; int begin = 0 ; int end = 0 ; foreach ( string tag in existsTags) { if ( tag. StartsWith ( String. Format ( "{0}{1}." , groupNamePrefix, block) ) && tag. Contains ( "." ) && tag. Contains ( "-" ) ) { string [ ] tagname = tag. Split ( new char [ ] { '.' } ) ; if ( tagname. Length == 2 ) { beginend = tagname[ 1 ] . Split ( new char [ ] { '-' } ) ; if ( beginend. Length == 2 ) { parseResult = int . TryParse ( beginend[ 0 ] , out begin) ; if ( parseResult) { parseResult = int . TryParse ( beginend[ 1 ] , out end) ; } #region 计算最小开始索引和最大结束索引 if ( begin < existsMinBegin) { existsMinBegin = begin; #region 判断标签值是否连续 if ( existsMaxEnd != 0 && begin != existsMaxEnd + 1 ) { isContinue = false ; } #endregion } if ( end > existsMaxEnd) { existsMaxEnd = end; } #endregion } } if ( parseResult) { } } } return existsMinBegin; } private void ReadTag ( string tagName) { UInt16[ ] buff = null ; if ( this . dicTags. ContainsKey ( tagName) ) { if ( this . myOpcHelper. ReadUInt16 ( this . dicTags[ tagName] , out buff) ) { if ( this . readResult. ContainsKey ( tagName) ) { this . readResult[ tagName] = buff; } else { this . readResult. Add ( tagName, buff) ; } } else { Console. WriteLine ( "Mesnac.Equip.OPC.OpcUa.OPCUA.Equip.ReadTag Exception 读取标签:[{0}]失败!" , tagName) ; } } else { Console. WriteLine ( "Mesnac.Equip.OPC.OpcUa.OPCUA.Equip.ReadTag Exception OPCUA Server中未定义此标签:[{0}]!" , tagName) ; } } private void InnerAutoRead ( ) { while ( this . _isOpen && this . _isClosing == false ) { try { if ( this . myOpcHelper == null ) { this . _isClosing = true ; this . State = false ; return ; } lock ( this ) { string [ ] keys = this . readResult. Keys. ToArray < string > ( ) ; foreach ( string key in keys) { this . ReadTag ( key) ; } } System. Threading. Thread. Sleep ( this . innerReadRate) ; } catch ( Exception ex) { Console. WriteLine ( "Mesnac.Equip.OPC.OpcUa.OPCUA.Equip.InnerAutoRead Exception : " + ex. Message) ; } } this . innerReadThread = null ; } #endregion #region 析构方法 ~ Equip ( ) { this . Close ( ) ; } #endregion }
}
代码下载
代码下载