using System; using System.Timers; 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 += (s, isConnected) => { ConnectionChanged?.Invoke(this, isConnected); if (!isConnected && _shouldBeConnected) StartReconnectTimer(); }; // 1초(1000ms)마다 연결 상태를 확인하고 재연결 시도 _reconnectTimer = new Timer(1000); _reconnectTimer.AutoReset = false; // 재진입 방지 _reconnectTimer.Elapsed += (s, e) => { if (_shouldBeConnected && !_communication.IsOpen) { Console.WriteLine($"[Service] Attempting to reconnect to {_communication.Name}..."); if (!_communication.Open()) { // 실패 시 다시 타이머 시작 if (_shouldBeConnected) _reconnectTimer.Start(); } } else if (_shouldBeConnected) { _reconnectTimer.Start(); } }; } 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); if (!_communication.Write($"{crc}{payload}\r\n")) { 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 void OnDataReceived(object sender, string rawData) { try { RawDataReceived?.Invoke(this, rawData); if (string.IsNullOrWhiteSpace(rawData)) return; // 헤더 분석 및 본문 추출 string body = SentinelParser.ExtractBody(rawData, out char typeCode); switch (typeCode) { case 'R': // 최종 결과 (Result Value) ResultReceived?.Invoke(this, rawData); var finalParsed = SentinelParser.ParseFinalResult(rawData); OnFinalResultParsed?.Invoke(this, finalParsed); break; case 'S': // 스트리밍 데이터 (Streaming Value) StreamingReceived?.Invoke(this, rawData); var streamParsed = SentinelParser.ParseStreamingValue(rawData); 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}"); } } public void Dispose() { Disconnect(); _reconnectTimer?.Stop(); _reconnectTimer?.Dispose(); _reconnectTimer = null; } } }