LGES-OC5-PtoP-Welder/docs/분석/2026/08/03/레시피저장_활성vs선택_교차오염_원인분석.md
2026-08-06 11:29:00 +09:00

23 KiB
Raw Blame History

레시피 저장 교차 오염 — 활성 레시피 ↔ 선택 레시피 상태 불일치 원인 분석

  • 작성일: 2026-08-03
  • 계기: 2026-08-01 현장. AutoRun 시퀀스 도중, 구(舊) 캘 레시피가 활성인 상태에서 신규 캘 레시피를 복사해 선택 레시피로 페이지 생성·Draw·저장했는데, 뷰를 이동했다 돌아오니 활성 레시피의 페이지가 선택 레시피에 덮어써진 현상
  • 대상 소스:
    • WeldingComponents/WeldingCommon/Utils/RecipeHelper.cs
    • WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingViewModel.cs
    • Machines/ILSOS_PrismaticRD_PTOP/Recipes/RMSHandlerForPTOP.cs
    • Machines/ILSOS_PrismaticRD_PTOP/Views/Main/UCMainAutoRunViewModel.cs
    • Machines/ILSOS_PrismaticRD_PTOP/Sequences/SeqAutoRunPTOP.cs
    • Machines/ILSOS_PrismaticRD_PTOP/EquipmentSetup.cs
    • WeldingComponents/LASA/GUI/Page/MainPage.cs, .../DB/Recipe.cs, .../CoreUnit/.../Canvas/Model.cs
  • 상태: 원인 구조 확정 (소스 수정 없음 — 분석만)
  • 관련: FieldHelper 격자배율차이 분석 — 여기서 말하는 "구 캘 레시피"의 정체(격자 2.406% 차이)

0. 결론

LASA 티칭 편집기는 프로세스 전역 단일 캔버스 하나뿐인데, "지금 캔버스에 무엇이 로드돼 있는가"와 "저장 버튼을 누르면 어디에 쓸 것인가"가 서로 다른 두 개의 전역 상태로 관리되고, 둘 중 하나만 갱신하는 코드 경로가 양쪽 다 존재한다.

상태 의미 위치
DB.Dao.SystemRecipe.WorkRecipeFile 캔버스에 실제 로드된 레시피 파일 MainPage.ReloadRecipe(MainPage.cs:466)에서만 갱신
RecipeHelper.SelectedRecipe 저장 시 쓸 대상 레시피 (id, name) RecipeHelper.cs:18 — static

RecipeHelper.SaveRecipe(name)은 캔버스 내용이 그 레시피인지 전혀 검증하지 않고 name 폴더에 통째로 덮어쓴다 (RecipeHelper.cs:36-39 → MainPage.cs:425-434 → Recipe.cs:79-120). 즉 두 상태가 어긋난 순간에 저장이 일어나면 무조건 남의 레시피 파일이 파괴된다.

핵심 결함 3개:

  1. SelectedRecipe를 건드리지 않고 캔버스만 바꾸는 경로 — RMSHandlerForPTOP.OnUpdateCurrentActivatedRecipe(RMSHandlerForPTOP.cs:62-66) → 이후 저장하면 활성 레시피 내용이 선택 레시피 파일로 기록됨. ← 이번 현장 증상과 방향 일치
  2. 캔버스를 건드리지 않고 SelectedRecipe만 활성 레시피로 바꿔치기하는 경로 — RecipeHelper.OnActiveReloaded() (SeqAutoRunPTOP.cs:425, UCMainAutoRunViewModel.cs:172/247, EquipmentSetup.cs:279, VM 생성자 :71). Aurora 레시피 화면의 콤보박스는 이 변경을 모른다 → 화면 표시와 저장 대상이 어긋남
  3. 레시피 활성화 순간, 편집 중이던 캔버스를 새 활성 레시피 파일에 그대로 써버림 — UCScannerObjectTeachingViewModel.OnActivatedRecipe(:156) RecipeHelper.SaveRecipe(activeRecipe.Name) — 사용자가 저장을 누른 적이 없는데 파일이 바뀐다

1. 구조 전제 — 캔버스는 하나뿐이다

MainPage(WinForms, XAML의 <lasa:MainPage x:Name="MainPageView"/>)
  └ ScannerCanvasCtrlTeaching.Canvas.Model  ← 유일한 편집 대상
        ↑ 참조 고정
     DB.Dao.WeldingLinesRecipe.Model  (MainPage.cs:175)
  • CommonHelper.SetMainPage(MainPageView) (UCScannerObjectTeaching.xaml.cs:35) → 전역 1개 (CommonHelper.cs:10)
  • SaveRecipe(path) = 라이브 캔버스를 path에 직렬화 (Recipe.cs:79, RecipeWeldingLines.Save)
  • ReloadRecipe(path) = 라이브 캔버스를 path 내용으로 전면 교체 (Model.Load → m_layers.Clear() 후 재구성, Model.cs:681-731 — 병합이 아니라 완전 치환)

따라서 "레시피 A 편집 중"과 "레시피 B 편집 중"은 동시에 존재할 수 없고, 전환은 오직 ReloadRecipe 호출 하나로만 이뤄진다. 전환을 놓치면 곧바로 오염이다.


2. 저장/로드 경로 지도 — 어떤 경로가 무엇을 갱신하나

# 경로 캔버스 SelectedRecipe 위험
1 IRecipeViewModel.OnChanged(recipe) ← recipe ← recipe 정상 (둘 다 갱신)
2 IRecipeViewModel.Reloaded(recipe) ← recipe ← recipe 정상
3 RecipeHelper.OnActiveReloaded() ← 활성 ← 활성 사용자 UI 선택을 무시하고 활성으로 강제
4 RMSHandlerForPTOP.OnUpdateCurrentActivatedRecipe() ← 활성 그대로 ★ 불일치 발생
5 UCScannerObjectTeachingViewModel.OnActivatedRecipe() (아래 참조) 그대로 ★ 활성 파일에 캔버스를 써버림
6 OnRecipeEntryChanged New 분기 (:110-115) 그대로 ← 신규 ★ 캔버스를 신규 레시피에 즉시 저장
7 OnSaved(id, recipeName) (:202-238) 그대로 그대로 ★ 인자 id/recipeName을 무시하고 SelectedRecipe로 저장

2-1. 경로 4 — 불일치를 만드는 지점

// RMSHandlerForPTOP.cs:56-66
aurora.EventObservable.AsObservable(CommonEventIds.EVENT_CHANGE_RECIPE_INFO)
      .ObserveOnDispatcher()
      .Select(param => (RecipeInfo)param.Param)
      .Subscribe(OnUpdateCurrentActivatedRecipe);

private void OnUpdateCurrentActivatedRecipe(RecipeInfo info)
{
    RecipeHelper.ReloadRecipe(info.Name);   // 캔버스 = 활성 레시피
    UploadActivatedRecipeParameter();       // SelectedRecipe 는 손대지 않음
}

EVENT_CHANGE_RECIPE_INFO는 OnActivatedRecipe(:159)와 OnSaved(:234)가 발행한다. 즉 레시피가 활성화될 때마다 캔버스는 활성 레시피로 바뀌지만 SelectedRecipe는 사용자가 고른 값 그대로 남는다. 이 상태에서 저장 버튼 → SaveRecipe(SelectedRecipe.name) → 활성 레시피의 페이지가 선택 레시피 파일에 기록.

2-2. 경로 5 — 저장 버튼 없이 파일이 바뀐다

// UCScannerObjectTeachingViewModel.cs:155-160
var activeRecipe = _RecipeManager.ActivatedRecipe();
RecipeHelper.SaveRecipe(activeRecipe.Name);   // ← 편집 중이던 캔버스를 활성 레시피에 덮어씀
_Aurora.EventPublisher.Publish(new EventMessage("OnActivatedRecipe", CommonEventIds.EVENT_CHANGE_RECIPE_INFO, activeRecipe));

레시피 활성화(SYS_RECIPE_ACTIVATED)는 PLC/AutoRun이 자동으로 일으킨다(SeqAutoRunPTOP.cs:403). 사용자가 B를 티칭하던 중에 시퀀스가 A를 활성화하면, B의 작업물이 A 파일에 저장된다(A 파괴). 그 직후 경로 4가 A를 캔버스로 로드하므로, 화면은 그대로인 듯 보이지만 파일 두 개가 이미 섞였다.

2-3. 경로 7 — Aurora가 알려준 대상을 버린다

public void OnSaved(int id, string recipeName)   // ← Aurora가 "지금 저장 중인 레시피"를 넘겨줌
{
    ...
    RecipeHelper.SaveRecipe(RecipeHelper.SelectedRecipe.name);   // ← 그런데 static 값을 씀

Aurora.xml:4024의 계약상 id/recipeName이 정답이다. 이 둘을 무시하고 static을 쓰는 순간, 위 1~6 어느 경로로든 static이 오염되면 저장이 엉뚱한 곳으로 간다. PreSaved(:181)도 마찬가지로 인자를 검증에 쓰지 않는다.


3. 재현 시나리오 (2026-08-01 현장과 일치)

전제: 활성 = A(구 캘, MOTORPOS 격자 2.4% 다른 동탄 레시피), 신규 복사본 = B

단계 동작 캔버스 SelectedRecipe A 파일 B 파일
1 사용자가 콤보에서 B 선택 → OnChanged(B) B B A B
2 B에 페이지 추가 + Draw + 저장 → OnSaved → SaveRecipe(B) B B A B(신규 작업물)
3 AutoRun 시퀀스가 ActivateRecipe(A) (SeqAutoRunPTOP.cs:403)
→ OnActivatedRecipe → SaveRecipe(A) (:156)
B B A ← B 내용으로 오염 B
4 같은 핸들러가 EVENT_CHANGE_RECIPE_INFO 발행 (:159)
→ 경로 4 ReloadRecipe(A)
A B (미갱신) A B
5 뷰 이동 후 Recipe 화면 복귀 — 화면에는 A의 페이지가 보임
(콤보는 여전히 B)
A B A B
6 저장 → SaveRecipe(SelectedRecipe.name = B) A B A B ← A의 페이지로 덮어써짐

→ 6단계가 보고된 증상 그대로다. "활성화된 레시피의 페이지가 선택된 레시피에 덮어써진다."

3-1. 왜 하필 "뷰 이동 후 복귀"가 트리거인가

캔버스는 뷰를 떠나도 파괴되지 않는 전역 WinForms 컨트롤이다. 따라서 4단계에서 이미 캔버스가 A로 바뀌어 있었지만, 사용자는 다른 메뉴에 있어서 보지 못했다. Recipe 화면으로 돌아와 처음 눈으로 확인하는 순간이 5단계이므로, 사용자에겐 "뷰를 이동하고 돌아오니 바뀌어 있더라"로 관측된다. 뷰 이동 자체가 원인은 아니다.

추가로 뷰가 재생성되는 경우 UCScannerObjectTeachingViewModel 생성자(:71)의 RecipeHelper.OnActiveReloaded()가 다시 돌면서 선택/캔버스를 모두 활성 레시피로 리셋하므로, 복귀 시점에 활성 레시피 화면이 뜨는 결과는 어느 쪽이든 동일하다.


4. 로그 근거 (docs/분석/Aurora_20260731.log)

전날 로그에 동일 구조가 그대로 찍혀 있다.

2471  18:24:06.658  SeqAutoRunPTOP: [CheckRecipeNoComp] ActivateRecipe (12 -> 21)
2475  18:24:06.704  UCScannerObjectTeachingViewModel: SYS_RECIPE_ACTIVATED OnActivatedRecipe
2477  18:24:06.754  UCScannerObjectTeachingViewModel: CommonEventIds.EVENT_CHANGE_RECIPE_INFO from [OnActivatedRecipe]
2566  18:24:13.120  UCScannerObjectTeachingViewModel: SYS_RECIPE_ACTIVATED OnActivatedRecipe
2568  18:24:13.170  ... EVENT_CHANGE_RECIPE_INFO from [OnActivatedRecipe]
2685  18:25:49.144  ... EVENT_CHANGE_RECIPE_INFO from [OnSaved] RecipeName [U-Frame_Pre_Welding]
2686  18:25:49.144  ... Saved Recipe : [U-Frame_Pre_Welding]
  • 2471–2477: AutoRun 시퀀스가 자동으로 레시피를 전환 → 3~4단계 경로가 실제로 발동함이 확인된다. 이때 SaveRecipe(activeRecipe.Name)(:156)은 로그를 남기지 않으므로 파일이 조용히 바뀐다.
  • 2685: [OnSaved]에서 이벤트가 발행됐다는 건 SelectedRecipe.id == activeRecipe.Id였다는 뜻(:230). 사용자가 명시적으로 그 레시피를 고른 적이 없어도, 경로 3(OnActiveReloaded)이 SelectedRecipe를 활성으로 바꿔놓았기 때문에 성립한다.

2026-08-01 당일 Aurora_20260801.log가 현장 데이터에 포함돼 있지 않다. 위 6단계 중 어느 저장이 최종적으로 B를 덮었는지(3단계 자동저장인지 6단계 수동저장인지) 확정하려면 당일 로그의 Saved Recipe : [...] / SYS_RECIPE_ACTIVATED 시각 순서가 필요하다. 확보되면 이 절에 덧붙일 것.


5. 부수 결함 (같은 뿌리, 별건으로 터질 수 있음)

5-1. 신규 레시피 생성 시 남의 내용이 즉시 기록됨

// UCScannerObjectTeachingViewModel.cs:110-115
if (param.AddedId != -1) {                              // New
    RecipeHelper.SelectedRecipe = (recipe.Id, recipe.Name);
    RecipeHelper.SaveRecipe(recipe.Name, includeModel: false);   // 현재 캔버스를 신규 레시피에 기록
} else if (param.CopiedToId != -1 && param.CopiedFromId != -1) { //Copy — 비어 있음
}

includeModel: false라 도형(Layer)은 안 쓰이고 용접 파라미터만 기록되지만, 그 파라미터는 "그 순간 캔버스에 로드돼 있던 레시피"의 것이다. 신규/복사본이 엉뚱한 파라미터로 출발한다. 현장 데이터의 Copy__U-Frame_Welding이 이 경로를 탔는지 확인 필요.

5-2. RollbackRecipe()가 의도대로 동작하지 않음

// RecipeHelper.cs:102-110
//AutoRun 완료 후 다시 Teching 중인 Recipe로 Rollback 해줘야 함
public static void RollbackRecipe()
    => Dispatcher.InvokeAsync(() => ReloadRecipe(SelectedRecipe.name));

주석의 의도는 "티칭하던 레시피로 복귀"인데, AutoRun 진입 시 경로 3이 이미 SelectedRecipe를 활성 레시피로 덮어썼기 때문에 롤백은 활성 레시피로 되돌아갈 뿐이다. 사실상 no-op. (호출: UCMainAutoRunViewModel.cs:442. 그리고 Auto→Manual 복귀 경로인 EquipmentSetup.cs:282는 주석 처리되어 아예 호출되지 않는다.)

5-3. Auto 진입 시 무조건 선택 레시피가 날아감

// EquipmentSetup.cs:276-283
if (status == eEqpStatus.Auto) {
    ...
    RecipeHelper.OnActiveReloaded();     // SelectedRecipe := 활성
} else {
    //RecipeHelper.RollbackRecipe();     // ← 복귀 없음
}

Auto 한 번 들어갔다 나오면 사용자가 고른 레시피 컨텍스트는 영구 소실된다.

5-4. bit15 Resume 경로도 동일

UCMainAutoRunViewModel.cs:246-247 — 팝업 후 ActivateRecipe + OnActiveReloaded(). 이 경로는 작업자가 Manual에서 티칭하도록 유도하는 흐름이므로, 티칭 시작 직전에 선택 상태를 갈아엎는 셈이라 가장 위험하다. 이번 현장 상황("AutoRun 시퀀스 도중, 활성 레시피가 있음에도 다른 레시피에서 작업")과 정황이 정확히 맞는다.


6. 수정 방향 (제안 — 미적용)

우선순위 순.

A. 저장 대상을 인자로 고정 (최소 변경, 효과 큼)

public void OnSaved(int id, string recipeName)
{
    // Aurora가 알려준 대상만 신뢰
    RecipeHelper.SaveRecipe(recipeName);

PreSaved(id, recipeName)에서 "캔버스에 로드된 레시피 ≠ 저장 대상"이면 저장 거부를 추가. 비교 기준은 DB.Dao.SystemRecipe.WorkRecipeFile (캔버스의 진짜 출처).

// PreSaved 안
var loaded = Path.GetFileName(Path.GetDirectoryName(DB.Dao.SystemRecipe.WorkRecipeFile));
if (loaded != recipeName) {
    MessageUtil.ShowErrorToast($"화면에 로드된 레시피({loaded})와 저장 대상({recipeName})이 다릅니다.");
    return false;
}

이 가드 하나만 넣어도 이번 사고는 저장 시점에 차단된다.

B. 캔버스와 SelectedRecipe를 한 함수로만 바꾸도록 통합

RecipeHelper에 SetCurrentRecipe(int id, string name) 하나만 두고, 내부에서 SelectedRecipe 대입 + ReloadRecipe를 항상 같이 수행. 경로 4(RMSHandlerForPTOP.OnUpdateCurrentActivatedRecipe)의 ReloadRecipe(info.Name) 단독 호출을 제거.

C. 자동 저장 제거

OnActivatedRecipe의 RecipeHelper.SaveRecipe(activeRecipe.Name)(:156) 삭제 또는 "캔버스가 이미 그 활성 레시피일 때만" 저장하도록 가드. 사용자가 누르지 않은 저장이 파일을 바꾸는 것은 어떤 경우에도 정당화되지 않는다.

D. 티칭 중 자동 레시피 전환 차단

eEqpStatus.Manual + Recipe 화면 활성 상태에서는 PLC/시퀀스발 ActivateRecipe가 캔버스를 갈아엎지 않도록 큐잉하거나, 최소한 작업자에게 확인 팝업.


7. 확인 필요 사항

  1. 2026-08-01 Aurora_20260801.log 확보 — 3~6단계 중 실제 발동 순서 확정 (§4 참고)
  2. Aurora 내부 UCRecipe가 뷰 재진입 시 Reloaded()를 호출하는지 — 호출한다면 5단계에서 자동 복구되므로, 실제 오염 시점이 3단계(자동 저장) 쪽으로 좁혀진다. Aurora.dll 소스가 없어 코드로는 확인 불가 → 현장 재현 시 로그로 판별
  3. Copy__U-Frame_Welding의 WeldingParameters 출처 — §5-1 경로를 탔는지 원본 U-Frame_Welding과 대조
  4. 손상 가능성이 있는 레시피 파일 목록 정리 후 _config_recipe/backup/ 과 대조 (백업 시점이 사고 이전인지 먼저 확인)

8. 코드 위치 요약

항목 위치
SelectedRecipe static WeldingCommon/Utils/RecipeHelper.cs:18
SaveRecipe (검증 없이 덮어씀) RecipeHelper.cs:36-39 → MainPage.cs:425-434 → Recipe.cs:79-120
ReloadRecipe (캔버스 전면 교체) RecipeHelper.cs:64-69 → MainPage.cs:460-486 → Model.cs:681-731
OnActiveReloaded (선택=활성 강제) RecipeHelper.cs:73-79
RollbackRecipe (사실상 no-op) RecipeHelper.cs:105-110
활성화 시 자동 저장 UCScannerObjectTeachingViewModel.cs:155-160
신규 생성 시 자동 저장 UCScannerObjectTeachingViewModel.cs:110-115
OnSaved 인자 무시 UCScannerObjectTeachingViewModel.cs:202-238 (저장은 :208)
캔버스만 교체(불일치 생성) RMSHandlerForPTOP.cs:62-66
시퀀스발 레시피 전환 SeqAutoRunPTOP.cs:403-426
AutoRun 시작/정지 시 갱신 UCMainAutoRunViewModel.cs:169-176, 324, 442
bit15 Manual 전환 경로 UCMainAutoRunViewModel.cs:240-249
Auto 진입 시 강제 갱신 EquipmentSetup.cs:276-283
캔버스 단일 인스턴스 CommonHelper.cs:10-23, UCScannerObjectTeaching.xaml.cs:35, MainPage.cs:175

9. [별건] AutoRun 정지 시 에코 워드(PC_RECIPE_NO/PC_PAGE_NO) 초기화 여부

  • 계기: 2026-08-03 검토 중 제기 — "OnSeqActionStop()에서 워드 초기화를 안 하는데, 레시피 NUM·PAGE NUM은 PLC가 이미 주니까 PC측은 초기화해도 되지 않나"
  • 본 문서 본론(레시피 파일 오염)과는 원인이 다른 별건이나, bit15 Resume 경로·AutoRun 정지 처리라는 같은 구간을 공유하므로 여기에 함께 기록한다.
  • 상태: 분석만 — 소스 수정 없음
  • 작업안: 작업지시서 2026-08-03 에코 워드 정지 시 초기화 정리

9-1. 현황 — OnSeqActionStop은 초기화하지 않는다 (사실)

// SeqAutoRunPTOP.cs:1209-1212
// 260512 워드
// 260621: 에코 워드(PC_PAGE_NO/PC_RECIPE_NO)는 정지 시 0으로 지우지 않음 — PLC가 정합 확인에 쓰는
//         마지막 핸드셰이크 값을 보존(다음 사이클에 어차피 재에코). 기존 0 초기화 2줄 제거.
Log.I("[OnSeqActionStop] PLC IO Output Word Init");   // ← 실제 동작 없이 문구만 남음

260621에 초기화 2줄이 제거되고 로그 문구만 남았다. 로그만 보면 초기화한 것처럼 보이는 함정.

9-2. 그러나 실제로는 이미 지워지고 있다 — SeqStatusMonitor

// SeqStatusMonitor.cs:94-109
bool plcAutoOff = ChangeChecker.IsChangedBoolMonitoring("PlcAutoStateOff",
                      _plc.ReadBit(eBitInput.PLC_AUTO_STATUS).data, false);      // M70501 하강엣지
bool pcAutoOff  = ChangeChecker.IsChangedBoolMonitoring("PcAutoStateOff",
                      _plc.ReadOutBit(eBitOutput.PC_AUTO_STATUS).data, false);   // M70001 하강엣지
bool weldReadyAlive = _plc.ReadBit(eBitInput.PLC_WELD_READY_REQ).data;           // M70515

if (plcAutoOff || (pcAutoOff && !weldReadyAlive)) {
    _plc.WriteWord(eWordOutput.PC_RECIPE_NO, 0);
    _plc.WriteWord(eWordOutput.PC_PAGE_NO, 0);
}

OnSeqActionStop:1200이 PC_AUTO_STATUS = false를 쓰므로 → StatusMonitor 루프(3노드 × NODE_SLEEP 20ms ≈ 60ms 주기)가 다음 스캔에 pcAutoOff 하강엣지를 감지 → M70515가 안 살아있으면 워드를 0으로 지운다.

정지 유형 M70515(bit15) 에코 워드 실제 결과
일반 정지 · 알람 정지 · OnForceError OFF StatusMonitor가 0으로 초기화
bit15 Resume 정지 (ShowRecipePageConfirmAndSwitchManual) ON 보존 ← 이 케이스만

→ "PC측은 초기화를 안 한다"는 bit15 구간에만 해당하고, 나머지 정지 경로는 이미 초기화되고 있다.

9-3. "PLC가 이미 주니 PC측은 초기화해도 된다" — 타당함

① PC는 자기 에코를 되읽지 않는다. 소스 전체에서 PC_RECIPE_NO/PC_PAGE_NO는 WriteWord만 존재하고 읽는 곳이 없다. 판정은 항상 PLC 입력 워드로 한다 — CheckRecipeNoComp:349(D70502 PLC_RECIPE_NO), CheckPageNoComp:571(D70501 PLC_PAGE_NO). → PC 내부 로직은 에코 워드 값에 전혀 의존하지 않는다. 0으로 지워도 PC 동작 불변.

② 재진입 시 반드시 재에코된다.

  • CheckRecipeNoComp — 동일 레시피여도 :395에서 에코 유지. :368 주석이 정확히 그 이유를 적어둠 ("정지 시 D70002=0으로 지우므로, 동일 레시피 재진입에서 에코를 건너뛰면 PLC가 0/이전값을 봄")
  • CheckPageNoComp — :563에서 매 페이지 무조건 에코

③ 260621에 보존으로 바꾼 원래 근거가 현재 코드에서는 성립하지 않는다.

  • 당시 전제(주석 :131, :260): "bit15 Resume은 핸드셰이크를 스킵하니 에코를 보존해야 한다"
  • 현재 ResumeMode의 시퀀스 내 소비처는 CheckCycleStop:872-877 한 곳뿐이고, 하는 일은 PC_WELD_READY_ACK OFF + 플래그 해제다. 핸드셰이크를 스킵하지 않는다.
  • OnAutoRunResume:283은 RunSequenceAutoRun()으로 시퀀스를 처음부터 재시작하며, :284 로그도 "Auto 재개(정상 핸드셰이크)"라고 적는다(옵션A).
  • → 주석(:131, :260 "핸드셰이크 스킵")과 실제 구현이 어긋나 있다. 옵션A로 바뀌며 스킵은 사라졌는데 보존 로직·주석만 남은 것으로 보인다.

9-4. 남은 리스크 — 소비자는 PC가 아니라 PLC 래더

가정 PLC 로직 0 초기화 시 영향
A D70002 == D70502(에코↔지령)를 상시 레벨로 인터락에 사용 정지 중 불일치 → 기동 금지/알람 가능
B PC_PAGE_CHANGE_COMPLETE(M70008) 등 ACK 비트와 함께 1회만 확인 정지 중 값은 무의미 → 무해
  • 현재 PC 코드는 B 전제로 작성돼 있다(CheckPageNoComp:563→573 — 에코 직후 ACK 비트를 세우는 구조).
  • 결정적 방증: 일반 정지에서는 이미 StatusMonitor가 0으로 지우고 있고, 현장에서 이로 인한 문제 보고가 없다. → "0으로 지워도 되는가"는 대부분의 정지 경로에서 이미 실증되고 있다. 미검증 구간은 bit15 정지뿐.
  • 최종 확인 필요: PLC 담당자에게 "D70001/D70002를 상시 비교하는 인터락이 있는지" 1줄 확인.

9-5. 결론

# 선택지 동작 변경 리스크
1 OnSeqActionStop에 0 초기화 2줄 복원 있음 StatusMonitor와 중복. OnSeqActionStop엔 M70515 예외가 없어 bit15 보존이 무력화 → 실질적으로 3번과 동일해짐
2 초기화 책임을 StatusMonitor로 일원화 명시 — 오해 소지 로그·죽은 주석만 정리 없음 없음 (권장 시작점)
3 bit15 보존 예외 제거 (StatusMonitor:104를 pcAutoOff로 단순화) 있음 9-3 ③ 근거로 논리적 타당. 실제 동작이 바뀌는 유일한 안 → bit15 정지 중 D70001/D70002=0에서 PLC가 Resume을 수락하는지 현장 검증 필요

세 선택지는 독립적으로 적용 가능하며, 전부 적용할 필요는 없다. 상세 절차는 9장 상단의 작업지시서 참조.