OPC UA网关(服务)

OPC概论

网上OPC UA服务端的介绍非常少,网关的更是找不到,找到的几乎都是收费的,最近静下心把OPC基金会的代码学习了一遍,结合一些大牛的文章,写了一个简单的OPC UA网关。目前网关的设备只支持西门子,因为只有西门子的仿真器足够强大(S7协议可以仿真),后续根据条件会加上三菱、欧姆龙等等。人过中年,总想踏踏实实做点事情,如果有好的工作平台希望大家能介绍一下,感谢!
OPC是应用于工业通信的,在windows环境的下一种通讯技术,原有的通信技术难以满足日益复杂的环境,在可扩展性,安全性,跨平台性方面的不足日益明显,所以OPC基金会在几年前提出了面向未来的架构设计的OPC 统一架构,简称OPC UA,截止目前为止,越来越多公司将OPC UA作为开放的数据标准.
OPC UA 基于Web服务(windows的WCF)开发的,同时又具有MQTT 可订阅的特性,部署方便、使用简单,容易被大家广泛的接受,于是随着工业4.0的发展逐渐普及

网关结构

流程
细节流程

1.读取配置文件并创建节点

(目前所有配置文件均为手动设置修改,比较下来还是手动配置快点)
节点配置包含一下字段
节点名称、父节点、节点类型,节点数据类型、PLC变量的地址
下面是节点的配置的文件,xml格式

<?xml version="1.0" encoding="UTF-8"?>
 <!-- 根目录-->
<Root>
 <!-- 目录-->
<Node Name="Motor1" ParentNode="Root" NodeType="Folder" >
<!-- 变量-->
<Node Name="Start1" ParentNode="Motor1" NodeType="Variable" DataType="DataTypeIds.Boolean" Adress="M10.0" >
</Node>
<Node Name="Stop1" ParentNode="Motor1" NodeType="Variable" DataType="DataTypeIds.Boolean" Adress="M10.1" >
</Node>
<Node Name="Speed1" ParentNode="Motor1" NodeType="Variable" DataType="DataTypeIds.Float" Adress="MD12" >
</Node>
</Node>
 <!-- 目录-->
<Node Name="Motor2" ParentNode="Root" NodeType="Folder"   >
<!-- 变量-->
<Node Name="Start2" ParentNode="Motor2" NodeType="Variable" DataType="DataTypeIds.Boolean" Adress="M20.0" >
</Node>
<Node Name="Stop2" ParentNode="Motor2" NodeType="Variable" DataType="DataTypeIds.Boolean" Adress="M20.1" >
</Node>
<Node Name="Speed2" ParentNode="Motor2" NodeType="Variable" DataType="DataTypeIds.Float" Adress="MD22" >
</Node>
</Node>
 <!-- 目录-->
<Node Name="DBTest" ParentNode="Root" NodeType="Folder" >
<!-- 变量-->
<Node Name="Name" ParentNode="DBTest" NodeType="Variable" DataType="DataTypeIds.String" Adress="DB1.DBD0" >
</Node>
<Node Name="Age" ParentNode="DBTest" NodeType="Variable" DataType="DataTypeIds.Int16" Adress="DB1.DBW256" >
</Node>
<Node Name="High" ParentNode="DBTest" NodeType="Variable" DataType="DataTypeIds.Float" Adress="DB1.DBD258" >
</Node>
<Node Name="Good" ParentNode="DBTest" NodeType="Variable" DataType="DataTypeIds.Boolean" Adress="DB1.DBX262.0" >
</Node>
<Node Name="SintTest" ParentNode="DBTest" NodeType="Variable" DataType="DataTypeIds.Int16" Adress="DB1.DBW264" >
</Node>
<Node Name="CharTest" ParentNode="DBTest" NodeType="Variable" DataType="DataTypeIds.ByteString" Adress="DB1.DBB266" >
</Node>
</Node>
</Root>

程序里定义了一个类,对应xml文件的格式,这样方便在OPC网关里创建地址空间和变量

 //节点的类
        public class OpcuaNode
        {
            //节点名称
            public string NodeName { get; set; }
            //父节点
            public string ParentNode { get; set; }
            //节点数据类型
            public string DataType { get; set; }
            //节点类型
            public string NodeType { get; set; }
            //节点的值
            public string NodeValue { get; set; }
            //节点的地址
            public string Adress { get; set; }
        }

2…读取服务配置并启动服务实例

配置是OPC基金会的代码里自带的,除了服务地址外其它不作修改
修改服务地址、端口和登录安全要求
是否允许匿名登录
##变量监控、PLC变量的读取和写入(S7net开源代码)
判断变量的数据类型读取或者写入,字符串和实数等单独处理
S7net支持的主要数据类型

 public static byte[] SerializeValue(object value)
        {
            switch (value.GetType().Name)
            {
                case "Boolean":
                    return new[] { (byte)((bool)value ? 1 : 0) };
                case "Byte":
                    return Types.Byte.ToByteArray((byte)value);
                case "Int16":
                    return Types.Int.ToByteArray((Int16)value);
                case "UInt16":
                    return Types.Word.ToByteArray((UInt16)value);
                case "Int32":
                    return Types.DInt.ToByteArray((Int32)value);
                case "UInt32":
                    return Types.DWord.ToByteArray((UInt32)value);
                case "Single":
                    return Types.Real.ToByteArray((float)value);
                case "Double":
                    return Types.LReal.ToByteArray((double)value);
                case "DateTime":
                    return Types.DateTime.ToByteArray((System.DateTime)value);
                case "Byte[]":
                    return (byte[])value;
                case "Int16[]":
                    return Types.Int.ToByteArray((Int16[])value);
                case "UInt16[]":
                    return Types.Word.ToByteArray((UInt16[])value);
                case "Int32[]":
                    return Types.DInt.ToByteArray((Int32[])value);
                case "UInt32[]":
                    return Types.DWord.ToByteArray((UInt32[])value);
                case "Single[]":
                    return Types.Real.ToByteArray((float[])value);
                case "Double[]":
                    return Types.LReal.ToByteArray((double[])value);
                case "String":
                    // Hack: This is backwards compatible with the old code, but functionally it's broken
                    // if the consumer does not pay attention to string length.
                    var stringVal = (string)value;
                    return Types.String.ToByteArray(stringVal, stringVal.Length);
                case "DateTime[]":
                    return Types.DateTime.ToByteArray((System.DateTime[])value);
                case "DateTimeLong[]":
                    return Types.DateTimeLong.ToByteArray((System.DateTime[])value);
                default:
                    throw new InvalidVariableTypeException();
            }
        }

OPC UA主要的数据类型

-<Aliases>

<Alias Alias="Boolean">i=1</Alias>

<Alias Alias="SByte">i=2</Alias>

<Alias Alias="Byte">i=3</Alias>

<Alias Alias="Int16">i=4</Alias>

<Alias Alias="UInt16">i=5</Alias>

<Alias Alias="Int32">i=6</Alias>

<Alias Alias="UInt32">i=7</Alias>

<Alias Alias="Int64">i=8</Alias>

<Alias Alias="UInt64">i=9</Alias>

<Alias Alias="Float">i=10</Alias>

<Alias Alias="Double">i=11</Alias>

<Alias Alias="DateTime">i=13</Alias>

<Alias Alias="String">i=12</Alias>
</Aliases>

编程时注意数据格式要对应,上面的数据类型是常用的

具体代码

1.节点读取和创建

读取文件并加载到树形控件

//连接PLC
                connectPlc();
                //判断PLC连接状态
                if (lianjie == 1&&server==null)
                {
                    //清空节点列表
                    opcuaNodes.Clear();
                    //清空列表
                    treeView1.Nodes.Clear();
                    //加载文件
                    doc.Load(xmlpath);
                    //遍历节点
                    RecursionTreeControl1(doc.DocumentElement, treeView1.Nodes);
                    //展开视图
                    treeView1.ExpandAll();

遍历的目录,同时创建节点类并依次赋值

 //遍历目录
        private void RecursionTreeControl1(XmlNode xmlNode, TreeNodeCollection nodes)
        {
            foreach (XmlNode node in xmlNode.ChildNodes)//循环遍历当前元素的子元素集合
            {
                if (node.NodeType.ToString() != "Comment")
                {
                    OpcuaNode node1 = new OpcuaNode();
                    TreeNode new_child = new TreeNode();
                    new_child.Text = node.Attributes["Name"].Value;
                    node1.NodeName = node.Attributes["Name"].Value;
                    node1.ParentNode = node.Attributes["ParentNode"].Value;
                    node1.NodeType = node.Attributes["NodeType"].Value;
                    //node1.DataType=node.Attributes["DataType"].Value;
                    //MessageBox.Show(node.InnerText);
                    opcuaNodes.Add(node1);
                    nodes.Add(new_child);
                    RecursionTreeControl2(node, new_child.Nodes);//遍历子节点
                }
            }
        }

遍历变量节点

//遍历子节点
        private void RecursionTreeControl2(XmlNode xmlNode, TreeNodeCollection nodes)
        {
            foreach (XmlNode node in xmlNode.ChildNodes)
            {
                if (node.NodeType.ToString() != "Comment")
                {
                    OpcuaNode node1 = new OpcuaNode();
                    TreeNode new_child = new TreeNode();
                    new_child.Text = node.Attributes["Name"].Value;
                    node1.NodeName = node.Attributes["Name"].Value;
                    node1.ParentNode = node.Attributes["ParentNode"].Value;
                    node1.NodeType = node.Attributes["NodeType"].Value;
                    node1.DataType = node.Attributes["DataType"].Value;
                    node1.Adress = node.Attributes["Adress"].Value;
                    //MessageBox.Show(node.InnerText);
                    opcuaNodes.Add(node1);
                    nodes.Add(new_child);//向当前TreeNodeCollection集合中添加当前节点
                }
            }
        }

创建OPC UA地址空间和对应的变量节点

public override void CreateAddressSpace(IDictionary<NodeId, IList<IReference>> externalReferences)
        {
            lock (Lock)
            {
                    IList<IReference> references = null;

                    if (!externalReferences.TryGetValue(ObjectIds.ObjectsFolder, out references))
                    {
                        externalReferences[ObjectIds.ObjectsFolder] = references = new List<IReference>();
                    }
                    FolderState root = CreateFolder(null, "MyOPC", "MyOPC");
                    root.AddReference(ReferenceTypes.Organizes, true, ObjectIds.ObjectsFolder);
                    references.Add(new NodeStateReference(ReferenceTypes.Organizes, false, root.NodeId));
                    root.EventNotifier = EventNotifiers.SubscribeToEvents;
                    AddRootNotifier(root);
                List<BaseDataVariableState> variables = new List<BaseDataVariableState>();
                    FolderState simulationFolder = null;
                    try
                    {
                        #region Scalar_Simulation
                        NodeId nodetype = DataTypeIds.Boolean;
                        for (int i = 0; i < Form1.opcuaNodes.Count; i++)
                        {
                            if (Form1.opcuaNodes[i].NodeType == "Folder")
                            {
                                simulationFolder = CreateFolder(root, Form1.opcuaNodes[i].NodeName, Form1.opcuaNodes[i].NodeName);
                            }
                            else
                            {
                                switch (Form1.opcuaNodes[i].DataType)
                                {
                                    case "DataTypeIds.Boolean":
                                        nodetype = DataTypeIds.Boolean;
                                        break;
                                    case "DataTypeIds.String":
                                        nodetype = DataTypeIds.String;
                                        break;
                                    case "DataTypeIds.Double":
                                        nodetype = DataTypeIds.Double;
                                        break;
                                    case "DataTypeIds.Float":
                                        nodetype = DataTypeIds.Float;
                                        break;
                                    case "DataTypeIds.Int16":
                                        nodetype = DataTypeIds.Int16;
                                        break;
                                    case "DataTypeIds.Int32":
                                        nodetype = DataTypeIds.Int32;
                                        break;
                                    //case "DataTypeIds.Int64":
                                    //    nodetype = DataTypeIds.Int64;
                                    //    break;
                                    case "DataTypeIds.UInt16":
                                        nodetype = DataTypeIds.UInt16;
                                        break;
                                    case "DataTypeIds.UInt32":
                                        nodetype = DataTypeIds.UInt32;
                                        break;
                                    //case "DataTypeIds.UInt64":
                                    //    nodetype = DataTypeIds.UInt64;
                                    //    break;
                                    case "DataTypeIds.ByteString":
                                        nodetype = DataTypeIds.ByteString;
                                        break;
                                    case "DataTypeIds.Byte":
                                        nodetype = DataTypeIds.Byte;
                                        break;
                                    case "DataTypeIds.DateTime":
                                        nodetype = DataTypeIds.DateTime;
                                        break;
                                        //case "DataTypeIds.Integer":
                                        //    nodetype = DataTypeIds.Integer;
                                        //    break;
                                        //case "DataTypeIds.UInteger":
                                        //    nodetype = DataTypeIds.UInteger;
                                        //    break;
                                        //case "DataTypeIds.Number":
                                        //    nodetype = DataTypeIds.Number;
                                        //    break;

                                }

                                CreateDynamicVariable(simulationFolder, Form1.opcuaNodes[i].NodeName, Form1.opcuaNodes[i].NodeName, nodetype, ValueRanks.Scalar, Form1.opcuaNodes[i].Adress);
                            }
                        }

                        #endregion
                    }
                    catch (Exception e)
                    {
                        Utils.LogError(e, "Error creating the ReferenceNodeManager address space.");
                    }
                AddPredefinedNode(SystemContext, root);
               //开启定时器,读PLC
                m_simulationTimer = new Timer(DoSimulation, null, 1000, 1000);
            }
        }

2.读取服务配置,启动服务

服务初始化

 //声明应用实例
        ApplicationInstance application;
        //声明服务
        ServerBase server;
        //声明服务配置
        ApplicationConfiguration config;
        private void UaServerInit()
        {
            try
            {
                // Initialize the user interface.
                Application.EnableVisualStyles();
                ApplicationInstance.MessageDlg = new ApplicationMessageDlg();
                application = new ApplicationInstance();
                application.ApplicationType = ApplicationType.Server;
                application.ConfigSectionName = "MyOPC.UA.Server";
                //读取服务配置
                config = application.LoadApplicationConfiguration(false).Result;
                //读取服务地址和端口
                textBox1.Text = config.ServerConfiguration.BaseAddresses.ElementAt(1).ToString();

            }
            catch (Exception ex)
            {
                ExceptionDlg.Show(application.ApplicationName, ex);
            }
        }

启动服务

 // check the application certificate.
                    bool certOk = application.CheckApplicationInstanceCertificate(false, 0).Result;
                    if (!certOk)
                    {
                        throw new Exception("Application instance certificate invalid!");
                    }

                    // Create server, add additional node managers
                    //服务实例化
                    server = new ReferenceServer();
                    //启动服务
                    // start the server.
                    application.Start(server).Wait();

                    // check whether the invalid certificates dialog should be displayed.
                    bool showCertificateValidationDialog = false;
                    ReferenceServerConfiguration refServerconfiguration = application.ApplicationConfiguration.ParseExtension<ReferenceServerConfiguration>();

                    if (refServerconfiguration != null)
                    {
                        showCertificateValidationDialog = refServerconfiguration.ShowCertificateValidationDialog;
                    }

                    // run the application interactively.
                    //绑定证书验证事件
                    if (showCertificateValidationDialog &&
                        !application.ApplicationConfiguration.SecurityConfiguration.AutoAcceptUntrustedCertificates)
                    {
                        application.ApplicationConfiguration.CertificateValidator.CertificateValidation += new CertificateValidationEventHandler(CertificateValidator_CertificateValidation);
                    }

证书验证对话框

/// <summary>
        /// Handles a certificate validation error.
        /// </summary>
        void CertificateValidator_CertificateValidation(CertificateValidator validator, CertificateValidationEventArgs e)
        {
            try
            {
                HandleCertificateValidationError(this, validator, e);
            }
            catch (Exception exception)
            {
                HandleException(this.Text, MethodBase.GetCurrentMethod(), exception);
            }
        }
        /// <summary>
        /// Handles a certificate validation error.
        /// </summary>
        /// <param name="caller">The caller's text is used as the caption of the <see cref="MessageBox"/> shown to provide details about the error.</param>
        /// <param name="validator">The validator (not used).</param>
        /// <param name="e">The <see cref="Opc.Ua.CertificateValidationEventArgs"/> instance event arguments provided when a certificate validation error occurs.</param>
        public static void HandleCertificateValidationError(Form caller, CertificateValidator validator, CertificateValidationEventArgs e)
        {
            StringBuilder buffer = new StringBuilder();
            buffer.AppendLine("Certificate could not be validated!");
            buffer.AppendLine("Validation error(s):");
            ServiceResult error = e.Error;
            while (error != null)
            {
                buffer.AppendFormat("- {0}\r\n", error.ToString().Split('\r', '\n').FirstOrDefault());
                error = error.InnerResult;
            }
            buffer.AppendFormat("\r\nSubject: {0}\r\n", e.Certificate.Subject);
            buffer.AppendFormat("Issuer: {0}\r\n", X509Utils.CompareDistinguishedName(e.Certificate.Subject, e.Certificate.Issuer)
                ? "Self-signed" : e.Certificate.Issuer);
            buffer.AppendFormat("Valid From: {0}\r\n", e.Certificate.NotBefore);
            buffer.AppendFormat("Valid To: {0}\r\n", e.Certificate.NotAfter);
            buffer.AppendFormat("Thumbprint: {0}\r\n\r\n", e.Certificate.Thumbprint);
            buffer.Append("Security certificate problems may indicate an attempt to intercept any data you send ");
            buffer.Append("to a server or to allow an untrusted client to connect to your server.");
            buffer.Append("\r\n\r\nAccept anyway?");

            if (MessageBox.Show(buffer.ToString(), caller.Text, MessageBoxButtons.YesNo) == DialogResult.Yes)
            {
                e.AcceptAll = true;
            }
        }

3. 监控变量(PLC变量的读取和写入)

1.定时读取

CreateAddressSpace 方法里开个定时器,代码见上面
m_simulationTimer = new Timer(DoSimulation, null, 1000, 1000);

//定时读取PLC并更新数据
        private void DoSimulation(object state)
        {
            try
            {
                lock (Lock)
                {
                    var timeStamp = DateTime.UtcNow;
                    foreach (BaseDataVariableState variable in m_dynamicNodes)
                    {
                        variable.Value = GetNewValue(variable);
                        variable.Timestamp = timeStamp;
                        variable.ClearChangeMasks(SystemContext, false);
                    }
                }
            }
            catch (Exception e)
            {
                Utils.LogError(e, "Unexpected error doing simulation.");
            }
        }

读取的具体代码

  //读取PLC
        private object GetNewValue(BaseVariableState variable)
        {
            object value = null;
                //判断变量地址是否为空
                if (variable.Description.Text.Length > 0)
                {
                    //实数处理
                    //if (variable.DataType == DataTypeIds.Float)
                    if (variable.DataType == DataTypeIds.Float || variable.DataType == DataTypeIds.Double)

                    {
                        value = Form1.plc.Read(variable.Description.Text);
                        var buffer = new byte[4];
                        buffer[3] = (byte)((uint)value >> 24);
                        buffer[2] = (byte)((uint)value >> 16);
                        buffer[1] = (byte)((uint)value >> 8);
                        buffer[0] = (byte)((uint)value >> 0);
                        value = (BitConverter.ToSingle(buffer, 0)).ToString();
                        return value;
                    }
                    //字符串处理
                    //if (variable.DataType == DataTypeIds.Float)
                    else if (variable.DataType == DataTypeIds.String)

                    {
                        int tt = variable.Description.Text.IndexOf(".");
                        int  dbnum = int.Parse(variable.Description.Text.Substring(2,1));
                        int  dbcount = int.Parse(variable.Description.Text.Substring(tt+4));
                        var count = (byte)Form1.plc.Read(DataType.DataBlock, dbnum, dbcount, VarType.Byte, 1);
                        value = Form1.plc.Read(DataType.DataBlock, dbnum, dbcount + 1, VarType.String, count+1).ToString().Substring(1);
                        return value;
                    }
                    //字符处理
                     else if (variable.DataType == DataTypeIds.ByteString)
                    {
                        value = Form1.plc.Read(variable.Description.Text);
                        byte[] array = new byte[1];
                        array[0] = (byte)(Convert.ToInt32(value)); //ASCII码强制转换二进制
                        value = Convert.ToString(System.Text.Encoding.ASCII.GetString(array));//str为ASCII码对应的字符
                        return value;
                    }
                    else
                    {
                        //读取变量
                        value = Form1.plc.Read(variable.Description.Text);
                        return value;
                    }
                }
                else
                {
                    value = null;
                }
                // skip Variant Null
                if (value is Variant variant)
                {
                    if (variant.Value == null)
                    {
                        value = null;
                    }
                }
            return value;
        }

2.客户端写入监控

为了数据的安全性,防止误写入,程序加了写入开关
在这里插入图片描述

/// <summary>
        /// Creates a new variable.
        /// </summary>
        private BaseDataVariableState CreateVariable(NodeState parent, string path, string name, NodeId dataType, int valueRank)
        {
            BaseDataVariableState variable = new BaseDataVariableState(parent);
            variable.SymbolicName = name;
            variable.ReferenceTypeId = ReferenceTypes.Organizes;
            variable.TypeDefinitionId = VariableTypeIds.BaseDataVariableType;
            variable.NodeId = new NodeId(path, NamespaceIndex);
            variable.BrowseName = new QualifiedName(path, NamespaceIndex);
            variable.DisplayName = new LocalizedText("en", name);
            variable.WriteMask = AttributeWriteMask.DisplayName | AttributeWriteMask.Description;
            variable.UserWriteMask = AttributeWriteMask.DisplayName | AttributeWriteMask.Description;
            variable.DataType = dataType;
            variable.ValueRank = valueRank;
            variable.AccessLevel = AccessLevels.CurrentReadOrWrite;
            variable.UserAccessLevel = AccessLevels.CurrentReadOrWrite;
            variable.Historizing = false;
            //variable.Value = GetNewValue(variable);
            variable.StatusCode = StatusCodes.Good;
            variable.Timestamp = DateTime.UtcNow;
            //绑定客户端写入事件
            variable.OnWriteValue = ToWritePLC;
            if (valueRank == ValueRanks.OneDimension)
            {
                variable.ArrayDimensions = new ReadOnlyList<uint>(new List<uint> { 0 });
            }
            else if (valueRank == ValueRanks.TwoDimensions)
            {
                variable.ArrayDimensions = new ReadOnlyList<uint>(new List<uint> { 0, 0 });
            }

            if (parent != null)
            {
                parent.AddChild(variable);
            }

            return variable;
        }

写入的具体代码

//写入PLC
        private ServiceResult ToWritePLC(
            ISystemContext context,
            NodeState node,
            NumericRange indexRange,
            QualifiedName dataEncoding,
            ref object value,
            ref StatusCode statusCode,
            ref DateTime timestamp)
        {
            if (!Form1.WriteProtect)
            {
                BaseVariableState variable = node as BaseVariableState;
                switch (variable.DataType.ToString())
                {
                    case "i=1"://Boolean
                        Form1.plc.Write(variable.Description.Text, value);
                        break;
                    case "i=12"://String
                        int tt = variable.Description.Text.IndexOf(".");
                        int dbnum = int.Parse(variable.Description.Text.Substring(2, 1));
                        int dbcount = int.Parse(variable.Description.Text.Substring(tt + 4));
                        var temp = Encoding.ASCII.GetBytes(value.ToString());
                        var bytes = S7.Net.Types.S7String.ToByteArray(value.ToString(), temp.Length);
                        Form1.plc.WriteBytes(DataType.DataBlock, dbnum, dbcount, bytes);
                        break;
                    case "i=11"://Double
                        Form1.plc.Write(variable.Description.Text, Convert.ToDouble(value));
                        break;
                    case "i=10"://Float
                        Form1.plc.Write(variable.Description.Text, Convert.ToSingle(value));
                        break;
                    case "i=4"://Int16
                        Form1.plc.Write(variable.Description.Text, Convert.ToInt16(value));
                        break;
                    case "i=6"://Int32
                        Form1.plc.Write(variable.Description.Text, Convert.ToInt32(value));
                        break;
                    //case "i=8"://Int64
                    //    Form1.plc.Write(variable.Description.Text, Convert.ToInt64(value));
                    //    break;
                    case "i=5"://UInt16
                        Form1.plc.Write(variable.Description.Text, Convert.ToUInt16(value));
                        break;
                    case "i=7"://UInt32
                        Form1.plc.Write(variable.Description.Text, Convert.ToUInt32(value));
                        break;
                    //case "i=9"://UInt64
                    //    Form1.plc.Write(variable.Description.Text, Convert.ToUInt64(value));
                    //    break;
                    case "i=15"://ByteString
                        var temp2 = Encoding.ASCII.GetBytes(value.ToString());
                        Form1.plc.Write(variable.Description.Text, temp2);
                        break;
                    case "i=3"://Byte
                        Form1.plc.Write(variable.Description.Text, Convert.ToByte(value));
                        break;
                    case "i=13"://DateTime
                        Form1.plc.Write(variable.Description.Text, Convert.ToDateTime(value));
                        break;
                    case "i=27"://Integer
                        Form1.plc.Write(variable.Description.Text, Convert.ToInt32(value));
                        break;
                    case "i=28"://UInteger
                        Form1.plc.Write(variable.Description.Text, Convert.ToInt32(value));
                        break;
                        //case "i=26"://Number
                        //    Form1.plc.Write(variable.Description.Text, value);
                        //break;
                }
            }
            return ServiceResult.Good;
        }
      

注意数据类型,一定不要搞错,否则写入的值会越界

运行调试

虚拟机采用S7-PLCSIM Advanced V4.0 SP1,IP设置很重要,否则连接不上
见图片
在这里插入图片描述
在这里插入图片描述
然后在虚拟机里运行仿真器,编写简单程序,下载监控运行,在本机运行OPC UA网关,打开OPC客户端监控或者修改PLC变量,测试正常!
在这里插入图片描述
在这里插入图片描述
源码下载地址:https://gitee.com/chenzunzhi/opcua

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值