본문으로 건너뛰기

OPC UA 드라이버 예제

OPC UA (OPC Unified Architecture) 드라이버 구현 가이드입니다.

📋 개요

OPC UA는 산업 4.0 시대의 표준 통신 프로토콜입니다.

학습 시간: 60분
난이도: ⭐⭐⭐ 고급


🎯 OPC UA 특징

  • 플랫폼 독립적: Windows, Linux, 임베디드
  • 보안: 암호화, 인증, 권한 관리
  • 정보 모델: 계층적 주소 공간
  • 메서드 호출: RPC 지원
  • 이벤트/알람: Pub/Sub

🔧 구현

프로젝트 생성

cafe init --name OpcUaDriver --template driver-full --git
cd OpcUaDriver/src

# OPC Foundation 라이브러리
dotnet add package OPCFoundation.NetStandard.Opc.Ua
dotnet add package OPCFoundation.NetStandard.Opc.Ua.Client

Driver 클래스

using Caffeine.Core.Abstractions;
using Caffeine.Core.Models;
using Microsoft.Extensions.Logging;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Configuration;

namespace OpcUaDriver;

public class OpcUaDriver : IDriver
{
private readonly ILogger<OpcUaDriver> _logger;
private readonly string _endpointUrl;
private readonly ApplicationConfiguration _appConfig;

private Session? _session;
private Subscription? _subscription;

public OpcUaDriver(
string endpointUrl = "opc.tcp://localhost:4840",
ILogger<OpcUaDriver>? logger = null)
{
_endpointUrl = endpointUrl;
_logger = logger ?? NullLogger<OpcUaDriver>.Instance;
_appConfig = CreateApplicationConfiguration();
}

public async Task<bool> ConnectAsync(CancellationToken cancellationToken = default)
{
try
{
_logger.LogInformation("Connecting to OPC UA server: {Url}", _endpointUrl);

// 1. Discover endpoints
var endpointDescription = CoreClientUtils.SelectEndpoint(
_endpointUrl,
useSecurity: false);

// 2. Create endpoint configuration
var endpointConfiguration = EndpointConfiguration.Create(_appConfig);
var endpoint = new ConfiguredEndpoint(
null,
endpointDescription,
endpointConfiguration);

// 3. Create session
_session = await Session.Create(
_appConfig,
endpoint,
updateBeforeConnect: false,
checkDomain: false,
sessionName: "Caffeine OPC UA Driver",
sessionTimeout: 60000,
userIdentity: new UserIdentity(new AnonymousIdentityToken()),
preferredLocales: null,
ct: cancellationToken);

_logger.LogInformation("✅ Connected to OPC UA server");

// 4. Setup subscription (optional)
CreateSubscription();

return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "OPC UA connection failed");
return false;
}
}

public async Task DisconnectAsync()
{
if (_subscription != null)
{
_subscription.Delete(deleteInServer: true);
_subscription = null;
}

if (_session != null)
{
await _session.CloseAsync();
_session.Dispose();
_session = null;
}

_logger.LogInformation("Disconnected from OPC UA server");
}

public async Task<TagValue?> ReadTagAsync(
string tagName,
CancellationToken cancellationToken = default)
{
if (_session == null)
{
throw new InvalidOperationException("Not connected");
}

try
{
// NodeId 파싱: "ns=2;s=Temperature"
var nodeId = NodeId.Parse(tagName);

// Read value
var value = await _session.ReadValueAsync(nodeId, cancellationToken);

return new TagValue(
tagName,
value.Value ?? 0,
ConvertStatusCode(value.StatusCode));
}
catch (Exception ex)
{
_logger.LogError(ex, "Read failed: {TagName}", tagName);
return new TagValue(tagName, 0, TagQuality.Bad);
}
}

public async Task<bool> WriteTagAsync(
string tagName,
object value,
CancellationToken cancellationToken = default)
{
if (_session == null)
{
throw new InvalidOperationException("Not connected");
}

try
{
var nodeId = NodeId.Parse(tagName);

var writeValue = new WriteValue
{
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant(value))
};

var writeValueCollection = new WriteValueCollection { writeValue };

var results = await _session.WriteAsync(
requestHeader: null,
nodesToWrite: writeValueCollection,
ct: cancellationToken);

var success = StatusCode.IsGood(results.Results[0]);

if (success)
{
_logger.LogInformation("✅ Write success: {TagName} = {Value}",
tagName, value);
}

return success;
}
catch (Exception ex)
{
_logger.LogError(ex, "Write failed: {TagName}", tagName);
return false;
}
}

// Subscription for real-time monitoring
private void CreateSubscription()
{
_subscription = new Subscription(_session!.DefaultSubscription)
{
PublishingInterval = 1000,
PublishingEnabled = true,
LifetimeCount = 1000,
KeepAliveCount = 10,
Priority = 100
};

_session.AddSubscription(_subscription);
_subscription.Create();
}

public void SubscribeToNode(
string nodeId,
Action<MonitoredItem, MonitoredItemNotificationEventArgs> callback)
{
if (_subscription == null)
{
throw new InvalidOperationException("Subscription not created");
}

var monitoredItem = new MonitoredItem(_subscription.DefaultItem)
{
DisplayName = nodeId,
StartNodeId = NodeId.Parse(nodeId),
SamplingInterval = 1000,
QueueSize = 10,
DiscardOldest = true
};

monitoredItem.Notification += callback;

_subscription.AddItem(monitoredItem);
_subscription.ApplyChanges();
}

// Helper Methods
private ApplicationConfiguration CreateApplicationConfiguration()
{
var config = new ApplicationConfiguration
{
ApplicationName = "Caffeine OPC UA Driver",
ApplicationType = ApplicationType.Client,
SecurityConfiguration = new SecurityConfiguration
{
ApplicationCertificate = new CertificateIdentifier(),
TrustedPeerCertificates = new CertificateTrustList(),
TrustedIssuerCertificates = new CertificateTrustList(),
RejectedCertificateStore = new CertificateTrustList(),
AutoAcceptUntrustedCertificates = true
},
TransportConfigurations = new TransportConfigurationCollection(),
TransportQuotas = new TransportQuotas
{
OperationTimeout = 15000
},
ClientConfiguration = new ClientConfiguration
{
DefaultSessionTimeout = 60000
}
};

config.Validate(ApplicationType.Client).GetAwaiter().GetResult();

return config;
}

private TagQuality ConvertStatusCode(StatusCode statusCode)
{
if (StatusCode.IsGood(statusCode))
return TagQuality.Good;
else if (StatusCode.IsUncertain(statusCode))
return TagQuality.Uncertain;
else
return TagQuality.Bad;
}
}

🧪 테스트

OPC UA 시뮬레이터

추천 도구:

  • Prosys OPC UA Simulation Server
  • KEPServerEX
  • OPC UA .NET Sample Server

📚 고급 기능

Browse 노드

public async Task<List<ReferenceDescription>> BrowseAsync(NodeId nodeId)
{
var browser = new Browser(_session!)
{
BrowseDirection = BrowseDirection.Forward,
ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences,
IncludeSubtypes = true,
NodeClassMask = 0,
ResultMask = (uint)BrowseResultMask.All
};

var references = await browser.BrowseAsync(nodeId);
return references.ToList();
}

이벤트 구독

public void SubscribeToEvents(NodeId eventTypeId, Action<EventFieldList> callback)
{
var monitoredItem = new MonitoredItem(_subscription!.DefaultItem)
{
StartNodeId = ObjectIds.Server,
AttributeId = Attributes.EventNotifier,
MonitoringMode = MonitoringMode.Reporting,
SamplingInterval = 0
};

monitoredItem.Notification += (item, e) =>
{
foreach (var eventField in (EventFieldList[])e.NotificationValue)
{
callback(eventField);
}
};

_subscription.AddItem(monitoredItem);
_subscription.ApplyChanges();
}

📚 참고 자료


작성일: 2026-01-28
버전: 2.0