162 lines
5.8 KiB
C#
162 lines
5.8 KiB
C#
using Opc.Ua;
|
|
using Opc.Ua.Client;
|
|
using OpcUaManager.Models;
|
|
|
|
namespace OpcUaManager.Services;
|
|
|
|
/// <summary>
|
|
/// 원본 HoneywellCrawler 를 Service 로 분리.
|
|
/// v1.5.374.70: BrowseNextAsync 가 튜플 반환이 아닌 BrowseNextResponse 객체 반환으로 변경됨.
|
|
/// </summary>
|
|
public class OpcCrawlerService
|
|
{
|
|
private readonly ILogger<OpcCrawlerService> _logger;
|
|
private readonly OpcSessionService _sessionSvc;
|
|
|
|
public OpcCrawlerService(ILogger<OpcCrawlerService> logger, OpcSessionService sessionSvc)
|
|
{
|
|
_logger = logger;
|
|
_sessionSvc = sessionSvc;
|
|
}
|
|
|
|
public async Task<CrawlResult> 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<TagMaster>();
|
|
|
|
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<TagMaster> 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> 로 변경됨
|
|
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<TagMaster> 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<string> SaveToCsvAsync(List<TagMaster> 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;
|
|
}
|
|
}
|