feat: 수동출사 레시피/페이지 정합성 인터락 (ISSUE-260904-03)

AutoRun 중 수동출사(Run Welding) 시 화면 띠의 PLC 지시와 실제 활성·로드 상태가 어긋난 채 출사되는 것을 차단. 검증 4종 - 활성==선택 / 활성==실제 로드파일(SelectedRecipeFile) / PLC지시 RECIPE==활성번호 / PLC지시 PAGE==현재 페이지. PLC 지시 2종은 bit15 구간에만 채워지므로 null 이면 생략. 검증 예외 시 fail-closed. 시퀀스는 무수정.
This commit is contained in:
노도연 2026-09-04 16:13:37 +09:00
parent 468499d559
commit f873e066e4

View File

@ -506,8 +506,97 @@ namespace WeldingCommon.Views
_SeqScoutFlyingVision.Stop(); _SeqScoutFlyingVision.Stop();
} }
#region [레시피/페이지 인터락 2026-09-04] 수동출사 전 정합성 검증 (요청 260904-03)
// 배경: AutoRun 진행 중 수동출사를 할 때, 작업자는 화면 상단 띠(SyncPlcGuide, :96)에 뜬
// "PLC 지시 RECIPE/PAGE" 를 보고 레시피와 페이지를 맞춘 뒤 Run Welding 을 누른다.
// 그 맞춤이 실제로 되어 있는지 확인하는 코드가 없어, 어긋난 채로 출사될 수 있었다.
//
// 검사 4종:
// ① 활성 레시피 == 선택 레시피
// ② 활성 레시피 == 실제 캔버스에 로드된 파일 ← 가장 중요
// LASA 는 레시피를 메모리에 1개만 유지하므로 활성과 화면 내용이 어긋날 수 있다.
// 판정 기준 DB.Recipe.Recipe.SelectedRecipeFile 은 Recipe.Load() 성공 시에만
// 세팅되는 실제 로드 이력이라 신뢰할 수 있다.
// ③ PLC 지시 RECIPE == 활성 레시피 번호
// ④ PLC 지시 PAGE == 현재 캔버스 페이지
//
// ③④ 의 Status.PlcGuide* 는 bit15(수동용접) 구간에만 채워진다(UCMainAutoRunViewModel:223).
// null 이면 해당 검사를 생략한다 — PLC 워드를 직접 읽으면 공용 코드가 설비에 종속되므로
// 의도적으로 Status 경유만 쓴다(260904 협의).
//
// 예외 시 fail-closed(차단). 검증이 안 되면 출사를 막는 것이 이 인터락의 목적이다.
// ※ HeadTheta 인터락(SeqAutoRunPTOP)과는 별개 기능이며 서로 영향이 없다.
private bool CheckRecipePageBeforeTeachingRun()
{
try {
var active = _Aurora.RecipeManager.ActivatedRecipe();
if (active == null) {
Log.E("[레시피/페이지 인터락] 활성 레시피 없음 → 출사 차단");
MessageUtil.CreateAndShowMessageDialog("출사 차단", "활성 레시피가 없습니다.");
return false;
}
var mismatches = new List<string>();
int curPage = CommonHelper.GetMainPage().GetCurrentPageIndex() + 1; // 0-base → 1-base
// ① 활성 == 선택
var selected = RecipeHelper.SelectedRecipe.name;
if (!string.IsNullOrWhiteSpace(selected) &&
!string.Equals(active.Name, selected, StringComparison.OrdinalIgnoreCase)) {
mismatches.Add($"활성 레시피 [{active.Name}] ≠ 선택 레시피 [{selected}]");
}
// ② 활성 == 실제 캔버스에 로드된 파일
var loadedFile = DB.Recipe.Recipe.SelectedRecipeFile;
if (!string.IsNullOrWhiteSpace(loadedFile)) {
var loadedName = System.IO.Path.GetFileName(System.IO.Path.GetDirectoryName(loadedFile));
if (!string.Equals(active.Name, loadedName, StringComparison.OrdinalIgnoreCase)) {
mismatches.Add($"활성 레시피 [{active.Name}] ≠ 화면 로드 [{loadedName}]");
}
}
// ③④ PLC 지시 (bit15 구간에만 채워짐)
var gRecipe = Status.PlcGuideRecipeNo;
var gPage = Status.PlcGuidePageNo;
if (gRecipe != null && gPage != null) {
if (gRecipe.Value != active.Id) {
mismatches.Add($"PLC 지시 RECIPE [{gRecipe}] ≠ 활성 레시피 번호 [{active.Id}]");
}
if (gPage.Value != curPage) {
mismatches.Add($"PLC 지시 PAGE [{gPage}] ≠ 현재 페이지 [{curPage}]");
}
}
if (mismatches.Count == 0) {
Log.I($"[레시피/페이지 인터락] 통과. 활성=[{active.Id}:{active.Name}] " +
$"페이지=[{curPage}] PLC지시=[{gRecipe}/{gPage}]");
return true;
}
Log.E($"[레시피/페이지 인터락] 출사 차단 — {string.Join(" / ", mismatches)}");
MessageUtil.CreateAndShowMessageDialog("출사 차단 - 레시피/페이지 불일치",
"다음 항목이 일치하지 않아 출사를 차단했습니다.\n\n · " +
string.Join("\n · ", mismatches) +
"\n\n레시피와 페이지를 다시 맞춘 뒤 시도하십시오.");
return false;
} catch (Exception ex) {
Log.E($"[레시피/페이지 인터락] 검사 실패 → 출사 차단(fail-closed): {ex.Message}");
MessageUtil.CreateAndShowMessageDialog("출사 차단",
$"레시피/페이지 검증 중 오류가 발생했습니다.\n\n{ex.Message}");
return false;
}
}
#endregion
private void PerformCmdRunTeachingRun(object obj) private void PerformCmdRunTeachingRun(object obj)
{ {
// [레시피/페이지 인터락 2026-09-04] 출사 전 정합성 검증. 불일치면 시퀀스를 태우지 않는다.
if (!CheckRecipePageBeforeTeachingRun()) {
return;
}
var result = MessageUtil.ShowYesNoMessageDialog("Teaching Run", "Start TeachingRun Sequence?"); var result = MessageUtil.ShowYesNoMessageDialog("Teaching Run", "Start TeachingRun Sequence?");
if (result == false) { if (result == false) {
return; return;