LGES-OC5-PtoP-Welder/docs/분석/2026/08/06/RecipePage_에코수렴루프_설계.md
NohDoyeon241104 c7f12b0bda feat: Recipe/Page 핸드셰이크 에코 수렴 루프 적용
PLC가 PC 에코 넘버(PC_PAGE_NO/PC_RECIPE_NO) 일치를 상시 확인하는 것으로 확정(260806)됨에 따라, PC 책임을 검증에서 '정확히 읽고 → 내부 상태 갱신 → 에코'로 전환.

- CheckRecipeNoComp/CheckPageNoComp를 수렴 루프로 재작성 (에코 후 되읽어 일치 확인, 어긋나면 재읽기, 맞는 즉시 다음 노드로)
- 에코 없이 return 하던 경로 제거 (RecipeNo=0/미등록 → 에코 안 하고 재시도)
- 범위 밖 PageNo: 3초 무한대기 → 200ms 재읽기, 에코 안 함
- Page 3회 수집(600ms)·Recipe 800ms 재폴링 제거 → 정상 흐름 지연 0
- while 탈출 후 context.Stopped 가드 추가
- ReadOutWord(eWordOutput) 헬퍼 추가 (ReadOutBit 패턴 준용)
- 시뮬레이션 가드: IsSimul이면 수렴 생략 (PLC 워드가 항상 0이라 못 빠져나감)

구버전은 #region 260806 으로 전량 주석 보존 — 원복 시 블록 교체.
분석: docs/분석/2026/08/06/
2026-08-06 11:41:58 +09:00

9.5 KiB
Raw Blame History

Recipe/Page 에코 수렴 루프 — 설계안

  • 작성일: 2026-08-06
  • 분류: 설계 제안 — 소스 미수정 · 미승인
  • 상위 문서: 에코 보장 방침 분석
  • 전제(2026-08-06 확인): PLC는 PC 에코 넘버가 자기 값과 같은지를 상시 확인한다. 30초는 확인 간격이 아니라 한 번의 확인 창(window) 이며, 그 안에서 계속 체크하고 안 맞으면 대기했다가 다시 30초 창을 반복한다. → 지연 페널티 없음

0. PLC가 상시 확인한다는 것이 바꾸는 것

앞 문서는 "에코가 안 나가면 PLC가 무한 대기"를 전제로 썼습니다.

항목 종전 판단 정정
에코 누락 영구 교착 자동 복구 — PC가 맞추는 즉시 진행 (30초를 기다리지 않음)
범위 밖 PageNo 읽은 값 그대로 에코하고 진행 에코하지 않고 재시도가 맞다 — 아래 참조

정정 근거: 범위 밖 값을 그대로 에코하면 PLC는 일치로 보고 넘어가고, PC는 폴백 레이어(SeqAutoRunPTOP.cs:626-628)로 용접합니다. 에코를 안 하면 PLC가 30초마다 재확인하며 기다릴 뿐이므로, 안 넘기는 쪽이 안전하고 복구도 자동입니다.

즉 PC가 할 일은 "제대로 읽고 → 내부 상태 갱신 → 에코" 뿐입니다. 값이 안 맞을 때만 다시 읽어 고쳐 쓰고, 맞는 즉시 다음 노드로 넘어갑니다.

루프는 짧아야 합니다. 정상 흐름에서는 1회 순회로 끝납니다. 이 노드에서 오래 머무는 것이 목적이 아닙니다 — 대기는 다음 노드가 합니다 (WeldingStartReadBit:705, PLC_WELD_TRIGGER_REQ ON 대기). 재읽기 루프는 어긋났을 때만 도는 보정 경로입니다.


1. 수렴 조건 (노드를 나갈 때 반드시 참)

노드 나갈 때 보장해야 할 것
CheckRecipeNoComp PC_RECIPE_NO == PLC_RECIPE_NO 그리고 활성 레시피 Id == 그 번호
CheckPageNoComp PC_PAGE_NO == PLC_PAGE_NO 그리고 _CurrentPageNo == PC_PAGE_NO - 1

★ 가장 중요한 함정 — 워드만 맞추면 안 된다

재읽기 → 내부 상태 갱신 → 에코를 항상 한 세트로 처리해야 합니다.

워드만 수렴시키고 _CurrentPageNo(:515)나 활성 레시피를 안 고치면, PLC는 일치로 보고 통과시키는데 PC는 옛 페이지 레이어로 용접합니다. 이 경우는 PLC 게이트로도 못 막습니다 — 에코가 일치하므로 PLC_WELD_TRIGGER_REQ가 정상적으로 나옵니다. 지금까지 "PLC가 검증해주니 안전하다"고 본 보호막이 여기서만 뚫립니다.


2. CheckRecipeNoComp 수렴형 (의사코드)

while (!context.Stopped)
{
    (err, plcRecipeNo) = ReadWord(PLC_RECIPE_NO)
    if (err != OK)              { 로그 + Sleep(200); continue; }      // 재시도, 중단 아님
    if (plcRecipeNo == 0)       { 정책 필요 → §4-①; Sleep(200); continue; }

    recipe = Recipes().FirstOrDefault(Id == plcRecipeNo)
    if (recipe == null)         { 토스트(N회마다) + Sleep; continue; } // 에코 안 함

    if (ActivatedRecipe()?.Id != recipe.Id)
    {
        ActivateRecipe(recipe.Id, "PLC Recipe Change")
        활성 반영 대기 (타임아웃 시 알람 후 루프 상단으로)             // 현재 :413-417은 리셋만 → P8
        Dispatcher.Invoke(RecipeHelper.OnActiveReloaded)              // Layers 동기 갱신 (현행 :425 유지)
    }

    werr = WriteWord(PC_RECIPE_NO, plcRecipeNo)
    if (werr != OK)             { 로그 + Sleep(200); continue; }

    echoed = base.ReadWord((int)PC_RECIPE_NO, D, isInput: false)      // 내가 쓴 값이 실제 반영됐나
    cur    = ReadWord(PLC_RECIPE_NO)                                  // 그 사이 PLC가 바꿨나
    if (echoed == plcRecipeNo && cur.data == plcRecipeNo) break;      // 수렴 완료

    Sleep(200)
}
if (context.Stopped) return null;

3. CheckPageNoComp 수렴형 (의사코드)

while (!context.Stopped)
{
    (err, p) = ReadWord(PLC_PAGE_NO)
    if (err != OK)                  { 로그 + Sleep(200); continue; }

    pageCount = Dispatcher.Invoke(() => Model.Layers.Count())         // 현행 :533 유지
    if (p < 1 || p > pageCount)     { 토스트(N회마다) + Sleep; continue; }  // 에코 안 함 → §0

    _CurrentPageNo = p - 1                                            // ★ 에코와 같은 세트
    werr = WriteWord(PC_PAGE_NO, p)
    if (werr != OK)                 { 로그 + Sleep(200); continue; }

    echoed = base.ReadWord((int)PC_PAGE_NO, D, isInput: false)
    cur    = ReadWord(PLC_PAGE_NO)
    if (echoed == p && cur.data == p) { plcPageNo = p; break; }       // 수렴 완료

    Sleep(200)
}
if (context.Stopped) return null;      // ← 현재 없는 가드 (P7)

CellId/LotId 읽기 → Status 반영                                        // 수렴 후 1회로 이동
EventPublisher.Publish(EVENT_AUTO_RUN_UPDATE_PAGENUM, _CurrentPageNo)

순서 제약: Recipe 수렴 → Page 수렴. Page 유효범위가 활성 Recipe의 Layers.Count()에 의존하기 때문입니다(:533). 현재 Flows 순서(:188-189)가 이미 이 순서라 변경 없습니다.


4. 결정이 필요한 항목

① PLC_RECIPE_NO == 0을 무엇으로 볼 것인가

안 해석 동작
A (현행 :353) 전환 의사 없음 현 활성 레시피 번호를 에코하고 통과
B (권장) 이상값 에코하지 않고 재시도

08/04에 **"PLC는 워드를 항상 써놓는다"**가 확인됐으므로, 0이 읽히면 정상 상태가 아닙니다 (통신 실패 시 ReadWord가 (err, 0)을 반환 — PLCBase.APIs.cs:105, :109). B를 권장하되 PLC 담당 확인이 필요합니다.

② 재시도 주기

PLC가 상시 확인하므로 PC가 맞추는 즉시 진행됩니다. 주기를 PLC에 맞출 필요가 없습니다. 정상 흐름은 1회 순회로 끝나므로 주기는 보정 경로에서만 의미가 있습니다 — 200ms 면 충분합니다. 알림은 폭주 방지를 위해 현행 방식(첫 회 + N회마다 — :497, :549)을 그대로 씁니다.

현행 Sleep(200) × 3회(:519)와 Sleep(3000)(:554)은 정상 흐름에 붙은 고정 지연입니다. 수렴형으로 바꾸면 정상 흐름의 지연이 0이 되어 사이클이 빨라집니다.

③ 수렴 실패가 오래 지속될 때

중단하지 않습니다. context.Stopped가 유일한 탈출구입니다. 다만 N회 이상 지속되면 토스트를 알람으로 승격할지 결정이 필요합니다. 그래야 작업자가 "왜 안 넘어가는지"를 화면에서 알 수 있습니다. (승격 기준 회수는 미정 — 확인 필요. PLC 30초 창과 맞출 이유는 없습니다.)


5. 되읽기에 쓸 API (실측 확인)

목적 호출 근거
PLC가 쓴 값 _PLC.ReadWord(eWordInput.PLC_PAGE_NO) PLCController.Logic.cs:16
내가 쓴 값(출력 워드) base.ReadWord((int)addr, ePLCDevice.D, isInput: false) PLCBase.APIs.cs:99(public) · 동일 패턴 사용례 PLCController.Logic.cs:28
쓰기 성공 여부 WriteWord(...) 의 int 반환값 PLCController.Logic.cs:17 — 현재 시퀀스는 버리고 있음

출력 워드 전용 헬퍼는 없습니다(ReadOutBit은 비트만 — Logic.cs:14). base.ReadWord 3인자 오버로드를 직접 부르거나, 대칭을 맞추려면 ReadOutWord(eWordOutput) 헬퍼를 PLCController.Logic.cs에 추가하는 편이 낫습니다. (제안)


6. 이 설계가 대체하는 기존 조치

기존 조치 상태 사유
P4 — Page 3회 수집 제거 수렴 루프가 대체. 값 안정성은 되읽기 일치로 확인
P5 — Recipe 800ms 재폴링 제거 수렴 루프가 대체
P2 — 에코 무조건화 일부 철회 미등록·범위 밖은 에코하지 않는 게 맞다(§0). "모든 경로 에코"가 아니라 "수렴할 때까지 재시도" 가 정답
P3 — 범위 밖 대기 제거 정정 대기 자체는 유지. 다만 3초 → 200ms, 토스트 유지, 탈출은 수렴
P1 — 상승엣지 전환 우선순위 하향 수렴 루프가 스테일 오독을 자동 복구한다. 여전히 유익하지만 필수는 아님
P6 — err 재읽기 유지·강화 수렴 루프의 재시도 조건으로 자연스럽게 편입
P7 — Stopped 가드 유지 §3 참조
P8 — 활성 대기 타임아웃 탈출 유지 수렴 루프 상단으로 돌아가야 재시도가 성립

7. 남는 리스크

  • §1의 함정 — 워드와 내부 상태를 따로 고치면 PLC 게이트가 무력화됩니다. 구현 시 리뷰 필수.
  • ResumeMode(bit15) 예외 — SeqStatusMonitor.cs:104, OnSeqActionStop:1227이 M70515 ON 구간에서 에코 워드를 보존합니다. 수렴 루프가 이 구간에 진입하면 보존값과 충돌할 수 있습니다. 가접 시나리오와 함께 검토 필요.
  • 에코가 맞은 뒤 PLC가 워드를 바꾸는 경우 — 수렴 루프는 노드 안에서 끝나므로 노드를 나간 뒤의 변경은 감지하지 않습니다. 이건 의도된 설계입니다 — PC는 맞춰 쓰고 넘어가고, 이후 대기는 WeldingStartReadBit가 담당합니다. [추론] PLC가 용접 완료 전에 워드를 바꾸지는 않을 것으로 보이나 미확인.