feat: Recipe/Page 재동기 노드 + Page 수렴 후 Recipe 최종 확인

- 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 제거 후 들여쓰기 복원).
This commit is contained in:
노도연 2026-08-19 11:59:36 +09:00
parent 832ee0b32e
commit 172a273b90

View File

@ -64,6 +64,10 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences
const string SEQ_CHECK_PAGENO = "[SeqAutoRunPTOP] CHECK PageNo"; const string SEQ_CHECK_PAGENO = "[SeqAutoRunPTOP] CHECK PageNo";
const string SEQ_CHECK_PAGENO_COMPLETE = "[SeqAutoRunPTOP] CHECK PageNo Complete"; // 260513 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_CHECK_WELDING_START_READ_BIT = "[SeqAutoRunPTOP] Check Welding Start ReadBit"; // 26 05 13
const string SEQ_WAIT_WELDING_START = "[SeqAutoRunPTOP] Wait Welding Start"; 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회마다만 표시 const int ECHO_TOAST_EVERY = 10; // 토스트 폭주 방지 — 첫 회차 + 이후 N회마다만 표시
#endregion #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 eListNo _WeldingScannerListNo;
private List<IDrawObject> _SelectedObjectList; private List<IDrawObject> _SelectedObjectList;
@ -155,6 +166,7 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences
sb.AddAction(SEQ_ENTER, Sequence_Enter); sb.AddAction(SEQ_ENTER, Sequence_Enter);
sb.AddDecision(SEQ_CHECK_INIT_CONDITION, CheckInitCondition); //분기 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_PAGENO, CheckPageNo); // 단일 트리거: PLC_PAGE_CHANGE_REQ(0x08) 대기
sb.AddAction(SEQ_CHECK_RECIPENO_COMPLETE, CheckRecipeNoComp); // Recipe 먼저 확정/전환 (Page보다 상위) sb.AddAction(SEQ_CHECK_RECIPENO_COMPLETE, CheckRecipeNoComp); // Recipe 먼저 확정/전환 (Page보다 상위)
sb.AddAction(SEQ_CHECK_PAGENO_COMPLETE, CheckPageNoComp); // 그다음 Page 확정 sb.AddAction(SEQ_CHECK_PAGENO_COMPLETE, CheckPageNoComp); // 그다음 Page 확정
@ -194,6 +206,7 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences
sb.Flows(sb.Init sb.Flows(sb.Init
> sb[SEQ_ENTER] > sb[SEQ_ENTER]
> sb[SEQ_CHECK_INIT_CONDITION] > sb[SEQ_CHECK_INIT_CONDITION]
> sb[SEQ_RESYNC_RECIPE_PAGE]
> sb[SEQ_CHECK_PAGENO] // 단일 트리거 PLC_PAGE_CHANGE_REQ(0x08) ON 대기 > sb[SEQ_CHECK_PAGENO] // 단일 트리거 PLC_PAGE_CHANGE_REQ(0x08) ON 대기
> sb[SEQ_CHECK_RECIPENO_COMPLETE] // Recipe 먼저 확정/전환 > sb[SEQ_CHECK_RECIPENO_COMPLETE] // Recipe 먼저 확정/전환
> sb[SEQ_CHECK_PAGENO_COMPLETE] // 그다음 Page 확정 > sb[SEQ_CHECK_PAGENO_COMPLETE] // 그다음 Page 확정
@ -342,8 +355,8 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences
// return SEQ_COMPLETE; // return SEQ_COMPLETE;
//} //}
Thread.Sleep(5); Thread.Sleep(5);
Log.I($"[CheckInitCondition] 조건 확인 완료 → CheckPageNo 이동"); Log.I($"[CheckInitCondition] 조건 확인 완료 → ResyncRecipePage 이동");
return SEQ_CHECK_PAGENO; return SEQ_RESYNC_RECIPE_PAGE;
} }
private int _CenterBeamPower; private int _CenterBeamPower;
@ -476,6 +489,13 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences
if (retry > 0) Thread.Sleep(ECHO_RETRY_MS); if (retry > 0) Thread.Sleep(ECHO_RETRY_MS);
retry++; retry++;
if (AbortIfResyncTimedOut())
{
Log.W($"[CheckRecipeNoComp] 재동기 상한 {RESYNC_TIMEOUT_SEC}s 초과 — 수렴 미완으로 포기 " +
$"(재시도 {retry}회차, 직전 회차 에코가 남아 있을 수 있음)");
return SequenceAction.Ok();
}
// ── 1. PLC가 준 Recipe No 읽기 ── // ── 1. PLC가 준 Recipe No 읽기 ──
var read = _PLC.ReadWord(eWordInput.PLC_RECIPE_NO); var read = _PLC.ReadWord(eWordInput.PLC_RECIPE_NO);
if (read.err != Hermes.E1.OK) if (read.err != Hermes.E1.OK)
@ -569,6 +589,97 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences
} }
#endregion #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는 실제 상위에서 동작 체크하고 시퀸스 동작한다. // 페이지 체크 - 페이지만 감시 AutoRunStatus는 실제 상위에서 동작 체크하고 시퀸스 동작한다.
private SequenceActionResult CheckPageNo(SequenceContext context, string id, SequenceActionResult param) private SequenceActionResult CheckPageNo(SequenceContext context, string id, SequenceActionResult param)
{ {
@ -751,61 +862,109 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences
// - while 탈출 후 context.Stopped 가드 추가 (종전에는 정지 중에도 미검증 값을 에코했다) // - while 탈출 후 context.Stopped 가드 추가 (종전에는 정지 중에도 미검증 값을 에코했다)
private SequenceActionResult CheckPageNoComp(SequenceContext context, string id, SequenceActionResult param) private SequenceActionResult CheckPageNoComp(SequenceContext context, string id, SequenceActionResult param)
{ {
Log.I("[CheckPageNoComp] PLC_PAGE_NO 수렴 시작");
Status.SetWorkingPosition(eWorkingPosition.LOADING_CELL); 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) while (!context.Stopped)
{ {
if (retry > 0) Thread.Sleep(ECHO_RETRY_MS); joint++;
retry++;
// ── 1. PLC가 준 Page No 읽기 ── Log.I("[CheckPageNoComp] PLC_PAGE_NO 수렴 시작");
var read = _PLC.ReadWord(eWordInput.PLC_PAGE_NO);
if (read.err != Hermes.E1.OK) int retry = 0;
while (!context.Stopped)
{ {
Log.E($"[CheckPageNoComp] PLC_PAGE_NO 읽기 실패 (err:{read.err}) — 재시도 {retry}회차"); if (retry > 0) Thread.Sleep(ECHO_RETRY_MS);
continue; 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페이지, 하드코딩 금지). // ── 5. 최종 확인: Recipe/Page 를 다시 읽어 둘 다 PC 확정값과 같아야 넘어간다 ──
// Model.Layers 접근은 UI 스레드로 직렬화 (레시피 reload와의 race 방지). // 시뮬레이션은 PLC 워드가 항상 0이라 최종 확인 대상이 아니다.
int recipePageCount = Application.Current.Dispatcher.Invoke(() => _ScannerCanvasUserCtrl.Model.Layers.Count()); if (_PLC.IsSimul) break;
if (p < 1 || p > recipePageCount)
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) Log.I($"---> [CheckPageNoComp] Recipe/Page 최종 확인 완료 ({joint}회차) - " +
MessageUtil.ShowErrorToast($"PLC Page No 이상: {p}\n모니터링 확인해주세요."); $"PC_RECIPE_NO: [{plcRecipeNo}], PC_PAGE_NO: [{plcPageNo}]");
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; 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; if (context.Stopped) return null;
@ -818,7 +977,14 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences
Log.I($"[CheckPageNoComp] CellId: [{cellId}], LotId: [{lotId}]"); Log.I($"[CheckPageNoComp] CellId: [{cellId}], LotId: [{lotId}]");
// PLC는 이 비트를 보지 않는다(260806 확인 — 게이트는 워드다). 기존 배선 호환을 위해 ON은 유지한다. // 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 페이지 업데이트 알림 // UI 페이지 업데이트 알림
Application.Current.AuroraEnv().EventPublisher.Publish( Application.Current.AuroraEnv().EventPublisher.Publish(