using System; using System.Diagnostics; 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 { /// /// 시험 진행 상황 이벤트 인자. /// public class TestProgressEventArgs : EventArgs { public int TestIndex { get; set; } // 0=LEFT, 1=RIGHT public string Message { get; set; } public string ColorHint { get; set; } // LightSeaGreen, LightYellow, LightBlue } /// /// 시험 결과 이벤트 인자. /// public class TestResultEventArgs : EventArgs { public int TestIndex { get; set; } public SensorIdData SensorData { get; set; } public string PcbBarcode { get; set; } // 신설 public string MeasuredValue { get; set; } public string Judgment { get; set; } // 프로그램 판정 (OK/NG) public string SensorJudgment { get; set; } // 센서 자체 판정 public bool SpecMismatch { get; set; } // SPEC 교차 검증 불일치 여부 public DateTime ProductionDate { get; set; } // DB 저장/검사 완료 일시 } /// /// 자동 시험 프로세스를 관리하는 서비스. /// 레거시 ClsTester.ProcessProc()를 새 아키텍처로 포팅. /// DIO 시작 신호 대기 → 제품 ID 읽기 → LEAK 시험 → 결과 출력 사이클. /// public class TestProcessService : IDisposable { private readonly IDioBoard _dioBoard; private readonly IIdSensorService _leftSensor; private readonly IIdSensorService _rightSensor; private readonly SentinelC28Service _sentinelService; private Housing.Login.StartupLoginWindowResult _loginInfo => leak_test_project.App.LoginResult; private Thread _leftThread; private Thread _rightThread; private volatile bool _leftTestStart = false; private volatile bool _rightTestStart = false; private volatile bool _running = true; private readonly SemaphoreSlim _testLock = new SemaphoreSlim(1, 1); // DIO 시작 신호를 폴링 대신 대기하기 위한 이벤트 private readonly AutoResetEvent _leftStartEvent = new AutoResetEvent(false); private readonly AutoResetEvent _rightStartEvent = new AutoResetEvent(false); // LEAK 시험 결과를 수신하기 위한 필드 private ParsedData _leftResult; private ParsedData _rightResult; private readonly ManualResetEventSlim _leftResultReady = new ManualResetEventSlim(false); private readonly ManualResetEventSlim _rightResultReady = new ManualResetEventSlim(false); /// 현재 실행 중인 테스트 인덱스 (0=LEFT, 1=RIGHT, -1=None) private volatile int _activeTestIndex = -1; public int ActiveTestIndex => _activeTestIndex; /// 시험 진행 상황 알림 public event EventHandler ProgressChanged; /// 시험 완료 결과 알림 public event EventHandler TestCompleted; /// 오류 메시지 알림 public event EventHandler ErrorOccurred; /// 센서 ID 판독 완료 알림 public event EventHandler<(int TestIndex, SensorIdData Data)> SensorReadComplete; /// 결과 초기화 요청 알림 public event EventHandler ResultClearRequested; /// 바코드 매핑 완료 알림 public event EventHandler<(int TestIndex, string IcSn, string PcbBarcode)> BarcodeMapped; public TestProcessService( IDioBoard dioBoard, IIdSensorService leftSensor, IIdSensorService rightSensor, SentinelC28Service sentinelService) { _dioBoard = dioBoard; _leftSensor = leftSensor; _rightSensor = rightSensor; _sentinelService = sentinelService; // DIO START 신호 이벤트 연결 _dioBoard.InputChanged += OnDioInputChanged; // C28 최종 결과 수신 이벤트 연결 (통합 서비스) _sentinelService.OnFinalResultParsed += OnSentinelFinalResult; } /// 자동 시험 프로세스를 시작합니다. public void Start() { _running = true; _leftThread = new Thread(() => ProcessProc(0)) { IsBackground = true, Name = "TestProc_LEFT" }; _rightThread = new Thread(() => ProcessProc(1)) { IsBackground = true, Name = "TestProc_RIGHT" }; _leftThread.Start(); _rightThread.Start(); Console.WriteLine("[TestProcess] Started LEFT and RIGHT test threads."); } /// 시뮬레이션용: 수동으로 시험 시작 트리거 public void TriggerTestStart(int testIndex) { if (testIndex == 0) { _leftTestStart = true; _leftStartEvent.Set(); } else { _rightTestStart = true; _rightStartEvent.Set(); } } /// /// ID 센서 단독 테스트를 수행합니다. /// 실제 검사와 동일한 시작 시퀀스를 재현합니다. /// public async System.Threading.Tasks.Task ExecuteSensorTestAsync(int testIndex) { bool lockAcquired = _testLock.Wait(0); if (!lockAcquired) { ResultClearRequested?.Invoke(this, testIndex); NotifyProgress(testIndex, "대기중", "LightYellow"); await _testLock.WaitAsync(); } try { _activeTestIndex = testIndex; bool isLeft = (testIndex == 0); NotifyProgress(testIndex, "시험 시작", "LightSeaGreen"); ResultClearRequested?.Invoke(this, testIndex); await System.Threading.Tasks.Task.Delay(500); // UI 피드백을 위한 짧은 대기 NotifyProgress(testIndex, "센서 정보 읽는 중", "LightSeaGreen"); var sensor = isLeft ? _leftSensor : _rightSensor; // 비동기로 센서 정보 판독 var sensorData = await sensor.ReadSensorAsync(); if (sensorData != null) { SensorReadComplete?.Invoke(this, (testIndex, sensorData)); NotifyProgress(testIndex, "ID 테스트 완료", "LightBlue"); } else { // 실패 시 NotifyError를 호출하지 않는 것도 실제 시퀀스와 동일 (센서 서비스가 이미 에러 이벤트 발생시킴) // 다만 UI 갱신을 위해 빈 데이터는 전송 sensorData = new SensorIdData { ID = "-" }; SensorReadComplete?.Invoke(this, (testIndex, sensorData)); NotifyProgress(testIndex, "ID 테스트 실패", "Red"); } } finally { _activeTestIndex = -1; _testLock.Release(); } } private void OnDioInputChanged(object sender, DioEventArgs e) { // DIO 시작 신호 감지 (OFF→ON) if (e.PointName == "LEFT_START" && e.NewValue) { // OK/NG 출력 초기화 _dioBoard.WriteOutput("LEFT_OK", false); _dioBoard.WriteOutput("LEFT_NG", false); _leftTestStart = true; _leftStartEvent.Set(); } else if (e.PointName == "RIGHT_START" && e.NewValue) { _dioBoard.WriteOutput("RIGHT_OK", false); _dioBoard.WriteOutput("RIGHT_NG", false); _rightTestStart = true; _rightStartEvent.Set(); } } /// /// 시험 프로세스 메인 루프 (레거시 ClsTester.ProcessProc 포팅). /// 백그라운드 스레드에서 실행됩니다. /// private void ProcessProc(int testIndex) { bool isLeft = (testIndex == 0); string side = isLeft ? "LEFT" : "RIGHT"; var startEvent = isLeft ? _leftStartEvent : _rightStartEvent; while (_running) { // 이벤트 신호 대기 (2ms 폴링 대신 신호 수신 시까지 차단) startEvent.WaitOne(); if (!_running) break; // 시작 신호 대기 bool shouldStart = isLeft ? _leftTestStart : _rightTestStart; if (!shouldStart) continue; // 순차 실행을 위한 락 획득 (좌/우 동시 테스트 방지) // 순차 실행을 위한 락 획득 (좌/우 동시 테스트 방지) bool lockAcquired = _testLock.Wait(0); if (!lockAcquired) { ResultClearRequested?.Invoke(this, testIndex); NotifyProgress(testIndex, "대기중", "LightYellow"); _testLock.Wait(); } try { _activeTestIndex = testIndex; // 하드웨어 통신 안정화를 위한 짧은 대기 (이전 채널 종료 후 여유 시간) Thread.Sleep(350); // 플래그 초기화 if (isLeft) _leftTestStart = false; else _rightTestStart = false; NotifyProgress(testIndex, "시험 시작", "LightSeaGreen"); ResultClearRequested?.Invoke(this, testIndex); // C28 LEAK 센서 수신 이벤트를 초기화 (제품 ID 통신 중 일찍 날아오는 Auto Result를 놓치지 않기 위함) var resultReady = isLeft ? _leftResultReady : _rightResultReady; resultReady.Reset(); if (isLeft) _leftResult = null; else _rightResult = null; // === 1단계: 센서 정보 읽기 === Debug.WriteLine($"[TestProcess-{side}] Phase 1: 센서 정보 읽기 시작"); NotifyProgress(testIndex, "센서 정보 읽는 중", "LightSeaGreen"); var sensor = isLeft ? _leftSensor : _rightSensor; var sensorData = sensor.ReadSensorAsync().GetAwaiter().GetResult(); if (sensorData != null) { SensorReadComplete?.Invoke(this, (testIndex, sensorData)); } else { NotifyError(testIndex, "ID 읽기 실패"); _dioBoard.WriteOutput(side + "_NG", true); var failedSensorData = new SensorIdData { ID = "-" }; SensorReadComplete?.Invoke(this, (testIndex, failedSensorData)); TestCompleted?.Invoke(this, new TestResultEventArgs { TestIndex = testIndex, SensorData = failedSensorData, PcbBarcode = "-", MeasuredValue = "0.0000", Judgment = "NG", SensorJudgment = "FAIL", SpecMismatch = false, ProductionDate = DateTime.Now }); } // === 1.5단계: PCB 바코드 DB 조회 (dbo.Housing_Assembly) === string pcbBarcode = null; var dbIniPath = Housing.Services.DatabaseSettings.GetDefaultFilePath(); var loginSettings = Housing.Services.LoginSettings.Load(dbIniPath); if (loginSettings.OfflinePreview) { pcbBarcode = "testPCBbarcode"; } else { try { var dbConnStr = Housing.Services.DatabaseSettings.Load(dbIniPath).ConnectionString; var dbService = new EolDatabaseService(dbConnStr); pcbBarcode = dbService.GetPcbBarcodeAsync(sensorData.ID).GetAwaiter().GetResult(); } catch (Exception ex) { string detailedErr = $"바코드 매핑 실패: [{ex.GetType().Name}] {ex.Message}"; NotifyError(testIndex, detailedErr); _dioBoard.WriteOutput(side + "_NG", true); var failedSensorData = new SensorIdData { ID = sensorData.ID }; TestCompleted?.Invoke(this, new TestResultEventArgs { TestIndex = testIndex, SensorData = failedSensorData, PcbBarcode = "-", MeasuredValue = "0.0000", Judgment = "NG", SensorJudgment = "FAIL", SpecMismatch = false, ProductionDate = DateTime.Now }); NotifyProgress(testIndex, "시험 완료", "LightBlue"); continue; } } if (string.IsNullOrEmpty(pcbBarcode)) { NotifyError(testIndex, "바코드 매핑 실패: 매핑 데이터 없음"); _dioBoard.WriteOutput(side + "_NG", true); var failedSensorData = new SensorIdData { ID = sensorData.ID }; TestCompleted?.Invoke(this, new TestResultEventArgs { TestIndex = testIndex, SensorData = failedSensorData, PcbBarcode = "-", MeasuredValue = "0.0000", Judgment = "NG", SensorJudgment = "FAIL", SpecMismatch = false, ProductionDate = DateTime.Now }); NotifyProgress(testIndex, "시험 완료", "LightBlue"); continue; } // 정상 매핑 시점 이벤트 발생 BarcodeMapped?.Invoke(this, (testIndex, sensorData.ID, pcbBarcode)); // === 3단계: LEAK 시험 수행 === Debug.WriteLine($"[TestProcess-{side}] Phase 3: LEAK 시험 대기 시작 (최대 30초)"); NotifyProgress(testIndex, "LEAK 시험중", "LightYellow"); // C28에서 결과를 수신할 때까지 대기 (레거시와 동일하게 30초 설정) bool received = resultReady.Wait(TimeSpan.FromSeconds(30)); if (!received) { Debug.WriteLine($"[TestProcess-{side}] ERROR: Sentinel C28 통신 타임아웃 발생!"); NotifyError(testIndex, "Sentinel C28 통신 타임아웃"); _dioBoard.WriteOutput(side + "_NG", true); // 타임아웃 시에도 UI(그리드)에 NG로 기록되도록 이벤트 발생 TestCompleted?.Invoke(this, new TestResultEventArgs { TestIndex = testIndex, SensorData = sensorData, MeasuredValue = "0.0000", Judgment = "NG", SensorJudgment = "T/O", // Timeout SpecMismatch = false, ProductionDate = DateTime.Now }); // 로그 파일에도 기록 var timeoutData = new InspectData { InspectDate = DateTime.Now.ToString("yyyy-MM-dd"), InspectTime = DateTime.Now.ToString("HH:mm:ss"), Channel = side, ProductId = sensorData.ID, IcSn = sensorData.ID, PcbBarcode = pcbBarcode, MeasuredValue = "0.0000", Judgment = "NG", SpecUL = ConfigManager.Current.SpecUL.ToString("F2"), SpecLL = ConfigManager.Current.SpecLL.ToString("F2"), Maker = _loginInfo?.Maker ?? string.Empty, Model = _loginInfo?.Model ?? string.Empty, Variant1 = _loginInfo?.Variant1 ?? string.Empty, Variant2 = _loginInfo?.Variant2 ?? string.Empty, Operator = _loginInfo?.Operator ?? string.Empty, Line = _loginInfo?.LineNo ?? string.Empty, LotNo = _loginInfo?.LotNo ?? string.Empty, JigNo = _loginInfo?.JigNo ?? string.Empty, ProductionDate = DateTime.Now }; FileLogger.LogInspectData(timeoutData); // === MSSQL 데이터베이스 비동기 저장 === try { dbIniPath = Housing.Services.DatabaseSettings.GetDefaultFilePath(); var dbConnStr = Housing.Services.DatabaseSettings.Load(dbIniPath).ConnectionString; var dbService = new EolDatabaseService(dbConnStr); _ = Task.Run(async () => { try { await dbService.SaveEolDataAsync(timeoutData, _loginInfo); NotifyProgress(testIndex, "DB 저장 완료", "LightBlue"); } catch (Exception ex) { NotifyError(testIndex, $"DB 저장 실패: [{ex.GetType().Name}] {ex.Message}"); } }); } catch (Exception ex) { Console.WriteLine("[DB Timeout Integration Error] " + ex.Message); } continue; } var leakResult = isLeft ? _leftResult : _rightResult; if (leakResult == null) { NotifyError(testIndex, "LEAK 센서 데이터 오류"); _dioBoard.WriteOutput(side + "_NG", true); continue; } // === 4단계: 판정 === double specUL = ConfigManager.Current.SpecUL; double specLL = ConfigManager.Current.SpecLL; string programJudgment = SentinelParser.EvaluateJudgment(leakResult.MeasuredValue, specUL, specLL); string sensorJudgment = leakResult.SensorJudgment ?? leakResult.Judgment ?? "-"; Debug.WriteLine($"[TestProcess-{side}] Phase 4: 프로그램 판정={programJudgment}, 센서자체판정={sensorJudgment}, 측정값={leakResult.MeasuredValue}"); // === 5단계: SPEC 교차 검증 === Debug.WriteLine($"[TestProcess-{side}] Phase 5: SPEC 교차 검증 수행"); bool specMismatch = false; string normalizedProgram = (programJudgment == "OK") ? "A" : "R"; string normalizedSensor = sensorJudgment; // 센서 판정이 Accept/Reject, Pass/Fail, OK/NG 형식인 경우 모두 A/R로 정규화 if (sensorJudgment == "OK" || sensorJudgment == "A" || sensorJudgment == "P") normalizedSensor = "A"; else if (sensorJudgment == "NG" || sensorJudgment == "R" || sensorJudgment == "F") normalizedSensor = "R"; if (normalizedProgram != normalizedSensor && normalizedSensor != "-") { specMismatch = true; Console.WriteLine($"[TestProcess] {side} SPEC Mismatch! Program={programJudgment}, Sensor={sensorJudgment}"); } // === 6단계: 로그 기록 === var inspectData = new InspectData { InspectDate = DateTime.Now.ToString("yyyy-MM-dd"), InspectTime = DateTime.Now.ToString("HH:mm:ss"), Channel = side, ProductId = sensorData.ID, IcSn = sensorData.ID, PcbBarcode = pcbBarcode, MeasuredValue = leakResult.MeasuredValueString, Judgment = programJudgment, SpecUL = specUL.ToString("F2"), SpecLL = specLL.ToString("F2"), Maker = _loginInfo?.Maker ?? string.Empty, Model = _loginInfo?.Model ?? string.Empty, Variant1 = _loginInfo?.Variant1 ?? string.Empty, Variant2 = _loginInfo?.Variant2 ?? string.Empty, Operator = _loginInfo?.Operator ?? string.Empty, Line = _loginInfo?.LineNo ?? string.Empty, LotNo = _loginInfo?.LotNo ?? string.Empty, JigNo = _loginInfo?.JigNo ?? string.Empty, ProductionDate = DateTime.Now }; FileLogger.LogInspectData(inspectData); // === MSSQL 데이터베이스 비동기 저장 === try { dbIniPath = Housing.Services.DatabaseSettings.GetDefaultFilePath(); var dbConnStr = Housing.Services.DatabaseSettings.Load(dbIniPath).ConnectionString; var dbService = new EolDatabaseService(dbConnStr); _ = Task.Run(async () => { try { await dbService.SaveEolDataAsync(inspectData, _loginInfo); NotifyProgress(testIndex, "DB 저장 완료", "LightBlue"); } catch (Exception ex) { NotifyError(testIndex, $"DB 저장 실패: [{ex.GetType().Name}] {ex.Message}"); } }); } catch (Exception ex) { Console.WriteLine("[DB Integration Error] " + ex.Message); } // === 7단계: DIO 출력 === if (programJudgment == "OK") { _dioBoard.WriteOutput(side + "_OK", true); } else { _dioBoard.WriteOutput(side + "_NG", true); } // 완료 알림 NotifyProgress(testIndex, "시험 완료", "LightBlue"); TestCompleted?.Invoke(this, new TestResultEventArgs { TestIndex = testIndex, SensorData = sensorData, PcbBarcode = pcbBarcode, MeasuredValue = leakResult.MeasuredValueString, Judgment = programJudgment, SensorJudgment = sensorJudgment, SpecMismatch = specMismatch, ProductionDate = inspectData.ProductionDate ?? DateTime.Now }); } finally { _activeTestIndex = -1; _testLock.Release(); } } } private void OnSentinelFinalResult(object sender, ParsedData data) { // 레거시 포맷 등 ChannelNo가 없는 경우, 현재 활성화된(테스트 중인) 채널로 할당 if (string.IsNullOrEmpty(data.ChannelNo)) { if (_activeTestIndex == 0) { data.ChannelNo = "C01"; } else if (_activeTestIndex == 1) { data.ChannelNo = "C02"; } } // ChannelNo(C01=LEFT, C02=RIGHT)에 따라 결과 라우팅 if (data.ChannelNo == "C01" || data.ChannelNo == "1") { _leftResult = data; _leftResultReady.Set(); } else if (data.ChannelNo == "C02" || data.ChannelNo == "2") { _rightResult = data; _rightResultReady.Set(); } } private void NotifyProgress(int testIndex, string message, string colorHint) { ProgressChanged?.Invoke(this, new TestProgressEventArgs { TestIndex = testIndex, Message = message, ColorHint = colorHint }); } private void NotifyError(int testIndex, string message) { ErrorOccurred?.Invoke(this, new TestProgressEventArgs { TestIndex = testIndex, Message = message, ColorHint = "Red" }); } public void Dispose() { _running = false; _leftResultReady.Set(); // 대기 중인 스레드 해제 _rightResultReady.Set(); _leftStartEvent.Set(); // 시작 대기 해제 _rightStartEvent.Set(); _dioBoard.InputChanged -= OnDioInputChanged; _sentinelService.OnFinalResultParsed -= OnSentinelFinalResult; _leftResultReady?.Dispose(); _rightResultReady?.Dispose(); _leftStartEvent?.Dispose(); _rightStartEvent?.Dispose(); Console.WriteLine("[TestProcess] Disposed."); } } }