00 OPC UA nedir — IEC 62541 ve legacy farkı
OPC UA (OPC Unified Architecture), OPC Foundation'ın 2008'de yayımladığı ve IEC 62541 olarak standartlaştırılan endüstriyel iletişim mimarisidir. Eski OPC protokollerinin (DA/HDA/AE) Windows COM/DCOM bağımlılığını ortadan kaldırarak platform bağımsız, güvenli ve birleşik bir çerçeve sunar.
Legacy OPC vs OPC UA
| Özellik | OPC Classic (DA/HDA/AE) | OPC UA |
|---|---|---|
| Transport | DCOM (Windows COM) | TCP/IP, HTTPS, WebSocket, MQTT, AMQP |
| Platform | Sadece Windows | Linux, Windows, RTOS, mikrodenetleyici |
| Güvenlik | Windows NTLM/Kerberos | X.509 sertifika, TLS 1.2/1.3 |
| Veri modeli | Ayrı (DA/HDA/AE) | Birleşik nesne modeli |
| Keşif | OPC Foundation LDS | Discovery, LDS, GDS |
| Pub/Sub | Yok | Part 14 (UADP, Broker) |
| Standart | OPC 1.x spesifikasyonu | IEC 62541 Bölüm 1-22 |
IEC 62541 bölümleri
01 Information model — NodeId, ObjectType, Variable, Method
OPC UA Information Model, sunucu adres alanındaki tüm varlıkları bir grafik olarak temsil eder. Her varlık bir "Node"dur; node'lar arasındaki "Reference" ilişkileri nesne hiyerarşisini ve özelliklerini tanımlar.
Node sınıfları
NodeId türleri
| Tür | Gösterim | Örnek |
|---|---|---|
| Numeric | ns=<ns>;i=<id> | ns=0;i=2253 (Server node) |
| String | ns=<ns>;s=<str> | ns=2;s=Kazan1.Sicaklik |
| GUID | ns=<ns>;g=<guid> | ns=1;g=550e8400-e29b-41d4… |
| ByteString | ns=<ns>;b=<base64> | Üretici özel kullanım |
Objects (ns=0;i=85)
└── Kazan1 [Object, ns=2;s=Kazan1]
├── Sicaklik [Variable, ns=2;s=Kazan1.Sicaklik]
│ DataType: Float, Unit: °C
│ AccessLevel: Read | StatusWrite | TimestampWrite
├── Basinc [Variable, ns=2;s=Kazan1.Basinc]
│ DataType: Float, Unit: bar
└── Calistir [Method, ns=2;s=Kazan1.Calistir]
InputArgs: hedefSicaklik (Float)
OutputArgs: sonuc (Int32)
02 Session ve güvenlik modeli
OPC UA güvenlik mimarisi üç katmandan oluşur: taşıma güvenliği (TLS), uygulama kimlik doğrulama (X.509) ve kullanıcı kimlik doğrulama (parola/sertifika/token). Bu katmanlar bağımsız yapılandırılabilir.
Güvenlik politikaları (Security Policy)
| Politika URI | İmza | Şifreleme | Kullanım |
|---|---|---|---|
| ...None | Yok | Yok | Sadece test/geliştirme |
| ...Basic128Rsa15 | RSA-SHA1 | AES-128 | Eski sistemler (önerilmez) |
| ...Basic256 | RSA-SHA1 | AES-256 | Geçiş dönemi |
| ...Basic256Sha256 | RSA-SHA256 | AES-256 | Önerilen minimum |
| ...Aes128_Sha256_RsaOaep | RSA-SHA256 | AES-128 + RSA-OAEP | Modern sistemler |
| ...Aes256_Sha256_RsaPss | RSA-PSS-SHA256 | AES-256 + RSA-PSS | Yüksek güvenlik |
Bağlantı akışı
İstemci Sunucu │ │ │── GetEndpoints ────────────────►│ (güvenlik politikaları listesi) │◄─ Endpoints ────────────────────│ │ │ │── OpenSecureChannel ───────────►│ (TLS benzeri handshake) │ (ClientCertificate, nonce) │ │◄─ OpenSecureChannelResponse ────│ (ServerCertificate, nonce) │ │ │── CreateSession ───────────────►│ │ (ApplicationDescription, │ │ ClientNonce) │ │◄─ CreateSessionResponse ────────│ (SessionId, AuthToken) │ │ │── ActivateSession ─────────────►│ (IdentityToken: user/pass) │◄─ ActivateSessionResponse ──────│ │ │ │ ─── Servisler (Read/Write/Subscribe) ───
Sertifika yönetimi (open62541)
# Sunucu self-signed sertifika oluşturma
openssl req -x509 -newkey rsa:2048 \
-keyout server_key.pem -out server_cert.pem \
-days 3650 -nodes \
-subj "/CN=OPC UA Server/O=EmbeddedDeck/C=TR" \
-extensions v3_req \
-addext "subjectAltName=URI:urn:embedded-deck:opcua:server"
# DER formatına çevir (open62541 için)
openssl x509 -in server_cert.pem -outform DER -out server_cert.der
openssl rsa -in server_key.pem -outform DER -out server_key.der
03 Services — Read/Write/Browse/Call
OPC UA, istek-yanıt modelinde çalışan kapsamlı bir servis kümesi tanımlar. Tüm servisler Secure Channel üzerinden UA Binary veya UA JSON ile kodlanarak iletilir.
Temel servisler
UA Binary encoding yapısı
Secure Channel üzerinde bir ReadRequest: ┌────────────────────────────────────────────────┐ │ Message Header (MessageType=MSG, 8 byte) │ │ Secure Channel Id (4 byte) │ │ Security Token Id (4 byte) │ │ Sequence Number (4 byte) │ │ Request Id (4 byte) │ ├────────────────────────────────────────────────┤ │ RequestHeader (authToken, timestamp, reqHandle)│ │ MaxAge (Double) │ │ TimestampsToReturn (Enum: Source/Server/Both) │ │ NodesToRead []: │ │ NodeId: ns=2;s=Kazan1.Sicaklik │ │ AttributeId: 13 (Value) │ └────────────────────────────────────────────────┘
04 Subscriptions ve MonitoredItems
OPC UA Subscription mekanizması, istemcinin belirli node'lardaki değişimleri periyodik olarak almasını sağlar. Polling'e göre çok daha verimlidir: sunucu yalnızca değişim olduğunda bildirim gönderir.
Subscription parametreleri
MonitoredItem parametreleri
import asyncio
from asyncua import Client
async def subscribe_temperatures():
url = "opc.tcp://localhost:4840"
async with Client(url=url) as client:
# Subscription oluştur (100 ms publishing interval)
subscription = await client.create_subscription(
period=100,
handler=DataChangeHandler()
)
# Sıcaklık node'una MonitoredItem ekle
node = client.get_node("ns=2;s=Kazan1.Sicaklik")
handle = await subscription.subscribe_data_change(
node,
sampling_interval=50, # 50 ms örnekleme
queuesize=10
)
await asyncio.sleep(10) # 10 saniye dinle
await subscription.unsubscribe(handle)
await subscription.delete()
class DataChangeHandler:
def datachange_notification(self, node, val, data):
print(f"Sıcaklık değişti: {val:.2f} °C")
print(f" StatusCode: {data.monitored_item.Value.StatusCode}")
print(f" SourceTimestamp: {data.monitored_item.Value.SourceTimestamp}")
asyncio.run(subscribe_temperatures())
05 Pub/Sub — UADP over MQTT/UDP
OPC UA Part 14 (Pub/Sub), subscription modelinin ötesinde, broker tabanlı veya broker-less veri yayımını tanımlar. UADP (UA Datagram Protocol) mesaj formatı MQTT, AMQP veya UDP üzerinden taşınabilir.
Pub/Sub kavramları
MQTT transport ile yayım
OPC UA Publisher MQTT Broker OPC UA Subscriber
│ │ │
│── NetworkMessage ────────────►│ │
│ Topic: opcua/kazan1/data │── NetworkMessage ───────►│
│ UADP binary payload │ (DataSetField decode) │
│ │ │
│ [Header][GroupHeader][DataSetMessage[Fields...]]
// open62541 PubSub JSON konfigürasyon örneği
{
"publishedDataSets": [{
"name": "KazanDataSet",
"fields": [
{"name": "Sicaklik", "nodeId": "ns=2;s=Kazan1.Sicaklik"},
{"name": "Basinc", "nodeId": "ns=2;s=Kazan1.Basinc"}
]
}],
"connections": [{
"name": "MQTTBaglantisi",
"transportProfileUri": "http://opcfoundation.org/UA-Profile/Transport/pubsub-mqtt-json",
"address": "mqtt://broker.local:1883",
"writerGroups": [{
"name": "KazanGroup",
"publishingInterval": 500,
"writerGroupId": 1,
"dataSetWriters": [{
"name": "KazanWriter",
"dataSetName": "KazanDataSet",
"mqttTopic": "opcua/kazan1/data"
}]
}]
}]
}
06 open62541 C kütüphanesi
open62541, Apache 2.0 lisansıyla yayımlanan, C99 ile yazılmış taşınabilir bir OPC UA implementasyonudur. Tek header (amalgamate) veya kütüphane olarak kullanılabilir; gömülü sistemlerden sunuculara kadar geniş yelpazeyi destekler.
Derleme ve kurulum
# Bağımlılıklar
sudo apt install cmake python3 python3-sphinx mbedtls-dev
# Kaynak kodu
git clone https://github.com/open62541/open62541.git
cd open62541
git checkout v1.3.8
mkdir build && cd build
cmake .. \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DUA_ENABLE_AMALGAMATION=ON \
-DUA_ENABLE_PUBSUB=ON \
-DUA_ENABLE_ENCRYPTION=MBEDTLS \
-DUA_ENABLE_SUBSCRIPTIONS=ON \
-DUA_LOGLEVEL=300 # UA_LOGLEVEL_INFO
make -j$(nproc)
sudo make install
OPC UA server oluşturma — temel yapı
/* opcua_server.c — open62541 ile minimal OPC UA server */
#include <open62541/server.h>
#include <open62541/server_config_default.h>
#include <signal.h>
#include <stdio.h>
static volatile UA_Boolean running = true;
static void stopHandler(int sig) { running = false; }
/* Custom nesne tipi: SicaklikSensoru */
static void addTemperatureSensorType(UA_Server *server)
{
UA_ObjectTypeAttributes otAttr = UA_ObjectTypeAttributes_default;
otAttr.displayName = UA_LOCALIZEDTEXT("tr", "Sıcaklık Sensörü");
UA_NodeId typeId = UA_NODEID_STRING(2, "SicaklikSensoru");
UA_Server_addObjectTypeNode(server,
typeId,
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEOBJECTTYPE),
UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE),
UA_QUALIFIEDNAME(2, "SicaklikSensoru"),
otAttr, NULL, NULL);
/* Sıcaklık değişkeni ekle */
UA_VariableAttributes vAttr = UA_VariableAttributes_default;
vAttr.displayName = UA_LOCALIZEDTEXT("tr", "Sıcaklık");
vAttr.dataType = UA_TYPES[UA_TYPES_FLOAT].typeId;
vAttr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;
UA_Float initVal = 0.0f;
UA_Variant_setScalar(&vAttr.value, &initVal, &UA_TYPES[UA_TYPES_FLOAT]);
UA_Server_addVariableNode(server,
UA_NODEID_STRING(2, "SicaklikSensoru.Sicaklik"),
typeId,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(2, "Sicaklik"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
vAttr, NULL, NULL);
}
/* Nesne örneği oluştur */
static void addSensorInstance(UA_Server *server,
const char *name, float initTemp)
{
UA_ObjectAttributes oAttr = UA_ObjectAttributes_default;
oAttr.displayName = UA_LOCALIZEDTEXT("tr", (char*)name);
UA_Server_addObjectNode(server,
UA_NODEID_STRING(2, (char*)name),
UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
UA_QUALIFIEDNAME(2, (char*)name),
UA_NODEID_STRING(2, "SicaklikSensoru"),
oAttr, NULL, NULL);
}
int main(void)
{
signal(SIGINT, stopHandler);
signal(SIGTERM, stopHandler);
UA_Server *server = UA_Server_new();
UA_ServerConfig_setDefault(UA_Server_getConfig(server));
/* Namespace 2 ekle */
UA_Server_addNamespace(server, "urn:embedded-deck:opcua");
/* Tip ve örnekler oluştur */
addTemperatureSensorType(server);
addSensorInstance(server, "Kazan1", 25.0f);
addSensorInstance(server, "Kazan2", 30.0f);
printf("OPC UA Server başlatılıyor: opc.tcp://localhost:4840\n");
UA_StatusCode sc = UA_Server_runUntilInterrupt(server);
UA_Server_delete(server);
return sc == UA_STATUSCODE_GOOD ? 0 : 1;
}
gcc -o opcua_server opcua_server.c \
$(pkg-config --cflags --libs open62541)
sudo ./opcua_server
07 asyncua Python kütüphanesi
asyncua (FreeOpcUa projesinin asyncio tabanlı halefi), Python ile OPC UA istemci ve sunucu geliştirmek için en popüler kütüphanedir. asyncio tabanlı yapısı sayesinde çok sayıda subscription bile tek thread'de verimli şekilde yönetilebilir.
pip install asyncua
İstemci: okuma ve yazma
import asyncio
from asyncua import Client
from asyncua.ua import DataValue, Variant, VariantType
async def main():
async with Client("opc.tcp://localhost:4840") as client:
# Namespace indeksi bul
nsidx = await client.get_namespace_index(
"urn:embedded-deck:opcua")
# Node'u al ve oku
node = await client.nodes.root.get_child([
"0:Objects", f"{nsidx}:Kazan1",
f"{nsidx}:Sicaklik"
])
value = await node.read_value()
print(f"Sıcaklık: {value:.2f} °C")
# Değer yaz
await node.write_value(
DataValue(Variant(72.5, VariantType.Float))
)
print("Yeni değer yazıldı: 72.5 °C")
asyncio.run(main())
Method çağırma
import asyncio
from asyncua import Client
async def call_method():
async with Client("opc.tcp://localhost:4840") as client:
nsidx = await client.get_namespace_index(
"urn:embedded-deck:opcua")
parent = await client.nodes.root.get_child([
"0:Objects", f"{nsidx}:Kazan1"
])
# Calistir metodunu çağır
# InputArgs: hedefSicaklik=80.0
result = await parent.call_method(
f"{nsidx}:Calistir",
80.0 # hedefSicaklik parametresi
)
print(f"Metod sonucu: {result}")
asyncio.run(call_method())
Subscription ile olay izleme
import asyncio
from asyncua import Client
from asyncua.common.subscription import SubHandler
class MyHandler(SubHandler):
def datachange_notification(self, node, val, data):
print(f"[DEĞİŞİM] {node}: {val}")
def event_notification(self, event):
print(f"[OLAY] {event.EventType}: {event.Message}")
async def subscribe():
async with Client("opc.tcp://localhost:4840") as client:
handler = MyHandler()
sub = await client.create_subscription(200, handler)
nsidx = await client.get_namespace_index(
"urn:embedded-deck:opcua")
nodes = [
client.get_node(f"ns={nsidx};s=Kazan1.Sicaklik"),
client.get_node(f"ns={nsidx};s=Kazan1.Basinc"),
]
handles = await sub.subscribe_data_change(nodes)
print("Abonelik aktif — Ctrl+C ile durdur")
try:
await asyncio.sleep(float('inf'))
except KeyboardInterrupt:
pass
finally:
await sub.unsubscribe(handles)
await sub.delete()
asyncio.run(subscribe())
08 Pratik: Raspberry Pi + DS18B20 + OPC UA server
Bu bölümde Raspberry Pi'ye bağlı DS18B20 dijital sıcaklık sensöründen veri okuyan ve bu veriyi OPC UA server üzerinden yayımlayan eksiksiz bir sistem kurulur.
Donanım bağlantısı
Raspberry Pi 4 DS18B20
3.3V (Pin 1) ─────────── VDD
GND (Pin 6) ─────────── GND
GPIO4 (Pin 7) ──┬──────── DATA
│
4.7 kΩ
│
3.3V (Pin 1) ───┘ (pull-up direnci)
# /boot/config.txt (veya /boot/firmware/config.txt)
dtoverlay=w1-gpio,gpiopin=4
# Modülleri yükle
sudo modprobe w1-gpio
sudo modprobe w1-therm
# Sensörü doğrula
ls /sys/bus/w1/devices/
# 28-XXXXXXXXXXXX w1_bus_master1
cat /sys/bus/w1/devices/28-*/w1_slave
# 50 05 55 05 7f a5 a5 66 8a : crc=8a YES
# 50 05 55 05 7f a5 a5 66 8a t=21250
# t=21250 → 21.250 °C
OPC UA server + DS18B20 entegrasyonu
#!/usr/bin/env python3
"""
ds18b20_opcua_server.py
DS18B20 sıcaklık sensörünü OPC UA üzerinden yayımlayan server.
Gereksinim: pip install asyncua
"""
import asyncio
import glob
import logging
from asyncua import Server
from asyncua.ua import VariantType
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
W1_BASE = "/sys/bus/w1/devices/"
def read_ds18b20() -> float:
"""DS18B20'den °C cinsinden sıcaklık oku."""
sensor_dirs = glob.glob(W1_BASE + "28-*")
if not sensor_dirs:
raise RuntimeError("DS18B20 sensörü bulunamadı!")
with open(sensor_dirs[0] + "/w1_slave") as f:
lines = f.readlines()
if "YES" not in lines[0]:
raise RuntimeError("CRC hatası — sensör yanıt vermiyor")
temp_str = lines[1].split("t=")[1].strip()
return float(temp_str) / 1000.0
async def update_sensor(server, temp_node, interval: float = 1.0):
"""Periyodik olarak sensörden oku ve OPC UA node'unu güncelle."""
while True:
try:
temp = read_ds18b20()
await temp_node.write_value(temp)
logger.debug(f"Sıcaklık güncellendi: {temp:.3f} °C")
except Exception as e:
logger.warning(f"Sensör okuma hatası: {e}")
await temp_node.write_value(float('nan'))
await asyncio.sleep(interval)
async def main():
server = Server()
await server.init()
server.set_endpoint("opc.tcp://0.0.0.0:4840/")
server.set_server_name("Raspberry Pi DS18B20 OPC UA Server")
# Namespace oluştur
uri = "urn:raspberry-pi:ds18b20:server"
nsidx = await server.register_namespace(uri)
# Objects altına sensör node'u ekle
objects = server.nodes.objects
sensor_obj = await objects.add_object(nsidx, "SicaklikSensoru")
# Sıcaklık değişkeni
temp_node = await sensor_obj.add_variable(
nsidx, "Sicaklik", 0.0
)
await temp_node.set_writable(False)
# Birim string'i
unit_node = await sensor_obj.add_variable(
nsidx, "Birim", "°C"
)
# Sensör ID'si
sensor_dirs = glob.glob(W1_BASE + "28-*")
sensor_id = sensor_dirs[0].split("/")[-1] if sensor_dirs else "N/A"
id_node = await sensor_obj.add_variable(
nsidx, "SensorID", sensor_id
)
async with server:
logger.info(f"OPC UA Server başlatıldı: opc.tcp://localhost:4840/")
logger.info(f"Sensör ID: {sensor_id}")
logger.info(f"Node: ns={nsidx};s=SicaklikSensoru.Sicaklik")
# Arka planda sensör güncelleme görevi
update_task = asyncio.create_task(
update_sensor(server, temp_node, interval=1.0)
)
try:
await asyncio.sleep(float('inf'))
except asyncio.CancelledError:
update_task.cancel()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
logger.info("Server durduruldu.")
Python istemcisi ile veriyi izleme
#!/usr/bin/env python3
"""Raspberry Pi OPC UA server'dan sıcaklık oku."""
import asyncio
from asyncua import Client
async def monitor():
uri = "urn:raspberry-pi:ds18b20:server"
async with Client("opc.tcp://raspberry-pi.local:4840/") as c:
nsidx = await c.get_namespace_index(uri)
sub = await c.create_subscription(500, handler=TempHandler())
node = c.get_node(f"ns={nsidx};s=SicaklikSensoru.Sicaklik")
await sub.subscribe_data_change(node)
print(f"İzleniyor: ns={nsidx};s=SicaklikSensoru.Sicaklik")
await asyncio.sleep(30)
class TempHandler:
def datachange_notification(self, node, val, data):
ts = data.monitored_item.Value.SourceTimestamp
print(f"[{ts:%H:%M:%S}] Sıcaklık: {val:.3f} °C")
asyncio.run(monitor())
Üretim ortamında server sertifikası ile Basic256Sha256 güvenlik politikasını etkinleştirin. asyncua, sertifika bazlı bağlantı için load_client_certificate() ve load_private_key() metodlarını sağlar.