using Npgsql; using Opc.Ua; using Opc.Ua.Client; using OpcPks.Core.Services; // OpcSessionManager 참조 using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace OpcPks.Collector.Services { public class OpcUaCollector { private readonly OpcSessionManager _sessionManager; private ISession _session; private Subscription _subscription; private readonly string _dbConnString = "Host=localhost;Username=postgres;Password=postgres;Database=opcdb"; public OpcUaCollector(OpcSessionManager sessionManager) { _sessionManager = sessionManager; } public async Task StartCollection() { try { // 1. 공통 세션 관리자로부터 하니웰 세션 가져오기 _session = await _sessionManager.GetSessionAsync(); // 2. DB(tag_master)에서 엔지니어가 등록한 감시 대상 노드 리스트 조회 var nodesToMonitor = await GetNodesFromMaster(); if (nodesToMonitor.Count == 0) { Console.WriteLine("⚠️ 감시 대상으로 등록된 태그가 없습니다."); return; } // 3. Subscription 생성 (1초 주기 감시) _subscription = new Subscription(_session.DefaultSubscription) { PublishingInterval = 1000 }; foreach (var nodeId in nodesToMonitor) { var item = new MonitoredItem(_subscription.DefaultItem) { DisplayName = nodeId, StartNodeId = nodeId }; // 중요: 값이 변했을 때만 실행될 이벤트 핸들러 등록 item.Notification += OnDataChangeReceived; _subscription.AddItem(item); } _session.AddSubscription(_subscription); _subscription.Create(); Console.WriteLine($"🚀 수집 엔진 가동 중: {nodesToMonitor.Count}개의 태그 감시 시작."); } catch (Exception ex) { Console.WriteLine($"❌ 수집 시작 중 오류 발생: {ex.Message}"); } } private void OnDataChangeReceived(MonitoredItem item, MonitoredItemNotificationEventArgs e) { foreach (var value in item.DequeueValues()) { // 하니웰에서 값이 변경되어 통보(COV)가 오면 DB 업데이트 수행 UpdateLiveDb(item.StartNodeId.ToString(), value.Value?.ToString() ?? "NULL", value.StatusCode.ToString()); } } private void UpdateLiveDb(string nodeId, string val, string quality) { try { using var conn = new NpgsqlConnection(_dbConnString); // IS DISTINCT FROM: 실제 값이 다를 때만 쓰기(Write)를 수행하여 SSD 부하 감소 var sql = @" UPDATE tag_live_data SET live_value = @val, quality = @q, last_updated = NOW() WHERE full_node_id = @id AND live_value IS DISTINCT FROM @val"; using var cmd = new NpgsqlCommand(sql, conn); cmd.Parameters.AddWithValue("id", nodeId); cmd.Parameters.AddWithValue("val", val); cmd.Parameters.AddWithValue("q", quality); conn.Open(); int affectedRows = cmd.ExecuteNonQuery(); if (affectedRows > 0) { Console.WriteLine($"[변경수집] {nodeId} -> {val} ({quality})"); } } catch (Exception ex) { Console.WriteLine($"❌ DB 업데이트 오류: {ex.Message}"); } } private async Task> GetNodesFromMaster() { var nodes = new List(); try { using var conn = new NpgsqlConnection(_dbConnString); await conn.OpenAsync(); // tag_master에서 활성화된(is_active) 태그들만 가져옴 var sql = "SELECT full_node_id FROM tag_master WHERE is_active = TRUE"; using var cmd = new NpgsqlCommand(sql, conn); using var reader = await cmd.ExecuteReaderAsync(); while (await reader.ReadAsync()) { nodes.Add(reader.GetString(0)); } } catch (Exception ex) { Console.WriteLine($"❌ 마스터 노드 조회 실패: {ex.Message}"); } return nodes; } public async Task StopCollection() { if (_subscription != null) { _subscription.Delete(true); } Console.WriteLine("⏹️ 수집 엔진 정지."); } } }