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 _discoveredTags = new(); public HoneywellCrawler(ISession session) { _session = session ?? throw new ArgumentNullException(nameof(session)); } /// /// 탐사 시작점 /// 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}"); } } } }