Files
dbserver/OpcConnectionTest/Program copy.cs.no1
2026-02-21 06:14:35 +09:00

359 lines
14 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<uint, StatusCodeInfo> _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<List<StatusCodeInfo>>(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}");
}
}
}
}