|
|
|
|
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
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 시험 진행 상황 이벤트 인자.
|
|
|
|
|
/// </summary>
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 시험 결과 이벤트 인자.
|
|
|
|
|
/// </summary>
|
|
|
|
|
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 교차 검증 불일치 여부
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 자동 시험 프로세스를 관리하는 서비스.
|
|
|
|
|
/// 레거시 ClsTester.ProcessProc()를 새 아키텍처로 포팅.
|
|
|
|
|
/// DIO 시작 신호 대기 → ZMDI 센서 읽기 → LEAK 시험 → 결과 출력 사이클.
|
|
|
|
|
/// </summary>
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
/// <summary>시험 진행 상황 알림</summary>
|
|
|
|
|
public event EventHandler<TestProgressEventArgs> ProgressChanged;
|
|
|
|
|
|
|
|
|
|
/// <summary>시험 완료 결과 알림</summary>
|
|
|
|
|
public event EventHandler<TestResultEventArgs> TestCompleted;
|
|
|
|
|
|
|
|
|
|
/// <summary>오류 메시지 알림</summary>
|
|
|
|
|
public event EventHandler<TestProgressEventArgs> ErrorOccurred;
|
|
|
|
|
|
|
|
|
|
/// <summary>센서 ID 판독 완료 알림</summary>
|
|
|
|
|
public event EventHandler<(int TestIndex, SensorIdData Data)> SensorReadComplete;
|
|
|
|
|
|
|
|
|
|
/// <summary>결과 초기화 요청 알림</summary>
|
|
|
|
|
public event EventHandler<int> 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>자동 시험 프로세스를 시작합니다.</summary>
|
|
|
|
|
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.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>시뮬레이션용: 수동으로 시험 시작 트리거</summary>
|
|
|
|
|
public void TriggerTestStart(int testIndex)
|
|
|
|
|
{
|
|
|
|
|
if (testIndex == 0) _leftTestStart = true;
|
|
|
|
|
else _rightTestStart = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// ID 센서 단독 테스트를 수행합니다.
|
|
|
|
|
/// 실제 검사와 동일한 시작 시퀀스를 재현합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 시험 프로세스 메인 루프 (레거시 ClsTester.ProcessProc 포팅).
|
|
|
|
|
/// 백그라운드 스레드에서 실행됩니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
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.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|