更新紀錄(僅初版,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。
flowchart TB
accTitle: OPC Classic 與 OPC UA 的不同
accDescr: 表示 OPC Classic 以 Windows 的 COM 與 DCOM 為基礎,分成 DA、A&E、HDA 等獨立規格,而 OPC UA 把這些功能整合進一個不依賴平台的規格的圖。
C["OPC Classic"] --> D["在 COM/DCOM(Windows)上執行"]
D --> DA["分成 DA、A&E、HDA 的規格"]
DA -->|"整合功能"| U["OPC UA(2008 年)"]
U --> P["不依賴特定平台"]
P --> S["標準化為 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。
flowchart TB
accTitle: 試用的組成
accDescr: 表示 C# 用戶端使用 OPC Foundation 的 .NET 程式庫,以加密的連線連到在 4840 連接埠等候的 asyncua 模擬設備伺服器的組成的圖。
A["C# 的用戶端(LineCollector)"] -->|"opc.tcp、SignAndEncrypt"| B["模擬的設備伺服器(asyncua)"]
A -.-> L["OPC Foundation 的 .NET 程式庫 2.0.1"]
B -.-> V["Line1 的 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 個變數。
flowchart TB
accTitle: 模擬設備的位址空間
accDescr: 表示 Objects 資料夾底下有 Line1 物件,其下有 Temperature、PartCount、Running、Setpoint 4 個變數的圖。
O["Objects"] --> L["Line1(物件)"]
L --> T["Temperature(變數)"]
L --> P["PartCount(變數)"]
L --> R["Running(變數)"]
L --> S["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。
flowchart TB
accTitle: 從命名空間 URI 建立 NodeId 的流程
accDescr: 表示用戶端以伺服器的 NamespaceArray 查詢命名空間的 URI 取得索引,再把該索引與識別碼組合成 NodeId 的圖。
U["命名空間的 URI"] --> A["伺服器的 NamespaceArray"]
A --> I["索引(此例為 2)"]
I --> N["NodeId ns=2;s=Line1.Temperature"]
K["識別碼 Line1.Temperature"] --> N
圖 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 期間記錄的值。
flowchart TB
accTitle: DataValue 的內容
accDescr: 表示讀取或通知傳回的 DataValue 擁有值、狀態碼、來源時間戳記、伺服器時間戳記 4 項,狀態碼為 Bad 時忽略值的圖。
D["DataValue"] --> V["值"]
D --> S["狀態碼"]
D --> TS["SourceTimestamp(來源的時間)"]
D --> TV["ServerTimestamp(伺服器的時間)"]
S -->|"若為 Bad"| X["忽略值"]
圖 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。
flowchart TB
accTitle: 訂閱與 MonitoredItem 的流程
accDescr: 表示 MonitoredItem 以取樣間隔取樣來源的值,把通過篩選的通知放進佇列,訂閱以發布間隔整合後送到用戶端,沒有通知時送出保持連線的圖。
S["來源(設備的值)"] --> M["MonitoredItem"]
M -->|"取樣間隔"| F["篩選(預設為值或狀態的變化)"]
F --> Q["佇列(滿出時 Overflow)"]
Q --> SUB["訂閱"]
SUB -->|"發布間隔"| C["用戶端"]
SUB -.->|"沒有通知時"| K["保持連線"]
圖 6: MonitoredItem 取樣後存進佇列,訂閱整合後送到用戶端。
上面的程式碼以取樣間隔 500 毫秒、佇列大小 5 訂閱了 Temperature 與 PartCount15。輸出中,溫度大約每 0.5 秒、生產數大約每 2 秒送達。最初的 2 筆是開始訂閱時各變數所擁有的值。生產數的 13:40:50.801 是開始訂閱之前值改變的時間。通知的時間顯示的是來源時間戳記,所以出現的是值改變的時間,而不是收到通知的時間。
重複讀取與訂閱的區分使用
重複讀取的方法可以由用戶端決定間隔,機制也單純。相對地,即使值沒有變化,每次都會往返請求與回應,比間隔更短的變化也會漏掉。訂閱由伺服器取樣、只送出變化,因此即使是變化少的值,通訊也不容易增加。不過即使是訂閱,比取樣間隔更短的變化也可能不會被通知。規格允許比指定更快地取樣,但並不要求11。
flowchart TB
accTitle: 重複讀取與訂閱的不同
accDescr: 表示重複讀取的方法由用戶端決定間隔並每次請求,訂閱由伺服器以取樣間隔取樣並只送出變化,兩者都可能取不到比間隔更短的變化的圖。
A["追蹤值的變化"] --> R["重複讀取"]
A --> S["訂閱"]
R --> R1["用戶端決定間隔、每次請求"]
S --> S1["伺服器取樣、只送出變化"]
R1 --> X["可能取不到比間隔更短的變化"]
S1 --> X
圖 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。
flowchart TB
accTitle: 到信任憑證為止的流程
accDescr: 表示第一次連線時用戶端拒絕伺服器的憑證並存到 rejected 存放區,確認指紋後移到 trusted 存放區,下次連線就會信任並能連線的圖。
A["第一次連線"] --> B["BadCertificateUntrusted"]
B --> C["把憑證存到 rejected 存放區"]
C --> D["和伺服器的管理者確認指紋"]
D --> E["移到 trusted 存放區"]
E --> F["下次連線時信任並能連線"]
圖 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。這次的實驗沒有試過這種情況。無論哪種情況,都要把斷線與重新連線的時間記錄下來,以便知道哪段時間沒有收到值。序列通訊設備的重新連線與記錄設計,也在〈序列通訊應用程式的陷阱〉中說明。
flowchart TB
accTitle: 停止伺服器時的記錄
accDescr: 表示訂閱中伺服器停止時,用戶端會重複重新連線,伺服器恢復後通知也會恢復,但停止期間的值不會送達,所以要把斷線與重新連線的時間記錄下來的圖。
A["訂閱中"] --> B["伺服器停止"]
B --> C["用戶端重複重新連線"]
C --> D["伺服器恢復、通知也恢復"]
B -.-> E["停止期間的值不會送達"]
E -.-> F["記錄斷線與重新連線的時間"]
圖 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 防火牆與業務應用程式〉。
flowchart TB
accTitle: 資料收集中容易卡關的地方
accDescr: 把設備資料收集中容易卡關的地方,分成記錄的內容(伺服器停止期間的值不會送達、要記錄哪一個時間戳記、不是 Good 的值的處理)與連線的準備(防火牆的連接埠、憑證的期限)來表示的圖。
A["資料收集中容易卡關的地方"] --> R["記錄的內容"]
A --> N["連線的準備"]
R --> B["停止期間的值不會送達"]
B ~~~ C["要用哪一個時間戳記"]
C ~~~ D["不是 Good 的值的處理"]
N --> E["防火牆的連接埠"]
E ~~~ F["憑證的期限"]
圖 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 裝置的方法〉。
flowchart TB
accTitle: 與設備通訊的選擇方式
accDescr: 表示設備有 OPC UA 的伺服器時以 OPC UA 瀏覽變數並收集,只有 Modbus/TCP 時以資料確認位址與格式並讀取暫存器,只有廠商的通訊時依照其資料的圖。
A["設備提供的通訊"] --> B{"有什麼?"}
B -->|"OPC UA 的伺服器"| C["瀏覽變數並收集"]
B -->|"只有 Modbus/TCP"| D["以資料確認位址與格式"]
B -->|"只有廠商的通訊"| E["依照廠商的資料與 SDK"]
D --> F["讀取暫存器並轉換成值"]
圖 11: 用什麼收集,多半取決於設備提供了什麼。
相關文章
- Dropped Frames with GigE Vision and USB3 Vision Cameras
- 序列通訊應用程式的陷阱 - 從重連到日誌設計
- 外部設備狀態檢查與顯示的最佳實務
- 在 Windows 應用程式中處理 USB 裝置的方法
- Windows 防火牆與業務應用程式
- Windows 憑證存放區實務指南
- Windows的時間同步(w32time)與業務系統
參考連結
-
OPC Foundation, Classic. OPC Classic 以 COM/DCOM 為基礎、DA、A&E、HDA 各規格,以及 2008 年 OPC UA 的公開與重新設計的理由。 ↩ ↩2
-
IEC, IEC TR 62541-1:2020 OPC Unified Architecture - Part 1: Overview and concepts. 處理 OPC UA 概念與概要的 IEC 62541 系列第 1 部(已由後續版本取代)。 ↩
-
IANA, Service Name and Transport Protocol Port Number Registry. 4840/TCP 的
opcua-tcp與 502/TCP 的mbap的登錄。 ↩ ↩2 ↩3 -
OPC Foundation, UA-.NETStandard 2.0.1: Getting started. 2.0 系列用戶端的寫法、
ReadValueAsync遇到 Bad 時擲出例外、拒絕與信任的存放區、伺服器預設不提供None,以及範例的連接埠號碼。 ↩ ↩2 ↩3 ↩4 ↩5 -
FreeOpcUa, asyncua(PyPI). 用於模擬設備伺服器的 Python OPC UA 程式庫。 ↩
-
OPC Foundation, OPC 10000-4 v1.05, 5.9.2 Browse. 沿著節點參照走的服務。 ↩
-
OPC Foundation, OPC 10000-3 v1.05, 8.2.2 NamespaceIndex. 在服務中以數值表示命名空間的 URI,該數值對應到 NamespaceArray 中的位置。 ↩
-
OPC Foundation, OPC 10000-4 v1.05, 5.11 Attribute Service Set. Read 與 Write 服務。 ↩
-
OPC Foundation, OPC 10000-4 v1.05, 7.11 DataValue. 值、狀態碼、SourceTimestamp、ServerTimestamp 的定義。 ↩ ↩2 ↩3
-
OPC Foundation, OPC 10000-4 v1.05, 7.38.1 StatusCode General. 上位 2 位元表示的 Good、Uncertain、Bad。 ↩ ↩2
-
OPC Foundation, OPC 10000-4 v1.05, 5.13.1.2 Sampling interval. 取樣間隔是盡力而為的週期。 ↩ ↩2
-
OPC Foundation, OPC 10000-4 v1.05, 7.22.2 DataChangeFilter. 沒有篩選時以值或狀態的變化通知。 ↩
-
OPC Foundation, OPC 10000-4 v1.05, 5.13.1.5 Queue parameters. 佇列大小、捨棄方式、Overflow 位元。 ↩ ↩2 ↩3
-
OPC Foundation, OPC 10000-4 v1.05, 5.14.1.1 Subscription model. 發布間隔與保持連線。 ↩
-
OPC Foundation, UA-.NETStandard 2.0.1: Subscriptions and Monitored Items Service Set.
SubscribeDataChangesAsync與MonitoredItemOptions的取樣間隔、佇列大小。 ↩ -
OPC Foundation, OPC 10000-4 v1.05, 7.20 MessageSecurityMode. None、Sign、SignAndEncrypt 的意義。 ↩
-
OPC Foundation, UA-.NETStandard 2.0.1: Certificates. 從 rejected 存放區移到信任的存放區的運作方式,以及不要在正式環境使用
AutoAcceptUntrustedCertificates。 ↩ ↩2 -
OPC Foundation, OPC 10000-4 v1.05, 7.40 UserIdentityToken parameters. 匿名、使用者名稱、X.509、已發行權杖 4 種。 ↩
-
OPC Foundation, OPC 10000-4 v1.05, 7.14 EndpointDescription. 端點接受的使用者權杖清單(userIdentityTokens)。 ↩
-
OPC Foundation, UA-.NETStandard 2.0.1: Sessions, Reconnection, and Subscription Engines.
ManagedSession內建重新連線的機制。 ↩ -
OPC Foundation, OPC 10000-4 v1.05, 5.14.6.1 Republish. 請伺服器從訂閱的重送用佇列再次送出通知訊息的服務。 ↩
-
Modbus-IDA, MODBUS Application Protocol Specification V1.1b(放在芝加哥大學伺服器上的副本). 4.3 節的資料模型(Discrete Input、Coil、Input Register、Holding Register)。 ↩
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
引數為什麼會壞掉 ── Windows 命令列引數的規則
在 Windows 上並不存在引數的陣列,傳給 CreateProcess 的是一條字串,分割由接收端負責。本文說明 CommandLineToArgvW・CRT・.NET 的分割規則,以及 .NET 的 ArgumentList 與 C++ 中正確的組裝方式。
多執行緒的實務最佳實踐 .NET篇 ── 在增加執行緒之前先決定的事
針對 .NET/C# 整理能防止「建立執行緒後偶爾當機、卡死」的設計定石。內容涵蓋不自行建立執行緒而改用 Task、減少共享可變狀態、鎖的紀律、以 CancellationToken 設計停止機制,以及 UI 執行緒的處理方式。
在 C# 與 PowerShell 中使用 WMI/CIM ── 硬體資訊取得、處理程序監控、遠端查詢實務指南
取得 PC 序號、監控磁碟可用空間、偵測處理程序啟動,這些需求的標準答案就是 WMI/CIM。本文說明 Get-CimInstance 等 CIM Cmdlet 的用法與從舊版 Get-WmiObject 的遷移、C# 中 System.Management 與 CIM A...
Windows I/O 的深層(第 4 回) ── 快取管理員:你的 WriteFile 究竟何時送達磁碟
本文是以圖解說明 Windows 快取管理員的系列第 4 回。整理了以檔案對應方式實作的快取、預先讀取與延遲寫入、FlushFileBuffers 與 FILE_FLAG_NO_BUFFERING 的分別使用時機,以及斷電導致資料消失的條件。
Windows I/O 的深層(第 3 回) ── I/O 完成埠(IOCP)與 .NET 執行緒集區:async/await 的地下室
本文是以圖解說明 I/O 完成埠(IOCP)的系列文章第 3 回。將完成佇列與執行緒數控制合而為一的設計、並行值與 LIFO 釋放、.NET 執行緒集區,直到 async/await 接續實際執行的執行緒為止,逐一整理。
與本主題相關的服務
本文連結到以下服務頁面,歡迎從最接近的入口查看。
Windows 應用程式開發
要製作從設備收集資料的 Windows 應用程式,並包含訂閱的設定、重新連線、時間戳記與狀態的記錄時,可以作為 Windows 應用程式開發來處理。
技術諮詢 & 設計審查
想決定要用 OPC UA 收集,還是使用 Modbus/TCP 或廠商的通訊,以及憑證要如何管理時,可以作為技術諮詢與設計審查來處理。
常見問題
整理諮詢這個主題時常見的問題。
- 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 防火牆中,中間有網路設備時要在該設備上,允許通往該連接埠的通訊。