using System; using System.Threading; 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 MeasuredValue { get; set; } public string Judgment { get; set; } // 프로그램 판정 (OK/NG) public string SensorJudgment { get; set; } // 센서 자체 판정 public bool SpecMismatch { get; set; } // SPEC 교차 검증 불일치 여부 } /// /// 자동 시험 프로세스를 관리하는 서비스. /// 레거시 ClsTester.ProcessProc()를 새 아키텍처로 포팅. /// DIO 시작 신호 대기 → ZMDI 센서 읽기 → LEAK 시험 → 결과 출력 사이클. /// public class TestProcessService : IDisposable { private readonly IDioBoard _dioBoard; private readonly IIdSensorService _leftSensor; private readonly IIdSensorService _rightSensor; private readonly SentinelC28Service _sentinelService; private Thread _leftThread; private Thread _rightThread; private volatile bool _leftTestStart = false; private volatile bool _rightTestStart = false; private volatile bool _running = true; // LEAK 시험 결과를 수신하기 위한 필드 private ParsedData _leftResult; private ParsedData _rightResult; private readonly ManualResetEventSlim _leftResultReady = new ManualResetEventSlim(false); private readonly ManualResetEventSlim _rightResultReady = new ManualResetEventSlim(false); /// 시험 진행 상황 알림 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 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; else _rightTestStart = true; } /// /// ID 센서 단독 테스트를 수행합니다. /// 실제 검사와 동일한 시작 시퀀스를 재현합니다. /// public async System.Threading.Tasks.Task ExecuteSensorTestAsync(int 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 System.Threading.Tasks.Task.Run(() => sensor.ReadSensor()); if (sensorData != null) { SensorReadComplete?.Invoke(this, (testIndex, sensorData)); // 실제 검사 시퀀스와 동일한 불량 제품 필터링 체크 if (sensorData.PrevResult != "F") { NotifyError(testIndex, "불량 제품 투입 (이전 결과: " + sensorData.PrevResult + ")"); return; } NotifyProgress(testIndex, "ID 테스트 완료", "LightBlue"); } else { // 실패 시 NotifyError를 호출하지 않는 것도 실제 시퀀스와 동일 (센서 서비스가 이미 에러 이벤트 발생시킴) // 다만 UI 갱신을 위해 빈 데이터는 전송 sensorData = new SensorIdData { ID = "-", PrevResult = "F" }; SensorReadComplete?.Invoke(this, (testIndex, sensorData)); NotifyProgress(testIndex, "ID 테스트 실패", "Red"); } } 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; } else if (e.PointName == "RIGHT_START" && e.NewValue) { _dioBoard.WriteOutput("RIGHT_OK", false); _dioBoard.WriteOutput("RIGHT_NG", false); _rightTestStart = true; } } /// /// 시험 프로세스 메인 루프 (레거시 ClsTester.ProcessProc 포팅). /// 백그라운드 스레드에서 실행됩니다. /// private void ProcessProc(int testIndex) { bool isLeft = (testIndex == 0); string side = isLeft ? "LEFT" : "RIGHT"; while (_running) { Thread.Sleep(2); // 시작 신호 대기 bool shouldStart = isLeft ? _leftTestStart : _rightTestStart; if (!shouldStart) continue; // 플래그 초기화 if (isLeft) _leftTestStart = false; else _rightTestStart = false; NotifyProgress(testIndex, "시험 시작", "LightSeaGreen"); ResultClearRequested?.Invoke(this, testIndex); // === 1단계: 센서 정보 읽기 === NotifyProgress(testIndex, "센서 정보 읽는 중", "LightSeaGreen"); var sensor = isLeft ? _leftSensor : _rightSensor; var sensorData = sensor.ReadSensor(); if (sensorData != null) { SensorReadComplete?.Invoke(this, (testIndex, sensorData)); } else { // 센서 읽기 실패해도 시험은 계속 진행 sensorData = new SensorIdData { ID = "-", PrevResult = "F" }; SensorReadComplete?.Invoke(this, (testIndex, sensorData)); } // === 2단계: 불량 제품 필터링 === if (sensorData.PrevResult != "F") { NotifyError(testIndex, "불량 제품 투입 (이전 결과: " + sensorData.PrevResult + ")"); continue; } // === 3단계: LEAK 시험 수행 === NotifyProgress(testIndex, "LEAK 시험중", "LightYellow"); // 결과 대기 리셋 var resultReady = isLeft ? _leftResultReady : _rightResultReady; resultReady.Reset(); if (isLeft) _leftResult = null; else _rightResult = null; // C28에서 결과를 수신할 때까지 대기 (레거시와 동일하게 30초 설정) bool received = resultReady.Wait(TimeSpan.FromSeconds(30)); if (!received) { NotifyError(testIndex, "LEAK 센서 통신 타임아웃"); _dioBoard.WriteOutput(side + "_NG", true); // 타임아웃 시에도 UI(그리드)에 NG로 기록되도록 이벤트 발생 TestCompleted?.Invoke(this, new TestResultEventArgs { TestIndex = testIndex, SensorData = sensorData, MeasuredValue = "0.000", Judgment = "NG", SensorJudgment = "T/O", // Timeout SpecMismatch = false }); // 로그 파일에도 기록 FileLogger.LogInspectData(new InspectData { InspectDate = DateTime.Now.ToString("yyyy-MM-dd"), InspectTime = DateTime.Now.ToString("HH:mm:ss"), Channel = side, ProductId = sensorData.ID, MeasuredValue = "0.000", Judgment = "NG", Mode = sensorData.McLine == "0" ? "개발" : "양산", LineNo = sensorData.LineNo, ProductType = sensorData.Item, SpecUL = ConfigManager.Current.SpecUL.ToString("F2"), SpecLL = ConfigManager.Current.SpecLL.ToString("F2"), Retest = "N" }); 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 ?? "-"; // === 5단계: SPEC 교차 검증 === bool specMismatch = false; string normalizedProgram = (programJudgment == "OK") ? "A" : "R"; string normalizedSensor = sensorJudgment; // 센서 판정이 Accept/Reject 형식인 경우 if (sensorJudgment == "OK" || sensorJudgment == "A") normalizedSensor = "A"; else if (sensorJudgment == "NG" || sensorJudgment == "R") 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, MeasuredValue = leakResult.MeasuredValue.ToString("F3"), Judgment = programJudgment, Mode = sensorData.McLine == "0" ? "개발" : "양산", LineNo = sensorData.LineNo, ProductType = sensorData.Item, SpecUL = specUL.ToString("F2"), SpecLL = specLL.ToString("F2"), Retest = "N" }; FileLogger.LogInspectData(inspectData); // === 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, MeasuredValue = leakResult.MeasuredValue.ToString("F3"), Judgment = programJudgment, SensorJudgment = sensorJudgment, SpecMismatch = specMismatch }); } } private void OnSentinelFinalResult(object sender, ParsedData data) { // 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(); _dioBoard.InputChanged -= OnDioInputChanged; _sentinelService.OnFinalResultParsed -= OnSentinelFinalResult; _leftResultReady?.Dispose(); _rightResultReady?.Dispose(); Console.WriteLine("[TestProcess] Disposed."); } } }