using Opc.Ua; using Opc.Ua.Client; using OpcUaManager.Models; namespace OpcUaManager.Services; /// /// 원본 HoneywellCrawler 를 Service 로 분리. /// v1.5.374.70: BrowseNextAsync 가 튜플 반환이 아닌 BrowseNextResponse 객체 반환으로 변경됨. /// public class OpcCrawlerService { private readonly ILogger _logger; private readonly OpcSessionService _sessionSvc; public OpcCrawlerService(ILogger logger, OpcSessionService sessionSvc) { _logger = logger; _sessionSvc = sessionSvc; } public async Task CrawlAsync(CrawlRequest req) { if (!_sessionSvc.IsConnected) return new CrawlResult { Success = false, Message = "세션이 연결되어 있지 않습니다." }; var session = _sessionSvc.GetRawSession(); if (session == null) return new CrawlResult { Success = false, Message = "Raw 세션을 가져올 수 없습니다." }; var tags = new List(); try { _logger.LogInformation("저인망 탐사 시작: {NodeId}", req.StartNodeId); NodeId rootNode = NodeId.Parse(req.StartNodeId); await BrowseRecursiveAsync(session, rootNode, 0, req.MaxDepth, tags); string csvPath = await SaveToCsvAsync(tags); _logger.LogInformation("탐사 완료: {Count}개 노드", tags.Count); return new CrawlResult { Success = true, Message = $"탐사 완료: {tags.Count}개 노드 발견", TotalNodes = tags.Count, Tags = tags, CsvPath = csvPath }; } catch (Exception ex) { _logger.LogError(ex, "Crawler 실행 오류"); return new CrawlResult { Success = false, Message = ex.Message, Tags = tags }; } } private async Task BrowseRecursiveAsync( Session session, NodeId nodeId, int level, int maxDepth, List tags) { try { // 원본 동일: BrowseAsync 객체 방식 BrowseDescription description = new() { NodeId = nodeId, BrowseDirection = BrowseDirection.Forward, ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences, IncludeSubtypes = true, NodeClassMask = (uint)(NodeClass.Variable | NodeClass.Object), ResultMask = (uint)BrowseResultMask.All }; BrowseResponse response = await session.BrowseAsync( null, null, 0, [description], default); if (response?.Results == null || response.Results.Count == 0) return; foreach (var result in response.Results) { await ProcessReferencesAsync(session, result.References, level, maxDepth, tags); byte[] cp = result.ContinuationPoint; while (cp != null && cp.Length > 0) { // FIX CS8130/CS1503: BrowseNextAsync 튜플 방식 → BrowseNextResponse 객체 방식 // v1.5.374.70 에서 반환 타입이 Task 로 변경됨 BrowseNextResponse nextResponse = await session.BrowseNextAsync( null, false, new ByteStringCollection { cp }, default); if (nextResponse?.Results != null && nextResponse.Results.Count > 0) { var nextResult = nextResponse.Results[0]; await ProcessReferencesAsync(session, nextResult.References, level, maxDepth, tags); cp = nextResult.ContinuationPoint; } else { break; } } } } catch (Exception ex) { // 원본과 동일: 특정 노드 권한 에러 무시 _logger.LogDebug("노드 탐색 건너뜀 [{Level}] {NodeId}: {Msg}", level, nodeId, ex.Message); } } private async Task ProcessReferencesAsync( Session session, ReferenceDescriptionCollection? references, int level, int maxDepth, List tags) { if (references == null || references.Count == 0) return; foreach (var rd in references) { NodeId childId = ExpandedNodeId.ToNodeId(rd.NodeId, session.NamespaceUris); tags.Add(new TagMaster { TagName = rd.BrowseName.Name ?? "Unknown", FullNodeId = childId.ToString(), NodeClass = rd.NodeClass.ToString(), Level = level }); _logger.LogDebug("{Indent}[{Class}] {Name} ({Id})", new string(' ', level * 2), rd.NodeClass, rd.BrowseName.Name, childId); if (rd.NodeClass == NodeClass.Object && level < maxDepth) await BrowseRecursiveAsync(session, childId, level + 1, maxDepth, tags); } } private async Task SaveToCsvAsync(List tags) { string path = Path.GetFullPath("Honeywell_FullMap.csv"); try { await using var sw = new StreamWriter(path); await sw.WriteLineAsync("Level,Class,Name,NodeId"); foreach (var tag in tags) await sw.WriteLineAsync($"{tag.Level},{tag.NodeClass},{tag.TagName},{tag.FullNodeId}"); _logger.LogInformation("CSV 저장 완료: {Path}", path); } catch (Exception ex) { _logger.LogError(ex, "CSV 저장 실패"); } return path; } }