using System; using System.Diagnostics; using System.Timers; using Timer = System.Timers.Timer; using leak_test_project.Infrastructure; using leak_test_project.Utils; namespace leak_test_project.Services { /// /// Sentinel C28 기기와의 통신 프로토콜을 관리하는 서비스. /// 자동 재연결(Auto-Reconnect) 및 예외 처리 포함. /// public class SentinelC28Service : IDisposable { private readonly ICommunication _communication; private int _sequence = 1; private Timer _reconnectTimer; private bool _shouldBeConnected = false; public event EventHandler RawDataReceived; public event EventHandler ResultReceived; public event EventHandler StreamingReceived; public event EventHandler ConnectionChanged; /// 파싱된 최종 검사 결과 알림 public event EventHandler OnFinalResultParsed; /// 파싱된 실시간 스트리밍 데이터 알림 public event EventHandler OnStreamingParsed; public SentinelC28Service(ICommunication communication) { _communication = communication; _communication.DataReceived += OnDataReceived; _communication.ConnectionStatusChanged += OnConnectionStatusChanged; // 1초(1000ms)마다 연결 상태를 확인하고 재연결 시도 _reconnectTimer = new Timer(1000); _reconnectTimer.AutoReset = false; // 재진입 방지 _reconnectTimer.Elapsed += (s, e) => { if (_shouldBeConnected && !_communication.IsOpen) { Debug.WriteLine($"[SentinelC28 Service] Attempting to reconnect to {_communication.Name}..."); Console.WriteLine($"[Service] Attempting to reconnect to {_communication.Name}..."); if (!_communication.Open()) { // 실패 시 다시 타이머 시작 if (_shouldBeConnected) _reconnectTimer.Start(); } } else if (_shouldBeConnected) { _reconnectTimer.Start(); } }; } private void OnConnectionStatusChanged(object sender, bool isConnected) { ConnectionChanged?.Invoke(this, isConnected); if (!isConnected && _shouldBeConnected) StartReconnectTimer(); } public bool Connect() { _shouldBeConnected = true; bool opened = _communication.Open(); if (!opened) StartReconnectTimer(); return opened; } public void Disconnect() { _shouldBeConnected = false; _reconnectTimer.Stop(); _communication.Close(); } private void StartReconnectTimer() { if (!_reconnectTimer.Enabled) _reconnectTimer.Start(); } public void SendCommand(string command, string dataTypeCode) { if (!_communication.IsOpen) return; try { string sequenceHex = _sequence.ToString("X2"); string lengthHex = command.Length.ToString("X3"); string payload = $"{sequenceHex}{lengthHex} {dataTypeCode}\t{command}"; string crc = SentinelCrc8.CalculateHex(payload); string fullCommand = $"{crc}{payload}\r\n"; Debug.WriteLine($"[SentinelC28 Service] Sending Command: {fullCommand.TrimEnd()}"); if (!_communication.Write(fullCommand)) { FileLogger.Log("ERROR", "[SentinelC28] Failed to send command: Communication channel closed."); } _sequence = (_sequence >= 255) ? 1 : _sequence + 1; } catch (Exception ex) { FileLogger.Log("ERROR", $"[SentinelC28] Error sending command: {ex.Message}"); } } private readonly System.Text.StringBuilder _receiveBuffer = new System.Text.StringBuilder(8192); private const int MaxBufferSize = 65536; // 64KB private void OnDataReceived(object sender, string rawData) { try { Debug.WriteLine($"[SentinelC28 Service] Raw Data Received: {rawData.Replace("\r", "\\r").Replace("\n", "\\n").Replace("\t", "\\t")}"); RawDataReceived?.Invoke(this, rawData); if (string.IsNullOrEmpty(rawData)) return; lock (_receiveBuffer) { if (_receiveBuffer.Length + rawData.Length > MaxBufferSize) { _receiveBuffer.Clear(); FileLogger.Log("WARNING", "[SentinelC28] Buffer size limit exceeded. Buffer cleared to prevent OOM."); } _receiveBuffer.Append(rawData); string content = _receiveBuffer.ToString(); int splitIndex; while ((splitIndex = GetFirstDelimiterIndex(content)) >= 0) { string frame = content.Substring(0, splitIndex); content = content.Substring(splitIndex + 1); // 버퍼 갱신 (처리한 데이터 잘라냄) _receiveBuffer.Clear(); _receiveBuffer.Append(content); if (string.IsNullOrWhiteSpace(frame)) continue; // 헤더 분석 및 본문 추출 string body = SentinelParser.ExtractBody(frame, out char typeCode); switch (typeCode) { case 'R': // 최종 결과 (Result Value) ResultReceived?.Invoke(this, frame); var finalParsed = SentinelParser.ParseFinalResult(frame); OnFinalResultParsed?.Invoke(this, finalParsed); break; case 'S': // 스트리밍 데이터 (Streaming Value) StreamingReceived?.Invoke(this, frame); var streamParsed = SentinelParser.ParseStreamingValue(frame); OnStreamingParsed?.Invoke(this, streamParsed); break; case 'M': // 일반 메시지 FileLogger.Log("INFO", $"[SentinelC28 Message] {body}"); break; } } } } catch (Exception ex) { FileLogger.Log("ERROR", $"[SentinelC28] Error parsing received data: {ex.Message}"); } } private int GetFirstDelimiterIndex(string text) { int indexN = text.IndexOf('\n'); int indexR = text.IndexOf('\r'); if (indexN >= 0 && indexR >= 0) return Math.Min(indexN, indexR); return indexN >= 0 ? indexN : indexR; } public void Dispose() { Disconnect(); // [Memory Leak 방지] 이벤트 핸들러 명시적 구독 해제 if (_communication != null) { _communication.DataReceived -= OnDataReceived; _communication.ConnectionStatusChanged -= OnConnectionStatusChanged; } _reconnectTimer?.Stop(); _reconnectTimer?.Dispose(); _reconnectTimer = null; } } }