OPC UA 入門 ── 用 Windows 應用程式收集 PLC 與設備的資料

· · Windows, OPC UA, 製造業, PLC, C#, .NET, 通訊, 憑證

分享到 X 分享到 LINE 分享到 Facebook
更新紀錄(僅初版,2026年11月01日 發布)
初次發布
引用本文(DOI: 10.5281/zenodo.23968380)

本文保存於 Zenodo。以下同時提供一律指向最新版本的 DOI,以及固定於您正在閱讀版本的 DOI。

Go Komura(2026)。〈OPC UA 入門 ── 用 Windows 應用程式收集 PLC 與設備的資料〉。小村軟體有限公司。https://doi.org/10.5281/zenodo.23968380 https://comcomponent.com/zh-TW/blog/opc-ua-windows-data-collection/

DOI(最新版本)
10.5281/zenodo.23968380
DOI(此版本)
10.5281/zenodo.23968381

用 Windows 應用程式收集 PLC 或設備的資料時,如果設備有 OPC UA 的伺服器,有時就不必為每家廠商各自製作通訊。不過只是連線並讀取值還不夠。若不了解和值一起傳回的狀態與時間戳記、通知變化的訂閱、以憑證允許連線的方式,以及斷線後如何恢復,收集到的資料日後就無法信任。

本文會說明 OPC UA 的機制中資料收集需要的部分,並附上以 C# 用戶端實際執行的結果。用戶端使用 OPC Foundation 的 .NET 程式庫,對方則使用模擬的設備伺服器。沒有在特定的 PLC 或設備上試過。各設備的設定請以該設備的資料確認。

從 GigE Vision 或 USB3 Vision 相機收集影像的內容整理在〈Dropped Frames with GigE Vision and USB3 Vision Cameras〉,序列通訊設備的內容整理在〈序列通訊應用程式的陷阱〉。

OPC UA 與 OPC Classic 的不同

OPC Classic 的規格使用 Windows 的 COM/DCOM,在軟體元件之間交換資料。處理製程資料的 OPC DA、處理警報的 OPC A&E、處理歷史資料的 OPC HDA,各自訂為獨立的規格1。

OPC Foundation 在 2008 年公開了 OPC UA(Unified Architecture)。OPC UA 是不依賴特定平台的服務導向架構,把 Classic 各規格的功能整合在一起處理。OPC Foundation 列出的重新設計理由包括:Microsoft 開始比 COM/DCOM 更重視跨平台的機制、需要在 Microsoft 以外的系統和嵌入式設備上執行、需要跨越防火牆安全地連線等1。

OPC UA 的規格在 IEC 也以 IEC 62541 系列成為標準2。通訊的連接埠方面,IANA 把 4840/TCP 登錄為 opcua-tcp3。

OPC Classic 與 OPC UA 的不同表示 OPC Classic 以 Windows 的 COM 與 DCOM 為基礎,分成 DA、A&E、HDA 等獨立規格,而 OPC UA 把這些功能整合進一個不依賴平台的規格的圖。整合功能OPC Classic在 COM/DCOM(Windows)上執行分成 DA、A&E、HDA 的規格OPC UA(2008 年)不依賴特定平台標準化為 IEC 62541

圖 1: OPC Classic 是 COM/DCOM 上的各個獨立規格,OPC UA 把它們整合成一個。

既有設備只有 OPC DA 伺服器時,需要使用 COM/DCOM 那一側的機制。本文只處理 OPC UA。

圖中實線表示始終成立的關係,虛線表示附帶條件的關係(成立條件寫在詳細頁面中各關係的說明中)。關係的完整清單(共 13 條,附依據與可信度)以及主要概念的定義,彙整在知識地圖詳細頁面(日文)。資料:JSON-LD / Turtle

試用的組成

執行的組成如下。

角色 使用的東西
用戶端 C#、.NET 10 SDK 10.0.114、NuGet 套件 OPCFoundation.NetStandard.Opc.Ua.Client 2.0.1
伺服器 以 Python 3.13 與 asyncua 2.1.0 製作的模擬設備(同一台電腦的 4840 連接埠)
作業系統 Linux(沒有試過在 Windows 上執行)

用戶端的程式庫是 OPC Foundation 公開的 .NET 實作。2.0 系列的用法依照同一個儲存庫的 Getting started 的寫法4。

試用的組成表示 C# 用戶端使用 OPC Foundation 的 .NET 程式庫,以加密的連線連到在 4840 連接埠等候的 asyncua 模擬設備伺服器的組成的圖。opc.tcp、SignAndEncryptC# 的用戶端(LineCollector)模擬的設備伺服器(asyncua)OPC Foundation 的 .NET 程式庫 2.0.1Line1 的 4 個變數

圖 2: 在同一台電腦內,從 C# 的用戶端連到模擬的設備伺服器。

模擬的設備伺服器

我用 asyncua 製作了一個小伺服器來代替設備5。在名為 Line1 的物件下,放了溫度(Temperature)、生產數(PartCount)、運轉中(Running)、設定值(Setpoint)4 個變數。溫度每 0.5 秒變化一次,生產數每 2 秒變化一次。安全性準備了不加密(None)與簽章並加密(Basic256Sha256 的 SignAndEncrypt)兩種。

# 動作確認用的模擬設備。不能取代正式環境的設備或 PLC。
import asyncio, math, random, socket
from pathlib import Path
from cryptography.x509.oid import ExtendedKeyUsageOID
from asyncua import Server, ua
from asyncua.crypto.cert_gen import setup_self_signed_certificate

HERE = Path(__file__).parent
APP_URI = "urn:komurasoft:demo-plc"


async def main():
    key, cert = HERE / "server_key.pem", HERE / "server_cert.der"
    await setup_self_signed_certificate(
        key, cert, APP_URI, socket.gethostname(),
        [ExtendedKeyUsageOID.SERVER_AUTH], {"commonName": "DemoPlcServer"})

    server = Server()
    await server.init()
    server.set_endpoint("opc.tcp://0.0.0.0:4840/demo/")
    server.set_server_name("DemoPlcServer")
    await server.set_application_uri(APP_URI)
    server.set_security_policy([
        ua.SecurityPolicyType.NoSecurity,
        ua.SecurityPolicyType.Basic256Sha256_SignAndEncrypt,
    ])
    await server.load_certificate(str(cert))
    await server.load_private_key(str(key))

    ns = await server.register_namespace("urn:komurasoft:demo-plc:line1")
    line1 = await server.nodes.objects.add_object(ua.NodeId("Line1", ns), "Line1")
    temperature = await line1.add_variable(ua.NodeId("Line1.Temperature", ns), "Temperature", 25.0)
    part_count = await line1.add_variable(
        ua.NodeId("Line1.PartCount", ns), "PartCount", ua.Variant(0, ua.VariantType.UInt32))
    await line1.add_variable(ua.NodeId("Line1.Running", ns), "Running", True)
    setpoint = await line1.add_variable(ua.NodeId("Line1.Setpoint", ns), "Setpoint", 30.0)
    await setpoint.set_writable()

    async with server:
        print("listening", flush=True)
        t, count = 0, 0
        while True:
            await asyncio.sleep(0.5)
            t += 1
            value = 25.0 + 2.0 * math.sin(t / 4) + random.uniform(-0.05, 0.05)
            await temperature.write_value(round(value, 2))
            if t % 4 == 0:
                count += 1
                await part_count.write_value(ua.Variant(count, ua.VariantType.UInt32))


asyncio.run(main())

這個伺服器不檢查連進來的用戶端的憑證就接受。這是測試用的設定,和正式環境設備的行為不同。

C# 的用戶端

專案檔加入了下面兩個套件。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client" Version="2.0.1" />
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
  </ItemGroup>

</Project>

用戶端依序進行連線、瀏覽、讀取、寫入、訂閱。各部分的意義從下一節起依序說明。

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Client.Subscriptions;
using Opc.Ua.Client.Subscriptions.Streaming;
using MonitoredItemOptions = Opc.Ua.Client.Subscriptions.MonitoredItems.MonitoredItemOptions;

string url = args.Length > 0 ? args[0] : "opc.tcp://localhost:4840/demo/";
int notifications = args.Length > 1 ? int.Parse(args[1]) : 6;
const string LineNamespace = "urn:komurasoft:demo-plc:line1";

HostApplicationBuilder builder = Host.CreateApplicationBuilder();
builder.Logging.SetMinimumLevel(LogLevel.Warning);

builder.Services
    .AddOpcUa()
    .AddClient(options =>
    {
        options.ApplicationName = "LineCollector";
        options.ApplicationUri = "urn:localhost:LineCollector";
        options.ProductUri = "urn:komurasoft:LineCollector";
        options.PkiRoot = Path.Combine(AppContext.BaseDirectory, "pki");
    })
    .AddDiscoveryAndConnect(options =>
    {
        options.DiscoveryUrl = url;
        options.SecurityMode = MessageSecurityMode.SignAndEncrypt;
        options.SecurityPolicyUri = SecurityPolicies.Basic256Sha256;
    });

using IHost host = builder.Build();
await host.StartAsync();
using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(2));
CancellationToken ct = timeout.Token;

try
{
    var connect = host.Services
        .GetRequiredService<Func<CancellationToken, Task<ManagedSession>>>();
    ManagedSession session = await connect(ct);
    await using (session)
    {
        Console.WriteLine($"接続: {session.ConfiguredEndpoint?.EndpointUrl}");

        // 命名空間索引由伺服器決定。從 URI 查出來再建立 NodeId
        ushort ns = (ushort)session.NamespaceUris.GetIndex(LineNamespace);
        var line1 = new NodeId("Line1", ns);
        var temperature = new NodeId("Line1.Temperature", ns);
        var partCount = new NodeId("Line1.PartCount", ns);
        var setpoint = new NodeId("Line1.Setpoint", ns);

        // 瀏覽:列出 Line1 底下的變數
        var browser = new Browser(session)
        {
            BrowseDirection = BrowseDirection.Forward,
            ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences,
            IncludeSubtypes = true,
            NodeClassMask = (uint)NodeClass.Variable,
        };
        Console.WriteLine("Line1 の変数:");
        foreach (ReferenceDescription child in await browser.BrowseAsync(line1, ct))
        {
            Console.WriteLine($"  {child.DisplayName}  {child.NodeId}");
        }

        // 讀取:不只看值,也看狀態與兩個時間戳記
        DataValue t = await session.ReadValueAsync(temperature, ct);
        Console.WriteLine($"Temperature = {t.WrappedValue}  状態 {t.StatusCode}");
        Console.WriteLine($"  Source {Hms(t.SourceTimestamp)} / Server {Hms(t.ServerTimestamp)}");

        // 讀取不存在的節點時,傳回的不是值而是 Bad 的狀態
        try
        {
            await session.ReadValueAsync(new NodeId("Line1.Pressure", ns), ct);
        }
        catch (ServiceResultException ex)
        {
            Console.WriteLine($"Line1.Pressure の読み取り: {ex.StatusCode}");
        }

        // 寫入:把 Setpoint 設為 32.5
        var write = new WriteValue
        {
            NodeId = setpoint,
            AttributeId = Attributes.Value,
            Value = new DataValue(new Variant(32.5)),
        };
        WriteResponse wr = await session.WriteAsync(null, new[] { write }, ct);
        Console.WriteLine($"Setpoint の書き込み: {wr.Results[0]}");

        // 訂閱:請伺服器每 500ms 取樣一次,並通知變化
        Console.WriteLine($"購読({notifications}件まで):");
        await foreach (DataValueChange change in session.DefaultStreaming
            .SubscribeDataChangesAsync(new[] { temperature, partCount }, options: new MonitoredItemOptions
            {
                SamplingInterval = TimeSpan.FromMilliseconds(500),
                QueueSize = 5,
            }, ct: ct)
            .TakeAsync(notifications)
            .WithCancellation(ct))
        {
            DataValue v = change.Value;
            string name = change.MonitoredItem?.Name.Split("s=")[^1] ?? "?";
            Console.WriteLine($"  {Hms(v.SourceTimestamp)}  {name,-17} {v.WrappedValue,6}  {v.StatusCode}");
        }
    }
}
catch (ServiceResultException ex)
{
    Console.Error.WriteLine($"OPC UA エラー {ex.StatusCode}: {ex.Message}");
    Environment.ExitCode = 1;
}
finally
{
    await host.StopAsync();
}

static string Hms(DateTimeUtc time) => time.ToDateTime().ToString("HH:mm:ss.fff");

第一次執行時沒能連線。原因會在「以憑證允許連線」一節說明。信任伺服器的憑證之後的輸出如下。省略了程式庫的警告記錄。

接続: opc.tcp://localhost:4840/demo/
Line1 の変数:
  Temperature  ns=2;s=Line1.Temperature
  PartCount  ns=2;s=Line1.PartCount
  Running  ns=2;s=Line1.Running
  Setpoint  ns=2;s=Line1.Setpoint
Temperature = 23.09  状態 Good [0x00000000]
  Source 13:40:51.302 / Server 13:40:51.302
Line1.Pressure の読み取り: BadNodeIdUnknown [0x80340000]
Setpoint の書き込み: Good [0x00000000]
購読(6件まで):
  13:40:51.302  Line1.Temperature  23.09  Good [0x00000000]
  13:40:50.801  Line1.PartCount       67  Good [0x00000000]
  13:40:51.802  Line1.Temperature  23.05  Good [0x00000000]
  13:40:52.304  Line1.Temperature  23.07  Good [0x00000000]
  13:40:52.806  Line1.Temperature   23.2  Good [0x00000000]
  13:40:52.806  Line1.PartCount       68  Good [0x00000000]

時間是 UTC。

設備的資料以位址空間的節點呈現

OPC UA 的伺服器把擁有的資料以節點的集合公開。這個集合稱為位址空間。節點有表示資料夾或設備的物件、擁有值的變數等種類,節點之間以參照相連。用戶端以 Browse 服務沿著參照走,調查有哪些節點6。

上面的輸出中,從 Line1 沿著階層參照找到了 4 個變數。

模擬設備的位址空間表示 Objects 資料夾底下有 Line1 物件,其下有 Temperature、PartCount、Running、Setpoint 4 個變數的圖。ObjectsLine1(物件)Temperature(變數)PartCount(變數)Running(變數)Setpoint(變數、可寫入)

圖 3: 模擬設備的位址空間。用戶端以 Browse 沿著參照找出節點。

NodeId 由命名空間與識別碼組成

節點以 NodeId 識別。輸出的 ns=2;s=Line1.Temperature 表示命名空間的索引是 2,識別碼是字串 Line1.Temperature。

命名空間的本體是 URI。規格規定在服務中以數值表示命名空間的 URI,該數值對應到伺服器的 NamespaceArray 中的位置。索引 0 是 OPC UA 的命名空間 http://opcfoundation.org/UA/7。

因此不要把索引的數值寫在程式碼裡,而是從 URI 查出索引。上面的程式碼以 session.NamespaceUris.GetIndex(LineNamespace) 從 URI 求出 2,再由此建立 NodeId。

從命名空間 URI 建立 NodeId 的流程表示用戶端以伺服器的 NamespaceArray 查詢命名空間的 URI 取得索引,再把該索引與識別碼組合成 NodeId 的圖。命名空間的 URI伺服器的 NamespaceArray索引(此例為 2)NodeId ns=2;s=Line1.Temperature識別碼 Line1.Temperature

圖 4: 命名空間的索引從 URI 查出,再與識別碼組合成 NodeId。

讀取與寫入時,值會和狀態與時間戳記一起傳回

變數的值以 Read 與 Write 服務讀寫8。讀取時傳回的不只是值。以 DataValue 這個結構傳回值、狀態碼、來源時間戳記與伺服器時間戳記9。

項目 意義
值 變數的值。狀態碼表示錯誤時忽略值,伺服器會設為 null
狀態碼(StatusCode) 伺服器是否能取得、提供該值
SourceTimestamp 資料的來源對值加上的時間
ServerTimestamp 伺服器收到值,或確認值正確的時間

狀態碼的上位 2 位元表示 Good(成功)、Uncertain(部分成功,依用途可能無法使用)、Bad(失敗)10。輸出的 Good [0x00000000] 是 Good。

讀取不存在的 Line1.Pressure 時,傳回的不是值,而是 BadNodeIdUnknown [0x80340000]。這個程式庫的 ReadValueAsync 在狀態為 Bad 時會擲出例外4。在記錄值的機制中,把狀態碼和值並列保存,日後就能分辨在 Bad 期間記錄的值。

DataValue 的內容表示讀取或通知傳回的 DataValue 擁有值、狀態碼、來源時間戳記、伺服器時間戳記 4 項,狀態碼為 Bad 時忽略值的圖。若為 BadDataValue值狀態碼SourceTimestamp(來源的時間)ServerTimestamp(伺服器的時間)忽略值

圖 5: 讀取的結果不只有值,還有狀態碼與兩個時間戳記。

寫入時對 Setpoint 寫入 32.5,結果的狀態碼傳回 Good。變數是否可寫入由伺服器決定。這個模擬伺服器只讓 Setpoint 可寫入。是否讓設備的設定值能從外部改寫,涉及設備的安全設計,不在本文的範圍內。

請伺服器通知變化:訂閱與 MonitoredItem

追蹤值的變化時,也可以由用戶端以固定間隔重複 Read。OPC UA 另外還有把值的監視交給伺服器、請它通知變化的機制。那就是訂閱(Subscription)與 MonitoredItem。

MonitoredItem 是監視的一個對象,擁有取樣間隔。取樣間隔是伺服器從來源取樣值的最快間隔,規格中稱為「盡力而為(best effort)」的週期11。樣本符合篩選條件時就會產生通知。若未指定篩選,則在值或狀態改變時通知12。

通知會先放進 MonitoredItem 的佇列。要事先決定佇列滿出時捨棄舊的還是新的,佇列大小為 2 以上時,狀態碼中會立起表示已捨棄的 Overflow 位元13。

訂閱把多個 MonitoredItem 整合在一起,每個發布間隔把尚未送出的通知送到用戶端。沒有通知可送的週期持續時,會送出表示訂閱仍然存活的保持連線(keep-alive)14。

訂閱與 MonitoredItem 的流程表示 MonitoredItem 以取樣間隔取樣來源的值,把通過篩選的通知放進佇列,訂閱以發布間隔整合後送到用戶端,沒有通知時送出保持連線的圖。取樣間隔發布間隔沒有通知時來源(設備的值)MonitoredItem篩選(預設為值或狀態的變化)佇列(滿出時 Overflow)訂閱用戶端保持連線

圖 6: MonitoredItem 取樣後存進佇列,訂閱整合後送到用戶端。

上面的程式碼以取樣間隔 500 毫秒、佇列大小 5 訂閱了 Temperature 與 PartCount15。輸出中,溫度大約每 0.5 秒、生產數大約每 2 秒送達。最初的 2 筆是開始訂閱時各變數所擁有的值。生產數的 13:40:50.801 是開始訂閱之前值改變的時間。通知的時間顯示的是來源時間戳記,所以出現的是值改變的時間,而不是收到通知的時間。

重複讀取與訂閱的區分使用

重複讀取的方法可以由用戶端決定間隔,機制也單純。相對地,即使值沒有變化,每次都會往返請求與回應,比間隔更短的變化也會漏掉。訂閱由伺服器取樣、只送出變化,因此即使是變化少的值,通訊也不容易增加。不過即使是訂閱,比取樣間隔更短的變化也可能不會被通知。規格允許比指定更快地取樣,但並不要求11。

重複讀取與訂閱的不同表示重複讀取的方法由用戶端決定間隔並每次請求,訂閱由伺服器以取樣間隔取樣並只送出變化,兩者都可能取不到比間隔更短的變化的圖。追蹤值的變化重複讀取訂閱用戶端決定間隔、每次請求伺服器取樣、只送出變化可能取不到比間隔更短的變化

圖 7: 無論哪種方法,都可能取不到比間隔更短的變化。

以憑證允許連線

OPC UA 的連線以 MessageSecurityMode 選擇訊息保護的程度。值有不保護的 None、只簽章的 Sign、簽章並加密的 SignAndEncrypt16。簽章與加密使用的演算法組合由安全性原則決定,本文使用 Basic256Sha256。

簽章與加密使用每個應用程式各自的憑證。用戶端與伺服器互相確認對方的憑證,拒絕與不信任的對象連線。以 OPC Foundation 的 .NET 程式庫製作的伺服器,預設不提供不加密的 None 原則4。

第一次連線停在 BadCertificateUntrusted

第一次執行時,用戶端不信任伺服器的憑證,所以連線被拒絕。以下是輸出的一部分。

fail: Opc.Ua.CertificateValidationCore[91]
      Certificate [Subject=CN=DemoPlcServer, CN=urn:komurasoft:demo-plc, Thumbprint=4E236DDA629FE2AB32D185BF681609FD6C525363, NotBefore=2026-11-01, NotAfter=2027-11-01, KeyAlgorithm=1.2.840.113549.1.1.1] validation failed with suppressible errors but was rejected. Reason=Id: BadCertificateUntrusted
...
OPC UA エラー BadCertificateUntrusted [0x801A0000]: [801A0000] (BadCertificateUntrusted) 'Could not verify security on OpenSecureChannel response.'

這個程式庫會把拒絕的憑證存到 rejected 存放區。管理者把它從那裡移到 trusted 存放區後,就會信任該對象17。這次把位於 pki/rejected/certs 的伺服器憑證移到 pki/trusted/certs,再執行一次就能連線了。移動之前,要和伺服器的管理者確認檔名中也含有的指紋4。

到信任憑證為止的流程表示第一次連線時用戶端拒絕伺服器的憑證並存到 rejected 存放區,確認指紋後移到 trusted 存放區,下次連線就會信任並能連線的圖。第一次連線BadCertificateUntrusted把憑證存到 rejected 存放區和伺服器的管理者確認指紋移到 trusted 存放區下次連線時信任並能連線

圖 8: 確認被拒絕的憑證後移到 trusted,就會信任該對象。

也有一個自動接受不信任憑證的設定 AutoAcceptUntrustedCertificates。預設是 false,程式庫的資料寫著它在開發環境中很有用,但在正式環境中不要使用17。

記錄中的 NotAfter=2027-11-01 是模擬伺服器憑證的有效期限。這是用 asyncua 的函式製作的自我簽署憑證,期限是製作日起算 1 年後。設備的憑證也有期限,所以要在運作流程中訂好在期限到期前更換、並讓對方信任的程序。Windows 憑證存放區的處理整理在〈Windows 憑證存放區實務指南〉。

使用者的驗證

以憑證確認的是應用程式之間。除此之外,開啟工作階段時還會傳遞表示使用者的權杖。規格中有匿名、使用者名稱與密碼、X.509 憑證、由外部發行的權杖 4 種18。這次的用戶端以匿名連線。伺服器接受哪些權杖,包含在伺服器的端點資訊中19。

現場資料收集中容易卡關的地方

接下來根據這次的實驗與規格,列出收集設備資料的機制中容易發生的問題。

伺服器停止期間的值不會送達

在訂閱途中停止伺服器,約 15 秒後重新啟動。這個程式庫的 ManagedSession 在連線中斷時會自動重新連線20。停止期間,用戶端一直重複重新連線。

warn: Opc.Ua.ChannelManager[6]
      Reconnect failed. Endpoint=opc.tcp://localhost:4840/demo/, Attempt=3, Outcome=transient-failure, Status=BadNotConnected, ErrorMessage=Could not connect to the remote endpoint.

伺服器恢復後,通知也恢復了。以下是訂閱輸出的一部分。

  13:41:13.364  Line1.Temperature  25.61  Good [0x00000000]
  13:41:13.865  Line1.Temperature  25.12  Good [0x00000000]
  13:41:34.285  Line1.Temperature  26.55  Good [0x00000000]
  13:41:33.784  Line1.PartCount        2  Good [0x00000000]
  13:41:34.786  Line1.Temperature  26.16  Good [0x00000000]

13 點 41 分 13 秒到 34 秒之間的值一筆也沒有送達。生產數從 78 回到了 2。這是因為重新啟動模擬伺服器後,數量從 0 重新計算。

這個例子中伺服器本身停止了,所以那段期間的值沒有留在任何地方。重新連線的機制會恢復連線,但不會事後補上停止期間的值。

只有通訊中斷、伺服器持續運作的情況就不同了。規格中有 Republish 服務,可以請伺服器從重送用的佇列再次送出沒送達的通知訊息21。MonitoredItem 的佇列中也會累積通知,但超過佇列大小的部分會被捨棄13。這次的實驗沒有試過這種情況。無論哪種情況,都要把斷線與重新連線的時間記錄下來,以便知道哪段時間沒有收到值。序列通訊設備的重新連線與記錄設計,也在〈序列通訊應用程式的陷阱〉中說明。

停止伺服器時的記錄表示訂閱中伺服器停止時,用戶端會重複重新連線,伺服器恢復後通知也會恢復,但停止期間的值不會送達,所以要把斷線與重新連線的時間記錄下來的圖。訂閱中伺服器停止用戶端重複重新連線伺服器恢復、通知也恢復停止期間的值不會送達記錄斷線與重新連線的時間

圖 9: 重新連線會恢復連線,但停止期間的值不會送達。

要記錄哪一個時間戳記

規格中,SourceTimestamp 是資料的來源對值加上的時間,應表示值改變的時間。ServerTimestamp 是伺服器收到值,或確認值正確的時間9。這次的模擬伺服器自己產生值,所以兩個時間相同。

想知道設備上值改變的時間時,使用 SourceTimestamp。不過該時間是以來源的時鐘加上的,所以設備或伺服器的時鐘有偏差,記錄也會偏差。若要把收集到的資料與其他記錄對照,把接收端電腦的時間也並列保存,日後就能確認偏差。Windows 端的時間同步整理在〈Windows的時間同步(w32time)與業務系統〉。

不要把狀態不是 Good 的值直接當數值處理

狀態碼為 Bad 時,值應被忽略9。Uncertain 時,依用途可能是無法使用的值10。放進圖表或統計之前先以狀態碼分類,並事先決定如何處理不是 Good 的值。佇列滿出時的 Overflow 位元也會立在狀態碼中13。設備的狀態要如何在畫面上顯示,在〈外部設備狀態檢查與顯示的最佳實務〉中說明。

在防火牆開放連接埠

IANA 登錄的 OPC UA 連接埠是 4840/TCP3。不過並不是所有伺服器都使用這個號碼。OPC Foundation 的 .NET 範例使用 62541 等連接埠4。以設備的端點 URL 確認連接埠號碼,允許通往該連接埠的連線。伺服器在 Windows 上執行時在該 Windows 防火牆中,中間有網路設備時在該設備上允許。輸入規則的建立方式整理在〈Windows 防火牆與業務應用程式〉。

資料收集中容易卡關的地方把設備資料收集中容易卡關的地方,分成記錄的內容(伺服器停止期間的值不會送達、要記錄哪一個時間戳記、不是 Good 的值的處理)與連線的準備(防火牆的連接埠、憑證的期限)來表示的圖。資料收集中容易卡關的地方記錄的內容連線的準備停止期間的值不會送達要用哪一個時間戳記不是 Good 的值的處理防火牆的連接埠憑證的期限

圖 10: 連線成功之後,也要確認記錄的缺漏、時間、狀態、連接埠與憑證。

與 Modbus/TCP 或廠商專屬通訊的區分使用

與 PLC 或設備的通訊,除了 OPC UA 之外,還有 Modbus/TCP 或 PLC 廠商提供的通訊。

Modbus 的規格以 4 個表格處理資料。唯讀的 1 位元 Discrete Input、可讀寫的 1 位元 Coil、唯讀的 16 位元 Input Register、可讀寫的 16 位元 Holding Register22。IANA 把 502/TCP 登錄為 Modbus 的應用程式協定(mbap)3。

這些表格裡放的是位元與 16 位元的字組,所以哪個位址以什麼格式放了什麼值,要以設備的資料確認後寫在用戶端。OPC UA 則如前面所見,伺服器把有名稱的變數公開在位址空間中,用戶端可以用 Browse 調查。值附有狀態碼與時間戳記,連線則有以憑證進行的允許。

要用哪一種,多半取決於設備提供了什麼。設備有 OPC UA 的伺服器就用它,只有 Modbus/TCP 就製作位址對照表來讀取。只有廠商的通訊或 SDK 時,就依照其資料。與 USB 裝置通訊方式的選擇整理在〈在 Windows 應用程式中處理 USB 裝置的方法〉。

與設備通訊的選擇方式表示設備有 OPC UA 的伺服器時以 OPC UA 瀏覽變數並收集,只有 Modbus/TCP 時以資料確認位址與格式並讀取暫存器,只有廠商的通訊時依照其資料的圖。OPC UA 的伺服器只有 Modbus/TCP只有廠商的通訊設備提供的通訊有什麼?瀏覽變數並收集以資料確認位址與格式依照廠商的資料與 SDK讀取暫存器並轉換成值

圖 11: 用什麼收集,多半取決於設備提供了什麼。

相關文章

參考連結

  1. OPC Foundation, Classic. OPC Classic 以 COM/DCOM 為基礎、DA、A&E、HDA 各規格,以及 2008 年 OPC UA 的公開與重新設計的理由。 ↩ ↩2

  2. IEC, IEC TR 62541-1:2020 OPC Unified Architecture - Part 1: Overview and concepts. 處理 OPC UA 概念與概要的 IEC 62541 系列第 1 部(已由後續版本取代)。 ↩

  3. IANA, Service Name and Transport Protocol Port Number Registry. 4840/TCP 的 opcua-tcp 與 502/TCP 的 mbap 的登錄。 ↩ ↩2 ↩3

  4. OPC Foundation, UA-.NETStandard 2.0.1: Getting started. 2.0 系列用戶端的寫法、ReadValueAsync 遇到 Bad 時擲出例外、拒絕與信任的存放區、伺服器預設不提供 None,以及範例的連接埠號碼。 ↩ ↩2 ↩3 ↩4 ↩5

  5. FreeOpcUa, asyncua(PyPI). 用於模擬設備伺服器的 Python OPC UA 程式庫。 ↩

  6. OPC Foundation, OPC 10000-4 v1.05, 5.9.2 Browse. 沿著節點參照走的服務。 ↩

  7. OPC Foundation, OPC 10000-3 v1.05, 8.2.2 NamespaceIndex. 在服務中以數值表示命名空間的 URI,該數值對應到 NamespaceArray 中的位置。 ↩

  8. OPC Foundation, OPC 10000-4 v1.05, 5.11 Attribute Service Set. Read 與 Write 服務。 ↩

  9. OPC Foundation, OPC 10000-4 v1.05, 7.11 DataValue. 值、狀態碼、SourceTimestamp、ServerTimestamp 的定義。 ↩ ↩2 ↩3

  10. OPC Foundation, OPC 10000-4 v1.05, 7.38.1 StatusCode General. 上位 2 位元表示的 Good、Uncertain、Bad。 ↩ ↩2

  11. OPC Foundation, OPC 10000-4 v1.05, 5.13.1.2 Sampling interval. 取樣間隔是盡力而為的週期。 ↩ ↩2

  12. OPC Foundation, OPC 10000-4 v1.05, 7.22.2 DataChangeFilter. 沒有篩選時以值或狀態的變化通知。 ↩

  13. OPC Foundation, OPC 10000-4 v1.05, 5.13.1.5 Queue parameters. 佇列大小、捨棄方式、Overflow 位元。 ↩ ↩2 ↩3

  14. OPC Foundation, OPC 10000-4 v1.05, 5.14.1.1 Subscription model. 發布間隔與保持連線。 ↩

  15. OPC Foundation, UA-.NETStandard 2.0.1: Subscriptions and Monitored Items Service Set. SubscribeDataChangesAsync 與 MonitoredItemOptions 的取樣間隔、佇列大小。 ↩

  16. OPC Foundation, OPC 10000-4 v1.05, 7.20 MessageSecurityMode. None、Sign、SignAndEncrypt 的意義。 ↩

  17. OPC Foundation, UA-.NETStandard 2.0.1: Certificates. 從 rejected 存放區移到信任的存放區的運作方式,以及不要在正式環境使用 AutoAcceptUntrustedCertificates。 ↩ ↩2

  18. OPC Foundation, OPC 10000-4 v1.05, 7.40 UserIdentityToken parameters. 匿名、使用者名稱、X.509、已發行權杖 4 種。 ↩

  19. OPC Foundation, OPC 10000-4 v1.05, 7.14 EndpointDescription. 端點接受的使用者權杖清單(userIdentityTokens)。 ↩

  20. OPC Foundation, UA-.NETStandard 2.0.1: Sessions, Reconnection, and Subscription Engines. ManagedSession 內建重新連線的機制。 ↩

  21. OPC Foundation, OPC 10000-4 v1.05, 5.14.6.1 Republish. 請伺服器從訂閱的重送用佇列再次送出通知訊息的服務。 ↩

  22. Modbus-IDA, MODBUS Application Protocol Specification V1.1b(放在芝加哥大學伺服器上的副本). 4.3 節的資料模型(Discrete Input、Coil、Input Register、Holding Register)。 ↩

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

常見問題

整理諮詢這個主題時常見的問題。

OPC UA 與 OPC Classic(OPC DA)有什麼不同?
OPC Classic 是以 Windows 的 COM/DCOM 交換資料的規格。OPC UA 是 2008 年公開、不依賴特定作業系統的規格,把 Classic 的功能整合在一起處理。OPC UA 的用戶端在 Windows 以外也能執行,也不需要 DCOM 的設定。
用 OPC UA 收集值時,以固定間隔讀取就可以了嗎?
值的數量少、間隔長的話,用讀取也能收集。若要追蹤值的變化,可以用訂閱把取樣交給伺服器,請它通知變化。無論哪一種,都要把和值一起傳回的狀態碼與時間戳記記錄下來。
連線時為什麼會出現 BadCertificateUntrusted?
因為用戶端還沒有信任伺服器的憑證。OPC Foundation 的 .NET 程式庫會把拒絕的憑證存到 rejected 存放區。先和伺服器的管理者確認憑證的指紋,再移到 trusted 存放區。自動接受憑證的設定只限於開發環境。
OPC UA 的連接埠號碼是多少?
IANA 把 4840/TCP 登錄為 opcua-tcp。不過實際的伺服器也可能使用別的連接埠,所以要在設備的設定中確認。伺服器在 Windows 上執行時要在 Windows 防火牆中,中間有網路設備時要在該設備上,允許通往該連接埠的通訊。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,擅長處理仍留有既有資產的專案,以及原因難以看清的故障調查。

回到部落格一覽