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