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.
292 lines
13 KiB
292 lines
13 KiB
using System;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using leak_test_project.Infrastructure;
|
|
using leak_test_project.Models;
|
|
|
|
namespace leak_test_project.Services
|
|
{
|
|
/// <summary>
|
|
/// ZMDI 센서와 시리얼 통신하여 제품 ID를 읽고 파싱하는 서비스.
|
|
/// 레거시 ClsSensorReader를 새 아키텍처로 포팅.
|
|
/// 동기식 통신이므로 반드시 백그라운드 스레드에서 호출해야 합니다.
|
|
/// </summary>
|
|
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"
|
|
};
|
|
|
|
/// <summary>진행 상황 메시지 이벤트</summary>
|
|
public event EventHandler<string> ProgressMessage;
|
|
|
|
/// <summary>오류 메시지 이벤트</summary>
|
|
public event EventHandler<string> ErrorMessage;
|
|
|
|
public ZmdiSensorService(ICommunication communication, int sensorIndex)
|
|
{
|
|
_comm = communication;
|
|
_sensorIndex = sensorIndex;
|
|
}
|
|
|
|
/// <summary>
|
|
/// ZMDI 센서에서 제품 ID를 읽고 파싱합니다.
|
|
/// 반드시 백그라운드 스레드에서 호출해야 합니다.
|
|
/// </summary>
|
|
/// <returns>성공 시 SensorIdData, 실패 시 null</returns>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>LowID 문자열을 파싱하여 년/월/일/시리얼/라인/항목을 추출합니다.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>명령을 전송하고 응답을 수신합니다. 최대 3회 재시도.</summary>
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>명령 전송 후 CR+LF 응답을 500ms 타임아웃으로 대기합니다.</summary>
|
|
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<string> 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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|