บทที่ 04 · Service Sets · Client-Server Operations

Services

"Services" คือชุดของ คำสั่ง ที่ Client ใช้คุยกับ Server — Read, Write, Subscribe, Browse, Call — บทนี้พาดู 9 Service Sets หลัก พร้อมเทียบกับ Modbus Function Codes ที่คุณรู้จักอยู่แล้ว

OPC UA เป็นมาตรฐานขนาดใหญ่

ก่อนเริ่ม — ขอเปิดภาพให้เห็นว่า OPC UA ไม่ใช่แค่เรื่อง Read/Write มาตรฐาน IEC 62541 มี 24 ส่วน (Parts) ครอบคลุมเรื่องต่างๆ ตั้งแต่ความปลอดภัย, การค้นหา, ไปจนถึงการรับส่งไฟล์:

IEC 62541 spec parts: Core 1-7, Access Type 8-11, PubSub 14, Utility parts 12-26
24 ส่วนของมาตรฐาน IEC 62541 — บางส่วนเป็น Core (จำเป็นเสมอ), บางส่วนเป็น Access Type (อ่านข้อมูลแบบต่างๆ), บางส่วนเป็น Utility (ค้นหา, รับส่งไฟล์, ...) — แต่ละ Part ฟรี โหลดอ่านได้จาก opcfoundation.org

บทนี้พุ่งเน้นที่ Part 4 — Services ซึ่งเป็นชั้นบริการที่ Client เรียกใช้ผ่าน Client-Server Pattern

9 Service Sets — บริการหลัก

OPC UA จัดบริการเป็น 9 กลุ่ม — แต่ละกลุ่มมีหลายบริการย่อย:

01
SecureChannel
เปิดช่องสื่อสารที่เข้ารหัส · แลกใบรับรอง (Certificates) · จัดการกุญแจ — ทำโดยอัตโนมัติก่อนเริ่ม Session
02
Session
เปิด/ปิด Session ของ User · Authenticate User (Username/Password, X.509, Token)
03
NodeManagement
Add/Modify/Delete Node ใน Address Space — สำหรับ Configuration Tool
04
View
Browse · Translate BrowsePath · เดินไป-มาในต้นไม้ของ Node — ใช้บ่อยที่สุด
05
Attribute
Read · Write · HistoryRead · HistoryWrite — อ่าน/เขียน Attributes ของ Node
06
Method
Call — เรียกฟังก์ชันใน Server (เช่น Start, Stop, Calibrate) พร้อม Argument
07
MonitoredItem
บอก Server ว่า "อยากให้แจ้งเมื่อค่าของ Node นี้เปลี่ยน" · ตั้ง Deadband, Sampling Interval
08
Subscription
กลุ่มของ MonitoredItem — Server ส่ง Publish notification กลับมาเป็นชุด
09
Query
ค้นหา Node ตามเงื่อนไข (Filter) — ใช้กับ Address Space ขนาดใหญ่

เทียบกับ Modbus Function Codes

ถ้าคุณคุ้นกับ Modbus จากบทเรียน PLC แล้ว — ลองดูเทียบกัน:

ทำอะไร Modbus Function Code OPC UA Service
อ่านค่าเดียว FC03 (Read Holding Registers) Read
เขียนค่าเดียว FC06 (Write Single Register) Write
อ่าน/เขียนหลายค่าพร้อมกัน FC23 (Read/Write Multiple) Read/Write รับ Array ในครั้งเดียว
รับแจ้งค่าเปลี่ยน ❌ ไม่มี — ต้อง Poll เอง Subscribe → Server push ให้เอง
เรียกฟังก์ชัน FC16 (Write Multiple) + ตีความเอง Call — มี Argument + Return จริง
อ่านข้อมูลเก่า ❌ ต้องเก็บแยก HistoryRead
รับ Alarm ❌ ต้องตรวจ Status Bit เอง Events & Conditions
ค้นหาว่ามีอะไร ❌ เปิด Manual Browse
Authenticate ❌ ไม่มี Session + SecureChannel

Read & Write — บริการที่ใช้บ่อยที่สุด

เทียบสมการให้ดู:

# Modbus (Python pseudocode)
from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient('192.168.1.10')
result = client.read_holding_registers(address=0, count=1, unit=5)
raw = result.registers[0]            # 235 (ตัวเลขดิบ)
temperature = raw / 10.0             # ต้อง scale เอง
# หน่วยคืออะไร? เปิด manual...

# OPC UA (Python with asyncua)
from asyncua import Client
async with Client(url='opc.tcp://plant-floor:4840') as client:
    node = client.get_node('ns=2;s=TempXmitter1.CurrentValue')
    temperature = await node.read_value()   # 23.5 (already in °C)
    unit = await node.read_attribute('EngineeringUnits')  # "°C"

สังเกตว่า Code ของ OPC UA สื่อความหมายตัวเอง — อ่านแล้วเข้าใจทันทีว่ากำลังอ่านอะไร ส่วน Modbus อ่านยังไงก็เห็นแค่ "register 0"

Read หลายค่าพร้อมกัน

OPC UA Read รับ Array ของ NodeId ในครั้งเดียว — ลด overhead ของ Network:

# Batch read — 1 round-trip แทนที่จะ N round-trip
node_ids = ['ns=2;s=Temp1', 'ns=2;s=Temp2', 'ns=2;s=Pressure1']
values = await client.read_values([client.get_node(n) for n in node_ids])
# values = [23.5, 76.2, 102.4]

Subscribe — Server แจ้งเอง ไม่ต้อง Poll

นี่คือ Game Changer เทียบกับ Modbus:

# สร้าง Subscription
async with Client('opc.tcp://...') as client:
    sub = await client.create_subscription(period=500, handler=on_change)
    # ขอให้แจ้งเมื่อ Temp1 เปลี่ยน (mais ไม่เกิน 500ms ก็ Sample 1 ครั้ง)
    await sub.subscribe_data_change(
        client.get_node('ns=2;s=Temp1'),
        sampling_interval=500,   # ms
        queue_size=10
    )

# Handler ทำงานเมื่อค่าเปลี่ยน — Server push เอง
async def on_change(node, value, data):
    print(f"{node} เปลี่ยนเป็น {value}")

ใน Modbus คุณต้องเขียน Loop:

# Modbus — ต้อง Poll เอง
last = 0
while True:
    val = read_modbus()
    if val != last:
        print(f"เปลี่ยนเป็น {val}")
        last = val
    sleep(0.5)  # Poll every 500ms ของ slave หลายตัว = network ล่ม

Deadband — กรอง Noise

Subscribe สามารถใส่ Deadband ได้ — เช่น "แจ้งเฉพาะเมื่อค่าเปลี่ยนเกิน 0.5°C" — ลด traffic อย่างมีนัยสำคัญสำหรับ Analog Signal ที่มี Noise:

await sub.subscribe_data_change(
    node,
    deadband_type='Absolute',
    deadband_value=0.5
)

Method Call — ฟังก์ชันจริง พร้อม Argument

ใน Modbus ถ้าอยากสั่ง Calibrate(targetTemp=100.0, duration=30s) คุณต้อง: เขียน Magic Value ลง Register 1000, รอ Bit Status, ตีความค่าผลใน Register 1001, …

OPC UA ทำได้ตรงๆ:

method_node = client.get_node('ns=2;s=TempXmitter1.Calibrate')
parent_node = client.get_node('ns=2;s=TempXmitter1')

# Call method — รับ argument พร้อม return value
result = await parent_node.call_method(
    method_node,
    100.0,    # targetTemp (Double)
    30        # duration in seconds (UInt32)
)
# result = สถานะ + ค่าใหม่ที่ Calibrate แล้ว

Method มี Input Arguments และ Output Arguments ที่นิยามใน Information Model Client ที่ Browse เจอจะรู้ทันทีว่าต้องส่ง argument อะไรไปบ้าง

Historical Access — ข้อมูลย้อนหลังในตัว

Server สามารถ buffer ค่าย้อนหลังของแต่ละ Variable — Client เรียก HistoryRead ดึงข้อมูลตามช่วงเวลาได้:

# อ่าน Temperature ของ Sensor นี้ย้อนหลัง 1 ชั่วโมง
node = client.get_node('ns=2;s=Temp1')
history = await node.read_raw_history(
    start_time=datetime.now() - timedelta(hours=1),
    end_time=datetime.now(),
    num_values=0  # all
)
for dv in history:
    print(f"{dv.SourceTimestamp}  {dv.Value.Value}")

ยังมี Aggregate Functions ในตัว — Average, Min, Max, StandardDeviation — ที่ Server คำนวณให้ Client ไม่ต้องดึงข้อมูลดิบทั้งหมดมา process เอง

Events & Conditions — Alarms ที่ฉลาด

Modbus ส่ง Alarm โดย toggle Status Bit แล้วให้ Master Poll — ไม่มีรายละเอียดว่าทำไม Alarm ใครเป็นผู้รับ Alarm ใครต้อง Ack

OPC UA มี Model ของ Alarm ในตัว:

แต่ละ Alarm มีข้อมูลครบ:

Event ที่ Client ได้รับ:
  EventId          : Unique ID
  Time             : 2026-05-19 14:32:17.123
  Source           : TempXmitter1
  Message          : "Temperature exceeded HighHigh limit"
  Severity         : 900 (range 1-1000)
  Active           : true
  Acknowledged     : false
  Comment          : (User comment ที่ใส่ตอน Ack)

หลายภาษาในตัว — Message field รองรับ LocalizedText — Client เลือกภาษาที่ต้องการ Server ก็ส่ง Message ให้ในภาษานั้น (เช่น ไทย/อังกฤษ)

Browse + TranslateBrowsePath

Browse ไม่ได้แค่เดินไปตามต้นไม้ — รองรับการเขียน path แบบ Filesystem ได้ด้วย:

# แทนที่จะใช้ NodeId (ที่จำยาก):
node = client.get_node('ns=2;s=TempXmitter1.CurrentValue')

# ใช้ BrowsePath (เหมือน path ของไฟล์):
node = await client.nodes.objects.get_child([
    "2:FieldBus",
    "2:TempXmitter1",
    "2:CurrentValue"
])

Server แปล Path เป็น NodeId ให้ — Code อ่านง่ายขึ้นมาก

สรุปบทที่ 04

บทต่อไปจะดู PubSub — รูปแบบการสื่อสารอีกแบบที่เหมาะกับ Many-to-Many และ Cloud