摘要:C# WPF基于 Opc.UaFx.Client 的客户端 订阅KEPServerEx6 服务器节点数据
主界面
C# WPF基于 Opc.UaFx.Client 的客户端
操作视频演示
代码:
MainView.xaml
<Window x:Class="Client.Wpf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Client.Wpf"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:MainViewModel Address="opc.tcp://localhost:49320/" NodeId="ns=2;s=_AdvancedTags.GroupCxy1.0002" />
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="14*" />
<ColumnDefinition Width="Auto" MinWidth="131" />
<ColumnDefinition Width="Auto" MinWidth="419" />
<ColumnDefinition Width="Auto" MinWidth="95" />
<ColumnDefinition Width="17*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="2" />
<RowDefinition Height="Auto" />
<RowDefinition Height="20" />
<RowDefinition Height="Auto" />
<RowDefinition Height="2" />
<RowDefinition Height="Auto" />
<RowDefinition Height="20" />
<RowDefinition Height="Auto" />
<RowDefinition Height="2*" />
</Grid.RowDefinitions>
<Label Content="Address of Server:"
HorizontalAlignment="Center"
Grid.Column="1" Margin="0,88,0,0" Grid.RowSpan="2" Width="114" />
<TextBox Grid.Column="2"
Grid.Row="1"
Margin="2,2,2,2"
Text="{Binding Address, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Label Content="{Binding AddressStatus}"
Grid.Column="2"
Grid.Row="2"
MaxWidth="400" Margin="2,2,94,20" Grid.RowSpan="3" Grid.ColumnSpan="2" />
<Label Content="Node to Subscribe:"
HorizontalAlignment="Left"
Grid.Column="1"
Grid.Row="4" Margin="11,32,0,2" Grid.RowSpan="3" Width="120" />
<TextBox Grid.Column="2"
Grid.Row="5"
Margin="2,2,2,2"
Text="{Binding NodeId, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Button Content="_Subscribe"
Grid.Column="3"
Grid.Row="5"
MinWidth="80"
Margin="2,2,2,2"
Command="{Binding SubscribeCommand}" />
<Label Content="{Binding NodeStatus}"
Grid.Column="2"
Grid.Row="7"
MaxWidth="400" Margin="2,0,94,24" Grid.RowSpan="2" Grid.ColumnSpan="2" />
<Label Content="Node Value:"
HorizontalAlignment="Center"
Grid.Column="1"
Grid.Row="8" Margin="0,47,0,170" Grid.RowSpan="3" Width="80" />
<TextBox Grid.Column="2"
Grid.Row="9"
Margin="2,2,2,2"
IsReadOnly="True"
Text="{Binding NodeValue, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
</Window>
MainViewModel.cs
namespace Client.Wpf
{
using System;
using Opc.UaFx;
using Opc.UaFx.Client;
public class MainViewModel : ViewModel//主视图模型
{
#region ---------- Private fields ----------
private Uri address;//服务器地址
private string addressStatus;//地址状态
private OpcClient client;//客户端
private OpcSubscription subscription;//订阅
private OpcNodeId nodeId;
private string nodeStatus;
private object nodeValue;
#endregion
#region ---------- Public constructors ----------
public MainViewModel()
: base()
{
this.address = new Uri("opc.tcp://localhost:49320");
this.addressStatus = "Enter address of server.";
this.nodeId = "ns=2;s=_AdvancedTags.GroupCxy1.0002";
this.nodeStatus = "Enter a node identifier and click 'Subscribe'.";
//声明委托命令: 执行函数,执行条件(地址有效,节点id有效)
this.SubscribeCommand = new DelegateCommand(
execute: (_) => this.Subscribe(),
canExecute: (_) => this.AddressIsValid && this.NodeIdIsValid);
}
#endregion
#region ---------- Public properties ----------
//地址属性
public string Address
{
get => this.address?.ToString();
set
{//修改地址 触发属性改变中事件
this.RaisePropertyChanging(nameof(this.Address));
this.RaisePropertyChanging(nameof(this.AddressIsValid));
if (Uri.TryCreate(value, UriKind.Absolute, out var result)
&& (result.Scheme == "opc.tcp" || result.Scheme == "https")) {
this.subscription?.Unsubscribe();//当前订阅存在则取消订阅
this.client?.Disconnect();//当前客户端存在则断开连接
this.client = null;//客户端置为空
this.address = result;//更新地址
//属性改变完毕事件
this.RaisePropertyChanged(nameof(this.Address));
this.RaisePropertyChanged(nameof(this.AddressIsValid));
this.SubscribeCommand.RaiseCanExecuteChanged();//执行条件变化
}
else {
this.AddressStatus = "Invalid address uri.";//地址状态
}
}
}
//地址有效属性
public bool AddressIsValid//地址非空 true
{
get => this.address != null;
}
//地址状态属性
public string AddressStatus
{
get => this.addressStatus;
set
{
this.RaisePropertyChanging(nameof(this.AddressStatus));//属性改变中
this.addressStatus = value;
this.RaisePropertyChanged(nameof(this.AddressStatus));
}
}
//节点id属性
public string NodeId
{
get => this.nodeId?.ToString();
set
{//设置前后都触发了属性改变事件
this.RaisePropertyChanging(nameof(this.NodeId));
this.RaisePropertyChanging(nameof(this.NodeIdIsValid));
if (OpcNodeId.TryParse(value, out var result)) {
this.nodeId = result;
this.RaisePropertyChanged(nameof(this.NodeId));
this.RaisePropertyChanged(nameof(this.NodeIdIsValid));
this.SubscribeCommand.RaiseCanExecuteChanged();
}
else {
this.NodeStatus = "Invalid node identifier.";
}
}
}
//节点有效属性
public bool NodeIdIsValid
{
get => this.nodeId != null && this.nodeId != OpcNodeId.Null;
}
//节点状态属性
public string NodeStatus
{
get => this.nodeStatus;
set
{
this.RaisePropertyChanging(nameof(this.NodeStatus));
this.nodeStatus = value;
this.RaisePropertyChanged(nameof(this.NodeStatus));
}
}
//节点值属性
public object NodeValue
{
get => this.nodeValue;
set
{
this.RaisePropertyChanging(nameof(this.NodeValue));
this.nodeValue = value;
this.RaisePropertyChanged(nameof(this.NodeValue));
}
}
//订阅命令
public DelegateCommand SubscribeCommand
{
get;
}
#endregion
#region ---------- Public methods ----------
//订阅节点,绑定数据改变回调函数
public void Subscribe()
{
try {
if (this.client == null) {
this.client = new OpcClient(this.address);
this.client.Connect();//连接服务器
}
if (this.subscription != null)
this.subscription.Unsubscribe();//取消之前的订阅
this.subscription = this.client.SubscribeDataChange(
this.nodeId,
this.HandleDataChangeReceived);//订阅数据改变,回调函数HandleDataChangeReceived
var monitoredItem = this.subscription.MonitoredItems[0];//监视项
var monitoredItemStatus = monitoredItem.Status;//监视项状态
if (monitoredItemStatus.Error != null)
this.NodeStatus = monitoredItemStatus.Error.Description;//节点状态-错误
}
catch (Exception ex) {
this.NodeStatus = ex.Message;//节点异常状态
}
}
#endregion
#region ---------- Private methods ----------
//处理接收到的数据改变
private void HandleDataChangeReceived(object sender, OpcDataChangeReceivedEventArgs e)
{
var value = e.Item.Value;
this.NodeStatus = value.Status.Description;//节点状态
this.NodeValue = $"{value.Value} ({e.MonitoredItem.NodeId})";//节点值
}
#endregion
}
}
基类ViewModel.cs
namespace Client.Wpf
{
using System.ComponentModel;
//所有视图模型基类: 属性已改变接口,属性改变中接口
public abstract class ViewModel : INotifyPropertyChanged, INotifyPropertyChanging
{
#region ---------- Protected constructors ----------
protected ViewModel()
: base()
{
}
#endregion
#region ----------公有事件 ----------
public event PropertyChangedEventHandler PropertyChanged;//属性已盖板事件处理器
public event PropertyChangingEventHandler PropertyChanging;//属性改变中事件处理器
#endregion
#region ---------- Protected methods ----------
//属性已改变方法
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
this.PropertyChanged?.Invoke(this, e);
}
//属性改变中方法
protected virtual void OnPropertyChanging(PropertyChangingEventArgs e)
{
this.PropertyChanging?.Invoke(this, e);
}
//触发属性已改变事件
protected void RaisePropertyChanged(string propertyName)
{
this.OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
//触发属性改变中事件
protected void RaisePropertyChanging(string propertyName)
{
this.OnPropertyChanging(new PropertyChangingEventArgs(propertyName));
}
#endregion
}
}
自定义命令DelegateCommand.cs
namespace Client.Wpf
{
using System;
using System.Windows.Input;
public class DelegateCommand : ICommand//定义一个委托命令
{
#region ---------- Private readonly fields ----------
private readonly Func<object, bool> canExecute;//函数:执行条件
private readonly Action<object> execute;//
#endregion
#region ---------- Public constructors ----------
public DelegateCommand(Action<object> execute)
: base()
{
this.execute = execute;
}
public DelegateCommand(Action<object> execute, Func<object, bool> canExecute)
: this(execute)
{
this.canExecute = canExecute;
}
#endregion
#region ---------- Public events ----------
public event EventHandler CanExecuteChanged;//表示将用于处理不具有事件数据的事件的方法。 执行条件变化
#endregion
#region ---------- Public methods ----------
public bool CanExecute(object parameter)
{
return this.canExecute(parameter);//
}
public void Execute(object parameter)
{
this.execute(parameter);
}
public void RaiseCanExecuteChanged()
{
this.CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
#endregion
}
}
参考:
GitHub - DzungV/Opc.UaFx.ServerSamples_Fixed
C# 读写opc ua服务器,浏览所有节点,读写节点,读历史数据,调用方法,订阅,批量订阅操作 - 开发者知识库
附录:uafx访问服务器自定义数据类型节点
namespace DataTypeNode
{
using System;
using Opc.UaFx;
using Opc.UaFx.Client;
/// <summary>
/// This sample demonstrates how to access and work with nodes which use custom data types.
/// 此示例演示如何访问和使用自定义数据类型的节点。
/// </summary>
public class Program
{
#region ---------- 公共静态方法----------
public static void Main(string[] args)
{
If the server domain name does not match localhost just replace it
e.g. with the IP address or name of the server machine.
///如果服务器域名与localhost不匹配,只需替换它
例如,使用服务器机器的IP地址或名称。
var client = new OpcClient("opc.tcp://localhost:49320");
client.Connect();
var statusNode = client.BrowseNode("ns=2;s=Machine_1/Status") as OpcVariableNodeInfo;
if (statusNode != null) {
var statusValues = statusNode.DataType.GetEnumMembers();
var currentStatus = client.ReadNode(statusNode.NodeId);
var currentStatusValue = null as OpcEnumMember;
foreach (var statusValue in statusValues) {
if (statusValue.Value == currentStatus.As<int>()) {
currentStatusValue = statusValue;
break;
}
}
Console.WriteLine(
"Status: {0} ({1})",
currentStatusValue.Value,
currentStatusValue.Name);
Console.WriteLine("-> " + currentStatusValue.Description);
Console.WriteLine();
Console.WriteLine("Possible status values...");
foreach (var statusValue in statusValues)
Console.WriteLine("{0} = {1}", statusValue.Value, statusValue.Name);
Console.Write("Enter new status: ");
var value = Console.ReadLine();
var newStatus = 0;
if (int.TryParse(value, out newStatus))
client.WriteNode(statusNode.NodeId, newStatus);
currentStatus = client.ReadNode(statusNode.NodeId);
foreach (var statusValue in statusValues) {
if (statusValue.Value == currentStatus.As<int>()) {
currentStatusValue = statusValue;
break;
}
}
Console.WriteLine();
Console.WriteLine(
"New Status: {0} ({1})",
currentStatusValue.Value,
currentStatusValue.Name);
Console.WriteLine("-> " + currentStatusValue.Description);
}
client.Disconnect();
Console.ReadKey(true);
}
#endregion
}
}
usfx访问模拟项节点
namespace AnalogItemNode
{
using System;
using Opc.UaFx.Client;
/// <summary>
/// This sample demonstrates how to access and work with nodes of the AnalogItemType.
/// 这个示例演示了如何访问和使用模拟itemtype的节点。
/// </summary>
public class Program
{
#region ---------- Public static methods ----------
public static void Main(string[] args)
{
If the server domain name does not match localhost just replace it
e.g. with the IP address or name of the server machine.
///如果服务器域名与localhost不匹配,只需替换它
例如,使用服务器机器的IP地址或名称。
var client = new OpcClient("opc.tcp://localhost:49320/");
client.Connect();
// The mapping of the UNECE codes to OPC UA (OpcEngineeringUnitInfo.UnitId) is available here:
//UNECE代码到OPC UA(OpcEngineeringUnitInfo.UnitId)的映射在这里
// http://www.opcfoundation.org/UA/EngineeringUnits/UNECE/UNECE_to_OPCUA.csv
var temperatureNode = client.BrowseNode("ns=2;s=Machine_1/Temperature") as OpcAnalogItemNodeInfo;//温度节点 模拟项节点
if (temperatureNode != null) {
var temperatureUnit = temperatureNode.EngineeringUnit;
var temperatureRange = temperatureNode.EngineeringUnitRange;
var temperature = client.ReadNode(temperatureNode.NodeId);//温度
Console.WriteLine(
"Temperature: {0} {1}, Range: {3} {1} to {4} {1} ({2})",
temperature.Value,
temperatureUnit.DisplayName,
temperatureUnit.Description,
temperatureRange.Low,
temperatureRange.High);
}
var pressureNode = client.BrowseNode("ns=2;s=Machine_1/Pressure") as OpcAnalogItemNodeInfo;//压力
if (pressureNode != null) {
var pressureUnit = pressureNode.EngineeringUnit;
var pressureInstrumentRange = pressureNode.InstrumentRange;
var pressure = client.ReadNode(pressureNode.NodeId);
Console.WriteLine(
"Pressure: {0} {1}, Range: {3} {1} to {4} {1} ({2})",
pressure.Value,
pressureUnit.DisplayName,
pressureUnit.Description,
pressureInstrumentRange.Low,
pressureInstrumentRange.High);
}
client.Disconnect();
Console.ReadKey(true);
}
#endregion
}
}
在代码配置中配置OPC UA客户端
namespace ConfiguredViaCode
{
using System;
using System.IO;
using Opc.UaFx;
using Opc.UaFx.Client;
/// <summary>
/// This sample demonstrates how to configure an OPC UA client using in code configuration.
/// 本示例演示如何在代码配置中配置OPC UA客户端。
/// </summary>
public class Program
{
#region ---------- Public static methods ----------
public static void Main(string[] args)
{
To simple use the in code configuration you just need to configure your client
instance using the Configuration property of it.
By default it is not necessary to explicitly configure an OPC UA client. But in case
of advanced and productive scenarios you will have to.
为了简单地使用代码内配置,您只需要使用它的configuration属性配置您的客户端实例。
默认情况下,不需要显式配置OPC UA客户端。但在高级和高效的情况下,你必须这样做。
var client = new OpcClient("opc.tcp://localhost:4840/SampleServer");
有不同的方法来配置客户端实例。
//第一种方法:使用默认配置,它使用来自环境的信息来设置客户端。要使用默认配置,要么不更改客户端实例的configuration属性,要么将其值设置为null(在Visual Basic中为Nothing)。
client.Configuration = null;
//第二种方法:实例化一个默认配置,操作它,并设置这些实例为客户端使用的新配置。
var configuration = new OpcApplicationConfiguration(OpcApplicationType.Client);
configuration.ClientConfiguration.DefaultSessionTimeout = 300000; // 5 Minutes
// ApplicationName
//
// Using the default configuration or a new instance of the OpcApplicationConfiguration
// means, that the client does use the value specified in the AssemblyTitleAttribute
// of your executable as the ApplicationName. This value should maybe changed to the
// ApplicationName specified in your client certificate. To do that just use a line of
// code like the following one.
//使用默认配置或OpcApplicationConfiguration的新实例意味着,
//客户端使用在可执行文件的AssemblyTitleAttribute中指定的值作为ApplicationName。
//这个值可能应该更改为客户端证书中指定的ApplicationName。要做到这一点,只需使用如下一行代码。
client.Configuration.ApplicationName = "My Application Name";
// Certificate Stores
//
// Using the default configuration or a new instance of the OpcApplicationConfiguration
// means, that the client does use the path
// "%CommonApplicationData%\OPC Foundation\CertificateStores\" as the root directory of
// - the "MachineDefault" application certificates store
// - the "RejectedCertificates" certificates store
// - the "UA Certificate Authorities" trusted issuer certificates store
// - the "UA Applications" trusted peer certificates store.
// While the first directory "%CommonApplicationData%" does point to (on a default
// windows installation to the C: drive) the path "C:\ProgramData" it is obvious that
// this directories are accessable by any user of the system and that the certificate
// stores are not in the same directory as your client application.
// In some scenarios it would be necessary to change the location of the used
// certificate store directories like the following code does demonstrate.
//证书存储
//使用默认配置或OpcApplicationConfiguration的新实例
//意味着,客户端确实使用路径“%CommonApplicationData%\OPC Foundation\CertificateStores\”作为根目录
// - "MachineDefault"应用程序证书存储
// - "RejectedCertificates"证书存储
// -“UA证书颁发机构”受信任的颁发者证书存储
// -“UA应用程序”可信对等证书存储。
//虽然第一个目录“%CommonApplicationData%”确实指向(在默认的windows安装中指向C:驱动器)路径“C:\ProgramData”,
//但很明显,该目录可由系统的任何用户访问,证书存储与您的客户端应用程序不在同一个目录中。
//在某些场景中,有必要更改所使用的证书存储目录的位置,如下面的代码所演示的那样。
var securityConfiguration = client.Configuration.SecurityConfiguration;
securityConfiguration.ApplicationCertificate.StorePath
= Path.Combine(Environment.CurrentDirectory, "App Certificates");
securityConfiguration.RejectedCertificateStore.StorePath
= Path.Combine(Environment.CurrentDirectory, "Rejected Certificates");
securityConfiguration.TrustedIssuerCertificates.StorePath
= Path.Combine(Environment.CurrentDirectory, "Trusted Issuer Certificates");
securityConfiguration.TrustedIssuerCertificates.StorePath
= Path.Combine(Environment.CurrentDirectory, "Trusted Peer Certificates");
// In case you want to take use of one of the SpecialFolders defined by the
// Environment.SpecialFolder enumeration you can use the enumeration members name as
// a placeholder in your custom StorePath like the following code does demonstrate.
//如果您想使用由环境定义的SpecialFolders之一。SpecialFolder枚举,
//您可以使用枚举成员名作为自定义StorePath中的占位符,如下面的代码所演示的那样。
securityConfiguration.ApplicationCertificate.StorePath
= @"%LocalApplicationData%\My Application\App Certificates";
securityConfiguration.RejectedCertificateStore.StorePath
= @"%LocalApplicationData%\My Application\Rejected Certificates";
securityConfiguration.TrustedIssuerCertificates.StorePath
= @"%LocalApplicationData%\My Application\Trusted Issuer Certificates";
securityConfiguration.TrustedPeerCertificates.StorePath
= @"%LocalApplicationData%\My Application\Trusted Peer Certificates";
It is not necessary that all certificate stores have to point to the same root
directory as above. Each store can also point to a totally different directory.
并非所有证书存储都必须指向如上所述的同一根目录。每个存储也可以指向完全不同的目录。
client.Configuration = configuration;
// 3rd Way: Directly change the default configuration of the client instance using the
// Configuration property.
//第三种方法:使用configuration属性直接更改客户端实例的默认配置。
client.Configuration.ClientConfiguration.DefaultSessionTimeout = 300000; // 5 Minutes
client.Connect();
client.Disconnect();
// In case you are using the OpcClientApplication class, you can directly configure
// your client/application using the Configuration property of the application instance
// as the following code does demonstrate.
//如果您正在使用OpcClientApplication类,您可以使用应用程序实例的Configuration属性直接配置您的客户机/应用程序,如下面的代码所演示的那样。
var app = new OpcClientApplication("opc.tcp://localhost:4840/SampleServer");
app.Configuration.ClientConfiguration.DefaultSessionTimeout = 300000; // 5 Minutes
app.Run();
}
#endregion
}
}
使用XML配置文件配置OPC UA客户端
namespace ConfiguredViaXml
{
using System;
using System.IO;
using Opc.UaFx;
using Opc.UaFx.Client;
/// <summary>
/// This sample demonstrates how to configure an OPC UA client using a XML configuration file.
/// 本示例演示如何使用XML配置文件配置OPC UA客户端。
/// </summary>
public class Program
{
#region ---------- 公共静态方法 ----------
public static void Main(string[] args)
{
To simple use the configuration stored within the XML configuration file
beside the client application you just need to load the configuration file as the
following code does demonstrate.
By default it is not necessary to explicitly configure an OPC UA client. But in case
of advanced and productive scenarios you will have to.
// There are different ways to load the client configuration.
为了简单地使用存储在客户机应用程序旁边的XML配置文件中的配置,您只需要加载配置文件,如下面的代码所示。
默认情况下,不需要显式配置OPC UA客户端。但在高级和高效的情况下,你必须这样做。
//加载客户端配置有不同的方法。
OpcApplicationConfiguration configuration = null;
// 第一种方法:使用文件路径加载客户端配置。
configuration = OpcApplicationConfiguration.LoadClientConfigFile(
Path.Combine(Environment.CurrentDirectory, "ClientConfig.xml"));
//第二种方法:加载App.config中特定部分指定的客户端配置。
configuration = OpcApplicationConfiguration.LoadClientConfig("Opc.UaFx.Client");
// 如果客户端uri域名与localhost不匹配,只需替换它
//例如,使用客户端机器的IP地址或名称。
var client = new OpcClient("opc.tcp://localhost:4840/SampleClient");
// To take use of the loaded client configuration, just set it on the client instance.
//要使用加载的客户端配置,只需在客户端实例上设置它。
client.Configuration = configuration;
client.Connect();
client.Disconnect();
// In case you are using the OpcClientApplication class, you can explicitly trigger
// loading a configuration file using the App.config as the following code does
// demonstrate.
//如果您正在使用OpcClientApplication类,您可以使用App.config显式触发加载配置文件,如下面的代码所示。
var app = new OpcClientApplication("opc.tcp://localhost:4840/SampleClient");
app.LoadConfiguration();
// Alternatively you can assign the manually loaded client configuration on the client
// instance used by the application instance, as the following code does demonstrate.
//或者,您可以在应用程序实例使用的客户机实例上分配手动加载的客户机配置,如下面的代码所示。
app.Client.Configuration = configuration;
app.Run();
}
#endregion
}
}