using System;
using System.Text;
using System.Threading;
using leak_test_project.Infrastructure;
using leak_test_project.Models;
namespace leak_test_project.Services
{
///
/// ZMDI 센서와 시리얼 통신하여 제품 ID를 읽고 파싱하는 서비스.
/// 레거시 ClsSensorReader를 새 아키텍처로 포팅.
/// 동기식 통신이므로 반드시 백그라운드 스레드에서 호출해야 합니다.
///
public class ZmdiSensorService
{
private readonly ICommunication _comm;
private readonly object _commSync = new object();
private readonly int _sensorIndex; // 0=LEFT, 1=RIGHT
// 레거시 명령 시퀀스 (ClsSensorReader의 commandList1~4) 그대로 유지
private readonly string[] _commandList1 = { "V", "Pr_D7", "Pr_D6", "Pr_D5", "r" };
private readonly string[] _commandList2 = { "tso31150" };
private readonly string[] _commandList3 = { "os_10", "t11005", "OWT7800272D1", "OR_78002",
"OW_780038AA55A", "OW_780011A", "OR_78002", "OW_780038AFF00", "OW_78001CF", "OR_78004" };
private readonly string[] _commandList4 = { "OW_7800140", "OR_78002", "OW_7800141",
"OR_78002", "OW_7800142", "OR_78002", "x9c_990:x" };
// 년/월/일 디코딩 테이블 (레거시 그대로)
private readonly string[] _yearHexList = {
"49","4A","4B","4C","4D","4E","4F","50","51","52","53","54","55","56","57","58","59","5A"
};
private readonly string[] _yearIdList = {
"I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"
};
private readonly string[] _monthList = { "1","2","3","4","5","6","7","8","9","A","B","C" };
private readonly string[] _dayHexList = {
"41","42","43","44","45","46","47","48","49","4A","4B","4C","4D","4E","4F","50",
"51","52","53","54","55","56","57","58","59","5A","31","32","33","34","35"
};
private readonly string[] _dayIdList = {
"A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P",
"Q","R","S","T","U","V","W","X","Y","Z","1","2","3","4","5"
};
/// 진행 상황 메시지 이벤트
public event EventHandler ProgressMessage;
/// 오류 메시지 이벤트
public event EventHandler ErrorMessage;
public ZmdiSensorService(ICommunication communication, int sensorIndex)
{
_comm = communication;
_sensorIndex = sensorIndex;
}
///
/// ZMDI 센서에서 제품 ID를 읽고 파싱합니다.
/// 반드시 백그라운드 스레드에서 호출해야 합니다.
///
/// 성공 시 SensorIdData, 실패 시 null
public SensorIdData ReadSensor()
{
string recvData = "";
try
{
var data = new SensorIdData();
// 1단계: 초기 명령 실행
ProgressMessage?.Invoke(this, "ZMDI 초기화 중...");
for (int i = 0; i < _commandList1.Length; i++)
{
if (!ExecuteCommand(_commandList1[i], ref recvData))
{
ErrorMessage?.Invoke(this, $"통신 실패 (cmd1: ZMDI 센서 초기화 및 통신 확인 단계)\r\n[송신값]: {_commandList1[i]}\r\n[수신값]: {recvData.Trim()}");
return null;
}
}
Thread.Sleep(1000);
// 2단계
ProgressMessage?.Invoke(this, "ZMDI 메모리 준비 중...");
for (int i = 0; i < _commandList2.Length; i++)
{
if (!ExecuteCommand(_commandList2[i], ref recvData))
{
ErrorMessage?.Invoke(this, $"통신 실패 (cmd2: ZMDI 메모리 접근 준비 단계)\r\n[송신값]: {_commandList2[i]}\r\n[수신값]: {recvData.Trim()}");
return null;
}
}
Thread.Sleep(200);
// 3단계
ProgressMessage?.Invoke(this, "ZMDI 데이터 수집 중...");
for (int i = 0; i < _commandList3.Length; i++)
{
if (!ExecuteCommand(_commandList3[i], ref recvData))
{
ErrorMessage?.Invoke(this, $"통신 실패 (cmd3: ZMDI 데이터 수집 설정 단계)\r\n[송신값]: {_commandList3[i]}\r\n[수신값]: {recvData.Trim()}");
return null;
}
}
// 4단계: ID 메모리 읽기 (3회)
ProgressMessage?.Invoke(this, "ZMDI ID 읽기 중...");
// 첫 번째 ID 레지스터
if (!ExecuteCommand(_commandList4[0], ref recvData)) { ErrorMessage?.Invoke(this, $"통신 실패 (ID 읽기 단계-1)\r\n[송신값]: {_commandList4[0]}\r\n[수신값]: {recvData.Trim()}"); return null; }
if (!ExecuteCommand(_commandList4[1], ref recvData)) { ErrorMessage?.Invoke(this, $"통신 실패 (ID 읽기 단계-2)\r\n[송신값]: {_commandList4[1]}\r\n[수신값]: {recvData.Trim()}"); return null; }
data.LowID = recvData.Length > 1 ? recvData.Substring(1).Replace("\r", "").Replace("\n", "") : "";
// 두 번째 ID 레지스터
if (!ExecuteCommand(_commandList4[2], ref recvData)) { ErrorMessage?.Invoke(this, $"통신 실패 (ID 읽기 단계-3)\r\n[송신값]: {_commandList4[2]}\r\n[수신값]: {recvData.Trim()}"); return null; }
if (!ExecuteCommand(_commandList4[3], ref recvData)) { ErrorMessage?.Invoke(this, $"통신 실패 (ID 읽기 단계-4)\r\n[송신값]: {_commandList4[3]}\r\n[수신값]: {recvData.Trim()}"); return null; }
data.LowID += recvData.Length > 1 ? recvData.Substring(1).Replace("\r", "").Replace("\n", "") : "";
// 세 번째 ID 레지스터
if (!ExecuteCommand(_commandList4[4], ref recvData)) { ErrorMessage?.Invoke(this, $"통신 실패 (ID 읽기 단계-5)\r\n[송신값]: {_commandList4[4]}\r\n[수신값]: {recvData.Trim()}"); return null; }
if (!ExecuteCommand(_commandList4[5], ref recvData)) { ErrorMessage?.Invoke(this, $"통신 실패 (ID 읽기 단계-6)\r\n[송신값]: {_commandList4[5]}\r\n[수신값]: {recvData.Trim()}"); return null; }
data.LowID += recvData.Length > 1 ? recvData.Substring(1).Replace("\r", "").Replace("\n", "") : "";
// 마지막 명령 실행
if (!ExecuteCommand(_commandList4[6], ref recvData)) { ErrorMessage?.Invoke(this, $"통신 실패 (ID 읽기 종료 단계)\r\n[송신값]: {_commandList4[6]}\r\n[수신값]: {recvData.Trim()}"); return null; }
// ID 파싱
if (!ParseLowId(data))
return null;
return data;
}
catch (Exception ex)
{
Console.WriteLine($"[ZMDI] ReadSensor exception: {ex.Message}");
ErrorMessage?.Invoke(this, $"센서 데이터 오류 (Exception: {ex.Message})\r\n[수신값]: {recvData.Trim()}");
return null;
}
}
/// LowID 문자열을 파싱하여 년/월/일/시리얼/라인/항목을 추출합니다.
private bool ParseLowId(SensorIdData data)
{
if (string.IsNullOrEmpty(data.LowID) || data.LowID.Length < 12)
{
ErrorMessage?.Invoke(this, $"센서 데이터 길이 부족 (길이: {data.LowID?.Length ?? 0})\r\n[수신값]: {data.LowID}");
return false;
}
string yearHex = data.LowID.Substring(0, 2);
string yearId = "";
data.Year = 0;
for (int i = 0; i < _yearHexList.Length; i++)
{
if (yearHex == _yearHexList[i])
{
data.Year = 2013 + i;
yearId = _yearIdList[i];
break;
}
}
if (data.Year <= 0) { ErrorMessage?.Invoke(this, $"센서 데이터 오류(년도 파싱 불가)\r\n[수신값]: {data.LowID}"); return false; }
string monthChar = data.LowID.Substring(2, 1);
string monthId = "";
data.Month = 0;
for (int i = 0; i < _monthList.Length; i++)
{
if (monthChar == _monthList[i])
{
data.Month = 1 + i;
monthId = monthChar;
break;
}
}
if (data.Month <= 0) { ErrorMessage?.Invoke(this, $"센서 데이터 오류(월 파싱 불가)\r\n[수신값]: {data.LowID}"); return false; }
string dayHex = data.LowID.Substring(3, 2);
string dayId = "";
data.Day = 0;
for (int i = 0; i < _dayHexList.Length; i++)
{
if (dayHex == _dayHexList[i])
{
data.Day = 1 + i;
dayId = _dayIdList[i];
break;
}
}
if (data.Day <= 0) { ErrorMessage?.Invoke(this, $"센서 데이터 오류(일 파싱 불가)\r\n[수신값]: {data.LowID}"); return false; }
// 시리얼 번호 계산 (16진수 → 10진수)
string serialHigh = data.LowID.Substring(5, 2);
string serialLow = data.LowID.Substring(7, 2);
int high = Convert.ToInt32(serialHigh, 16) << 8;
int low = Convert.ToInt32(serialLow, 16);
data.Serial = (high + low).ToString().PadLeft(5, '0');
// MC Line 및 라인 번호 (비트 단위 파싱)
string nibble = data.LowID.Substring(9, 1);
string binary = Convert.ToString(Convert.ToInt32(nibble, 16), 2).PadLeft(4, '0');
data.McLine = binary.Substring(0, 1);
string lineNoBits = binary.Substring(1, 3);
data.LineNo = Convert.ToInt32(lineNoBits, 2).ToString();
// PrevResult (이전 검사 결과)
data.PrevResult = data.LowID.Substring(10, 1);
// 제품 Item
data.Item = data.LowID.Substring(11, 1);
// 최종 ID 조합
data.ID = yearId + monthId + dayId + data.Serial + data.LineNo + data.Item;
return true;
}
/// 명령을 전송하고 응답을 수신합니다. 최대 3회 재시도.
private bool ExecuteCommand(string sendCommand, ref string recvData)
{
lock (_commSync)
{
int retryCount = 0;
while (true)
{
if (SendCommandWaitResponse(sendCommand, ref recvData))
return true;
Thread.Sleep(300);
retryCount++;
if (retryCount > 3)
return false;
}
}
}
/// 명령 전송 후 CR+LF 응답을 500ms 타임아웃으로 대기합니다.
private bool SendCommandWaitResponse(string sendCommand, ref string recvData)
{
string fullCommand = sendCommand + "\r\n";
if (!_comm.IsOpen)
{
if (!_comm.Open())
return false;
}
recvData = "";
// 핵심 버그 수정: 이전 명령어의 잔류 응답 데이터 비우기
_comm.ClearBuffer();
// 500ms 타임아웃으로 응답 대기 (레거시 동일)
DateTime deadline = DateTime.Now.AddMilliseconds(500);
// 동기식 수신을 위한 임시 버퍼
string buffer = "";
EventHandler handler = null;
handler = (s, data) => { buffer += data; };
// 핵심 버그 수정: 명령어를 쓰기 '전에' 이벤트 핸들러를 먼저 등록하여 아주 빠른 응답 유실 방지
_comm.DataReceived += handler;
bool success = false;
try
{
if (!_comm.Write(fullCommand))
return false;
while (DateTime.Now < deadline)
{
Thread.Sleep(2);
if (buffer.IndexOf("\r\n") >= 0)
{
recvData = buffer;
success = true;
break;
}
}
// 타임아웃 발생 시에도 현재까지 수신된 버퍼 내용을 반환 (디버깅용)
if (!success)
recvData = buffer;
return success;
}
finally
{
_comm.DataReceived -= handler;
}
}
}
}