using System; using System.Text; using System.Threading; using System.Threading.Tasks; using leak_test_project.Infrastructure; using leak_test_project.Utils; namespace leak_test_project.Services { /// /// 신규 4253 보드와의 통신을 관리하는 서비스. /// 키워드를 기준으로 데이터를 수신하며, 상태 확인 및 ID 읽기 기능을 제공함. /// public class Board4253Service : IDisposable { private readonly ICommunication _communication; private readonly StringBuilder _receiveBuffer = new StringBuilder(); private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); private TaskCompletionSource _responseTcs; public int TimeoutMs { get; set; } = 5000; // 보드가 응답을 주는데 2초 이상 걸리므로 무조건 길게 대기 private bool _shouldBeConnected = false; private System.Timers.Timer _reconnectTimer; public event EventHandler ErrorOccurred; public event EventHandler ConnectionChanged; public string LastResponse { get; private set; } = ""; public string GetLastBuffer() { string currentBuf = _receiveBuffer.ToString().Trim(); return string.IsNullOrEmpty(currentBuf) ? LastResponse : currentBuf; } public Board4253Service(ICommunication communication) { _communication = communication; _communication.DataReceived += OnDataReceived; _communication.ConnectionStatusChanged += (s, isConnected) => { ConnectionChanged?.Invoke(this, isConnected); if (!isConnected && _shouldBeConnected) StartReconnectTimer(); }; _reconnectTimer = new System.Timers.Timer(1000); _reconnectTimer.AutoReset = false; _reconnectTimer.Elapsed += (s, e) => { if (_shouldBeConnected && !_communication.IsOpen) { if (!_communication.Open()) { if (_shouldBeConnected) _reconnectTimer.Start(); } } else if (_shouldBeConnected) { _reconnectTimer.Start(); } }; } private void StartReconnectTimer() { if (_reconnectTimer != null && !_reconnectTimer.Enabled) _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(); } /// /// 4253 보드의 상태를 확인합니다. /// /// 성공 여부 public async Task CheckStatusAsync(int channel = 1) { string response = await SendCommandAsync($"x00c_00{channel}101:owt28006727ea97c7801\r\n"); if (response == null) { return false; } if (response.IndexOf("Success", StringComparison.OrdinalIgnoreCase) >= 0) { return true; } if (response.IndexOf("Fail", StringComparison.OrdinalIgnoreCase) >= 0) { return false; } // Success도 Fail도 없는 알 수 없는 응답인 경우 return false; } /// /// 4253 보드로부터 16자리 ID를 읽어옵니다. /// /// 16자리 ID 문자열, 실패 시 null public async Task ReadIdAsync(int channel = 1) { string response = await SendCommandAsync($"x00c_00{channel}101:ow2800326003e\r\n"); if (response == null) { return null; } if (response.Contains("Fail")) { return null; } string id = ExtractId(response); if (string.IsNullOrEmpty(id)) { return null; } return id; } private string ExtractId(string response) { if (string.IsNullOrEmpty(response)) return null; // 보드 응답에는 보낸 명령어가 에코되어 포함되므로, 줄바꿈으로 나누어 실제 데이터 라인을 찾음 string[] lines = response.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { string trimmed = line.Trim(); if (trimmed.Contains("x00c_") || trimmed.IndexOf("", StringComparison.OrdinalIgnoreCase) >= 0 || trimmed.IndexOf("Success", StringComparison.OrdinalIgnoreCase) >= 0) continue; // 16자리 영숫자 ID 추출 var match = System.Text.RegularExpressions.Regex.Match(trimmed, @"[A-Za-z0-9]{16}"); if (match.Success) return match.Value; } return null; } private async Task SendCommandAsync(string command) { await _lock.WaitAsync(); try { int retryCount = 0; while (retryCount <= 3) { if (!_communication.IsOpen) { if (!_communication.Open()) { retryCount++; await Task.Delay(300); continue; } } _receiveBuffer.Clear(); LastResponse = ""; _responseTcs = new TaskCompletionSource(); _communication.ClearBuffer(); // 이전에 남아있던 패킷 조각 완벽히 제거 _communication.Write(command); using (var cts = new CancellationTokenSource(TimeoutMs)) { cts.Token.Register(() => _responseTcs.TrySetCanceled()); try { return await _responseTcs.Task; } catch (OperationCanceledException) { retryCount++; string currentBuffer = _receiveBuffer.ToString().Trim(); FileLogger.Log("WARNING", $"[Board4253] Timeout waiting for response (Retry {retryCount}/3). Command: {command.Trim()}, ReceivedSoFar: {currentBuffer}"); _receiveBuffer.Clear(); if (retryCount <= 3) await Task.Delay(300); } } } FileLogger.Log("ERROR", $"[Board4253] Failed to receive response after 3 retries: {command.Trim()}"); return null; } finally { _responseTcs = null; _lock.Release(); } } private void OnDataReceived(object sender, string data) { _receiveBuffer.Append(data); string currentContent = _receiveBuffer.ToString(); bool isComplete = false; if (currentContent.IndexOf("", StringComparison.OrdinalIgnoreCase) >= 0 || currentContent.IndexOf("Success", StringComparison.OrdinalIgnoreCase) >= 0 || currentContent.IndexOf("Fail", StringComparison.OrdinalIgnoreCase) >= 0) { isComplete = true; } else { // 가 오지 않았더라도, 명령어 에코가 아닌 줄에서 16자리 ID를 발견하면 즉시 완료 처리 string[] lines = currentContent.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { string trimmed = line.Trim(); if (trimmed.Contains("x00c_")) continue; var match = System.Text.RegularExpressions.Regex.Match(trimmed, @"[A-Za-z0-9]{16}"); if (match.Success) { isComplete = true; break; } } } if (isComplete) { if (_responseTcs != null && !_responseTcs.Task.IsCompleted) { LastResponse = currentContent.Trim(); _responseTcs.TrySetResult(currentContent); } _receiveBuffer.Clear(); } } public void Dispose() { Disconnect(); _communication.DataReceived -= OnDataReceived; _reconnectTimer?.Dispose(); _lock.Dispose(); } } }