using Opc.Ua; using Opc.Ua.Client; using Opc.Ua.Configuration; using System.Security.Cryptography.X509Certificates; namespace OpcConnectionTest { class Program { static async Task Main(string[] args) { Console.WriteLine("=== Experion OPC UA Connection Test ===\n"); // 1. 기본 경로 및 설정 정보 string serverUrl = "opc.tcp://192.168.0.20:4840"; string basePkiPath = Path.GetFullPath("pki"); string pfxPath = Path.Combine(basePkiPath, "own/certs/OpcTestClient.pfx"); // 필수 폴더 자동 생성 (Issuer/Rejected 저장소 에러 방지) Directory.CreateDirectory(Path.Combine(basePkiPath, "issuers/certs")); Directory.CreateDirectory(Path.Combine(basePkiPath, "rejected/certs")); Directory.CreateDirectory(Path.Combine(basePkiPath, "trusted/certs")); Console.WriteLine($"Server: {serverUrl}"); Console.WriteLine($"PKI Root: {basePkiPath}\n"); ISession? session = null; try { // 2. Application Configuration 설정 var config = new ApplicationConfiguration() { ApplicationName = "OpcTestClient", ApplicationType = ApplicationType.Client, ApplicationUri = "urn:OpcTestClient", SecurityConfiguration = new SecurityConfiguration { ApplicationCertificate = new CertificateIdentifier { StoreType = "Directory", StorePath = Path.Combine(basePkiPath, "own"), SubjectName = "CN=OpcTestClient" }, TrustedPeerCertificates = new CertificateTrustList { StoreType = "Directory", StorePath = Path.Combine(basePkiPath, "trusted") }, // 💡 중요: Issuer와 Rejected 경로를 명시해야 에러가 나지 않습니다. TrustedIssuerCertificates = new CertificateTrustList { StoreType = "Directory", StorePath = Path.Combine(basePkiPath, "issuers") }, RejectedCertificateStore = new CertificateTrustList { StoreType = "Directory", StorePath = Path.Combine(basePkiPath, "rejected") }, AutoAcceptUntrustedCertificates = true, RejectSHA1SignedCertificates = false, AddAppCertToTrustedStore = true }, TransportQuotas = new TransportQuotas { OperationTimeout = 15000 }, ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 } }; // 설정 검증 await config.ValidateAsync(ApplicationType.Client); // 3. 인증서 수동 주입 (리눅스 PFX 인식 해결) if (File.Exists(pfxPath)) { X509Certificate2 clientCertificate = new X509Certificate2(pfxPath, ""); config.SecurityConfiguration.ApplicationCertificate.Certificate = clientCertificate; Console.WriteLine($"✅ 인증서 수동 로드 성공: {clientCertificate.Subject}"); } else { Console.WriteLine($"❌ PFX 파일을 찾을 수 없습니다! 경로 확인: {pfxPath}"); return; } // 4. Endpoint 검색 Console.WriteLine("\nStep 1: Discovering endpoints..."); var endpointConfig = EndpointConfiguration.Create(config); using (var discoveryClient = DiscoveryClient.Create(new Uri(serverUrl), endpointConfig)) { var endpoints = discoveryClient.GetEndpoints(null); if (endpoints == null || endpoints.Count == 0) { Console.WriteLine("❌ No endpoints found!"); return; } Console.WriteLine($"✅ Found {endpoints.Count} endpoint(s)"); // 가장 높은 보안 수준의 엔드포인트 선택 var selectedEndpoint = endpoints.OrderByDescending(e => e.SecurityLevel).First(); Console.WriteLine($"👉 Selected: {selectedEndpoint.SecurityMode} | {selectedEndpoint.SecurityPolicyUri}"); // 5. Session 생성 (mngr 계정) Console.WriteLine("\nStep 2: Creating session with mngr account..."); var token = new UserNameIdentityToken { UserName = "mngr", DecryptedPassword = System.Text.Encoding.UTF8.GetBytes("mngr") }; var userIdentity = new UserIdentity(token); var configuredEndpoint = new ConfiguredEndpoint(null, selectedEndpoint, endpointConfig); session = await Session.Create( config, configuredEndpoint, updateBeforeConnect: true, "OPC Test Session", 60000, userIdentity, null); } Console.WriteLine($"✅ Connected! Session ID: {session.SessionId}"); // 6. 데이터 읽기 및 브라우징 테스트 Console.WriteLine("\nStep 3: Reading server info..."); await ReadServerInfoAsync(session); Console.WriteLine("\nStep 4: Checking redundancy..."); await CheckRedundancyAsync(session); Console.WriteLine("\nStep 5: Browsing nodes..."); await BrowseNodesAsync(session); Console.WriteLine("\n✅ All tests completed successfully!"); } catch (Exception ex) { Console.WriteLine($"\n❌ Error: {ex.Message}"); if (ex.InnerException != null) Console.WriteLine($" Inner: {ex.InnerException.Message}"); } finally { if (session != null) { await session.CloseAsync(); Console.WriteLine("\nSession closed."); } } Console.WriteLine("\nPress Enter to exit..."); Console.ReadLine(); } // --- 헬퍼 메서드들 --- static async Task ReadServerInfoAsync(ISession session) { var state = await session.ReadValueAsync(new NodeId(Variables.Server_ServerStatus_State)); var time = await session.ReadValueAsync(new NodeId(Variables.Server_ServerStatus_CurrentTime)); Console.WriteLine($" Server State: {state.Value}"); Console.WriteLine($" Server Time: {time.Value}"); } static async Task CheckRedundancyAsync(ISession session) { var serviceLevel = await session.ReadValueAsync(new NodeId(Variables.Server_ServiceLevel)); byte level = Convert.ToByte(serviceLevel.Value); string status = level >= 200 ? "PRIMARY" : (level > 0 ? "SECONDARY" : "STANDALONE"); Console.WriteLine($" Service Level: {level} ({status})"); } static async Task BrowseNodesAsync(ISession session) { var browser = new Browser(session) { BrowseDirection = BrowseDirection.Forward, ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences, IncludeSubtypes = true, NodeClassMask = (int)(NodeClass.Object | NodeClass.Variable) }; var references = await browser.BrowseAsync(ObjectIds.ObjectsFolder); Console.WriteLine($" Found {references.Count} nodes in Objects folder. Top 5:"); foreach (var r in references.Take(5)) { Console.WriteLine($" - {r.DisplayName} ({r.NodeClass})"); } } } }