前置準備
- Visual Studio 2019/2022 + .NET Framework 4.7.2+ / .NET 6+
- NuGet 安裝:
OpcNetApi、OpcNetApi.Com(或參考本機 DLL)
- OPC Server:Kepware / Matrikon / Siemens SIMATIC NET 等支援 OPC DA 2.0/3.0
核心類別架構
| 類別 | 命名空間 | 用途 |
Server | Opc.Da | OPC DA 伺服器連線與管理 |
Subscription | Opc.Da | 訂閱群組(Group),含更新率、死區等參數 |
Item | Opc.Da | 單一標籤(Tag)定義 |
ItemValueResult | Opc.Da | 讀取結果:值、品質、時間戳 |
ServerEnumerator | OpcCom | 列舉本機/遠端可用 OPC Server |
完整可執行主程式
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Opc;
using Opc.Da;
using OpcCom;
namespace OpcDaClientDemo
{
public class OpcClient : IDisposable
{
private Server _server;
private Subscription _subscription;
private readonly string _serverName;
private readonly string _host;
public OpcClient(string serverName, string host = "localhost")
{
_serverName = serverName;
_host = host;
}
public bool Connect()
{
// 1. 列舉可用伺服器
var enumerator = new ServerEnumerator();
Server[] servers = enumerator.GetAvailableServers(Specification.COM_DA_20, _host, null);
if (servers == null || servers.Length == 0)
{
Console.WriteLine($"主機 {_host} 無可用 OPC Server");
return false;
}
// 2. 依名稱尋找目標伺服器
_server = servers.FirstOrDefault(s =>
string.Equals(s.Name, _serverName, StringComparison.OrdinalIgnoreCase));
if (_server == null)
{
Console.WriteLine($"找不到伺服器: {_serverName}");
Console.WriteLine("可用伺服器: " + string.Join(", ", servers.Select(s => s.Name)));
return false;
}
// 3. 建立連線
try
{
_server.Connect();
Console.WriteLine($"已連線至 {_server.Name}");
return true;
}
catch (Exception ex)
{
Console.WriteLine($"連線失敗: {ex.Message}");
return false;
}
}
public Subscription CreateSubscription(string groupName, int updateRateMs = 1000)
{
var state = new SubscriptionState
{
Name = groupName,
Active = true,
UpdateRate = updateRateMs,
Deadband = 0, // 0 = 任何變化皆通知
ClientHandle = Guid.NewGuid().ToString()
};
_subscription = (Subscription)_server.CreateSubscription(state);
return _subscription;
}
public void AddItems(IEnumerable itemIds)
{
var items = itemIds.Select((id, i) => new Item
{
ItemName = id,
ClientHandle = Guid.NewGuid().ToString()
}).ToArray();
_subscription.AddItems(items);
Console.WriteLine($"已新增 {items.Length} 項標籤");
}
public void SubscribeDataChanged(EventHandler handler)
{
_subscription.DataChanged += handler;
}
// 同步讀取
public ItemValueResult[] ReadAll() => _subscription.Read(_subscription.Items);
// 同步寫入
public void WriteAll(Dictionary values)
{
var itemValues = _subscription.Items.Select(item => new ItemValue(item)
{
Value = values.TryGetValue(item.ItemName, out var v) ? v : null
}).ToArray();
_subscription.Write(itemValues);
}
// 非同步讀取/寫入可依需求實作 IRequest 回調
public void Dispose()
{
if (_subscription != null)
{
_subscription.DataChanged -= OnDataChanged;
_subscription.RemoveItems(_subscription.Items);
_server?.CancelSubscription(_subscription);
_subscription.Dispose();
_subscription = null;
}
_server?.Disconnect();
_server = null;
}
private void OnDataChanged(object subscriptionHandle, object requestHandle, ItemValueResult[] values)
{
foreach (var v in values)
{
if (v.Quality == Quality.Good)
Console.WriteLine($"[DataChanged] {v.ItemName} = {v.Value} @ {v.Timestamp}");
}
}
}
class Program
{
static void Main()
{
// 請替換為實際伺服器名稱(可從列舉結果取得)
const string SERVER_NAME = "Kepware.KEPServerEx.V6";
const string HOST = "localhost";
using (var client = new OpcClient(SERVER_NAME, HOST))
{
if (!client.Connect()) return;
client.CreateSubscription("DemoGroup", 500);
client.AddItems(new[] { "Channel1.Device1.Tag1", "Channel1.Device1.Tag2" });
client.SubscribeDataChanged((s, e) => { /* 處理變更 */ });
// 示範同步讀寫
var read = client.ReadAll();
foreach (var r in read) Console.WriteLine($"Read: {r.ItemName} = {r.Value}");
client.WriteAll(new Dictionary
{
["Channel1.Device1.Tag1"] = 123,
["Channel1.Device1.Tag2"] = true
});
Console.WriteLine("按 Enter 結束...");
Console.ReadLine();
}
}
}
}
關鍵參數說明
- UpdateRate:伺服器端掃描週期(毫秒),太小會增加 CPU/網路負載,建議 ≥ 100ms。
- Deadband:浮點數值變化門檻,0 表示任何微小變化都推送;設定合理值可減少網路流量。
- Active:false 時伺服器不掃描該群組,適合暫停監控。
- ClientHandle/ServerHandle:客戶端/伺服器端識別碼,回調時用於對應標籤。
常見問題排查
| 現象 | 原因 | 解決 |
| GetAvailableServers 回傳空 | DCOM 權限 / 防火牆 / OPC Server 未啟動 | 設定 DCOMCNFG、開放 TCP 135/動態埠、確認服務運行 |
| Connect 拋出 0x800706BA (RPC_S_SERVER_UNAVAILABLE) | 遠端主機不可達 / DCOM 未授權 | 檢查網路、遠端 DCOM 設定「啟動及啟動權限」加入匿名/Everyone |
| Item 加入後 Quality = Bad | ItemName 路徑錯誤 / 標籤不存在 / 權限不足 | 用 OPC Client 工具驗證 ItemID、確認伺服器端標籤定義 |
| DataChanged 不觸發 | Subscription.Active=false / Deadband 過大 / 伺服器不支援訂閱 | 檢查 SubscriptionState、改用輪詢 Read 確認資料更新 |
進階主題
- 非同步讀寫:呼叫
subscription.Read(items, cancelId, callback, out request),避免阻塞 UI 執行緒。
- OPC UA 遷移:新專案建議直接使用
OPC Foundation UA .NET Standard,支援跨平台、資訊安全、複雜資料模型。
- 高可用性:實作重連機制、斷線偵測、資料緩衝與補傳。
總結
OpcNetApi 是經典 OPC DA (COM/DCOM) 的 .NET 封裝,核心流程為:列舉伺服器 → 連線 → 建立 Subscription → 加入 Item → 同步/非同步讀寫或訂閱 DataChanged。本文提供完整封裝類別與錯誤排查表,可直接作為工業資料採集模組的基礎。新專案若無舊系統相容需求,優先評估 OPC UA。