using Opc.Ua; using Opc.Ua.Client; using Opc.Ua.Configuration; using System.Security.Cryptography.X509Certificates; using System.Text.Json; namespace OpcConnectionTest { public class StatusCodeInfo { public string Name { get; set; } = ""; public string Hex { get; set; } = ""; public ulong Decimal { get; set; } public string Description { get; set; } = ""; } class Program { [Obsolete] static Dictionary _statusCodeMap = new(); static void LoadStatusCodes() { try { string path = Path.Combine(Directory.GetCurrentDirectory(), "statuscode.json"); if (!File.Exists(path)) { Console.WriteLine("⚠️ statuscode.json 파일을 찾을 수 없습니다."); return; } var json = File.ReadAllText(path); var list = JsonSerializer.Deserialize>(json); if (list != null) { foreach (var item in list) { _statusCodeMap[(uint)item.Decimal] = item; } } Console.WriteLine($"✅ StatusCode { _statusCodeMap.Count }개 로드 완료\n"); } catch (Exception ex) { Console.WriteLine($"❌ statuscode.json 로드 실패: {ex.Message}"); } } static async Task Main(string[] args) { Console.WriteLine("=== Experion OPC UA Connection Test ===\n"); LoadStatusCodes(); // ⚠️ Experion 서버 IP 주소 수정 string endpoint = "opc.tcp://192.168.0.20:4840"; // ⚠️ Honeywell CA로 서명된 클라이언트 인증서 경로 및 비밀번호 string pfxPath = Path.Combine(Directory.GetCurrentDirectory(), "pki/own/certs/OpcTestClient.pfx"); string pfxPassword = ""; // ⚠️ PFX 비밀번호 (없으면 빈 문자열) // ⚠️ Experion Windows 계정 정보 string userName = "mngr"; // 예: "DOMAIN\\user" 또는 "localuser" string password = "mngr"; Console.WriteLine($"Server: {endpoint}"); Console.WriteLine($"User: {userName}\n"); // PFX 파일 존재 여부 확인 if (!File.Exists(pfxPath)) { Console.WriteLine($"❌ PFX 파일을 찾을 수 없습니다: {pfxPath}"); Console.WriteLine("\nPress Enter to exit..."); Console.ReadLine(); return; } // Honeywell CA 서명 인증서 로드 // Exportable: OPC UA 채널 서명 시 개인키 접근 필요 X509Certificate2 clientCertificate = new X509Certificate2( pfxPath, pfxPassword, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet); Console.WriteLine($"✅ 인증서 로드: {clientCertificate.Subject}"); Console.WriteLine($" 유효기간: {clientCertificate.NotBefore:yyyy-MM-dd} ~ {clientCertificate.NotAfter:yyyy-MM-dd}"); Console.WriteLine($" 개인키 포함: {clientCertificate.HasPrivateKey}\n"); if (!clientCertificate.HasPrivateKey) { Console.WriteLine("❌ 개인키가 없습니다. PFX 파일을 확인하세요."); Console.WriteLine("\nPress Enter to exit..."); Console.ReadLine(); return; } ISession? session = null; try { // 1. Configuration 생성 var config = new ApplicationConfiguration() { ApplicationName = "OPC Test Client", ApplicationType = ApplicationType.Client, ApplicationUri = "urn:OpcTestClient", SecurityConfiguration = new SecurityConfiguration { ApplicationCertificate = new CertificateIdentifier { StoreType = "Directory", StorePath = Path.GetFullPath("pki/own"), SubjectName = clientCertificate.Subject, // Honeywell CA 서명 인증서를 직접 주입 Certificate = clientCertificate }, TrustedPeerCertificates = new CertificateTrustList { StoreType = "Directory", StorePath = Path.GetFullPath("pki/trusted") }, TrustedIssuerCertificates = new CertificateTrustList { StoreType = "Directory", // ⚠️ Honeywell 내부 CA 인증서를 이 폴더에 배치해야 합니다 // Experion Station에서 CA 인증서를 내보내어 pki/issuers/certs/ 에 복사 StorePath = Path.GetFullPath("pki/issuers") }, RejectedCertificateStore = new CertificateTrustList { StoreType = "Directory", StorePath = Path.GetFullPath("pki/rejected") }, // Honeywell CA 서명 인증서이므로 자동 수락 불필요 — false 권장 // 테스트 중 서버 인증서 검증 실패 시 true로 임시 변경 가능 AutoAcceptUntrustedCertificates = false, RejectSHA1SignedCertificates = false, AddAppCertToTrustedStore = false }, TransportConfigurations = new TransportConfigurationCollection(), TransportQuotas = new TransportQuotas { OperationTimeout = 15000 }, ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 } }; // 서버 인증서 검증 실패 시 로그만 출력하는 핸들러 // Honeywell CA를 pki/issuers/에 제대로 넣으면 이 핸들러까지 오지 않아야 정상 // 불가피한 경우 e.Accept = true; 주석 해제 (운영 환경에서는 사용 금지) config.CertificateValidator.CertificateValidation += (validator, e) => { Console.WriteLine($" ⚠️ 서버 인증서 검증 이슈: {e.Error.StatusCode} — {e.Certificate.Subject}"); // e.Accept = true; }; await config.ValidateAsync(ApplicationType.Client); Console.WriteLine("Step 1: Discovering endpoints..."); // 2. Endpoint 검색 var endpointConfig = EndpointConfiguration.Create(config); endpointConfig.OperationTimeout = 10000; var discoveryClient = await DiscoveryClient.CreateAsync( new Uri(endpoint), endpointConfig, config, DiagnosticsMasks.None, CancellationToken.None); var endpoints = await discoveryClient.GetEndpointsAsync(null); discoveryClient.Dispose(); if (endpoints == null || endpoints.Count == 0) { Console.WriteLine("❌ No endpoints found!"); return; } Console.WriteLine($"✅ Found {endpoints.Count} endpoint(s)"); foreach (var ep in endpoints) { Console.WriteLine($" - {ep.EndpointUrl} | Security: {ep.SecurityMode} | Policy: {ep.SecurityPolicyUri?.Split('#').Last()}"); } // SignAndEncrypt 엔드포인트 우선 선택, 없으면 Sign, 없으면 첫 번째 var selectedEndpoint = endpoints.FirstOrDefault(e => e.SecurityMode == MessageSecurityMode.SignAndEncrypt) ?? endpoints.FirstOrDefault(e => e.SecurityMode == MessageSecurityMode.Sign) ?? endpoints[0]; Console.WriteLine($"\n선택된 엔드포인트: {selectedEndpoint.EndpointUrl} ({selectedEndpoint.SecurityMode})"); Console.WriteLine("\nStep 2: Creating session..."); // 3. ConfiguredEndpoint 생성 var configuredEndpoint = new ConfiguredEndpoint( null, selectedEndpoint, EndpointConfiguration.Create(config)); // 4. Session 생성 — UserIdentity에 Windows 계정 사용 // Experion R530은 Windows 계정을 OPC UA UserNameIdentityToken으로 인증 var userIdentity = new UserIdentity(new UserNameIdentityToken { UserName = userName, DecryptedPassword = System.Text.Encoding.UTF8.GetBytes(password) }); session = await Session.Create( config, configuredEndpoint, false, "OPC Test Session", 60000, userIdentity, null); Console.WriteLine($"✅ Connected!"); Console.WriteLine($" Session ID: {session.SessionId}"); 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!"); } catch (ServiceResultException sre) { uint code = (uint)sre.StatusCode; // Console.WriteLine($"\n❌ OPC UA 오류: {sre.Message}"); Console.WriteLine($" StatusCode: 0x{code:X8} ({code})"); if (_statusCodeMap.TryGetValue(code, out var info)) { Console.WriteLine($" Name: {info.Name}"); Console.WriteLine($" Description: {info.Description}"); } else { Console.WriteLine(" ⚠️ statuscode.json에 정의되지 않은 코드입니다."); } } finally { if (session != null) { try { Console.WriteLine("\nClosing session..."); await session.CloseAsync(); session.Dispose(); Console.WriteLine("✅ Session closed"); } catch { } } } Console.WriteLine("\nPress Enter to exit..."); Console.ReadLine(); } static async Task ReadServerInfoAsync(ISession session) { try { var state = await session.ReadValueAsync( new NodeId(Variables.Server_ServerStatus_State)); Console.WriteLine($" State: {state.Value}"); var time = await session.ReadValueAsync( new NodeId(Variables.Server_ServerStatus_CurrentTime)); Console.WriteLine($" Time: {time.Value}"); } catch (Exception ex) { Console.WriteLine($" Error: {ex.Message}"); } } static async Task CheckRedundancyAsync(ISession session) { try { var serviceLevel = await session.ReadValueAsync( new NodeId(Variables.Server_ServiceLevel)); byte level = Convert.ToByte(serviceLevel.Value); Console.WriteLine($" Service Level: {level}"); if (level >= 200) Console.WriteLine(" ✅ PRIMARY server"); else if (level > 0) Console.WriteLine(" ⚠️ SECONDARY server"); else Console.WriteLine(" ℹ️ STANDALONE"); } catch (Exception ex) { Console.WriteLine($" Error: {ex.Message}"); } } static async Task BrowseNodesAsync(ISession session) { try { 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} top-level nodes:"); int count = 0; foreach (var r in references) { Console.WriteLine($" {r.DisplayName} ({r.NodeClass})"); if (++count >= 5) { Console.WriteLine($" ... and {references.Count - 5} more"); break; } } } catch (Exception ex) { Console.WriteLine($" Error: {ex.Message}"); } } } }