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/
9.5 KiB
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_REQON 대기). 재읽기 루프는 어긋났을 때만 도는 보정 경로입니다.
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.ReadWord3인자 오버로드를 직접 부르거나, 대칭을 맞추려면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가 용접 완료 전에 워드를 바꾸지는 않을 것으로 보이나 미확인.