분리후 첫 Crawling 성공 모델
This commit is contained in:
145
OpcPksPlatform/OpcPks.Core/Services/HoneywellCrawler.cs
Normal file
145
OpcPksPlatform/OpcPks.Core/Services/HoneywellCrawler.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Client;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using OpcPks.Core.Models;
|
||||
|
||||
namespace OpcPks.Core.Services
|
||||
{
|
||||
public class HoneywellCrawler
|
||||
{
|
||||
// ✅ Session 대신 ISession 인터페이스를 사용하여 호환성 확보
|
||||
private readonly ISession _session;
|
||||
private readonly List<TagMaster> _discoveredTags = new();
|
||||
|
||||
public HoneywellCrawler(ISession session)
|
||||
{
|
||||
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 탐사 시작점
|
||||
/// </summary>
|
||||
public async Task RunAsync(string startNodeId, string csvPath)
|
||||
{
|
||||
Console.WriteLine($"\n🚀 하니웰 자산모델 탐사 시작: {startNodeId}");
|
||||
|
||||
try
|
||||
{
|
||||
_discoveredTags.Clear();
|
||||
NodeId rootNode = NodeId.Parse(startNodeId);
|
||||
await BrowseRecursiveAsync(rootNode, 0);
|
||||
|
||||
Console.WriteLine("\n===============================================");
|
||||
Console.WriteLine($"✅ 탐사 완료! 총 {_discoveredTags.Count}개의 항목 발견.");
|
||||
Console.WriteLine("===============================================");
|
||||
|
||||
SaveToCsv(csvPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"❌ 탐사 중 오류: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BrowseRecursiveAsync(NodeId nodeId, int level)
|
||||
{
|
||||
try
|
||||
{
|
||||
BrowseDescription description = new() {
|
||||
NodeId = nodeId,
|
||||
BrowseDirection = BrowseDirection.Forward,
|
||||
ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences,
|
||||
IncludeSubtypes = true,
|
||||
NodeClassMask = (uint)(NodeClass.Variable | NodeClass.Object),
|
||||
ResultMask = (uint)BrowseResultMask.All
|
||||
};
|
||||
|
||||
// BrowseAsync 호출
|
||||
BrowseResponse response = await _session.BrowseAsync(null, null, 0, new BrowseDescriptionCollection { description }, default);
|
||||
|
||||
if (response?.Results == null || response.Results.Count == 0) return;
|
||||
|
||||
foreach (var result in response.Results)
|
||||
{
|
||||
await ProcessReferencesAsync(result.References, level);
|
||||
|
||||
byte[] cp = result.ContinuationPoint;
|
||||
while (cp != null && cp.Length > 0)
|
||||
{
|
||||
// ✅ ByteStringCollection을 사용하여 라이브러리 표준 인자 타입 일치화
|
||||
var cpCollection = new ByteStringCollection { cp };
|
||||
BrowseNextResponse nextResponse = await _session.BrowseNextAsync(null, false, cpCollection, default);
|
||||
|
||||
if (nextResponse?.Results != null && nextResponse.Results.Count > 0)
|
||||
{
|
||||
await ProcessReferencesAsync(nextResponse.Results[0].References, level);
|
||||
cp = nextResponse.Results[0].ContinuationPoint;
|
||||
}
|
||||
else
|
||||
{
|
||||
cp = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception) { /* 접근 권한 없는 노드는 무시 */ }
|
||||
}
|
||||
|
||||
private async Task ProcessReferencesAsync(ReferenceDescriptionCollection references, int level)
|
||||
{
|
||||
if (references == null || references.Count == 0) return;
|
||||
|
||||
foreach (var rd in references)
|
||||
{
|
||||
// ExpandedNodeId -> NodeId 변환
|
||||
NodeId childId = ExpandedNodeId.ToNodeId(rd.NodeId, _session.NamespaceUris);
|
||||
|
||||
// TagMaster 모델 규격에 맞춰 데이터 추가
|
||||
_discoveredTags.Add(new TagMaster
|
||||
{
|
||||
TagName = rd.BrowseName.Name ?? "Unknown",
|
||||
FullNodeId = childId.ToString(),
|
||||
NodeClass = rd.NodeClass.ToString(),
|
||||
Level = level
|
||||
});
|
||||
|
||||
string indent = new string(' ', level * 2);
|
||||
Console.WriteLine($"{indent} [{rd.NodeClass}] {rd.BrowseName.Name} (ID: {childId})");
|
||||
|
||||
// 하위 폴더(Object)가 있고 탐사 깊이가 5단계 미만이면 재귀 탐색
|
||||
if (rd.NodeClass == NodeClass.Object && level < 5)
|
||||
{
|
||||
await BrowseRecursiveAsync(childId, level + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveToCsv(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 디렉토리가 없으면 생성
|
||||
string dir = Path.GetDirectoryName(filePath);
|
||||
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
|
||||
using StreamWriter sw = new(filePath);
|
||||
sw.WriteLine("Level,Class,Name,NodeId");
|
||||
foreach (var tag in _discoveredTags)
|
||||
{
|
||||
sw.WriteLine($"{tag.Level},{tag.NodeClass},{tag.TagName},{tag.FullNodeId}");
|
||||
}
|
||||
Console.WriteLine($"💾 CSV 저장 완료: {filePath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"❌ CSV 저장 실패: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
108
OpcPksPlatform/OpcPks.Core/Services/OpcSessionManager.cs
Normal file
108
OpcPksPlatform/OpcPks.Core/Services/OpcSessionManager.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Client;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Net;
|
||||
|
||||
namespace OpcPks.Core.Services
|
||||
{
|
||||
public class OpcSessionManager
|
||||
{
|
||||
private ISession? _session;
|
||||
public ISession? Session => _session;
|
||||
|
||||
public async Task<ISession> GetSessionAsync()
|
||||
{
|
||||
if (_session != null && _session.Connected) return _session;
|
||||
|
||||
string serverHostName = "192.168.0.20";
|
||||
string endpointUrl = $"opc.tcp://{serverHostName}:4840";
|
||||
string applicationUri = "urn:dbsvr:OpcTestClient";
|
||||
string userName = "mngr";
|
||||
string password = "mngr";
|
||||
string pfxPassword = ""; // openssl에서 확인한 비밀번호가 있다면 입력
|
||||
|
||||
string baseDataPath = "/home/pacer/projects/OpcPksPlatform/OpcPks.Core/Data/pki";
|
||||
string pfxFilePath = Path.Combine(baseDataPath, "own/private/OpcTestClient.pfx");
|
||||
|
||||
var config = new ApplicationConfiguration {
|
||||
ApplicationName = "OpcTestClient",
|
||||
ApplicationType = ApplicationType.Client,
|
||||
ApplicationUri = applicationUri,
|
||||
SecurityConfiguration = new SecurityConfiguration {
|
||||
ApplicationCertificate = new CertificateIdentifier {
|
||||
StoreType = "Directory",
|
||||
StorePath = Path.Combine(baseDataPath, "own"),
|
||||
SubjectName = "OpcTestClient"
|
||||
},
|
||||
TrustedPeerCertificates = new CertificateTrustList {
|
||||
StoreType = "Directory",
|
||||
StorePath = Path.Combine(baseDataPath, "trusted")
|
||||
},
|
||||
AutoAcceptUntrustedCertificates = true,
|
||||
AddAppCertToTrustedStore = true
|
||||
},
|
||||
TransportQuotas = new TransportQuotas { OperationTimeout = 60000 },
|
||||
ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 }
|
||||
};
|
||||
|
||||
// 1. [인증서 강제 로드] Validate 호출 전에 인증서를 명시적으로 로드합니다.
|
||||
if (!File.Exists(pfxFilePath)) {
|
||||
throw new Exception($"❌ PFX 파일을 찾을 수 없습니다: {pfxFilePath}");
|
||||
}
|
||||
|
||||
try {
|
||||
// 리눅스 .NET 호환용 플래그
|
||||
var cert = new X509Certificate2(
|
||||
pfxFilePath,
|
||||
pfxPassword,
|
||||
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet
|
||||
);
|
||||
|
||||
// config에 인증서 직접 주입
|
||||
config.SecurityConfiguration.ApplicationCertificate.Certificate = cert;
|
||||
Console.WriteLine($"✅ [인증서 로드] {cert.Subject}");
|
||||
} catch (Exception ex) {
|
||||
Console.WriteLine($"❌ [인증서 에러] PFX 로드 실패: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
|
||||
// 2. 설정 검증 (인증서 주입 후 실행)
|
||||
await config.Validate(ApplicationType.Client);
|
||||
|
||||
// Validate 이후 인증서가 풀렸을 경우를 대비해 다시 확인
|
||||
if (config.SecurityConfiguration.ApplicationCertificate.Certificate == null) {
|
||||
Console.WriteLine("⚠️ [경고] Validate 과정에서 인증서 설정이 유실되어 재할당합니다.");
|
||||
config.SecurityConfiguration.ApplicationCertificate.Certificate = new X509Certificate2(pfxFilePath, pfxPassword, X509KeyStorageFlags.MachineKeySet);
|
||||
}
|
||||
|
||||
config.CertificateValidator.CertificateValidation += (s, e) => { e.Accept = true; };
|
||||
|
||||
// 3. 엔드포인트 선택 및 보안 정책 로그
|
||||
var endpointDescription = CoreClientUtils.SelectEndpoint(endpointUrl, true);
|
||||
var endpointConfiguration = EndpointConfiguration.Create(config);
|
||||
var configuredEndpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration);
|
||||
|
||||
Console.WriteLine($"📡 [세션] 연결 시도: {endpointUrl}");
|
||||
Console.WriteLine($"🔐 [보안] {endpointDescription.SecurityPolicyUri} ({endpointDescription.SecurityMode})");
|
||||
|
||||
// 4. 세션 생성 시도
|
||||
try {
|
||||
_session = await Opc.Ua.Client.Session.Create(
|
||||
config,
|
||||
configuredEndpoint,
|
||||
true,
|
||||
"OpcPksSession",
|
||||
60000,
|
||||
new UserIdentity(userName, password),
|
||||
null
|
||||
);
|
||||
Console.WriteLine("🚀 [성공] 하니웰 서버와 연결되었습니다!");
|
||||
} catch (ServiceResultException srex) {
|
||||
Console.WriteLine($"❌ [OPC 에러] {srex.StatusCode}: {srex.Message}");
|
||||
throw;
|
||||
}
|
||||
|
||||
return _session;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user