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();
}
}
}