# Recipe/Page 에코 수렴 루프 — 설계안 - 작성일: 2026-08-06 - 분류: **설계 제안 — 소스 미수정 · 미승인** - 상위 문서: [에코 보장 방침 분석](./RecipePage_에코보장_방침_분석.md) - 전제(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` 수렴형 (의사코드) ```text 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` 수렴형 (의사코드) ```text 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가 용접 완료 전에 워드를 바꾸지는 않을 것으로 보이나 **미확인**.