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;
}
}