From c7f12b0bda30c4f11d2fdfc5ae2b4b600f81b9e4 Mon Sep 17 00:00:00 2001 From: NohDoyeon241104 Date: Thu, 6 Aug 2026 11:41:58 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Recipe/Page=20=ED=95=B8=EB=93=9C?= =?UTF-8?q?=EC=85=B0=EC=9D=B4=ED=81=AC=20=EC=97=90=EC=BD=94=20=EC=88=98?= =?UTF-8?q?=EB=A0=B4=20=EB=A3=A8=ED=94=84=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/ --- .../Device/PLC/PLCController.Logic.cs | 7 + .../Sequences/SeqAutoRunPTOP.cs | 301 +++++++++++++++++- .../2026/08/06/RecipePage_에코보장_방침_분석.md | 183 +++++++++++ .../2026/08/06/RecipePage_에코수렴루프_설계.md | 187 +++++++++++ 4 files changed, 664 insertions(+), 14 deletions(-) create mode 100644 docs/분석/2026/08/06/RecipePage_에코보장_방침_분석.md create mode 100644 docs/분석/2026/08/06/RecipePage_에코수렴루프_설계.md diff --git a/Machines/ILSOS_PrismaticRD_PTOP/Device/PLC/PLCController.Logic.cs b/Machines/ILSOS_PrismaticRD_PTOP/Device/PLC/PLCController.Logic.cs index 775ad8d..fa84346 100644 --- a/Machines/ILSOS_PrismaticRD_PTOP/Device/PLC/PLCController.Logic.cs +++ b/Machines/ILSOS_PrismaticRD_PTOP/Device/PLC/PLCController.Logic.cs @@ -16,6 +16,13 @@ namespace ILSOS_PrismaticRD_PTOP.Device public (int err, short data) ReadWord(eWordInput address) => base.ReadWord((ushort)address, ePLCDevice.D); public int WriteWord(eWordOutput addrOffset, short data, ePLCDevice device = ePLCDevice.D, bool isOutput = true) => base.WriteWord((int)addrOffset, data, ePLCDevice.D, isOutput); + #region 260806 신규 — 출력 워드 되읽기 (원복: 이 region 통째로 삭제) + // 비트에는 ReadOutBit(위)이 있는데 워드에는 대응 헬퍼가 없어 base.ReadWord(..., isInput:false)를 + // 직접 불러야 했다(WriteWordToBit 참고). 에코 워드(PC_PAGE_NO/PC_RECIPE_NO)가 실제로 + // 반영됐는지 확인하는 용도. + public (int err, short data) ReadOutWord(eWordOutput address) => base.ReadWord((ushort)address, ePLCDevice.D, isInput: false); + #endregion + public (int err, int data) ReadDWord(eWordInput address) => base.ReadDWord((ushort)address, ePLCDevice.D); public int WriteDWord(eWordOutput addrOffset, int data, ePLCDevice device = ePLCDevice.D, bool isOutput = true) => base.WriteDWord((int)addrOffset, data, ePLCDevice.D, isOutput); public (int err, short[] datas) ReadWords(eWordInput startAddress, eWordInput endAddress) => base.ReadWordBlock((int)startAddress, endAddress - startAddress + 1, ePLCDevice.D); diff --git a/Machines/ILSOS_PrismaticRD_PTOP/Sequences/SeqAutoRunPTOP.cs b/Machines/ILSOS_PrismaticRD_PTOP/Sequences/SeqAutoRunPTOP.cs index 963f612..ed1d6a3 100644 --- a/Machines/ILSOS_PrismaticRD_PTOP/Sequences/SeqAutoRunPTOP.cs +++ b/Machines/ILSOS_PrismaticRD_PTOP/Sequences/SeqAutoRunPTOP.cs @@ -7,6 +7,8 @@ using AuroraLaserScanner; using AuroraPLC; using CoreUnit.Control.UserControl.Canvas; using CoreUnit.Scanner.UserControl.ScannerCanvas; +// 260806: PLC 반환 err 판정은 Hermes.E1.OK 를 완전 한정명으로 쓴다. +// using Hermes; 를 새로 넣으면 이 파일이 이미 여는 다수 네임스페이스와 이름이 겹칠 수 있어 피했다. using ILSOS_PrismaticRD_PTOP.Device; using System; using System.Collections.Generic; @@ -122,6 +124,11 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences // const int PLC_TIMEOUT = 60; // sec, Timeout은 시퀀스 정지가 아니라 타임아웃 시간마다 로그만 남기도록 한다 + #region 260806 신규 — 에코 수렴 루프 상수 (원복: 이 region 통째로 삭제) + const int ECHO_RETRY_MS = 200; // 수렴 재시도 간격. 정상 흐름은 1회 순회로 끝나므로 보정 경로에서만 쓰인다 + const int ECHO_TOAST_EVERY = 10; // 토스트 폭주 방지 — 첫 회차 + 이후 N회마다만 표시 + #endregion + private eListNo _WeldingScannerListNo; private List _SelectedObjectList; @@ -133,7 +140,10 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences private short plcPageNo = 0;// [0] 페이지 1 =음극 A [1]페이지 2= 양극 P(C) - private List plcPageNoList = new List(); + #region 260806 구버전 보존 — Page 3회 수집용 필드 (원복: 아래 주석을 푼다) + // 수렴 루프로 대체돼 참조가 없어졌다. 삭제하지 않고 남긴다. + //private List plcPageNoList = new List(); + #endregion private short plcRecipeNo = 0; // 260618 PLC Recipe No (11/21/22/31/32) @@ -340,6 +350,8 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences private int _RingBeamPower; private int _CenterRingRatio; + #region 260806 구버전 보존 — CheckRecipeNoComp (원복: 아래 블록 주석을 풀고 뒤의 260806 신규 region을 제거) + /* // CheckRecipeNoComp: 단일 트리거(PAGE_CHANGE_REQ 0x08) 통합 — Page보다 먼저 Recipe를 확정/전환. // PLC_RECIPE_NO 1회 읽기 → 0/미등록/현재활성과 동일이면 전환·워드 재기록 생략(시퀀스 정지 없음). // 실제로 다를 때만 ActivateRecipe + UI 스레드 동기 reload(Model.Layers 갱신 보장) + PC_RECIPE_NO 에코. @@ -431,6 +443,131 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences return SequenceAction.Ok(); } + */ + #endregion + + #region 260806 신규 — CheckRecipeNoComp 에코 수렴형 (원복: 이 region 제거 후 위 구버전 주석 해제) + // 방침(2026-08-06 PLC 확정): PLC는 PC가 에코한 PC_RECIPE_NO 가 자기 값과 같아지는지를 상시 확인한다. + // 따라서 PC의 책임은 "정확히 읽고 → 레시피를 실제로 활성화하고 → 에코" 뿐이다. + // 어긋났을 때만 다시 읽어 고쳐 쓰고, 맞는 즉시 다음 노드로 넘어간다(대기는 다음 노드가 한다). + // + // 노드를 나갈 때 보장: PC_RECIPE_NO == PLC_RECIPE_NO 그리고 활성 레시피 Id == 그 번호 + // ★ 워드만 맞추면 안 된다. 활성 레시피를 같이 갱신하지 않으면 PLC는 일치로 보고 통과시키는데 + // PC는 옛 레시피로 용접한다. 이 경우는 PLC 게이트로도 막히지 않는다(에코가 맞으니 트리거가 나온다). + private SequenceActionResult CheckRecipeNoComp(SequenceContext context, string id, SequenceActionResult param) + { + Log.I("[CheckRecipeNoComp] PLC_RECIPE_NO 수렴 시작"); + + // 시뮬레이션: PLC 워드가 항상 0이라 아래 수렴 루프를 빠져나갈 수 없다. + // → 종전 동작(현 활성 레시피 유지 + 에코)으로 통과시킨다. + if (_PLC.IsSimul) + { + var simRecipe = _RecipeManager.ActivatedRecipe(); + plcRecipeNo = (short)(simRecipe == null ? 0 : simRecipe.Id); + _PLC.WriteWord(eWordOutput.PC_RECIPE_NO, plcRecipeNo); + Log.I($"[CheckRecipeNoComp] 시뮬레이션 → 수렴 생략, 현 활성 레시피({plcRecipeNo}) 에코 → CheckPageNoComp 이동"); + return SequenceAction.Ok(); + } + + int retry = 0; + + while (!context.Stopped) + { + if (retry > 0) Thread.Sleep(ECHO_RETRY_MS); + retry++; + + // ── 1. PLC가 준 Recipe No 읽기 ── + var read = _PLC.ReadWord(eWordInput.PLC_RECIPE_NO); + if (read.err != Hermes.E1.OK) + { + Log.E($"[CheckRecipeNoComp] PLC_RECIPE_NO 읽기 실패 (err:{read.err}) — 재시도 {retry}회차"); + continue; + } + + short readRecipeNo = read.data; + + // 0은 정상 상태가 아니다. PLC는 워드를 항상 써놓는다(260804 확인). + // 미연결·미활성이면 ReadWord가 (NOT_READY/NOT_CONNECTED, 0)을 돌려주므로 여기서도 걸러진다. + if (readRecipeNo == 0) + { + if (retry == 1 || retry % ECHO_TOAST_EVERY == 0) + MessageUtil.ShowErrorToast("PLC Recipe No = 0\n모니터링 확인해주세요."); + Log.E($"[CheckRecipeNoComp] PLC_RECIPE_NO=0 (이상값) — 에코하지 않고 재시도 {retry}회차"); + continue; + } + + // 등록 레시피 조회(.Id 1:1, 하드코딩 금지). 미등록이면 에코하지 않는다 — + // PLC가 넘어가지 않으므로 잘못된 레시피로 용접이 나갈 수 없다. + var recipe = _RecipeManager.Recipes().FirstOrDefault(x => x.Id == readRecipeNo); + if (recipe == null) + { + if (retry == 1 || retry % ECHO_TOAST_EVERY == 0) + MessageUtil.ShowErrorToast($"미등록 레시피: {readRecipeNo}\n레시피 확인해주세요."); + Log.E($"[CheckRecipeNoComp] {readRecipeNo}번 레시피 미등록 — 에코하지 않고 재시도 {retry}회차"); + continue; + } + + // ── 2. 내부 상태 갱신: 실제 레시피 활성화 (에코와 같은 세트) ── + var preActivated = _RecipeManager.ActivatedRecipe(); + if (preActivated == null || preActivated.Id != recipe.Id) + { + _RecipeManager.ActivateRecipe(recipe.Id, "PLC Recipe Change"); + Log.I($"[CheckRecipeNoComp] ActivateRecipe ({(preActivated == null ? -1 : preActivated.Id)} -> {recipe.Id})"); + + if (!WaitRecipeActivated(context, recipe.Id)) + { + if (context.Stopped) return null; + Log.W($"[CheckRecipeNoComp] ActivateRecipe({recipe.Id}) 반영 대기 TIMEOUT — 에코하지 않고 재시도 {retry}회차"); + continue; + } + + // ★ Model.Layers를 새 레시피로 동기 갱신 (UI 스레드). + // ActivateRecipe의 이벤트 체인(ObserveOnDispatcher 2단)은 비동기라 + // Page 검증 전 Layers 갱신을 보장 못 한다. + Application.Current.Dispatcher.Invoke(() => RecipeHelper.OnActiveReloaded()); + Log.I("[CheckRecipeNoComp] Model.Layers 동기 reload 완료 (UI thread)"); + } + + // ── 3. 에코 ── + int werr = _PLC.WriteWord(eWordOutput.PC_RECIPE_NO, readRecipeNo); + if (werr != Hermes.E1.OK) + { + Log.E($"[CheckRecipeNoComp] PC_RECIPE_NO 쓰기 실패 (err:{werr}) — 재시도 {retry}회차"); + continue; + } + + // ── 4. 되읽어 수렴 확인 (내가 쓴 값이 반영됐는가 + PLC가 여전히 같은 값인가) ── + short echoed = _PLC.ReadOutWord(eWordOutput.PC_RECIPE_NO).data; + short current = _PLC.ReadWord(eWordInput.PLC_RECIPE_NO).data; + if (echoed == readRecipeNo && current == readRecipeNo) + { + plcRecipeNo = readRecipeNo; + Log.I($"---> [CheckRecipeNoComp] 수렴 완료 ({retry}회차) - PC_RECIPE_NO: [{plcRecipeNo}] → CheckPageNoComp 이동"); + break; + } + + Log.W($"[CheckRecipeNoComp] 미수렴 - 읽음:[{readRecipeNo}] 에코:[{echoed}] 현재PLC:[{current}] — 재시도 {retry}회차"); + } + + if (context.Stopped) return null; + return SequenceAction.Ok(); + } + + // ActivatedRecipe().Id 가 targetId 로 바뀔 때까지 대기. 타임아웃이면 false를 돌려주고 호출부가 재시도한다. + // (구버전 :408-419는 타임아웃 시 시작시각만 리셋해 빠져나가지 못했다) + private bool WaitRecipeActivated(SequenceContext context, int targetId) + { + var start = DateTime.Now; + while (!context.Stopped) + { + var cur = _RecipeManager.ActivatedRecipe(); + if (cur != null && cur.Id == targetId) return true; + if (DateTime.Now.Subtract(start).TotalSeconds > PLC_TIMEOUT) return false; + Thread.Sleep(10); + } + return false; + } + #endregion // 페이지 체크 - 페이지만 감시 AutoRunStatus는 실제 상위에서 동작 체크하고 시퀸스 동작한다. private SequenceActionResult CheckPageNo(SequenceContext context, string id, SequenceActionResult param) @@ -487,12 +624,16 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences } + #region 260806 구버전 보존 — CheckPageNoComp (원복: 아래 블록 주석을 풀고 뒤의 260806 신규 region을 제거) + /* private SequenceActionResult CheckPageNoComp(SequenceContext context, string id, SequenceActionResult param) { Log.I($"[CheckPageNoComp] PLC 페이지 번호 읽기 시작 (3회 연속 검증)"); Status.SetWorkingPosition(eWorkingPosition.LOADING_CELL); int checknum = 0; + int invalidPageWait = 0; // 260805: 범위 밖 PageNo로 대기한 회차 (중단하지 않고 대기) + const int TOAST_EVERY = 10; // 토스트 폭주 방지 — 첫 회차 + 이후 N회마다만 표시 plcPageNoList = new List(); @@ -531,18 +672,26 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences int recipePageCount = Application.Current.Dispatcher.Invoke(() => _ScannerCanvasUserCtrl.Model.Layers.Count()); if (plcPageNo > recipePageCount || plcPageNo < 1) { - Log.E($"[CheckPageNoComp] Invalid PageNo received: {plcPageNo} (활성 Recipe 페이지 수: {recipePageCount})"); + // 260805 가드: 종전에는 여기서 return null 이었다. Aurora에서 Action이 null을 돌려주면 + // 시퀀스가 중단되므로(sequence-builder 규칙: "null 중단"), 일시적인 오독 한 번에 + // AutoRun이 죽고 작업자가 재시작해야 했다. + // → 중단하지 않고 이 노드에서 대기하며 재수집한다. 작업자가 토스트를 보고 + // 모니터링에서 값을 확인·조치하면 다음 회차에 정상값을 읽어 그대로 이어간다. + // ※ recipePageCount는 매 회차 활성 Recipe의 Layers 수로 다시 구한다. + // Recipe마다 페이지 수가 다르고, 레시피 반영이 늦으면 이 값도 늦게 맞으므로 + // 재시도 자체가 그 지연을 흡수한다. + // ※ 빠져나가는 경로는 while 조건의 context.Stopped — Stop 버튼은 정상 동작한다. + invalidPageWait++; + Log.E($"[CheckPageNoComp] Invalid PageNo received: {plcPageNo} " + + $"(활성 Recipe 페이지 수: {recipePageCount}) — 대기 {invalidPageWait}회차, 시퀀스 중단하지 않음"); - // 디버그 끝내고 삭제 - //MessageUtil.ShowErrorToast($"ReadWord -> {plcPageNo}"); - MessageUtil.ShowErrorToast($"ReadWord -> {plcPageNo}"); + if (invalidPageWait == 1 || invalidPageWait % TOAST_EVERY == 0) + MessageUtil.ShowErrorToast($"PLC Page No 이상: {plcPageNo}\n모니터링 확인해주세요."); + + plcPageNoList.Clear(); + checknum = 0; Thread.Sleep(3000); - - // 나중에 알람 처리해. - // 에러 발생 시 시퀀스 중단 또는 기본값 처리 (현장 상황에 따라 선택) - //AlarmManager.Alarm(AlarmCode.ALARM_SEQUENCE_TeachingRun_InitVariableError); - - return null; + continue; } else { @@ -580,12 +729,106 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences Application.Current.AuroraEnv().EventPublisher.Publish(new EventMessage("", CommonEventIds.EVENT_AUTO_RUN_UPDATE_PAGENUM, (int)_CurrentPageNo)); } - Thread.Sleep(5); // 추후 제거 + Thread.Sleep(5); // 추후 제거 Log.I($"[CheckPageNoComp] 페이지 확정 - PC_PAGE_NO: [{plcPageNo}] 에코 완료 → WeldingList 업로드 이동"); return SequenceAction.Ok(); } + */ + #endregion + + #region 260806 신규 — CheckPageNoComp 에코 수렴형 (원복: 이 region 제거 후 위 구버전 주석 해제) + // 노드를 나갈 때 보장: PC_PAGE_NO == PLC_PAGE_NO 그리고 _CurrentPageNo == PC_PAGE_NO - 1 + // ★ 워드만 맞추고 _CurrentPageNo를 갱신하지 않으면 PLC는 일치로 보고 통과시키는데 + // PC는 옛 페이지 레이어로 용접한다. 반드시 "재읽기 → _CurrentPageNo 갱신 → 에코"를 한 세트로 처리한다. + // + // 구버전과의 차이: + // - 3회 수집(Sleep 200×3) 제거 → 정상 흐름 지연 0. 값 안정성은 되읽기 일치로 확인한다. + // (구버전 로그는 "3회 연속 검증"이라 했으나 3개 값을 비교하는 코드가 없었다) + // - 범위 밖 PageNo: 종전 Sleep(3000) 무한 대기 → 200ms 재읽기. 에코하지 않으므로 + // PLC가 넘어가지 않아 잘못된 페이지로 용접이 나갈 수 없다. + // - 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; + + while (!context.Stopped) + { + if (retry > 0) Thread.Sleep(ECHO_RETRY_MS); + retry++; + + // ── 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. 되읽어 수렴 확인 ── + 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}회차"); + } + + if (context.Stopped) return null; + + // ── 수렴 후 1회만 수행 ── + string cellId = _PLC.CellId(); + string lotId = _PLC.LotId(); + Status.GetProcessingWeldingData().CellId = cellId; + Status.GetProcessingWeldingData().LotId = lotId; + Log.I($"[CheckPageNoComp] CellId: [{cellId}], LotId: [{lotId}]"); + + // PLC는 이 비트를 보지 않는다(260806 확인 — 게이트는 워드다). 기존 배선 호환을 위해 ON은 유지한다. + _PLC.WriteBit(eBitOutput.PC_PAGE_CHANGE_COMPLETE, true); + + // UI 페이지 업데이트 알림 + Application.Current.AuroraEnv().EventPublisher.Publish( + new EventMessage("", CommonEventIds.EVENT_AUTO_RUN_UPDATE_PAGENUM, (int)_CurrentPageNo)); + + Log.I($"[CheckPageNoComp] 페이지 확정 - PC_PAGE_NO: [{plcPageNo}] 에코 완료 → WeldingList 업로드 이동"); + + return SequenceAction.Ok(); + } + #endregion + private bool LaserReadyResponse() { Log.I($"[LaserReadyResponse] 레이저 Ready 응답 (현재 no-op)"); @@ -1208,8 +1451,38 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences // 260512 워드 // 260621: 에코 워드(PC_PAGE_NO/PC_RECIPE_NO)는 정지 시 0으로 지우지 않음 — PLC가 정합 확인에 쓰는 - // 마지막 핸드셰이크 값을 보존(다음 사이클에 어차피 재에코). 기존 0 초기화 2줄 제거. - Log.I("[OnSeqActionStop] PLC IO Output Word Init"); + // 마지막 핸드셰이크 값을 보존(다음 사이클에 어차피 재에코). + #region 260805 정지 모드만 로그로 남긴다 (워드 0 초기화는 하지 않음) + // 0 초기화를 넣었다가 롤백했다. 지울 필요가 없는 이유 — + // ① 재진입 시 CheckRecipeNoComp / CheckPageNoComp 가 어차피 워드를 재에코한다. + // ② PLC가 보는 핸드셰이크 게이트는 워드가 아니라 PC_PAGE_CHANGE_COMPLETE 비트이고, + // 그 비트는 바로 위에서 이미 false로 내렸다 → 잔재 워드를 유효값으로 읽을 경로가 없다. + // ③ bit15 Resume(Manual 수동 출사 후 이어하기)은 직전 Recipe/Page 에코가 남아 있어야 한다. + // 아래 분기는 진단용이다. 워드를 쓰지 않으므로 PLC 동작에는 영향이 없다. + // 0 초기화가 필요하다고 판단되면 PLC 담당 확인 후 !ResumeMode 쪽에만 WriteWord를 넣는다. + // 로그 값은 캐시된 마지막 핸드셰이크 값이다 — 정지(특히 ForceError) 시점에 PLC를 다시 읽지 않는다. + #region 260806 정정 — 위 ②는 사실과 반대다 (원복: 이 region 통째로 삭제. 위 원문은 그대로 보존돼 있다) + // 2026-08-06 PLC측 시퀀스 확정 결과: + // · PLC가 보는 핸드셰이크 게이트는 **워드**(PC_PAGE_NO / PC_RECIPE_NO)다. + // PC 에코 넘버가 자기 값과 같아지는지를 상시 확인하고, 같으면 다음 스텝으로 진행한다. + // · PC_PAGE_CHANGE_COMPLETE(M70008)는 PLC가 **보지 않는다**. 따라서 위 ②의 + // "비트를 내렸으니 잔재 워드를 읽을 경로가 없다"는 논리는 성립하지 않는다. + // 그럼에도 동작은 바꾸지 않는다 — 워드 0 초기화는 SeqStatusMonitor.UpdatePLC()가 + // Auto OFF 시 이미 담당하고 있고(M70515 ON 구간은 예외로 보존), 여기서 중복으로 쓰면 + // bit15 Resume이 의존하는 보존값을 깨뜨린다(위 ③). + // 분석: docs/분석/2026/08/06/RecipePage_에코보장_방침_분석.md + #endregion + if (!ResumeMode) + { + Log.I($"[OnSeqActionStop] 일반 정지 (ResumeMode=false) → 에코 워드 보존, 0 초기화 안 함 " + + $"(마지막 핸드셰이크 PageNo:{plcPageNo}, RecipeNo:{plcRecipeNo})"); + } + else + { + Log.I($"[OnSeqActionStop] Resume 정지 (ResumeMode=true, bit15 Manual 전환) → 에코 워드 반드시 보존 " + + $"(마지막 핸드셰이크 PageNo:{plcPageNo}, RecipeNo:{plcRecipeNo})"); + } + #endregion diff --git a/docs/분석/2026/08/06/RecipePage_에코보장_방침_분석.md b/docs/분석/2026/08/06/RecipePage_에코보장_방침_분석.md new file mode 100644 index 0000000..eff3947 --- /dev/null +++ b/docs/분석/2026/08/06/RecipePage_에코보장_방침_분석.md @@ -0,0 +1,183 @@ +# Recipe/Page 핸드셰이크 — "에코 보장" 방침 전환 분석 + +- 작성일: 2026-08-06 +- 대상 브랜치: `feature/ubisam-pptx-integration` +- 분류: **분석 전용 — 소스 미수정** +- 발단: PLC측 시퀀스 확정 완료. **PLC는 Page/Recipe 넘버 일치만 보고 넘어간다**(2026-08-06 확인) +- 관련: [AutoRun 재기동 워드 미반영 원인분석](../04/AutoRun재기동_Recipe_Page워드_미반영_원인분석.md) +- **후속: [에코 수렴 루프 설계안](./RecipePage_에코수렴루프_설계.md)** — 2026-08-06 추가 확인 + (**PLC는 일치를 상시 확인한다**. 30초는 확인 간격이 아니라 한 번의 확인 창이며 반복된다)으로 + §5 조치표 일부가 정정됐습니다. **설계안을 우선 참조하세요.** + +--- + +## 0. 결론 + +**PC측 책임이 "검증"에서 "에코 보장"으로 바뀝니다.** + +PLC가 넘버 일치를 판정하므로, PC가 자체 검증으로 **대기·중단하는 코드는 전부 교착 요인**입니다. +PC가 멈추면 에코가 안 나가고, 에코가 안 나가면 PLC도 멈춰 **양쪽이 서로 기다립니다.** + +현재 코드에는 **에코를 하지 않고 빠져나가거나 무한 대기하는 경로가 4건** 있습니다(§2). +전부 "PC가 신중하게 확인하려고" 넣은 것인데, 새 방침에서는 정확히 반대로 작동합니다. + +> **단, "잘 써주기"의 전제는 "제대로 읽기"입니다.** 틀린 값을 에코하면 PLC가 불일치로 안 넘어가 +> 결국 멈춥니다. 따라서 [08/04 §4](../04/AutoRun재기동_Recipe_Page워드_미반영_원인분석.md)의 +> **상승엣지 전환(조치 A)은 이 방침에서도 그대로 유효**하며, 오히려 전제 조건이 됩니다. → §4 + +--- + +## 1. 확정된 프로토콜 (2026-08-06) + +| 주체 | 동작 | +|---|---| +| PLC | Recipe·Page 워드를 **동시에** 쓴다 → `M70508`(`PLC_PAGE_CHANGE_REQ`, bit 0x08) ON | +| PLC | `PC_PAGE_NO`·`PC_RECIPE_NO`가 자기가 쓴 값과 **같아지면 다음 스텝으로 진행** | +| PC | bit8 감지 → Recipe 읽기·활성화 → Page 읽기 → 에코 | + +**게이트는 워드다.** `PC_PAGE_CHANGE_COMPLETE`(M70008, `eBitOutput` 0x08)는 PLC가 보지 않는다. + +### 코드 주석이 이와 반대로 적혀 있다 + +`SeqAutoRunPTOP.cs:1225` (`OnSeqActionStop`), 에코 워드 0 초기화를 **롤백한 근거**: + +``` +② PLC가 보는 핸드셰이크 게이트는 워드가 아니라 PC_PAGE_CHANGE_COMPLETE 비트이고, + 그 비트는 바로 위에서 이미 false로 내렸다 → 잔재 워드를 유효값으로 읽을 경로가 없다. +``` + +→ **이 전제는 틀렸습니다.** 롤백 판단의 근거가 무효이므로 재검토 대상입니다. +다만 워드 0 초기화는 `SeqStatusMonitor.cs:104-109`가 Auto OFF 시 이미 수행하므로 +**중복 구현 상태**입니다(`OnSeqActionStop`은 로그만, `:1231-1240`). + +### 안전 측면은 PLC가 커버한다 (검증됨) + +용접 실행은 `PLC_WELD_TRIGGER_REQ`(M70516) ON 대기가 **별도 게이트**입니다 +(`SeqAutoRunPTOP.cs:196`, `:705`). PC가 페이지를 오독하면 → 에코 불일치 → PLC가 트리거를 +주지 않음 → **용접이 나가지 않습니다.** 오독이 잘못된 용접으로 이어지는 경로는 없습니다. + +> 남는 비용은 **안전이 아니라 정지 시간**입니다. 그래서 이 문서의 초점은 "멈춤 제거"입니다. + +--- + +## 2. ★ 핑퐁을 끊는 지점 전수 (에코 누락·무한 대기) + +### ① `CheckRecipeNoComp` — 에코 없이 return 하는 경로 2건 + +> **정정(2026-08-06):** PLC는 일치를 **상시 확인**하므로 아래 "대기"는 영구 교착이 아닙니다. +> PC가 맞추는 **즉시** 진행됩니다. → [수렴 루프 설계안 §0](./RecipePage_에코수렴루프_설계.md) + +| 위치 | 조건 | `PC_RECIPE_NO` | PLC 결과 | +|---|---|---|---| +| `:353-357` | `plcRecipeNo == 0` | **미기록** (직전 값 잔존) | 불일치 → 재시도 대기 (PC는 복구 시도 안 함) | +| `:361-365` | 미등록 번호 (`Log.W` 한 줄) | **미기록** | 불일치 → 재시도 대기 (PC는 복구 시도 안 함) | +| `:395-398` | 이미 활성 + 4회 폴링 동안 불변 | `preActivated.Id` | PLC 요구값과 다르면 대기 | +| `:429` | 실제 전환 완료 | `plcRecipeNo` | 통과 | + +`ReadWord`는 `(int err, short data)`인데(`PLCController.Logic.cs:16`) `:349`가 `.data`만 씁니다. +PLC 미연결·미활성이면 **에러 없이 `0`을 반환**하므로(`PLCBase.APIs.cs:105`, `:109`) +통신 이상이 자동으로 `:353` 경로에 빠집니다. 화면 표시 없음, 로그 1줄뿐. + +### ② `CheckPageNoComp:534-556` — 범위 밖이면 3초 주기 무한 대기 + +260805에 `return null`(시퀀스 중단)에서 대기 루프로 바꾼 지점입니다. 중단은 아니지만 +**에코를 하지 않은 채 머무릅니다.** PLC도 대기 중이므로 **완전 교착**입니다. +탈출구는 `while (!context.Stopped)`(`:501`) 하나 — 작업자가 정지를 눌러야 합니다. + +### ③ `CheckRecipeNoComp:408-419` — ActivateRecipe 반영 대기가 사실상 무한 + +타임아웃 분기(`:413-417`)가 `Log.W` 후 **`actStart`를 리셋**합니다. 빠져나가지 않습니다. + +### ④ `CheckPageNoComp:501` while 탈출 후 `Stopped` 가드 없음 + +형제 노드는 둘 다 있습니다(`CheckPageNo:483`, `CheckRecipeNoComp:420`). +정지로 루프를 빠져나와도 `:573`이 실행돼 **미검증 값을 에코**합니다. +워드가 게이트이므로 이건 로그 오염이 아니라 **정지한 PC가 PLC에 GO를 쏘는 것**입니다. + +--- + +## 3. 방침에 비추어 과잉인 코드 + +| 위치 | 현재 | 새 방침에서의 평가 | +|---|---|---| +| `CheckPageNoComp:499-566` | Page 3회 수집 (`Sleep(200)` × 3) | **과잉.** 최소 600ms 지연만 발생. 값 검증은 PLC 몫 | +| `:492`, `:527` 로그 | "3회 연속 검증" | **거짓 로그.** 3개 값을 비교하는 코드가 없다 (`:561`은 출력뿐, 판정은 마지막 값 `plcPageNo` 하나) | +| `CheckRecipeNoComp:374-399` | 값 변화를 최대 4회(약 800ms) 기다림 | **과잉 + 유해.** 잘못된 트리거 시점의 우회책. [08/04 조치 B](../04/AutoRun재기동_Recipe_Page워드_미반영_원인분석.md)와 동일 판정 | +| `CheckPageNoComp:581-584` | 에코 후 재읽기·비교 → ACK 비트 | **무효.** PLC가 M70008을 안 본다. 불일치여도 `Ok()` 반환(`:597`)이라 PC 흐름에도 영향 없음. **PC 로그용으로만 의미** | + +> `:374-399`의 `count++ < 3`은 실제로 **4회 순회**합니다(count 0·1·2·3). 주석("3번 체크하자")과 불일치. + +--- + +## 4. "잘 써주기"의 전제 — 읽기 시점 + +새 방침은 PC측 검증을 덜어내지만, **읽는 값 자체가 틀리면 에코도 틀리고 결국 멈춥니다.** + +`CheckPageNo:457`은 `PLC_PAGE_CHANGE_REQ`를 **레벨(ON)로 판정**합니다. 상승엣지가 아닙니다. +PLC 프로토콜이 `워드 write → REQ raise` 순서이므로([08/04 §4](../04/AutoRun재기동_Recipe_Page워드_미반영_원인분석.md)), +**상승엣지 직후에 읽으면 워드는 항상 새 값**입니다. 반면 레벨 판정은 이미 ON인 잔재 신호에 +붙어 **갱신 전 값**을 읽습니다. + +| 트리거 판정 | 워드 정합성 | 에코 결과 | +|---|---|---| +| 레벨 (현재) | 보장 없음 | 옛 값 에코 → PLC 불일치 → **멈춤** | +| 상승엣지 | 보장됨 | 새 값 에코 → **핑퐁 성립** | + +→ **엣지 전환은 검증 강화가 아니라 "정확히 1회 읽고 즉시 에코"를 가능하게 하는 선행 조치**입니다. +엣지로 바꾸면 §3의 3회 수집·800ms 재폴링을 **둘 다 안전하게 제거**할 수 있습니다. + +--- + +## 5. 조치 후보 (미승인 — 승인 시 별도 작업지시서) + +> ⚠ **이 표는 [수렴 루프 설계안 §6](./RecipePage_에코수렴루프_설계.md)에서 갱신됐습니다.** +> P2·P3 정정, P4·P5 제거, P1 우선순위 하향. **설계안 §6을 정본으로 봅니다.** + +원칙: **PC는 자기가 읽은 값·실제 상태를 지체 없이 에코한다. 일치 판정은 PLC에 맡긴다.** + +| # | 대상 | 내용 | 근거 | +|---|---|---|---| +| **P1** | `CheckPageNo:457` | 레벨 → **상승엣지** 판정 | §4 (= 08/04 조치 A) | +| **P2** | `CheckRecipeNoComp:353-365` | 에코 없이 return 하는 2경로에 **에코 추가**(현재 활성 레시피 번호) + 알람/토스트 | §2-① | +| **P3** | `CheckPageNoComp:534-556` | 무한 대기 제거 → **읽은 값 그대로 에코**하고 진행. Layer 선택은 기존 폴백(`:626-628`) 사용 | §2-② | +| P4 | `CheckPageNoComp:499-566` | 3회 수집 → **1회 읽기**로 축소 | §3 (P1 선행) | +| P5 | `CheckRecipeNoComp:374-399` | 800ms 재폴링 **제거** | §3 (P1 선행) | +| P6 | `:349`, `:510` | `err` 확인 — **중단이 아니라 재읽기** 목적. `err != OK`면 즉시 재시도 | §2-① | +| P7 | `CheckPageNoComp:501` 직후 | `if (context.Stopped) return null;` 추가 | §2-④ | +| P8 | `CheckRecipeNoComp:413-417` | 타임아웃 시 리셋 대신 **탈출 + 알람** | §2-③ | +| P9 | `:492`, `:527` | 로그 문구 정정 ("3회 연속 검증" → 실제 동작) | §3 · `rules.md` #1 | +| P10 | `:1219-1241` | 주석 ②의 잘못된 전제 정정 + `SeqStatusMonitor`와 중복 정리 | §1 | + +**P1~P3이 "멈춤 제거"의 직접 조치입니다.** P4·P5는 P1 적용 후에만 안전합니다. + +### 주의 — `ResumeMode`(bit15) 예외 + +`OnSeqActionStop:1227`·`:1236-1240`, `SeqStatusMonitor:104`는 **M70515 ON 구간에서 에코 워드를 +보존**합니다. 가접 Resume이 직전 Recipe/Page 에코에 의존하기 때문입니다. +P2·P10 적용 시 **이 예외를 반드시 유지**해야 합니다. + +--- + +## 6. PLC측 질문 정리 (08/04 ★ 항목 갱신) + +| # | 질문 | 상태 | +|---|---|---| +| ~~★3~~ | 워드를 쓴 뒤 REQ를 올리는가 | **해소(08/04)** — `write → raise` 확정 | +| ~~★4~~ | `PC_PAGE_CHANGE_COMPLETE` 하강을 REQ 재발행 조건으로 쓰는가 | **해소(08/06)** — PLC는 넘버만 본다 | +| ★1 | **Manual 전환 / PC 정지 시 M70508을 PLC가 OFF 시키는가** | **미해소** — P1(엣지 전환) 필요성의 근거 | +| ★2 | M70515(Weld Ready REQ)를 PLC가 언제 OFF 시키는가 | 미해소 — [bit15 Resume 교착](../04/AutoRun재기동_Recipe_Page워드_미반영_원인분석.md) | +| ★5 | `PC_WELD_READY_ACK`(M70015)를 레벨로 보는가 펄스로 보는가 | 미해소 | + +> ★1이 "OFF 시킨다"로 확인되면 P1 없이도 대부분 해소되지만, **엣지 판정이 더 강한 보장**이라 +> 어느 쪽이든 P1을 권장합니다. + +--- + +## 7. 부수 확인 사항 + +- `CheckPageNoComp:521-525` — CellId/LotId를 매 회차 읽어 `Status`에 즉시 대입. 읽기 실패 시 + `string.Empty`가 그대로 들어갑니다(`PLCController.Address.cs:227`, `:239`). 빈 값 체크 없음. +- `CheckRecipeNoComp:376`의 재폴링이 `plcRecipeNo` 필드를 덮어써, `ScoutSaveTag:886` 파일명에 + 0이 박힐 수 있습니다. (경미 — P5로 함께 해소) +- 에코 워드 0 초기화 주체가 `SeqStatusMonitor:106-107` 한 곳입니다. `OnSeqActionStop`은 로그만 + 남깁니다(`:1231-1240`). 두 파일의 주석이 서로 다른 전제를 적고 있어 정리가 필요합니다. diff --git a/docs/분석/2026/08/06/RecipePage_에코수렴루프_설계.md b/docs/분석/2026/08/06/RecipePage_에코수렴루프_설계.md new file mode 100644 index 0000000..05808b8 --- /dev/null +++ b/docs/분석/2026/08/06/RecipePage_에코수렴루프_설계.md @@ -0,0 +1,187 @@ +# 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가 용접 완료 전에 워드를 바꾸지는 않을 것으로 보이나 **미확인**.