ประวัติการแก้ไข (ฉบับแรก เผยแพร่เมื่อ 1 Nov 2026)
- เผยแพร่ครั้งแรก
อ้างอิงบทความนี้(DOI: 10.5281/zenodo.23968413)
บทความนี้ถูกเก็บถาวรใน Zenodo ด้านล่างมีทั้ง DOI ที่ชี้ไปยังฉบับล่าสุดเสมอ และ DOI ที่ผูกกับฉบับที่คุณกำลังอ่าน
Go Komura (2026). เริ่มต้น OPC UA ── เก็บข้อมูลจาก PLC และอุปกรณ์ด้วยแอป Windows. KomuraSoft LLC. https://doi.org/10.5281/zenodo.23968413 https://comcomponent.com/th/blog/opc-ua-windows-data-collection/
- DOI (ฉบับล่าสุด)
- 10.5281/zenodo.23968413
- DOI (ฉบับนี้)
- 10.5281/zenodo.23968414
เมื่อเก็บข้อมูลจาก PLC หรืออุปกรณ์เข้าแอป Windows ถ้าอุปกรณ์มีเซิร์ฟเวอร์ OPC UA ก็อาจไม่ต้องเขียนการสื่อสารแยกสำหรับผู้ผลิตแต่ละราย แต่แค่เชื่อมต่อแล้วอ่านค่ายังไม่พอ ถ้าไม่รู้เรื่องสถานะและเวลาประทับที่ส่งมาพร้อมค่า subscription ที่แจ้งการเปลี่ยนแปลง การอนุญาตการเชื่อมต่อด้วยใบรับรอง และวิธีกลับมาทำงานหลังการเชื่อมต่อขาด ข้อมูลที่เก็บมาก็จะเชื่อถือไม่ได้ในภายหลัง
บทความนี้อธิบายส่วนของ OPC UA ที่จำเป็นต่อการเก็บข้อมูล พร้อมผลที่ได้จากการรันไคลเอนต์ C# จริง ฝั่งไคลเอนต์ใช้ไลบรารี .NET ของ OPC Foundation และอีกฝั่งใช้เซิร์ฟเวอร์อุปกรณ์จำลอง ยังไม่ได้ทดสอบกับ PLC หรืออุปกรณ์รุ่นใดโดยเฉพาะ การตั้งค่าของแต่ละอุปกรณ์ให้ตรวจสอบจากเอกสารของอุปกรณ์นั้น
เรื่องการเก็บภาพจากกล้อง GigE Vision และ USB3 Vision อยู่ใน “Dropped Frames with GigE Vision and USB3 Vision Cameras” และเรื่องอุปกรณ์ที่สื่อสารแบบอนุกรมอยู่ใน “Serial Communication App Pitfalls”
ความแตกต่างระหว่าง OPC UA กับ OPC Classic
ข้อกำหนดของ OPC Classic ใช้ COM/DCOM ของ Windows ในการแลกเปลี่ยนข้อมูลระหว่างส่วนประกอบซอฟต์แวร์ โดยกำหนดเป็นข้อกำหนดแยกกัน ได้แก่ OPC DA สำหรับข้อมูลกระบวนการ OPC A&E สำหรับการแจ้งเตือน และ OPC HDA สำหรับข้อมูลย้อนหลัง1
OPC Foundation เผยแพร่ OPC UA (Unified Architecture) ในปี 2008 OPC UA เป็นสถาปัตยกรรมแบบ service-oriented ที่ไม่ขึ้นกับแพลตฟอร์มใดโดยเฉพาะ และรวมความสามารถของข้อกำหนดแต่ละฉบับของ 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 ทำงานบน COM และ DCOM ของ Windows และแยกเป็นข้อกำหนด 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 บน PC เครื่องเดียวกัน) |
| ระบบปฏิบัติการ | Linux (ยังไม่ได้ทดสอบการรันบน Windows) |
ไลบรารีฝั่งไคลเอนต์เป็นการ implement สำหรับ .NET ที่ OPC Foundation เผยแพร่ วิธีใช้ของรุ่น 2.0 เขียนตามแบบใน Getting started ของ repository เดียวกัน4
flowchart TB
accTitle: การตั้งค่าที่ใช้ทดสอบ
accDescr: แผนภาพแสดงว่าไคลเอนต์ C# ใช้ไลบรารี .NET ของ OPC Foundation และเชื่อมต่อแบบเข้ารหัสไปยังเซิร์ฟเวอร์อุปกรณ์จำลองของ asyncua ที่รอรับอยู่บนพอร์ต 4840
A["ไคลเอนต์ C# (LineCollector)"] -->|"opc.tcp, SignAndEncrypt"| B["เซิร์ฟเวอร์อุปกรณ์จำลอง (asyncua)"]
A -.-> L["ไลบรารี .NET ของ OPC Foundation 2.0.1"]
B -.-> V["ตัวแปร 4 ตัวของ Line1"]
ภาพ 2: เชื่อมต่อจากไคลเอนต์ C# ไปยังเซิร์ฟเวอร์อุปกรณ์จำลองภายใน PC เครื่องเดียวกัน
เซิร์ฟเวอร์อุปกรณ์จำลอง
แทนที่จะใช้อุปกรณ์จริง ได้สร้างเซิร์ฟเวอร์ขนาดเล็กด้วย asyncua5 ใต้ออบเจ็กต์ชื่อ Line1 มีตัวแปร 4 ตัว ได้แก่ อุณหภูมิ (Temperature) จำนวนชิ้นงานที่ผลิต (PartCount) สถานะกำลังทำงาน (Running) และค่าที่ตั้งไว้ (Setpoint) อุณหภูมิเปลี่ยนทุก 0.5 วินาที และจำนวนชิ้นงานเปลี่ยนทุก 2 วินาที ด้านความปลอดภัยเตรียมไว้ 2 แบบ คือไม่เข้ารหัส (None) และลงนามพร้อมเข้ารหัส (SignAndEncrypt ของ Basic256Sha256)
# อุปกรณ์จำลองสำหรับทดสอบ ไม่ได้ใช้แทนอุปกรณ์หรือ 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#
ในไฟล์โปรเจกต์ใส่แพ็กเกจ 2 ตัวต่อไปนี้
<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>
ไคลเอนต์ทำการเชื่อมต่อ browse อ่าน เขียน และ subscribe ตามลำดับ ความหมายของแต่ละส่วนจะอธิบายทีละส่วนในหัวข้อถัดไป
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);
// Browse: แสดงรายการตัวแปรที่อยู่ใต้ 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}");
}
// Read: ดูไม่เฉพาะค่า แต่ดูสถานะและเวลาประทับทั้งสองด้วย
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}");
}
// Write: ตั้ง 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]}");
// Subscribe: ขอให้เซิร์ฟเวอร์สุ่มตัวอย่างทุก 500 ms และแจ้งเมื่อมีการเปลี่ยนแปลง
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
ข้อมูลของอุปกรณ์ปรากฏเป็นโหนดใน address space
เซิร์ฟเวอร์ OPC UA เปิดเผยข้อมูลที่มีอยู่ในรูปของกลุ่มโหนด กลุ่มนี้เรียกว่า address space โหนดมีหลายชนิด เช่น ออบเจ็กต์ที่แทนโฟลเดอร์หรืออุปกรณ์ และตัวแปรที่เก็บค่า โหนดเชื่อมกันด้วย reference ไคลเอนต์ใช้บริการ Browse เพื่อไล่ตาม reference และตรวจดูว่ามีโหนดอะไรบ้าง6
ในผลลัพธ์ข้างต้น ไคลเอนต์ไล่ตาม reference แบบลำดับชั้นจาก Line1 และพบตัวแปร 4 ตัว
flowchart TB
accTitle: Address space ของอุปกรณ์จำลอง
accDescr: แผนภาพแสดงว่าใต้โฟลเดอร์ Objects มีออบเจ็กต์ Line1 และใต้ Line1 มีตัวแปร 4 ตัว ได้แก่ Temperature, PartCount, Running และ Setpoint
O["Objects"] --> L["Line1 (ออบเจ็กต์)"]
L --> T["Temperature (ตัวแปร)"]
L --> P["PartCount (ตัวแปร)"]
L --> R["Running (ตัวแปร)"]
L --> S["Setpoint (ตัวแปร เขียนได้)"]
ภาพ 3: Address space ของอุปกรณ์จำลอง ไคลเอนต์ใช้ Browse ไล่ตาม reference เพื่อหาโหนด
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) หาเลข 2 จาก URI แล้วนำไปสร้าง NodeId
flowchart TB
accTitle: ขั้นตอนการสร้าง NodeId จาก URI ของเนมสเปซ
accDescr: แผนภาพแสดงว่าไคลเอนต์นำ URI ของเนมสเปซไปค้นใน NamespaceArray ของเซิร์ฟเวอร์เพื่อได้ดัชนี แล้วรวมดัชนีนั้นกับตัวระบุเป็น NodeId
U["URI ของเนมสเปซ"] --> A["NamespaceArray ของเซิร์ฟเวอร์"]
A --> I["ดัชนี (ในตัวอย่างนี้คือ 2)"]
I --> N["NodeId ns=2;s=Line1.Temperature"]
K["ตัวระบุ Line1.Temperature"] --> N
ภาพ 4: ค้นดัชนีของเนมสเปซจาก URI แล้วรวมกับตัวระบุเป็น NodeId
การอ่านและเขียนจะได้สถานะและเวลาประทับมาพร้อมค่า
ค่าของตัวแปรอ่านและเขียนด้วยบริการ Read และ Write8 สิ่งที่ได้จากการอ่านไม่ได้มีแค่ค่า แต่ได้โครงสร้างชื่อ DataValue ซึ่งมีค่า รหัสสถานะ source timestamp และ server timestamp9
| รายการ | ความหมาย |
|---|---|
| ค่า | ค่าของตัวแปร เมื่อรหัสสถานะบ่งบอกข้อผิดพลาดให้ไม่สนใจค่านี้ และเซิร์ฟเวอร์จะตั้งเป็น null |
| รหัสสถานะ (StatusCode) | เซิร์ฟเวอร์ได้มาและส่งค่านั้นได้หรือไม่ |
| SourceTimestamp | เวลาที่ต้นทางของข้อมูลประทับให้กับค่า |
| ServerTimestamp | เวลาที่เซิร์ฟเวอร์ได้รับค่า หรือรู้ว่าค่านั้นถูกต้อง |
บิตบนสุด 2 บิตของรหัสสถานะบอก Good (สำเร็จ) Uncertain (สำเร็จบางส่วน อาจใช้ไม่ได้ขึ้นกับการใช้งาน) และ Bad (ล้มเหลว)10 ค่า Good [0x00000000] ในผลลัพธ์คือ Good
เมื่ออ่าน Line1.Pressure ที่ไม่มีอยู่ ได้ BadNodeIdUnknown [0x80340000] แทนค่า ReadValueAsync ของไลบรารีนี้จะโยน exception เมื่อสถานะเป็น Bad4 ในระบบที่บันทึกค่า ถ้าเก็บรหัสสถานะไว้คู่กับค่าด้วย ภายหลังก็แยกได้ว่าค่าใดถูกบันทึกในช่วงที่เป็น Bad
flowchart TB
accTitle: สิ่งที่อยู่ใน DataValue
accDescr: แผนภาพแสดงว่า DataValue ที่ได้จากการอ่านหรือการแจ้งเตือนมี 4 อย่าง ได้แก่ ค่า รหัสสถานะ source timestamp และ server timestamp และถ้ารหัสสถานะเป็น Bad จะไม่สนใจค่า
D["DataValue"] --> V["ค่า"]
D --> S["รหัสสถานะ"]
D --> TS["SourceTimestamp (เวลาของต้นทาง)"]
D --> TV["ServerTimestamp (เวลาของเซิร์ฟเวอร์)"]
S -->|"ถ้าเป็น Bad"| X["ไม่สนใจค่า"]
ภาพ 5: ผลการอ่านไม่ได้มีแค่ค่า แต่มีรหัสสถานะและเวลาประทับ 2 ค่า
ในการเขียน ได้เขียน 32.5 ลงใน Setpoint และได้ Good เป็นรหัสสถานะของผลลัพธ์ ตัวแปรใดเขียนได้นั้นเซิร์ฟเวอร์เป็นผู้กำหนด เซิร์ฟเวอร์จำลองนี้ให้เขียนได้เฉพาะ Setpoint การจะยอมให้เปลี่ยนค่าที่ตั้งไว้ของอุปกรณ์จากภายนอกหรือไม่ เกี่ยวข้องกับการออกแบบด้านความปลอดภัยของอุปกรณ์ จึงอยู่นอกขอบเขตของบทความนี้
ให้แจ้งการเปลี่ยนแปลง: subscription และ MonitoredItem
เมื่อต้องการติดตามการเปลี่ยนแปลงของค่า ไคลเอนต์อาจ Read ซ้ำเป็นช่วงเวลาคงที่ก็ได้ แต่ OPC UA ยังมีอีกกลไกหนึ่ง คือฝากการเฝ้าดูค่าไว้กับเซิร์ฟเวอร์และให้แจ้งการเปลี่ยนแปลงกลับมา นั่นคือ subscription และ MonitoredItem
MonitoredItem คือเป้าหมายที่เฝ้าดูหนึ่งรายการ และมี sampling interval sampling interval คือช่วงเวลาที่เร็วที่สุดที่เซิร์ฟเวอร์สุ่มตัวอย่างค่าจากต้นทาง และข้อกำหนดเรียกว่าเป็นรอบแบบ “best effort”11 เมื่อตัวอย่างผ่านเงื่อนไขของตัวกรอง จะเกิดการแจ้งเตือน ถ้าไม่ระบุตัวกรอง จะแจ้งเมื่อค่าหรือสถานะเปลี่ยน12
การแจ้งเตือนจะเข้าคิวของ MonitoredItem ก่อน เมื่อคิวเต็มจะทิ้งรายการเก่าหรือใหม่ตามที่กำหนดไว้ และถ้าขนาดคิวตั้งแต่ 2 ขึ้นไป บิต Overflow ที่บอกว่ามีการทิ้งจะถูกตั้งในรหัสสถานะ13
Subscription รวม MonitoredItem หลายตัวไว้ด้วยกัน และส่งการแจ้งเตือนที่ยังไม่ได้ส่งไปให้ไคลเอนต์ทุก publishing interval ถ้ารอบที่ไม่มีการแจ้งเตือนจะส่งเกิดต่อเนื่องกัน ก็จะส่ง keep-alive เพื่อบอกว่า subscription ยังทำงานอยู่14
flowchart TB
accTitle: ขั้นตอนของ subscription และ MonitoredItem
accDescr: แผนภาพแสดงว่า MonitoredItem สุ่มตัวอย่างค่าจากต้นทางตาม sampling interval ใส่การแจ้งเตือนที่ผ่านตัวกรองลงคิว จากนั้น subscription รวมส่งไปยังไคลเอนต์ตาม publishing interval และถ้าไม่มีการแจ้งเตือนจะส่ง keep-alive
S["ต้นทาง (ค่าของอุปกรณ์)"] --> M["MonitoredItem"]
M -->|"Sampling interval"| F["ตัวกรอง (ค่าเริ่มต้นคือค่าหรือสถานะเปลี่ยน)"]
F --> Q["คิว (ถ้าเต็มจะเกิด Overflow)"]
Q --> SUB["Subscription"]
SUB -->|"Publishing interval"| C["ไคลเอนต์"]
SUB -.->|"ถ้าไม่มีการแจ้งเตือน"| K["Keep-alive"]
ภาพ 6: MonitoredItem สุ่มตัวอย่างและเก็บไว้ในคิว แล้ว subscription รวมส่งไปยังไคลเอนต์
ในโค้ดข้างต้นได้ subscribe Temperature และ PartCount ด้วย sampling interval 500 มิลลิวินาที และขนาดคิว 515 ในผลลัพธ์ อุณหภูมิมาถึงประมาณทุก 0.5 วินาที และจำนวนชิ้นงานมาถึงประมาณทุก 2 วินาที 2 รายการแรกคือค่าที่ตัวแปรแต่ละตัวมีอยู่ ณ เวลาที่เริ่ม subscribe ส่วน 13:40:50.801 ของจำนวนชิ้นงานคือเวลาที่ค่าเปลี่ยนก่อนเริ่ม subscribe เวลาของการแจ้งเตือนแสดงด้วย source timestamp จึงเป็นเวลาที่ค่าเปลี่ยน ไม่ใช่เวลาที่ได้รับการแจ้งเตือน
เลือกใช้การอ่านซ้ำหรือ subscription
การอ่านซ้ำทำให้ฝั่งไคลเอนต์กำหนดช่วงเวลาได้ และกลไกเรียบง่าย แต่ถึงค่าไม่เปลี่ยน คำขอและคำตอบก็ยังวิ่งไปมาทุกครั้ง และพลาดการเปลี่ยนแปลงที่สั้นกว่าช่วงเวลา ส่วน subscription เซิร์ฟเวอร์สุ่มตัวอย่างแล้วส่งเฉพาะการเปลี่ยนแปลง การสื่อสารจึงไม่ค่อยเพิ่มแม้ค่าจะเปลี่ยนน้อย อย่างไรก็ตาม แม้ใช้ subscription การเปลี่ยนแปลงที่สั้นกว่า sampling interval ก็อาจไม่ถูกแจ้ง ข้อกำหนดอนุญาตให้สุ่มตัวอย่างเร็วกว่าที่ระบุ แต่ไม่ได้บังคับ11
flowchart TB
accTitle: ความแตกต่างระหว่างการอ่านซ้ำกับ subscription
accDescr: แผนภาพแสดงว่าการอ่านซ้ำนั้นไคลเอนต์กำหนดช่วงเวลาและส่งคำขอทุกครั้ง ส่วน subscription เซิร์ฟเวอร์สุ่มตัวอย่างตาม sampling interval และส่งเฉพาะการเปลี่ยนแปลง และทั้งสองวิธีอาจเก็บการเปลี่ยนแปลงที่สั้นกว่าช่วงเวลาไม่ได้
A["ติดตามการเปลี่ยนแปลงของค่า"] --> R["อ่านซ้ำ"]
A --> S["Subscribe"]
R --> R1["ไคลเอนต์กำหนดช่วงเวลาและขอทุกครั้ง"]
S --> S1["เซิร์ฟเวอร์สุ่มตัวอย่างและส่งเฉพาะการเปลี่ยนแปลง"]
R1 --> X["อาจเก็บการเปลี่ยนแปลงที่สั้นกว่าช่วงเวลาไม่ได้"]
S1 --> X
ภาพ 7: ไม่ว่าวิธีใด ก็อาจเก็บการเปลี่ยนแปลงที่สั้นกว่าช่วงเวลาไม่ได้
อนุญาตการเชื่อมต่อด้วยใบรับรอง
ในการเชื่อมต่อ OPC UA ระดับการป้องกันข้อความเลือกด้วย MessageSecurityMode ค่าที่มีคือ None ที่ไม่ป้องกัน Sign ที่ลงนามอย่างเดียว และ SignAndEncrypt ที่ลงนามและเข้ารหัส16 ชุดอัลกอริทึมที่ใช้ลงนามและเข้ารหัสกำหนดด้วย security policy และในบทความนี้ใช้ Basic256Sha256
การลงนามและการเข้ารหัสใช้ใบรับรองของแต่ละแอปพลิเคชัน ไคลเอนต์และเซิร์ฟเวอร์ตรวจสอบใบรับรองของกันและกัน และปฏิเสธการเชื่อมต่อกับฝ่ายที่ไม่เชื่อถือ เซิร์ฟเวอร์ที่สร้างด้วยไลบรารี .NET ของ OPC Foundation จะไม่เปิดให้ใช้นโยบาย 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 แล้วรันใหม่ จึงเชื่อมต่อได้ ก่อนย้ายให้ตรวจ thumbprint ซึ่งอยู่ในชื่อไฟล์ด้วย กับผู้ดูแลเซิร์ฟเวอร์4
flowchart TB
accTitle: ขั้นตอนจนถึงการเชื่อถือใบรับรอง
accDescr: แผนภาพแสดงว่าในการเชื่อมต่อครั้งแรก ไคลเอนต์ปฏิเสธใบรับรองของเซิร์ฟเวอร์และเก็บไว้ในที่เก็บ rejected และเมื่อตรวจ thumbprint แล้วย้ายไปยังที่เก็บ trusted การเชื่อมต่อครั้งถัดไปจะเชื่อถือและเชื่อมต่อได้
A["การเชื่อมต่อครั้งแรก"] --> B["BadCertificateUntrusted"]
B --> C["เก็บใบรับรองไว้ในที่เก็บ rejected"]
C --> D["ตรวจ thumbprint กับผู้ดูแลเซิร์ฟเวอร์"]
D --> E["ย้ายไปยังที่เก็บ trusted"]
E --> F["ครั้งถัดไปเชื่อถือและเชื่อมต่อได้"]
ภาพ 8: เมื่อตรวจใบรับรองที่ถูกปฏิเสธแล้วย้ายไปยัง trusted ไคลเอนต์จะเชื่อถือฝ่ายนั้น
ยังมีการตั้งค่าชื่อ AutoAcceptUntrustedCertificates ที่รับใบรับรองที่ไม่เชื่อถือโดยอัตโนมัติ ค่าเริ่มต้นคือ false และเอกสารของไลบรารีเขียนว่ามีประโยชน์ในสภาพแวดล้อมการพัฒนา แต่ไม่ควรใช้ในระบบจริง17
NotAfter=2027-11-01 ในล็อกคือวันหมดอายุของใบรับรองของเซิร์ฟเวอร์จำลอง เป็นใบรับรองแบบ self-signed ที่สร้างด้วยฟังก์ชันของ asyncua และหมดอายุ 1 ปีหลังวันที่สร้าง ใบรับรองของอุปกรณ์ก็มีวันหมดอายุเช่นกัน จึงควรกำหนดขั้นตอนในการปฏิบัติงานไว้ว่าจะเปลี่ยนใบรับรองก่อนหมดอายุและให้อีกฝ่ายเชื่อถืออย่างไร การจัดการที่เก็บใบรับรองของ Windows อยู่ใน “คู่มือปฏิบัติที่เก็บใบรับรองของ Windows”
การยืนยันตัวตนของผู้ใช้
สิ่งที่ตรวจสอบด้วยใบรับรองคือแอปพลิเคชันกับแอปพลิเคชัน นอกจากนั้น เมื่อเปิดเซสชันจะส่งโทเค็นที่แทนผู้ใช้ด้วย ข้อกำหนดมี 4 แบบ ได้แก่ anonymous ชื่อผู้ใช้และรหัสผ่าน ใบรับรอง X.509 และโทเค็นที่ออกโดยภายนอก18 ไคลเอนต์ครั้งนี้เชื่อมต่อแบบ anonymous เซิร์ฟเวอร์รับโทเค็นแบบใดนั้นอยู่ในข้อมูล endpoint ของเซิร์ฟเวอร์19
จุดที่มักติดขัดในการเก็บข้อมูลหน้างาน
จากนี้จะยกปัญหาที่มักเกิดในระบบเก็บข้อมูลจากอุปกรณ์ โดยอิงจากการทดลองครั้งนี้และข้อกำหนด
ค่าในช่วงที่เซิร์ฟเวอร์หยุดทำงานจะไม่มาถึง
ระหว่าง subscribe ได้หยุดเซิร์ฟเวอร์ แล้วเริ่มใหม่หลังจากประมาณ 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.
เมื่อเซิร์ฟเวอร์กลับมา การแจ้งเตือนก็กลับมาด้วย นี่คือส่วนหนึ่งของผลลัพธ์ของ subscription
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 ถึง 13:41:34 ไม่มาถึงเลยแม้แต่รายการเดียว จำนวนชิ้นงานกลับจาก 78 เป็น 2 เนื่องจากเซิร์ฟเวอร์จำลองถูกเริ่มใหม่และนับใหม่ตั้งแต่ 0
ในตัวอย่างนี้ตัวเซิร์ฟเวอร์เองหยุดทำงาน ค่าในช่วงนั้นจึงไม่เหลืออยู่ที่ใดเลย กลไกการเชื่อมต่อใหม่จะคืนการเชื่อมต่อให้ แต่ไม่ได้เติมค่าในช่วงที่หยุดย้อนหลังให้
ถ้าขาดเพียงการสื่อสารและเซิร์ฟเวอร์ยังทำงานอยู่ จะต่างออกไป ข้อกำหนดมีบริการ Republish ที่ขอให้เซิร์ฟเวอร์ส่งข้อความแจ้งเตือนที่ส่งไม่ถึงซ้ำจากคิวสำหรับส่งซ้ำ21 คิวของ MonitoredItem ก็สะสมการแจ้งเตือนไว้ แต่ส่วนที่เกินขนาดคิวจะถูกทิ้ง13 การทดลองครั้งนี้ไม่ได้ทดสอบกรณีนี้ ไม่ว่ากรณีใด ให้บันทึกเวลาที่การเชื่อมต่อขาดและเวลาที่เชื่อมต่อใหม่ไว้ เพื่อให้รู้ว่าค่าไม่มาในช่วงเวลาใด การเชื่อมต่อใหม่และการออกแบบล็อกของอุปกรณ์ที่สื่อสารแบบอนุกรมกล่าวถึงใน “Serial Communication App Pitfalls” ด้วย
flowchart TB
accTitle: การบันทึกเมื่อหยุดเซิร์ฟเวอร์
accDescr: แผนภาพแสดงว่าเมื่อเซิร์ฟเวอร์หยุดระหว่าง subscribe ไคลเอนต์จะพยายามเชื่อมต่อใหม่ซ้ำ เมื่อเซิร์ฟเวอร์กลับมาการแจ้งเตือนก็กลับมา แต่ค่าในช่วงที่หยุดไม่มาถึง จึงต้องบันทึกเวลาที่ขาดและเวลาที่เชื่อมต่อใหม่
A["กำลัง subscribe"] --> B["เซิร์ฟเวอร์หยุด"]
B --> C["ไคลเอนต์พยายามเชื่อมต่อใหม่ซ้ำ"]
C --> D["เซิร์ฟเวอร์กลับมาและการแจ้งเตือนกลับมา"]
B -.-> E["ค่าในช่วงที่หยุดไม่มาถึง"]
E -.-> F["บันทึกเวลาที่ขาดและเวลาที่เชื่อมต่อใหม่"]
ภาพ 9: การเชื่อมต่อใหม่คืนการเชื่อมต่อให้ แต่ค่าในช่วงที่เซิร์ฟเวอร์หยุดไม่มาถึง
ควรบันทึกเวลาประทับตัวใด
ข้อกำหนดระบุว่า SourceTimestamp คือเวลาที่ต้นทางของข้อมูลประทับให้กับค่า และควรแสดงเวลาที่ค่าเปลี่ยน ส่วน ServerTimestamp คือเวลาที่เซิร์ฟเวอร์ได้รับค่าหรือรู้ว่าค่านั้นถูกต้อง9 เซิร์ฟเวอร์จำลองครั้งนี้สร้างค่าขึ้นเอง ทั้งสองเวลาจึงตรงกัน
ถ้าต้องการรู้เวลาที่ค่าเปลี่ยนในอุปกรณ์ ให้ใช้ SourceTimestamp แต่เวลานั้นประทับด้วยนาฬิกาของต้นทาง ถ้านาฬิกาของอุปกรณ์หรือเซิร์ฟเวอร์คลาดเคลื่อน บันทึกก็คลาดเคลื่อนตาม ถ้าจะนำข้อมูลที่เก็บมาไปเทียบกับบันทึกอื่น ให้เก็บเวลาของ PC ที่รับข้อมูลไว้คู่กันด้วย เพื่อตรวจความคลาดเคลื่อนได้ภายหลัง การซิงค์เวลาฝั่ง Windows อยู่ใน “Windows Time Synchronization (w32time) and Business Systems”
อย่าใช้ค่าที่สถานะไม่ใช่ Good เป็นตัวเลขโดยตรง
เมื่อรหัสสถานะเป็น Bad ให้ไม่สนใจค่า9 เมื่อเป็น Uncertain ค่าอาจใช้ไม่ได้ขึ้นกับการใช้งาน10 ก่อนนำไปใส่กราฟหรือรวมผล ให้คัดแยกด้วยรหัสสถานะ และกำหนดว่าจะจัดการค่าที่ไม่ใช่ Good อย่างไร บิต Overflow เมื่อคิวเต็มก็ถูกตั้งไว้ในรหัสสถานะด้วย13 การแสดงสถานะของอุปกรณ์บนหน้าจอกล่าวถึงใน “Best Practices for Checking and Displaying External Device State”
เปิดพอร์ตในไฟร์วอลล์
พอร์ตของ OPC UA ที่ลงทะเบียนกับ IANA คือ 4840/TCP3 แต่ไม่ใช่ทุกเซิร์ฟเวอร์ที่ใช้หมายเลขนี้ ตัวอย่าง .NET ของ OPC Foundation ใช้พอร์ตอย่าง 625414 ให้ตรวจหมายเลขพอร์ตจาก URL ของ endpoint ของอุปกรณ์ และอนุญาตการเชื่อมต่อไปยังพอร์ตนั้น ถ้าเซิร์ฟเวอร์ทำงานบน Windows ให้อนุญาตใน Windows Firewall ของเครื่องนั้น และถ้ามีอุปกรณ์เครือข่ายอยู่ระหว่างทาง ให้อนุญาตที่อุปกรณ์นั้น วิธีสร้างกฎขาเข้าอยู่ใน “ไฟร์วอลล์ Windows กับแอปธุรกิจ”
flowchart TB
accTitle: จุดที่มักติดขัดในการเก็บข้อมูล
accDescr: แผนภาพแบ่งจุดที่มักติดขัดในการเก็บข้อมูลจากอุปกรณ์เป็น 2 กลุ่ม คือเนื้อหาของบันทึก (ค่าในช่วงที่เซิร์ฟเวอร์หยุดไม่มาถึง ควรบันทึกเวลาประทับตัวใด และการจัดการค่าที่ไม่ใช่ 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 ตาราง ได้แก่ Discrete Input ขนาด 1 บิตที่อ่านได้อย่างเดียว Coil ขนาด 1 บิตที่อ่านและเขียนได้ Input Register ขนาด 16 บิตที่อ่านได้อย่างเดียว และ Holding Register ขนาด 16 บิตที่อ่านและเขียนได้22 IANA ลงทะเบียน 502/TCP ไว้สำหรับโปรโตคอลแอปพลิเคชันของ Modbus (mbap)3
ตารางเหล่านี้เก็บได้เพียงบิตและเวิร์ดขนาด 16 บิต จึงต้องตรวจจากเอกสารของอุปกรณ์ว่าแอดเดรสใดเก็บค่าอะไรในรูปแบบใด แล้วเขียนไว้ฝั่งไคลเอนต์ ส่วน OPC UA อย่างที่เห็นมา เซิร์ฟเวอร์เปิดเผยตัวแปรที่มีชื่อไว้ใน address space และไคลเอนต์ตรวจดูได้ด้วย Browse ค่ามีรหัสสถานะและเวลาประทับมาด้วย และการเชื่อมต่อมีการอนุญาตด้วยใบรับรอง
จะใช้แบบใดมักขึ้นอยู่กับว่าอุปกรณ์มีอะไรให้ ถ้าอุปกรณ์มีเซิร์ฟเวอร์ OPC UA ก็ใช้ OPC UA ถ้ามีแต่ Modbus/TCP ก็ทำตารางจับคู่แอดเดรสแล้วอ่าน ถ้ามีเพียงการสื่อสารหรือ SDK ของผู้ผลิต ก็ทำตามเอกสารนั้น การเลือกวิธีสื่อสารกับอุปกรณ์ USB อยู่ใน “How to Work with USB Devices from a Windows App”
flowchart TB
accTitle: วิธีเลือกการสื่อสารกับอุปกรณ์
accDescr: แผนภาพแสดงว่าถ้าอุปกรณ์มีเซิร์ฟเวอร์ OPC UA ให้ browse ตัวแปรด้วย OPC UA แล้วเก็บข้อมูล ถ้ามีแต่ Modbus/TCP ให้ตรวจแอดเดรสและรูปแบบจากเอกสารแล้วอ่านรีจิสเตอร์ และถ้ามีแต่การสื่อสารของผู้ผลิตให้ทำตามเอกสารของผู้ผลิต
A["การสื่อสารที่อุปกรณ์มีให้"] --> B{"มีอะไรบ้าง"}
B -->|"เซิร์ฟเวอร์ OPC UA"| C["Browse ตัวแปรแล้วเก็บข้อมูล"]
B -->|"Modbus/TCP เท่านั้น"| D["ตรวจแอดเดรสและรูปแบบจากเอกสาร"]
B -->|"การสื่อสารของผู้ผลิตเท่านั้น"| E["ทำตามเอกสารและ SDK ของผู้ผลิต"]
D --> F["อ่านรีจิสเตอร์แล้วแปลงเป็นค่า"]
ภาพ 11: จะเก็บข้อมูลด้วยวิธีใด มักขึ้นอยู่กับว่าอุปกรณ์มีอะไรให้
บทความที่เกี่ยวข้อง
- Dropped Frames with GigE Vision and USB3 Vision Cameras
- Serial Communication App Pitfalls
- Best Practices for Checking and Displaying External Device State
- How to Work with USB Devices from a Windows App
- ไฟร์วอลล์ Windows กับแอปธุรกิจ
- คู่มือปฏิบัติที่เก็บใบรับรองของ Windows
- Windows Time Synchronization (w32time) and Business Systems
เอกสารอ้างอิง
-
OPC Foundation, Classic. OPC Classic อยู่บนพื้นฐานของ COM/DCOM ข้อกำหนด DA, A&E และ HDA การเผยแพร่ OPC UA ในปี 2008 และเหตุผลของการสร้างใหม่ ↩ ↩2
-
IEC, IEC TR 62541-1:2020 OPC Unified Architecture - Part 1: Overview and concepts. ส่วนที่ 1 ของชุด IEC 62541 ที่กล่าวถึงแนวคิดและภาพรวมของ OPC UA (ถูกแทนที่ด้วยฉบับใหม่แล้ว) ↩
-
IANA, Service Name and Transport Protocol Port Number Registry. การลงทะเบียน
opcua-tcpบน 4840/TCP และmbapบน 502/TCP ↩ ↩2 ↩3 -
OPC Foundation, UA-.NETStandard 2.0.1: Getting started. วิธีเขียนไคลเอนต์รุ่น 2.0 การที่
ReadValueAsyncโยน exception เมื่อเป็น Bad ที่เก็บ rejected และ trusted การที่เซิร์ฟเวอร์ไม่เปิดNoneโดยค่าเริ่มต้น และหมายเลขพอร์ตของตัวอย่าง ↩ ↩2 ↩3 ↩4 ↩5 -
FreeOpcUa, asyncua (PyPI). ไลบรารี OPC UA ของ Python ที่ใช้สร้างเซิร์ฟเวอร์อุปกรณ์จำลอง ↩
-
OPC Foundation, OPC 10000-4 v1.05, 5.9.2 Browse. บริการที่ไล่ตาม reference ของโหนด ↩
-
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. Good, Uncertain และ Bad ที่บอกด้วยบิตบนสุด 2 บิต ↩ ↩2
-
OPC Foundation, OPC 10000-4 v1.05, 5.13.1.2 Sampling interval. sampling interval เป็นรอบแบบ best effort ↩ ↩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. Publishing interval และ keep-alive ↩
-
OPC Foundation, UA-.NETStandard 2.0.1: Subscriptions and Monitored Items Service Set.
SubscribeDataChangesAsyncและ sampling interval กับขนาดคิวใน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. 4 แบบ ได้แก่ anonymous ชื่อผู้ใช้ X.509 และโทเค็นที่ออกให้ ↩
-
OPC Foundation, OPC 10000-4 v1.05, 7.14 EndpointDescription. รายการโทเค็นผู้ใช้ที่ endpoint รับ (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. บริการที่ขอให้ส่งข้อความแจ้งเตือนซ้ำจากคิวสำหรับส่งซ้ำของ subscription ↩
-
Modbus-IDA, MODBUS Application Protocol Specification V1.1b (สำเนาที่วางไว้บนเซิร์ฟเวอร์ของมหาวิทยาลัยชิคาโก). โมเดลข้อมูลในหัวข้อ 4.3 (Discrete Input, Coil, Input Register, Holding Register) ↩
บทความที่เกี่ยวข้อง
บทความล่าสุดที่มีแท็กเดียวกัน เพื่อเจาะลึกหัวข้อใกล้เคียง
แนวปฏิบัติมัลติเธรดเชิงปฏิบัติ: ฉบับ .NET — สิ่งที่ต้องตัดสินก่อนเพิ่มเธรด
เรียบเรียงหลักออกแบบที่กันไม่ให้โค้ดมัลติเธรด .NET/C# «แครชหรือค้างเป็นครั้งคราว»: อย่าสร้างเธรดเอง ให้ขี่ Task ลดสถานะที่เปลี่ยนแปลงร่วม...
ใช้ WMI/CIM จาก C# และ PowerShell — คู่มือปฏิบัติการดึงข้อมูลฮาร์ดแวร์ เฝ้าโพรเซส และสอบถามระยะไกล
หมายเลขซีเรียลของพีซี การเฝ้าพื้นที่ว่างดิสก์ และการตรวจจับการเริ่มโพรเซส คำตอบมาตรฐานคือ WMI/CIM บทความนี้อธิบายการใช้ cmdlet CIM เช่น G...
เชิงลึกของ Windows I/O (ตอนที่ 4) — Cache Manager: WriteFile ของคุณไปถึงดิสก์เมื่อใด
ตอนที่ 4 ของซีรีส์ที่อธิบาย Cache Manager ของ Windows ด้วยแผนภาพ จัดแคชที่อิมพลีเมนต์เป็นการแมปไฟล์ การอ่านล่วงหน้ากับการเขียนแบบหน่วง กา...
Named Pipe ในทางปฏิบัติ — IPC มาตรฐานของ Windows ตั้งแต่การออกแบบถึงความปลอดภัย
คู่มือเชิงปฏิบัติเรื่อง named pipe ซึ่งเป็นการสื่อสารระหว่างโปรเซสมาตรฐานของ Windows บทความนี้จัดระเบียบจากแหล่งปฐมภูมิ ทั้งการเลือกระหว่...
Spurious Wakeup — ทำไม condition variable จึงตื่น "โดยไม่ถูกแจ้ง" และวิธีรออย่างถูกต้องบน Windows
การรอของ condition variable สามารถกลับได้แม้ไม่มีการแจ้งมาถึง (spurious wakeup) บทความนี้อธิบายจากการอิมพลีเมนต์ของ Windows ว่าทำไมข้อกำห...
หัวข้อที่เกี่ยวข้อง
นี่คือหน้าหัวข้อที่ใกล้เคียงกับเรื่องนี้ จากบทความนี้ คุณสามารถไปต่อยังบริการที่เกี่ยวข้องและบทความอื่น ๆ ได้
หัวข้อเทคนิคของ Windows
ประตูสู่หัวข้อเทคนิคเรื่องการพัฒนา Windows การตรวจสอบบั๊ก และการใช้ประโยชน์จากสินทรัพย์เดิม
บริการที่เกี่ยวข้องกับหัวข้อนี้
บทความนี้เชื่อมโยงกับหน้าบริการต่อไปนี้ โปรดเริ่มดูจากหน้าที่ใกล้เคียงที่สุด
พัฒนาแอป Windows
หากต้องการสร้างแอป Windows ที่เก็บข้อมูลจากอุปกรณ์ รวมถึงการตั้งค่า subscription การเชื่อมต่อใหม่ และการบันทึกเวลาประทับกับสถานะ สามารถรับเป็นงานพัฒนาแอป Windows ได้
ที่ปรึกษาเทคนิคและรีวิวการออกแบบ
หากต้องการตัดสินใจว่าจะเก็บข้อมูลด้วย OPC UA หรือใช้ Modbus/TCP หรือการสื่อสารเฉพาะของผู้ผลิต และจะจัดการใบรับรองอย่างไร สามารถรับเป็นการให้คำปรึกษาทางเทคนิคและการรีวิวการออกแบบได้
คำถามที่พบบ่อย
คำถามที่มักพบในการปรึกษาเกี่ยวกับหัวข้อของบทความนี้
- OPC UA กับ OPC Classic (OPC DA) ต่างกันอย่างไร
- OPC Classic เป็นข้อกำหนดที่แลกเปลี่ยนข้อมูลผ่าน COM/DCOM ของ Windows ส่วน OPC UA เป็นข้อกำหนดที่เผยแพร่ในปี 2008 ไม่ขึ้นกับระบบปฏิบัติการใดโดยเฉพาะ และรวมความสามารถของ Classic ไว้ด้วยกัน ไคลเอนต์ OPC UA ทำงานนอก Windows ได้ด้วย และไม่ต้องตั้งค่า DCOM
- เมื่อเก็บค่าด้วย OPC UA อ่านเป็นช่วงเวลาคงที่ก็พอหรือไม่
- ถ้าค่ามีจำนวนน้อยและช่วงเวลายาว การอ่านก็เก็บข้อมูลได้ หากต้องการติดตามการเปลี่ยนแปลงของค่า ยังมีวิธีใช้ subscription เพื่อให้เซิร์ฟเวอร์เป็นผู้สุ่มตัวอย่างและแจ้งการเปลี่ยนแปลงให้ ไม่ว่าแบบไหน ให้บันทึกรหัสสถานะและเวลาประทับที่ส่งมาพร้อมค่าด้วย
- ทำไมเชื่อมต่อแล้วได้ BadCertificateUntrusted
- เพราะไคลเอนต์ยังไม่เชื่อถือใบรับรองของเซิร์ฟเวอร์ ไลบรารี .NET ของ OPC Foundation จะเก็บใบรับรองที่ปฏิเสธไว้ในที่เก็บ rejected ให้ตรวจ thumbprint ของใบรับรองกับผู้ดูแลเซิร์ฟเวอร์ก่อน แล้วจึงย้ายไปยังที่เก็บ trusted การตั้งค่าที่รับใบรับรองโดยอัตโนมัติให้ใช้เฉพาะในสภาพแวดล้อมการพัฒนาเท่านั้น
- OPC UA ใช้พอร์ตหมายเลขอะไร
- IANA ลงทะเบียน 4840/TCP ไว้ในชื่อ opcua-tcp แต่เซิร์ฟเวอร์จริงบางครั้งใช้พอร์ตอื่น จึงต้องตรวจในการตั้งค่าของอุปกรณ์ ถ้าเซิร์ฟเวอร์ทำงานบน Windows ต้องอนุญาตการสื่อสารไปยังพอร์ตนั้นใน Windows Firewall และถ้ามีอุปกรณ์เครือข่ายอยู่ระหว่างทาง ก็ต้องอนุญาตที่อุปกรณ์นั้นด้วย