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 { /// /// ZMDI 센서와 시리얼 통신하여 제품 ID를 읽고 파싱하는 서비스. /// 자동 재연결(Auto-Reconnect) 및 예외 처리 포함. /// public class ZmdiSensorService : IDisposable { 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[] _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 event EventHandler 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(1000); _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; } /// /// 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) { FileLogger.Log("ERROR", $"[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; } } } }