From 172a273b9018956add00c989518f486ed8da1ade Mon Sep 17 00:00:00 2001 From: NohDoyeon241104 Date: Wed, 19 Aug 2026 11:59:36 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Recipe/Page=20=EC=9E=AC=EB=8F=99?= =?UTF-8?q?=EA=B8=B0=20=EB=85=B8=EB=93=9C=20+=20Page=20=EC=88=98=EB=A0=B4?= =?UTF-8?q?=20=ED=9B=84=20Recipe=20=EC=B5=9C=EC=A2=85=20=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 260813: ResyncRecipePage 노드 추가. ResumeMode·시뮬·PLC 워드 무효 시 재동기를 생략하고 CheckPageNo 의 REQ 대기로 넘긴다. 재동기 상한 10초(RESYNC_TIMEOUT_SEC) 초과 시 수렴 미완으로 포기. - 260818: CheckPageNoComp 를 outer while 로 감싸 Page 수렴 후 PLC 의 Recipe/Page 를 다시 읽어 PC 확정값과 대조. 어긋나면 Recipe 부터 재확정 후 Page 재수렴. 260812 15:17:29 로그 대응 — Page 수렴 중 PLC 가 Page 를 6→2 로 바꾸며 Recipe 도 함께 바꿨을 때 PC 가 옛 Recipe(22)를 에코한 채 남아 PLC_WELD_TRIGGER_REQ 가 오지 않고 타임아웃만 반복되던 문제. 원복: 각 region 통째로 삭제 (260818 은 outer while 제거 후 들여쓰기 복원). --- .../Sequences/SeqAutoRunPTOP.cs | 256 +++++++++++++++--- 1 file changed, 211 insertions(+), 45 deletions(-) diff --git a/Machines/ILSOS_PrismaticRD_PTOP/Sequences/SeqAutoRunPTOP.cs b/Machines/ILSOS_PrismaticRD_PTOP/Sequences/SeqAutoRunPTOP.cs index 4aebf18..a0b6e7c 100644 --- a/Machines/ILSOS_PrismaticRD_PTOP/Sequences/SeqAutoRunPTOP.cs +++ b/Machines/ILSOS_PrismaticRD_PTOP/Sequences/SeqAutoRunPTOP.cs @@ -64,6 +64,10 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences const string SEQ_CHECK_PAGENO = "[SeqAutoRunPTOP] CHECK PageNo"; const string SEQ_CHECK_PAGENO_COMPLETE = "[SeqAutoRunPTOP] CHECK PageNo Complete"; // 260513 + #region 260813 + const string SEQ_RESYNC_RECIPE_PAGE = "[SeqAutoRunPTOP] Resync Recipe/Page"; + #endregion + const string SEQ_CHECK_WELDING_START_READ_BIT = "[SeqAutoRunPTOP] Check Welding Start ReadBit"; // 26 05 13 const string SEQ_WAIT_WELDING_START = "[SeqAutoRunPTOP] Wait Welding Start"; @@ -129,6 +133,13 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences const int ECHO_TOAST_EVERY = 10; // 토스트 폭주 방지 — 첫 회차 + 이후 N회마다만 표시 #endregion + #region 260813 + private bool _InResyncNode = false; + private bool _ResyncAborted = false; + private DateTime _ResyncStartTime; + const int RESYNC_TIMEOUT_SEC = 10; + #endregion + private eListNo _WeldingScannerListNo; private List _SelectedObjectList; @@ -155,6 +166,7 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences sb.AddAction(SEQ_ENTER, Sequence_Enter); sb.AddDecision(SEQ_CHECK_INIT_CONDITION, CheckInitCondition); //분기 + sb.AddAction(SEQ_RESYNC_RECIPE_PAGE, ResyncRecipePage); sb.AddAction(SEQ_CHECK_PAGENO, CheckPageNo); // 단일 트리거: PLC_PAGE_CHANGE_REQ(0x08) 대기 sb.AddAction(SEQ_CHECK_RECIPENO_COMPLETE, CheckRecipeNoComp); // Recipe 먼저 확정/전환 (Page보다 상위) sb.AddAction(SEQ_CHECK_PAGENO_COMPLETE, CheckPageNoComp); // 그다음 Page 확정 @@ -194,6 +206,7 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences sb.Flows(sb.Init > sb[SEQ_ENTER] > sb[SEQ_CHECK_INIT_CONDITION] + > sb[SEQ_RESYNC_RECIPE_PAGE] > sb[SEQ_CHECK_PAGENO] // 단일 트리거 PLC_PAGE_CHANGE_REQ(0x08) ON 대기 > sb[SEQ_CHECK_RECIPENO_COMPLETE] // Recipe 먼저 확정/전환 > sb[SEQ_CHECK_PAGENO_COMPLETE] // 그다음 Page 확정 @@ -342,8 +355,8 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences // return SEQ_COMPLETE; //} Thread.Sleep(5); - Log.I($"[CheckInitCondition] 조건 확인 완료 → CheckPageNo 이동"); - return SEQ_CHECK_PAGENO; + Log.I($"[CheckInitCondition] 조건 확인 완료 → ResyncRecipePage 이동"); + return SEQ_RESYNC_RECIPE_PAGE; } private int _CenterBeamPower; @@ -476,6 +489,13 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences if (retry > 0) Thread.Sleep(ECHO_RETRY_MS); retry++; + if (AbortIfResyncTimedOut()) + { + Log.W($"[CheckRecipeNoComp] 재동기 상한 {RESYNC_TIMEOUT_SEC}s 초과 — 수렴 미완으로 포기 " + + $"(재시도 {retry}회차, 직전 회차 에코가 남아 있을 수 있음)"); + return SequenceAction.Ok(); + } + // ── 1. PLC가 준 Recipe No 읽기 ── var read = _PLC.ReadWord(eWordInput.PLC_RECIPE_NO); if (read.err != Hermes.E1.OK) @@ -569,6 +589,97 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences } #endregion + #region 260813 + private SequenceActionResult ResyncRecipePage(SequenceContext context, string id, SequenceActionResult param) + { + if (ResumeMode) + { + Log.I("[ResyncRecipePage] ResumeMode=true → 재동기 생략 → CheckPageNo 이동"); + return SequenceAction.Ok(); + } + + if (_PLC == null) + { + Log.E("[ResyncRecipePage] _PLC instance is null! 재동기 불가"); + return null; + } + + if (_PLC.IsSimul) + { + Log.I("[ResyncRecipePage] 시뮬레이션 → 재동기 생략 → CheckPageNo 이동"); + return SequenceAction.Ok(); + } + + var recipeRead = _PLC.ReadWord(eWordInput.PLC_RECIPE_NO); + var pageRead = _PLC.ReadWord(eWordInput.PLC_PAGE_NO); + + bool recipeValid = recipeRead.err == Hermes.E1.OK + && recipeRead.data != 0 + && _RecipeManager.Recipes().Any(x => x.Id == recipeRead.data); + + if (pageRead.err != Hermes.E1.OK || pageRead.data < 1 || !recipeValid) + { + Log.W($"[ResyncRecipePage] PLC 워드가 재동기 조건 미충족 " + + $"(RecipeNo:{recipeRead.data}/err:{recipeRead.err}/유효:{recipeValid}, " + + $"PageNo:{pageRead.data}/err:{pageRead.err}) → 재동기 생략, CheckPageNo 의 REQ 대기로 진행"); + return SequenceAction.Ok(); + } + + Log.I($"[ResyncRecipePage] 재기동 재동기 시작 — PLC 값(RecipeNo:{recipeRead.data}, " + + $"PageNo:{pageRead.data})으로 Recipe/Page 확정 후 에코"); + + try + { + Application.Current.Dispatcher.Invoke(() => RecipeHelper.OnActiveReloaded()); + Log.I("[ResyncRecipePage] Model.Layers 선(先) reload 완료 (UI thread)"); + } + catch (Exception ex) + { + Log.E($"[ResyncRecipePage] Model.Layers 선(先) reload 실패 ({ex.GetType().Name}: {ex.Message}) " + + $"→ 재동기 생략, CheckPageNo 의 REQ 대기로 진행"); + MessageUtil.ShowErrorToast("레시피 로드 실패\n재동기를 건너뜁니다. 레시피를 확인해주세요."); + return SequenceAction.Ok(); + } + + _ResyncStartTime = DateTime.Now; + _ResyncAborted = false; + _InResyncNode = true; + try + { + if (CheckRecipeNoComp(context, id, param) == null) return null; + if (_ResyncAborted) return ResyncGiveUp("Recipe"); + + if (CheckPageNoComp(context, id, param) == null) return null; + if (_ResyncAborted) return ResyncGiveUp("Page"); + } + finally + { + _InResyncNode = false; + } + + Log.I($"---> [ResyncRecipePage] 재동기 완료 (RecipeNo:{plcRecipeNo}, PageNo:{plcPageNo}, " + + $"PC_Index:{_CurrentPageNo}) → CheckPageNo 이동"); + return SequenceAction.Ok(); + } + + private bool AbortIfResyncTimedOut() + { + if (!_InResyncNode) return false; + if (DateTime.Now.Subtract(_ResyncStartTime).TotalSeconds <= RESYNC_TIMEOUT_SEC) return false; + + _ResyncAborted = true; + return true; + } + + private SequenceActionResult ResyncGiveUp(string stage) + { + Log.W($"[ResyncRecipePage] {stage} 수렴 실패(상한 {RESYNC_TIMEOUT_SEC}s 초과) → 재동기 포기, " + + $"CheckPageNo 의 REQ 대기로 진행 " + + $"(직전 회차에 쓴 에코/내부 상태는 남아 있을 수 있음 — RecipeNo:{plcRecipeNo}, PageNo:{plcPageNo})"); + return SequenceAction.Ok(); + } + #endregion + // 페이지 체크 - 페이지만 감시 AutoRunStatus는 실제 상위에서 동작 체크하고 시퀸스 동작한다. private SequenceActionResult CheckPageNo(SequenceContext context, string id, SequenceActionResult param) { @@ -751,61 +862,109 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences // - while 탈출 후 context.Stopped 가드 추가 (종전에는 정지 중에도 미검증 값을 에코했다) private SequenceActionResult CheckPageNoComp(SequenceContext context, string id, SequenceActionResult param) { - Log.I("[CheckPageNoComp] PLC_PAGE_NO 수렴 시작"); Status.SetWorkingPosition(eWorkingPosition.LOADING_CELL); - int retry = 0; - + // 260818: Recipe/Page 최종 확인 (원복: 이 outer while 과 아래 "최종 확인" 블록을 제거하고 + // 안쪽 블록의 들여쓰기만 되돌리면 260806 상태가 된다) + // 문제(260812 로그 15:17:29): CheckRecipeNoComp 가 Recipe 를 22로 확정하고 나온 뒤 + // Page 수렴 중 PLC 가 Page 를 6→2로 바꿨다. Page 만 재수렴하고 넘어갔으나 PLC 는 그때 + // Recipe 도 함께 바꿨을 수 있고, PC 는 옛 값(22)을 에코한 채였다. PLC 게이트는 두 워드를 + // 모두 보므로 PLC_WELD_TRIGGER_REQ 가 끝내 오지 않고 타임아웃만 반복됐다. + // → Page 수렴이 끝나면 PLC 의 Recipe/Page 를 다시 읽어 PC 확정값과 둘 다 같은지 확인하고, + // 어긋났으면 Recipe 부터 다시 확정(CheckRecipeNoComp)한 뒤 Page 를 재수렴한다. + int joint = 0; while (!context.Stopped) { - if (retry > 0) Thread.Sleep(ECHO_RETRY_MS); - retry++; + joint++; - // ── 1. PLC가 준 Page No 읽기 ── - var read = _PLC.ReadWord(eWordInput.PLC_PAGE_NO); - if (read.err != Hermes.E1.OK) + Log.I("[CheckPageNoComp] PLC_PAGE_NO 수렴 시작"); + + int retry = 0; + + while (!context.Stopped) { - Log.E($"[CheckPageNoComp] PLC_PAGE_NO 읽기 실패 (err:{read.err}) — 재시도 {retry}회차"); - continue; + if (retry > 0) Thread.Sleep(ECHO_RETRY_MS); + retry++; + + if (AbortIfResyncTimedOut()) + { + Log.W($"[CheckPageNoComp] 재동기 상한 {RESYNC_TIMEOUT_SEC}s 초과 — 수렴 미완으로 포기 " + + $"(재시도 {retry}회차, 직전 회차 에코가 남아 있을 수 있음)"); + return SequenceAction.Ok(); + } + + // ── 1. PLC가 준 Page No 읽기 ── + var read = _PLC.ReadWord(eWordInput.PLC_PAGE_NO); + if (read.err != Hermes.E1.OK) + { + Log.E($"[CheckPageNoComp] PLC_PAGE_NO 읽기 실패 (err:{read.err}) — 재시도 {retry}회차"); + continue; + } + + short p = read.data; + + // 상한은 활성 Recipe의 페이지 수(레이어 수)로 가변 검증한다 (Recipe별 2~5페이지, 하드코딩 금지). + // Model.Layers 접근은 UI 스레드로 직렬화 (레시피 reload와의 race 방지). + int recipePageCount = Application.Current.Dispatcher.Invoke(() => _ScannerCanvasUserCtrl.Model.Layers.Count()); + if (p < 1 || p > recipePageCount) + { + if (retry == 1 || retry % ECHO_TOAST_EVERY == 0) + MessageUtil.ShowErrorToast($"PLC Page No 이상: {p}\n모니터링 확인해주세요."); + Log.E($"[CheckPageNoComp] Invalid PageNo: {p} (활성 Recipe 페이지 수: {recipePageCount}) " + + $"— 에코하지 않고 재시도 {retry}회차"); + continue; + } + + // ── 2. 내부 상태 갱신 (에코와 같은 세트) ── + _CurrentPageNo = p - 1; // 1-based to 0-based // page index + + // ── 3. 에코 ── + int werr = _PLC.WriteWord(eWordOutput.PC_PAGE_NO, p); + if (werr != Hermes.E1.OK) + { + Log.E($"[CheckPageNoComp] PC_PAGE_NO 쓰기 실패 (err:{werr}) — 재시도 {retry}회차"); + continue; + } + + // ── 4. 되읽어 수렴 확인 ── + Thread.Sleep(200); + short echoed = _PLC.ReadOutWord(eWordOutput.PC_PAGE_NO).data; + short current = _PLC.ReadWord(eWordInput.PLC_PAGE_NO).data; + if (echoed == p && current == p) + { + plcPageNo = p; + Log.I($"---> [CheckPageNoComp] 수렴 완료 ({retry}회차) - PC_PAGE_NO: [{plcPageNo}], PC_Index: [{_CurrentPageNo}]"); + break; + } + + Log.W($"[CheckPageNoComp] 미수렴 - 읽음:[{p}] 에코:[{echoed}] 현재PLC:[{current}] — 재시도 {retry}회차"); } - short p = read.data; + if (context.Stopped) return null; - // 상한은 활성 Recipe의 페이지 수(레이어 수)로 가변 검증한다 (Recipe별 2~5페이지, 하드코딩 금지). - // Model.Layers 접근은 UI 스레드로 직렬화 (레시피 reload와의 race 방지). - int recipePageCount = Application.Current.Dispatcher.Invoke(() => _ScannerCanvasUserCtrl.Model.Layers.Count()); - if (p < 1 || p > recipePageCount) + // ── 5. 최종 확인: Recipe/Page 를 다시 읽어 둘 다 PC 확정값과 같아야 넘어간다 ── + // 시뮬레이션은 PLC 워드가 항상 0이라 최종 확인 대상이 아니다. + if (_PLC.IsSimul) break; + + short curRecipe = _PLC.ReadWord(eWordInput.PLC_RECIPE_NO).data; + short curPage = _PLC.ReadWord(eWordInput.PLC_PAGE_NO).data; + + if (curRecipe == plcRecipeNo && curPage == plcPageNo) { - if (retry == 1 || retry % ECHO_TOAST_EVERY == 0) - MessageUtil.ShowErrorToast($"PLC Page No 이상: {p}\n모니터링 확인해주세요."); - Log.E($"[CheckPageNoComp] Invalid PageNo: {p} (활성 Recipe 페이지 수: {recipePageCount}) " + - $"— 에코하지 않고 재시도 {retry}회차"); - continue; - } - - // ── 2. 내부 상태 갱신 (에코와 같은 세트) ── - _CurrentPageNo = p - 1; // 1-based to 0-based // page index - - // ── 3. 에코 ── - int werr = _PLC.WriteWord(eWordOutput.PC_PAGE_NO, p); - if (werr != Hermes.E1.OK) - { - Log.E($"[CheckPageNoComp] PC_PAGE_NO 쓰기 실패 (err:{werr}) — 재시도 {retry}회차"); - continue; - } - - // ── 4. 되읽어 수렴 확인 ── - Thread.Sleep(200); - short echoed = _PLC.ReadOutWord(eWordOutput.PC_PAGE_NO).data; - short current = _PLC.ReadWord(eWordInput.PLC_PAGE_NO).data; - if (echoed == p && current == p) - { - plcPageNo = p; - Log.I($"---> [CheckPageNoComp] 수렴 완료 ({retry}회차) - PC_PAGE_NO: [{plcPageNo}], PC_Index: [{_CurrentPageNo}]"); + Log.I($"---> [CheckPageNoComp] Recipe/Page 최종 확인 완료 ({joint}회차) - " + + $"PC_RECIPE_NO: [{plcRecipeNo}], PC_PAGE_NO: [{plcPageNo}]"); break; } - Log.W($"[CheckPageNoComp] 미수렴 - 읽음:[{p}] 에코:[{echoed}] 현재PLC:[{current}] — 재시도 {retry}회차"); + if (joint == 1 || joint % ECHO_TOAST_EVERY == 0) + MessageUtil.ShowErrorToast($"확정 후 Recipe/Page 변경 감지\n" + + $"Recipe {plcRecipeNo}→{curRecipe}, Page {plcPageNo}→{curPage}, 재동기화 중"); + Log.W($"[CheckPageNoComp] 최종 확인 불일치 - Recipe PLC:[{curRecipe}]/에코:[{plcRecipeNo}], " + + $"Page PLC:[{curPage}]/에코:[{plcPageNo}] — Recipe 재확정 후 Page 재수렴 {joint}회차"); + + // Recipe 부터 다시 확정한다(ActivateRecipe + Model.Layers reload + PC_RECIPE_NO 재에코). + // 레시피가 바뀌면 Layers(페이지 수)도 바뀌므로 Page 는 위에서 다시 수렴한다. + if (CheckRecipeNoComp(context, id, param) == null) return null; } if (context.Stopped) return null; @@ -818,7 +977,14 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences Log.I($"[CheckPageNoComp] CellId: [{cellId}], LotId: [{lotId}]"); // PLC는 이 비트를 보지 않는다(260806 확인 — 게이트는 워드다). 기존 배선 호환을 위해 ON은 유지한다. - _PLC.WriteBit(eBitOutput.PC_PAGE_CHANGE_COMPLETE, true); + if (_InResyncNode) + { + Log.I("[CheckPageNoComp] 재동기 경로 → PC_PAGE_CHANGE_COMPLETE 쓰기 생략 (REQ 미수신 상태)"); + } + else + { + _PLC.WriteBit(eBitOutput.PC_PAGE_CHANGE_COMPLETE, true); + } // UI 페이지 업데이트 알림 Application.Current.AuroraEnv().EventPublisher.Publish(