首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# 使用 OpcNetApi 實作 OPC DA 客戶端完整教學

     編輯:關於C#

前置準備

  • 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

核心類別架構

類別命名空間用途
ServerOpc.DaOPC DA 伺服器連線與管理
SubscriptionOpc.Da訂閱群組(Group),含更新率、死區等參數
ItemOpc.Da單一標籤(Tag)定義
ItemValueResultOpc.Da讀取結果:值、品質、時間戳
ServerEnumeratorOpcCom列舉本機/遠端可用 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 = BadItemName 路徑錯誤 / 標籤不存在 / 權限不足用 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。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved