리크 테스트 gui
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

611 lines
27 KiB

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
{
/// <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 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 저장/검사 완료 일시
}
/// <summary>
/// 자동 시험 프로세스를 관리하는 서비스.
/// 레거시 ClsTester.ProcessProc()를 새 아키텍처로 포팅.
/// DIO 시작 신호 대기 → 제품 ID 읽기 → LEAK 시험 → 결과 출력 사이클.
/// </summary>
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);
/// <summary>현재 실행 중인 테스트 인덱스 (0=LEFT, 1=RIGHT, -1=None)</summary>
private volatile int _activeTestIndex = -1;
public int ActiveTestIndex => _activeTestIndex;
/// <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;
/// <summary>바코드 매핑 완료 알림</summary>
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;
}
/// <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;
_leftStartEvent.Set();
}
else
{
_rightTestStart = true;
_rightStartEvent.Set();
}
}
/// <summary>
/// ID 센서 단독 테스트를 수행합니다.
/// 실제 검사와 동일한 시작 시퀀스를 재현합니다.
/// </summary>
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();
}
}
/// <summary>
/// 시험 프로세스 메인 루프 (레거시 ClsTester.ProcessProc 포팅).
/// 백그라운드 스레드에서 실행됩니다.
/// </summary>
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.");
}
}
}