리크 테스트 gui
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

489 lines
19 KiB

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using leak_test_project.Infrastructure;
using leak_test_project.Models;
using leak_test_project.Utils;
namespace leak_test_project.Services
{
/// <summary>
/// 신규 4251 보드와의 통신을 관리하는 서비스.
/// <end> 키워드를 기준으로 데이터를 수신하며, 상태 확인 및 ID 읽기 기능을 제공함.
/// </summary>
public class Board4251Service : IDisposable
{
private readonly ICommunication _communication;
private readonly StringBuilder _receiveBuffer = new StringBuilder();
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
private TaskCompletionSource<string> _responseTcs;
public int TimeoutMs { get; set; } = 5000; // 보드가 응답을 주는데 2초 이상 걸리므로 무조건 길게 대기
private bool _shouldBeConnected = false;
private System.Timers.Timer _reconnectTimer;
public event EventHandler<string> ErrorOccurred;
public event EventHandler<bool> ConnectionChanged;
public string LastResponse { get; private set; } = "";
public string GetLastBuffer()
{
string currentBuf = _receiveBuffer.ToString().Trim();
return string.IsNullOrEmpty(currentBuf) ? LastResponse : currentBuf;
}
public Board4251Service(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();
}
/// <summary>
/// 4251 보드의 상태를 확인합니다.
/// </summary>
/// <returns>성공 여부</returns>
public async Task<bool> 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;
}
/// <summary>
/// 4251 보드로부터 16자리 ID를 읽어옵니다.
/// </summary>
/// <returns>16자리 ID 문자열, 실패 시 null</returns>
public async Task<string> 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("<end>", 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;
}
public async Task<string> 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<string>();
_communication.ClearBuffer(); // 이전에 남아있던 패킷 조각 완벽히 제거
Debug.WriteLine($"[Board4251] Sending Command: {command.TrimEnd()}");
_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", $"[Board4251] Timeout waiting for response (Retry {retryCount}/3). Command: {command.Trim()}, ReceivedSoFar: {currentBuffer}");
_receiveBuffer.Clear();
if (retryCount <= 3) await Task.Delay(300);
}
}
}
FileLogger.Log("ERROR", $"[Board4251] Failed to receive response after 3 retries: {command.Trim()}");
return null;
}
finally
{
_responseTcs = null;
_lock.Release();
}
}
private void OnDataReceived(object sender, string data)
{
Debug.WriteLine($"[Board4251] Raw Data Chunk Received: {data.Replace("\r", "\\r").Replace("\n", "\\n").Replace("\t", "\\t")}");
_receiveBuffer.Append(data);
string currentContent = _receiveBuffer.ToString();
bool isComplete = false;
if (currentContent.IndexOf("<end>", StringComparison.OrdinalIgnoreCase) >= 0 ||
currentContent.IndexOf("Success", StringComparison.OrdinalIgnoreCase) >= 0 ||
currentContent.IndexOf("Fail", StringComparison.OrdinalIgnoreCase) >= 0 ||
currentContent.IndexOf("<ACK>OFF", StringComparison.OrdinalIgnoreCase) >= 0)
{
isComplete = true;
}
else
{
// <end>가 오지 않았더라도, 명령어 에코가 아닌 줄에서 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();
}
}
/// <summary>
/// 신규 4251 보드를 사용하여 제품 ID를 읽는 센서 서비스.
/// ZmdiSensorService와 동일한 구조로 구현되어 교체가 용이함.
/// </summary>
public class Board4251SensorService : IIdSensorService
{
private readonly Board4251Service _service;
private readonly int _sensorIndex;
public event EventHandler<string> ProgressMessage;
public event EventHandler<string> ErrorMessage;
public event EventHandler<bool> ConnectionChanged;
public Board4251SensorService(Board4251Service service, int sensorIndex)
{
_service = service;
_sensorIndex = sensorIndex;
// 좌우 오류 간섭을 막기 위해 공용 에러 이벤트 구독 해제
_service.ConnectionChanged += (s, isConnected) => ConnectionChanged?.Invoke(this, isConnected);
}
public bool Connect() => _service.Connect();
public void Disconnect() => _service.Disconnect();
public SensorIdData ReadSensor()
{
try
{
int channel = _sensorIndex + 1;
// 0. 초기화 (x00o 전송)
ProgressMessage?.Invoke(this, "4251 보드 초기화 중...");
// x00o 명령을 보내고 응답 내용에 상관없이 다음 단계 진행 (최대 5초 대기)
Task.Run(() => _service.SendCommandAsync("x00o\r\n")).Wait(5000);
Task.Delay(350).Wait(); // 보드 안정화 대기
// 1. 보드 상태 확인 (Fail인 경우 중단)
ProgressMessage?.Invoke(this, "4251 보드 데이터 읽는 중...");
int extendedTimeout = 15000;
string statusCmd = $"x00c_00{channel}101:owt28006727ea97c7801";
var statusTask = Task.Run(() => _service.CheckStatusAsync(channel));
if (!statusTask.Wait(extendedTimeout))
{
string buf = _service.GetLastBuffer();
string displayBuf = string.IsNullOrEmpty(buf) ? "수신된 데이터 없음" : buf;
ErrorMessage?.Invoke(this, $"통신 실패 (4251 보드 CH{channel} 상태 타임아웃)\r\n[송신값]: {statusCmd}\r\n[수신값]: {displayBuf}");
return null;
}
if (!statusTask.Result)
{
string buf = _service.GetLastBuffer();
string displayBuf = string.IsNullOrEmpty(buf) ? "수신된 데이터 없음" : buf;
ErrorMessage?.Invoke(this, $"통신 실패 (4251 보드 CH{channel} 상태 이상 또는 Fail)\r\n[송신값]: {statusCmd}\r\n[수신값]: {displayBuf}");
return null;
}
// 2. ID 읽기 (끝자리가 F일 경우 최대 3회 시도)
string idCmd = $"x00c_00{channel}101:ow2800326003e";
string rawId = null;
int maxAttempts = 3;
for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
var idTask = Task.Run(() => _service.ReadIdAsync(channel));
if (!idTask.Wait(extendedTimeout))
{
if (attempt == maxAttempts)
{
string buf = _service.GetLastBuffer();
string displayBuf = string.IsNullOrEmpty(buf) ? "수신된 데이터 없음" : buf;
ErrorMessage?.Invoke(this, $"통신 실패 (4251 보드 CH{channel} ID 대기 타임아웃)\r\n[송신값]: {idCmd}\r\n[수신값]: {displayBuf}");
return null;
}
continue;
}
rawId = idTask.Result;
if (string.IsNullOrEmpty(rawId))
{
if (attempt == maxAttempts)
{
string buf = _service.GetLastBuffer();
string displayBuf = string.IsNullOrEmpty(buf) ? "수신된 데이터 없음" : buf;
ErrorMessage?.Invoke(this, $"통신 실패 (4251 보드 CH{channel} ID 응답 없거나 파싱 오류)\r\n[송신값]: {idCmd}\r\n[수신값]: {displayBuf}");
return null;
}
continue;
}
// 정상적으로 파싱된 경우, 끝자리가 'F'인지 확인
if (rawId.EndsWith("F", StringComparison.OrdinalIgnoreCase))
{
if (attempt < maxAttempts)
{
ProgressMessage?.Invoke(this, $"ID 읽기 재시도 중... ({attempt}/{maxAttempts})");
Task.Delay(350).Wait(); // 재시도 전 약간의 딜레이
continue;
}
else
{
ProgressMessage?.Invoke(this, $"최대 재시도(2회 추가) 초과. 끝자리가 F인 ID({rawId})를 사용합니다.");
}
}
// 제대로 된 값을 얻었거나 최대 횟수에 도달하면 루프 탈출
break;
}
// 3. SensorIdData 객체 구성 (16자리 ID를 각 필드에 적절히 분배)
// 신규 보드는 16자리 전체가 ID이므로, 파싱 로직 없이 통째로 넣거나
// 특정 규칙이 있다면 여기서 분할함.
var data = new SensorIdData
{
LowID = rawId,
ID = rawId, // 16자리 전체를 ID로 사용
Serial = "", // 시리얼 번호는 현재 존재하지 않으므로 강제로 파싱하지 않음
Item = "N/A",
PrevResult = "F" // '불량제품 투입' 필터를 통과하기 위한 강제 초기화
};
ProgressMessage?.Invoke(this, "ID 읽기 성공");
return data;
}
catch (Exception ex)
{
ErrorMessage?.Invoke(this, $"4251 보드 읽기 중 예외 발생: {ex.Message}");
return null;
}
}
public void Dispose()
{
// 공유 서비스(Board4251Service)는 외부(HomeViewModel)에서 관리하므로 여기서 해제하지 않음
}
}
/// <summary>
/// 시리얼 통신을 기반으로 동작하는 신규 4251 DIO 보드 구현체.
/// 기존 RealDioBoard(Legacy)와 교체 가능함.
/// </summary>
public class Board4251DioBoard : IDioBoard
{
private readonly Board4251Service _service;
private readonly List<DioPoint> _inputs = new List<DioPoint>();
private readonly List<DioPoint> _outputs = new List<DioPoint>();
private bool _isDisposed = false;
#pragma warning disable 0067
public event EventHandler<DioEventArgs> InputChanged;
#pragma warning restore 0067
public event EventHandler<string> ErrorOccurred;
public Board4251DioBoard(Board4251Service service)
{
_service = service;
_service.ErrorOccurred += (s, msg) => ErrorOccurred?.Invoke(this, msg);
InitializePoints();
}
private void InitializePoints()
{
// 실제 보드 구성에 맞게 입출력 포인트 정의 (DioConfigParser 기반 혹은 하드코딩)
// 일단 기존 프로젝트 구성과 호환되도록 빈 리스트 혹은 기본값 설정
var config = DioConfigParser.LoadDefault();
_inputs.AddRange(config.InputPoints);
_outputs.AddRange(config.OutputPoints);
}
public bool Initialize()
{
// 시리얼 연결 시도
try {
if (!_service.Connect())
{
ErrorOccurred?.Invoke(this, $"4251 Board: Failed to open serial port.");
return false;
}
// 보드 상태 확인
var statusTask = Task.Run(() => _service.CheckStatusAsync());
if (!statusTask.Wait(5000))
{
ErrorOccurred?.Invoke(this, "4251 Board: Initialization Timeout (CheckStatus).");
return false;
}
return statusTask.Result;
} catch (Exception ex) {
ErrorOccurred?.Invoke(this, $"4251 Board: Initialization Error - {ex.Message}");
return false;
}
}
public bool ReadInput(string pointName)
{
// 신규 보드의 입력 읽기 프로토콜이 필요한 부분 (현재 ReadId 등만 구현됨)
// 구현 계획에는 ID 읽기와 상태 확인만 있었으므로,
// 실제 DIO 기능을 위해선 추가적인 시리얼 명령이 필요할 수 있음.
// 일단 true/false 로직 구현
return false;
}
public void WriteOutput(string pointName, bool value)
{
// 보드 출력 제어 명령 전송 (예시 프로토콜 필요)
// _service.SendCommandAsync(...) 호출 형태가 될 것임.
}
public List<DioPoint> GetInputPoints() => _inputs;
public List<DioPoint> GetOutputPoints() => _outputs;
public void Dispose()
{
if (!_isDisposed)
{
_isDisposed = true;
_service.Disconnect();
_service.Dispose();
}
}
}
}