using System ;
using System.Text ;
using System.Threading ;
using System.Timers ;
using leak_test_project.Infrastructure ;
using leak_test_project.Models ;
using leak_test_project.Utils ;
namespace leak_test_project.Services
{
/// <summary>
/// ZMDI 센서와 시리얼 통신하여 제품 ID를 읽고 파싱하는 서비스.
/// 자동 재연결(Auto-Reconnect) 및 예외 처리 포함.
/// </summary>
public class ZmdiSensorService : IIdSensorService
{
private readonly ICommunication _ comm ;
private readonly object _ commSync = new object ( ) ;
private readonly int _ sensorIndex ; // 0=LEFT, 1=RIGHT
private System . Timers . Timer _ reconnectTimer ;
private bool _ shouldBeConnected = false ;
// 레거시 명령 시퀀스 (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 [ ] _d ayHexList = {
"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 [ ] _d ayIdList = {
"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 ;
/// <summary>연결 상태 변경 이벤트</summary>
public event EventHandler < bool > ConnectionChanged ;
public ZmdiSensorService ( ICommunication communication , int sensorIndex )
{
_ comm = communication ;
_ sensorIndex = sensorIndex ;
_ comm . ConnectionStatusChanged + = ( s , isConnected ) = > {
ConnectionChanged ? . Invoke ( this , isConnected ) ;
if ( ! isConnected & & _ shouldBeConnected ) StartReconnectTimer ( ) ;
} ;
// 1초(1000ms)마다 연결 상태를 확인하고 재연결 시도
_ reconnectTimer = new System . Timers . Timer ( 1 0 0 0 ) ;
_ reconnectTimer . AutoReset = false ; // 재진입 방지
_ reconnectTimer . Elapsed + = ( s , e ) = > {
if ( _ shouldBeConnected & & ! _ comm . IsOpen )
{
if ( ! _ comm . Open ( ) )
{
if ( _ shouldBeConnected ) _ reconnectTimer . Start ( ) ;
}
}
else if ( _ shouldBeConnected )
{
_ reconnectTimer . Start ( ) ;
}
} ;
}
public bool Connect ( )
{
_ shouldBeConnected = true ;
bool opened = _ comm . Open ( ) ;
if ( ! opened ) StartReconnectTimer ( ) ;
return opened ;
}
public void Disconnect ( )
{
_ shouldBeConnected = false ;
_ reconnectTimer ? . Stop ( ) ;
_ comm . Close ( ) ;
}
private void StartReconnectTimer ( )
{
if ( _ reconnectTimer ! = null & & ! _ reconnectTimer . Enabled ) _ reconnectTimer . Start ( ) ;
}
public void Dispose ( )
{
Disconnect ( ) ;
_ reconnectTimer ? . Dispose ( ) ;
_ reconnectTimer = null ;
}
/// <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 ( 1 0 0 0 ) ;
// 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 ( 2 0 0 ) ;
// 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 )
{
FileLogger . Log ( "ERROR" , $"[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 < 1 2 )
{
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 = 2 0 1 3 + 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 < _d ayHexList . Length ; i + + )
{
if ( dayHex = = _d ayHexList [ i ] )
{
data . Day = 1 + i ;
dayId = _d ayIdList [ 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 , 1 6 ) < < 8 ;
int low = Convert . ToInt32 ( serialLow , 1 6 ) ;
data . Serial = ( high + low ) . ToString ( ) . PadLeft ( 5 , '0' ) ;
// MC Line 및 라인 번호 (비트 단위 파싱)
string nibble = data . LowID . Substring ( 9 , 1 ) ;
string binary = Convert . ToString ( Convert . ToInt32 ( nibble , 1 6 ) , 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 ( 1 0 , 1 ) ;
// 제품 Item
data . Item = data . LowID . Substring ( 1 1 , 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 ( 3 0 0 ) ;
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 ( 5 0 0 ) ;
// 동기식 수신을 위한 임시 버퍼
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 ;
}
}
}
}