在工业4.0时代,设备间的通信变得越来越重要。你是否也遇到过这样的痛点:需要与各种工业设备通信,但每个设备的协议都不一样?传统的COM、Modbus等协议配置复杂,维护困难?今天我们就用C#来打造一个现代化的OPC UA客户端,让设备通信变得简单优雅!
本文将带你从架构设计到界面实现,完整构建一个生产级的OPC UA Bridge应用。无论你是工业软件开发者,还是想了解现代C# WPF开发最佳实践的程序员,这篇文章都会让你收获满满。
传统工业通信面临的三大痛点:
OPC UA作为新一代工业通信标准,完美解决了这些问题:
我们采用分层架构设计,确保代码的可维护性和可测试性:
Markdown📦 AppOPCUABridge ├── 📁 Models # 数据模型层 ├── 📁 Services # 业务服务层 ├── 📁 ViewModels # 视图模型层 ├── 📁 Views # 用户界面层 └── 📁 Converters # 数据转换器

首先定义我们的核心数据模型,这是整个系统的基础:
C#public class OPCUANode : INotifyPropertyChanged
{
private string _nodeId;
private string _displayName;
private object _value;
private DataQuality _quality;
private DateTime _lastUpdate;
public string NodeId
{
get => _nodeId;
set
{
_nodeId = value;
OnPropertyChanged(nameof(NodeId));
}
}
public string DisplayName
{
get => _displayName;
set
{
_displayName = value;
OnPropertyChanged(nameof(DisplayName));
}
}
public object Value
{
get => _value;
set
{
_value = value;
LastUpdate = DateTime.Now;
OnPropertyChanged(nameof(Value));
}
}
public DataQuality Quality
{
get => _quality;
set
{
_quality = value;
OnPropertyChanged(nameof(Quality));
}
}
public DateTime LastUpdate
{
get => _lastUpdate;
set
{
_lastUpdate = value;
OnPropertyChanged(nameof(LastUpdate));
}
}
public ObservableCollection<OPCUANode> Children { get; set; }
= new ObservableCollection<OPCUANode>();
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
设计亮点:
INotifyPropertyChanged确保UI实时更新Children集合数据质量是工业通信的关键,我们需要详细的质量信息:
C#public enum QualityStatus
{
Good, // 数据良好
Bad, // 数据异常
Uncertain, // 数据不确定
Unknown // 未知状态
}
public class DataQuality : INotifyPropertyChanged
{
private QualityStatus _status;
private string _description;
private DateTime _timestamp;
private double _confidenceLevel;
public QualityStatus Status
{
get => _status;
set
{
_status = value;
OnPropertyChanged(nameof(Status));
}
}
public string Description
{
get => _description;
set
{
_description = value;
OnPropertyChanged(nameof(Description));
}
}
public double ConfidenceLevel
{
get => _confidenceLevel;
set
{
// 限制在0-1之间
_confidenceLevel = Math.Max(0, Math.Min(1, value));
OnPropertyChanged(nameof(ConfidenceLevel));
}
}
}
良好的接口设计是可维护代码的基础:
C#public interface IOPCUABridge : IDisposable
{
// 基础连接管理
Task<bool> ConnectAsync(string endpoint);
Task DisconnectAsync();
bool IsConnected { get; }
// 节点浏览和数据读取
Task<List<NamespaceModel>> GetNamespacesAsync();
Task<List<OPCUANode>> BrowseNodesAsync(string namespaceUri, string parentNodeId = null);
// 单个和批量操作
Task<object> ReadValueAsync(string nodeId, string namespaceUri);
Task<T> ReadValueAsync<T>(string nodeId);
Task<List<object>> BatchReadAsync(string[] nodeIds);
Task<bool> WriteValueAsync(string nodeId, string namespaceUri, object value);
// 订阅监听
void AddMonitoredItem(string key, string nodeId);
void RemoveMonitoredItem(string key);
// 数据质量分析
Task<DataQuality> AnalyzeQualityAsync(OPCUANode node);
// 事件通知
event EventHandler<OPCUANode> NodeValueChanged;
}
设计原则:
C#using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AppOPCUABridge.Models;
using OpcUaHelper;
using Opc.Ua;
using Opc.Ua.Client;
namespace AppOPCUABridge.Services
{
public class OPCUABridgeImpl : IOPCUABridge
{
private OpcUaClient _client;
private readonly QualityAnalyzer _qualityAnalyzer;
private bool _isConnected = false;
public bool IsConnected => _isConnected;
public event EventHandler<OPCUANode> NodeValueChanged;
public OPCUABridgeImpl()
{
_qualityAnalyzer = new QualityAnalyzer();
}
public async Task<bool> ConnectAsync(string endpoint)
{
try
{
_client = new OpcUaClient();
_client.UserIdentity = new UserIdentity(new AnonymousIdentityToken());
_client.OpcStatusChange += OnOpcStatusChange;
await _client.ConnectServer(endpoint);
return _isConnected;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"连接失败: {ex.Message}");
_isConnected = false;
return false;
}
}
private void OnOpcStatusChange(object sender, OpcUaStatusEventArgs e)
{
_isConnected = !e.Error;
System.Diagnostics.Debug.WriteLine($"OPC UA连接状态: {(_isConnected ? "成功" : "失败")}");
}
// 详细日志和错误处理
public async Task<object> ReadValueAsync(string nodeId, string namespaceUri)
{
if (!IsConnected)
{
System.Diagnostics.Debug.WriteLine($"未连接,无法读取节点: {nodeId}");
return null;
}
return await Task.Run(() =>
{
try
{
System.Diagnostics.Debug.WriteLine($"开始读取节点: {nodeId}");
// 尝试直接使用字符串nodeId
var dataValue = _client.ReadNode(nodeId);
if (dataValue != null)
{
var value = dataValue.Value;
System.Diagnostics.Debug.WriteLine($"读取成功 - 节点: {nodeId}, 值: {value}, 类型: {value?.GetType().Name}");
return value;
}
else
{
System.Diagnostics.Debug.WriteLine($"读取返回null - 节点: {nodeId}");
return null;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"读取节点 {nodeId} 异常: {ex.Message}");
return null;
}
});
}
// 改进批量读取方法
public async Task<List<object>> BatchReadAsync(string[] nodeIds)
{
if (!IsConnected || nodeIds == null || nodeIds.Length == 0)
return new List<object>();
return await Task.Run(() =>
{
var results = new List<object>();
try
{
System.Diagnostics.Debug.WriteLine($"开始批量读取 {nodeIds.Length} 个节点");
// 逐个读取以获得更好的错误处理
foreach (var nodeId in nodeIds)
{
try
{
var dataValue = _client.ReadNode(nodeId);
var value = dataValue?.Value;
results.Add(value);
System.Diagnostics.Debug.WriteLine($"批量读取 - 节点: {nodeId}, 值: {value}");
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"批量读取节点 {nodeId} 失败: {ex.Message}");
results.Add(null);
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"批量读取整体失败: {ex.Message}");
}
return results;
});
}
private async Task<List<OPCUANode>> GetManufacturingNodes(string namespaceUri)
{
var nodes = new List<OPCUANode>();
var nodeDefinitions = new[]
{
("ns=2;s=Channel1.Device1.B1", "B1", "布尔变量1"),
("ns=2;s=Channel1.Device1.B2", "B2", "布尔变量2"),
("ns=2;s=Channel1.Device1.B3", "B3", "布尔变量3"),
("ns=2;s=Channel1.Device1.F1", "F1", "浮点变量1"),
("ns=2;s=Channel1.Device1.I1", "I1", "整数变量1"),
("ns=2;s=Channel1.Device1.L1", "L1", "长整数变量1"),
("ns=2;s=Channel1.Device1.S1", "S1", "字符串变量1"),
("ns=2;s=Channel1.Device1.S2", "S2", "字符串变量2")
};
foreach (var (nodeId, displayName, description) in nodeDefinitions)
{
var node = new OPCUANode
{
NodeId = nodeId,
DisplayName = displayName,
NamespaceUri = namespaceUri,
Quality = new DataQuality
{
Status = QualityStatus.Unknown,
Timestamp = DateTime.Now,
Description = description,
ConfidenceLevel = 0.0
}
};
// 立即读取值并分析质量
try
{
var value = await ReadValueAsync(nodeId, namespaceUri);
node.Value = value;
node.LastUpdate = DateTime.Now;
// 重新分析质量
node.Quality = await _qualityAnalyzer.AnalyzeAsync(node);
System.Diagnostics.Debug.WriteLine($"节点创建完成: {displayName} = {value}, 质量: {node.Quality.Status}");
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"创建节点 {displayName} 失败: {ex.Message}");
node.Quality.Status = QualityStatus.Bad;
node.Quality.Description = $"创建失败: {ex.Message}";
}
nodes.Add(node);
}
return nodes;
}
public async Task DisconnectAsync()
{
await Task.Run(() =>
{
try
{
if (_client != null)
{
_client.OpcStatusChange -= OnOpcStatusChange;
_client.Disconnect();
_client = null;
}
_isConnected = false;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"断开连接失败: {ex.Message}");
}
});
}
public async Task<List<NamespaceModel>> GetNamespacesAsync()
{
if (!IsConnected) return new List<NamespaceModel>();
return await Task.Run(() =>
{
try
{
var namespaces = new List<NamespaceModel>();
var session = _client.Session;
if (session?.NamespaceUris != null)
{
for (int i = 0; i < session.NamespaceUris.Count; i++)
{
var uri = session.NamespaceUris.GetString((uint)i);
if (!string.IsNullOrEmpty(uri))
{
namespaces.Add(new NamespaceModel
{
Index = i,
Uri = uri,
Description = GetNamespaceDescription(uri)
});
}
}
}
if (namespaces.Count == 0)
{
namespaces.AddRange(GetDefaultNamespaces());
}
return namespaces;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"获取命名空间失败: {ex.Message}");
return GetDefaultNamespaces();
}
});
}
private List<NamespaceModel> GetDefaultNamespaces()
{
return new List<NamespaceModel>
{
new NamespaceModel
{
Index = 0,
Uri = "http://opcfoundation.org/UA/",
Description = "OPC UA Standard Namespace"
},
new NamespaceModel
{
Index = 1,
Uri = "urn:localhost:OPCUA:SimulationServer",
Description = "Simulation Server Namespace"
},
new NamespaceModel
{
Index = 2,
Uri = "http://company.com/Manufacturing/",
Description = "Manufacturing Process Namespace"
}
};
}
private string GetNamespaceDescription(string uri)
{
return uri switch
{
"http://opcfoundation.org/UA/" => "OPC UA Standard Namespace",
"urn:localhost:OPCUA:SimulationServer" => "Simulation Server Namespace",
var s when s.Contains("Manufacturing") => "Manufacturing Process Namespace",
var s when s.Contains("OPCUA") => "OPC UA Application Namespace",
_ => $"Custom Namespace: {uri.Split('/').LastOrDefault()}"
};
}
public async Task<List<OPCUANode>> BrowseNodesAsync(string namespaceUri, string parentNodeId = null)
{
if (!IsConnected) return new List<OPCUANode>();
return await Task.Run(async () =>
{
try
{
var nodes = new List<OPCUANode>();
// 总是返回预定义的制造节点用于测试
nodes.AddRange(await GetManufacturingNodes(namespaceUri));
return nodes;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"浏览节点失败: {ex.Message}");
return new List<OPCUANode>();
}
});
}
public async Task<T> ReadValueAsync<T>(string nodeId)
{
var value = await ReadValueAsync(nodeId, null);
try
{
return value != null ? (T)Convert.ChangeType(value, typeof(T)) : default(T);
}
catch
{
return default(T);
}
}
public async Task<bool> WriteValueAsync(string nodeId, string namespaceUri, object value)
{
if (!IsConnected) return false;
return await Task.Run(() =>
{
try
{
return _client.WriteNode(nodeId, value);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"写入失败: {ex.Message}");
return false;
}
});
}
public async Task<bool> WriteValueAsync<T>(string nodeId, T value)
{
return await WriteValueAsync(nodeId, null, value);
}
public async Task<List<T>> BatchReadAsync<T>(string[] nodeIds)
{
var values = await BatchReadAsync(nodeIds);
return values.Select(v =>
{
try
{
return v != null ? (T)Convert.ChangeType(v, typeof(T)) : default(T);
}
catch
{
return default(T);
}
}).ToList();
}
public async Task<bool> BatchWriteAsync(string[] nodeIds, object[] values)
{
if (!IsConnected) return false;
return await Task.Run(() =>
{
try
{
return _client.WriteNodes(nodeIds, values);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"批量写入失败: {ex.Message}");
return false;
}
});
}
public void AddMonitoredItem(string key, string nodeId)
{
try
{
if (IsConnected)
{
_client.AddSubscription(key, nodeId, OnMonitorCallback);
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"添加监听失败: {ex.Message}");
}
}
public void RemoveMonitoredItem(string key)
{
try
{
if (IsConnected)
{
_client.RemoveSubscription(key);
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"移除监听失败: {ex.Message}");
}
}
private void OnMonitorCallback(string key, MonitoredItem monitoredItem, MonitoredItemNotificationEventArgs args)
{
try
{
var notification = args.NotificationValue as MonitoredItemNotification;
if (notification != null)
{
var node = new OPCUANode
{
NodeId = monitoredItem.StartNodeId.ToString(),
DisplayName = key,
Value = notification.Value?.WrappedValue.Value,
LastUpdate = DateTime.Now
};
Task.Run(async () =>
{
node.Quality = await _qualityAnalyzer.AnalyzeAsync(node);
NodeValueChanged?.Invoke(this, node);
});
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"监听回调失败: {ex.Message}");
}
}
public async Task<DataQuality> AnalyzeQualityAsync(OPCUANode node)
{
return await _qualityAnalyzer.AnalyzeAsync(node);
}
public void Dispose()
{
try
{
if (_client != null)
{
_client.OpcStatusChange -= OnOpcStatusChange;
_client.Disconnect();
_client = null;
}
_isConnected = false;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"释放资源失败: {ex.Message}");
}
}
}
}
实现要点:
首先实现一个通用的命令类:
C#public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Func<object, bool> _canExecute;
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public bool CanExecute(object parameter) => _canExecute?.Invoke(parameter) ?? true;
public void Execute(object parameter) => _execute(parameter);
}
C#public class MainViewModel : INotifyPropertyChanged
{
private readonly IOPCUABridge _opcuaBridge;
private string _serverEndpoint = "opc.tcp://127.0.0.1:49320";
private bool _isConnected;
private string _statusMessage = "未连接";
public ObservableCollection<NamespaceModel> Namespaces { get; set; }
= new ObservableCollection<NamespaceModel>();
public ObservableCollection<OPCUANode> Nodes { get; set; }
= new ObservableCollection<OPCUANode>();
// 数据质量统计 - 实时计算
public int GoodQualityCount => Nodes.Count(n => n.Quality?.Status == QualityStatus.Good);
public int BadQualityCount => Nodes.Count(n => n.Quality?.Status == QualityStatus.Bad);
// 命令定义
public ICommand ConnectCommand { get; }
public ICommand DisconnectCommand { get; }
public ICommand BatchReadCommand { get; }
public MainViewModel()
{
_opcuaBridge = new OPCUABridgeImpl();
// 初始化命令 - 注意条件判断
ConnectCommand = new RelayCommand(async _ => await ConnectAsync(), _ => !IsConnected);
DisconnectCommand = new RelayCommand(async _ => await DisconnectAsync(), _ => IsConnected);
BatchReadCommand = new RelayCommand(async _ => await BatchReadAsync(), _ => IsConnected);
// 订阅数据变化事件
_opcuaBridge.NodeValueChanged += OnNodeValueChanged;
Nodes.CollectionChanged += (s, e) => UpdateQualityStatistics();
}
private async Task BatchReadAsync()
{
if (!IsConnected) return;
try
{
StatusMessage = "正在批量读取数据...";
string[] nodeIds = {
"ns=2;s=Channel1.Device1.B1",
"ns=2;s=Channel1.Device1.F1",
"ns=2;s=Channel1.Device1.I1"
};
var values = await _opcuaBridge.BatchReadAsync(nodeIds);
// 更新UI中的节点数据
for (int i = 0; i < Math.Min(nodeIds.Length, values.Count); i++)
{
var node = Nodes.FirstOrDefault(n => n.NodeId == nodeIds[i]);
if (node != null)
{
node.Value = values[i];
await _opcuaBridge.AnalyzeQualityAsync(node);
}
}
StatusMessage = $"批量读取完成,更新了 {values.Count} 个节点";
UpdateQualityStatistics();
}
catch (Exception ex)
{
StatusMessage = $"批量读取失败: {ex.Message}";
}
}
private void OnNodeValueChanged(object sender, OPCUANode node)
{
// 确保在UI线程更新
Application.Current?.Dispatcher.Invoke(() =>
{
var existingNode = Nodes.FirstOrDefault(n => n.NodeId == node.NodeId);
if (existingNode != null)
{
existingNode.Value = node.Value;
existingNode.Quality = node.Quality;
UpdateQualityStatistics();
}
});
}
}


ViewModel设计精髓:
XML<Window.Resources>
<!-- Apple 官方色彩系统 -->
<SolidColorBrush x:Key="AppleBlue" Color="#007AFF"/>
<SolidColorBrush x:Key="AppleGray" Color="#8E8E93"/>
<SolidColorBrush x:Key="AppleRed" Color="#FF3B30"/>
<SolidColorBrush x:Key="AppleGreen" Color="#34C759"/>
<!-- 阴影效果 -->
<DropShadowEffect x:Key="AppleShadow"
BlurRadius="20"
ShadowDepth="8"
Color="#10000000"
Opacity="0.1"/>
</Window.Resources>
XML<Style x:Key="AppleButton" TargetType="Button">
<Setter Property="Background" Value="{StaticResource AppleBlue}"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="20,12"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="12"
Effect="{StaticResource AppleShadow}">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#0051D5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
XML<DataTemplate x:Key="QualityStatusTemplate">
<Border CornerRadius="6" Padding="6,3">
<Border.Style>
<Style TargetType="Border">
<Style.Triggers>
<DataTrigger Binding="{Binding Quality.Status}" Value="Good">
<Setter Property="Background" Value="#E8F5E8"/>
</DataTrigger>
<DataTrigger Binding="{Binding Quality.Status}" Value="Bad">
<Setter Property="Background" Value="#FFEBEE"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<StackPanel Orientation="Horizontal">
<Ellipse Width="8" Height="8" Fill="{StaticResource AppleGreen}"/>
<TextBlock Text="{Binding Quality.Status}"
FontWeight="Medium"
Margin="6,0,0,0"/>
</StackPanel>
</Border>
</DataTemplate>
C#// ❌ 错误写法 - 阻塞UI线程
public void BadExample()
{
var result = _opcuaBridge.ReadValueAsync(nodeId).Result; // 永远不要这样做!
}
// ✅ 正确写法 - 完全异步
public async Task GoodExample()
{
try
{
var result = await _opcuaBridge.ReadValueAsync(nodeId);
// 处理结果
}
catch (Exception ex)
{
// 异常处理
}
}
C#public class MainViewModel : INotifyPropertyChanged, IDisposable
{
public void Dispose()
{
try
{
// 取消事件订阅,防止内存泄漏
if (_opcuaBridge != null)
{
_opcuaBridge.NodeValueChanged -= OnNodeValueChanged;
_opcuaBridge.Dispose();
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"释放资源失败: {ex.Message}");
}
}
}
问题:后台线程直接更新UI导致异常
解决方案:
C#// 确保在UI线程执行更新
Application.Current?.Dispatcher.Invoke(() =>
{
// UI更新代码
Nodes.Add(newNode);
});
问题:事件订阅没有正确取消
解决方案:实现IDisposable接口,在Dispose中取消所有订阅
问题:async void方法中的异常无法捕获
解决方案:
C#// ❌ 避免使用 async void
public async void BadMethod() { }
// ✅ 使用 async Task
public async Task GoodMethod() { }
通过这个完整的OPC UA Bridge项目,我们实现了:
三个"收藏级"代码模板:
这套架构不仅适用于OPC UA客户端,同样可以应用到其他工业通信协议的开发中。掌握了这些技巧,你就能轻松构建出专业级的工业软件应用。
延伸学习建议:
你在工业软件开发中遇到过哪些通信协议的坑?或者对这个架构设计有什么更好的建议?欢迎在评论区分享你的经验!
觉得这篇文章对你有帮助的话,请转发给更多需要的C#开发同行吧! 🚀
相关信息
通过网盘分享的文件:AppOPCUABridge.zip 链接: https://pan.baidu.com/s/1O_79z8s8cTuKvkes9ciQbQ?pwd=dkb1 提取码: dkb1 --来自百度网盘超级会员v9的分享
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!